diff --git a/README.md b/README.md index d391c54..dfdf9b4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ -# L-systems-renderer +# L-systems Renderer -L-systems renderer in Exponential Idle. +An implementation of L-systems in Exponential Idle. + +## Installation + +Two editions of the LSR are available. The Classic edition is lighter in terms +of processing, but does not support parametric or context-sensitive syntax. + +- [Classic](./classic.js) +- [Parametric](./parametric.js) + +Navigate to the edition you want to download, click on the link, then click on +`Raw`. You will be delivered to the theory's raw code. Copy the page's URL. + +Then, access the custom theory panel within the game (unlocked after finishing +T9, the last theory) then enter the picking menu. Press the `+` symbol and +paste the URL in. ## Features -Supported L-system features: +- Parametric, context-sensitive (2L) systems - Stochastic (randomised) rules - 3D turtle controls - Polygon modelling @@ -20,25 +35,14 @@ Be sure to back it up to another save before updating, and in case it's corrupted, please contact me. From now on, there's also an option to export the internal state in the theory menu, so please back it up. -## Installation - -Current version: 0.21.1 - -Navigate to [`renderer.js`](./renderer.js) and click on `Raw`. You will be -delivered to this theory's raw code. Copy the page's URL. - -Then, access the custom theory panel within the game (unlocked after finishing -the Convergence Test) then enter the picking menu. Press the `+` symbol and -paste the URL in. - ## Screenshots -![ss1](screenshots/33.jpg "Lilac branch") +![ss1](screenshots/35.jpg "Lilac branch") -![ss2](screenshots/34.jpg "Single lilac model") +![ss2](screenshots/36.jpg "Sierpinski's Triangle") -![ss3](screenshots/32.jpg "Clover model") +![ss3](screenshots/38.jpg "Stamp stick") -![ss3](screenshots/31.jpg "3D Hilbert curve") +![ss3](screenshots/37.jpg "LS menu") -![ss3](screenshots/30.jpg "Sierpinski triangle") +![ss3](screenshots/39.jpg "Guide menu") diff --git a/TODO.md b/TODO.md index bf94cbb..48e5ca0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,65 +1,76 @@ # LSR: To-do List - [LSR: To-do List](#lsr-to-do-list) - - [1.00, the Mistletoe Update](#100-the-mistletoe-update) + - [Considerations](#considerations) + - [1.0: Completed](#10-completed) - [0.21: Completed](#021-completed) - [0.20: Completed](#020-completed) - [Impossible or Scrapped](#impossible-or-scrapped) -## 1.00, the Mistletoe Update +## Considerations - [ ] Add more comments in the code -- [ ] Turtle controls summary page in guide - [ ] A more detailed README - Showcases the power of tickspeed and stroke options - Discusses limitations of the game - Like a blog post, sort of - [ ] Localisation -- [ ] Folders for saved systems? - - Directory separator? -- [ ] Compress internal state? LZW, LZP +- [ ] Folders for saved systems - [ ] Ask Gilles about changing the spline used in 3D graph -- [ ] How about locking rotation? (for Navier Stokes) -- [ ] Rework quickdraw / BT logic - - [ ] Add a cooldown system - - [ ] Hesitate on both ends if hesitation is on? - - [ ] If the stack hasn't detected any movement, just don't do anything - (because of the ignored stuff) - - [ ] Remove the backtrack list option - - Also, backtrack on the way back and forward needs different lists? +- Too much hassle to implement, defer them to LG? + - [ ] Option to use old tropisme -- [ ] Investigate Houdini stochastic syntax for weighted derivations -`[left_ctx<] symbol [>right_ctx] [:condition] = replacement [:probability]` - - Can multiple derivations on the same rule still be made? Ruins parity - - Allow both modes to exist +## 1.0: Completed -- [ ] Change how models work: - - Stochastic models are no longer available - - Please define stochastic rules for the actual symbol instead, pre-model - - Models are no longer recursively derived - - This will remove their permanence - - Instead, they will be processed by the renderer using a queue - - The processing code will be positioned below (?) the main renderer loop - - Almost the exact same code as main loop, but with `~` queue controls - - Having a model also means that symbol should be ignored (no extra F) - - Easy, just add the model map's keys to the set - - Update the manual - - Backtrack models? - -- [ ] Parametric systems - - Different classes or merge into regular? - - Different classes means less overhead processing for regular systems - - Regex magic to separate string to actual sequence of symbols? +- [x] Complete new manual +- [x] Make a mistletoe for P-LSR + - To prevent lag, probably only create one stage with full models +- [x] Tropism direction + - Maybe can have 4 arguments +- [x] Issue: multiple parameters not working at all + - Cause: splitting commas -> Solution: semi-colons +- [x] Split workload for ancestree (AND for derive too if this works well) + - [x] Apply split technique to sequence menu +- [ ] Compress internal state? LZW, LZP + - [x] Or just separate into multiple lines, with an add button +- [x] Parametric systems + - [x] Input context-ignored symbols + - [x] New option in constructor for MathExpression variables + - [x] Button in LS menu next to axiom entry + - Different classes + - Button in LS menu / save menu to switch modes? + - Theory settings? + - Lemma stages? + - Make `f` move without returning like `F` + - Top-down processing priority + - [x] Regex magic to separate string into actual sequence of symbols + - [x] Issue: regex for nested brackets in parameters? - Equal comparison changed to `==` instead of `=` in abop to differentiate from the syntax - Store as an extra array of objects - Round brackets gonna make it hard for actual drawing - although maybe it would've been already stripped down by the time it gets to the turtle - `~`: Parameters of the following symbol can control model size in rule - -- [ ] Context sensitivity +- [x] Split into two versions: Classic Renderer and Paramatric L-systems Rdr +- [x] Fix precision problem with BigNumber and/or Quaternions + - Source of error: missing a conversion of BN -> Number in LS constructor +- [x] Turning angles (and tropism) + - Formula expressions allow shenanigans such as `360/7`, mostly + - [ ] Button to change mode between degrees and radians? + - Probably scrap too lol +- [x] Revamp sequence menu + - Level 0: 10 chars (`View`) + - [x] Classic LS: displays the entire string in a label + - [ ] Parametric LS: displays symbols and params side by side, one symbol/row + - Scrapped lol + - [x] Stage navigation +- [x] Investigate Houdini stochastic syntax for weighted derivations +`[left_ctx<] symbol [>right_ctx] [:condition] = replacement [:probability]` + - Stochastic in PLSR: has to be on the same line +- [x] Context sensitivity - `b < a > c → aa` - - Skipping over brackets? hmm, difficult + - Parametric: **context ignore** replaces regular ignore list in LS menu + - [x] Skipping over brackets? hmm, difficult - Mapping vertex depth, can be checked by tracking brackets and stacks. - Actually it's pretty hard to make an algo for this, because it could be: `01[23[456][45][4]][234]2`, and you would have to find all the 2s if you @@ -70,23 +81,63 @@ - Ancestor means the index of whomever the fuck is on top of the idx stack. - Does this work with the dynamic loading system with all the data passing? - If stored by maps: Keys can be any data type. - - If stored by objects: - ```js - // v2 - let contextRulesLR = - { - 'ABC': 'D', // Two ways: AC = D - 'FBG': 'H' // FG = H - }; - let contextRulesL = - { - 'AB': 'E' // One way: AA = E - }; - ``` + - If stored by objects: written in `parametric_lsystem.js` +- [x] Update docstrings +- [x] Buttons in LS menu to move up down (swap rules) + - How to update rule entries? +- [x] Issue: Renderer.tick() cuts off 1 tick at the backtrack tail end +- [x] Investigate tropism (capital T) + - [x] Separate starting quaternions for upright and sideways + - Stop swizzling! + - Tropism is still the same direction, so we can simulate sideways vines + - Houdini: Apply tropism vector (gravity). This angles the turtle towards the negative Y axis. The amount of change is governed by g. The default change is to use the Gravity parameter. +- [x] Investigate vertical heading (`$`) (NOT helio-tropism) + - Houdini: $(x,y,z) + Rotates the turtle so the up vector is (0,1,0). Points the turtle in the direction of the point (x,y,z). Default behavior is only to orient and not to change the direction. + - Abop: The symbol $ rolls the turtle around its own axis so that vector L pointing to the left of the turtle is brought to a horizontal position. Consequently, the branch plane is “closest to a horizontal plane,” as required by Honda’s model. From a technical point of view, $ modifies the turtle orientation in space according to the formulae + L = V × H / |V × H| and U = H × L, + where vectors H, L and U are the heading (`\ /`), left (`& ^`) and up (`+ -`) vectors associated with the turtle, V is the direction opposite to gravity, and |A| denotes the length of vector A. + - main: roll the turtle around the H axis so that H and U lie in a common vertical plane with U closest to up +- [x] Add T and $ to gude +- [x] Issue: models are broken because of backtrack rework +- [x] Change how models work: + - Stochastic models are no longer available + - Instead define stochastic rules for the actual symbol instead, pre-model + - Models are no longer recursively derived + - This will remove their permanence and increase performance + - Instead, they will be processed by the renderer using a queue + - The processing code will be positioned below (?) the main renderer loop + - Almost the exact same code as main loop, but with `~` queue controls + - Having a model also means that symbol should be ignored (no extra F) + - Easy, just add the model map's keys to the set + - Update the manual + - Backtrack models? +- [x] Update guide about models +- Can multiple derivations on the same rule still be made? Ruins parity + - [x] Allow both modes to exist (ONLY in Classic) +- [x] Implement new RNG (Xorshift) instead of th fucjuing Lcg +- [x] Screen adaptive button sizes (40, 44, 48) plus other stuff maybe +- [x] Rework quickdraw / BT logic (**good enough!**) + - Progress: Regular, backtrack, quickdraw done + - Test suites: + - [x] Arrow (8/8) + - [x] Cantor (8/8) + - [ ] Snowflake (0/8) - still wasting time on ignored shits + - [x] Add a cooldown system + - [x] Hesitate on both ends if hesitation is on? + - [x] When just pushing onto the stack normally (on an F point), don't push + if the position hasn't changed, because the orientation doesn't matter if + the stack point isn't a `[` bracket point + - [x] Issue: quick BT still forces hesitation on the way forward + - [x] If the `]]]]]` are stacked, hesitation time is massive + - [x] If the stack hasn't detected any movement, just don't do anything + - [x] Hesitate only when the turtle was moved compared to last time? + - [ ] Does it work with ignored? No + (because of the ignored stuff) + - [x] The pop in `]` might be a problem if the branch stack has nothing yet? + - [x] Remove the backtrack list option + - Also, backtrack on the way back and forward needs different lists? +- [x] Turtle controls summary page in guide ## 0.21: Completed @@ -181,11 +232,11 @@ needs to store two boolean properties `isContextSensitive` and `isParametric`. `Parametric L-systems Renderer`, with a better randomiser lol - Scrapped. Just make a new class. - Strict model format for each symbol - - Array of Vector3s denoting a path of vertices - - Don't write `(0, 0, 0)` at start or end + - Array of Vector3s den 0)` at start or end - Flow: the previous path ended at `(0, 0, 0)` of this path. We'll follow through the model's queue one by one until we reach the end. But we won't - go back to `(0, 0, 0)`, we go forward to the next symbol. This would allow + go back to `(0, 0, 0)`, weoting a path of vertices + - Don't write `(0, 0, go forward to the next symbol. This would allow us to draw different lengthed lines if we defined the model to include only one point: `(L, 0, 0)`. But... that would delay processing by one turn? - Well, we can't include the tilde then. Hardcoded models can still be a thing @@ -195,4 +246,17 @@ needs to store two boolean properties `isContextSensitive` and `isParametric`. - How are they referenced in systems? As a list of names? An ID that is the index of an array? - Model storage - - How to load? How to edit? \ No newline at end of file + - How to load? How to edit? +- Counter argument to new model format: + - Permanence can be solved by cutting with `%`, albeit with a 1 tick cost for + renderer hesitation (or 2) + - Solution: Renderer only pushes on `[` if the position / ori has changed + - This makes more sense thematically, as cut wounds will leave scars + - Lemma's Garden: this will slightly slow down growth + - If models were to be processed by renderer, then they cannot evolve + - On the other hand, they can 'evolve' in another sense where each parameter + value can have a different model + - [x] Having a model also means that symbol should be ignored (no extra F) + - Easy, just add the model map's keys to the set + - [x] Update the manual + - Lemma's Garden: Complex models such as flowers will bog down the growth cost diff --git a/renderer.js b/classic.js similarity index 65% rename from renderer.js rename to classic.js index d739dfa..658441c 100644 --- a/renderer.js +++ b/classic.js @@ -1,20 +1,14 @@ /* L-systems Renderer implementation in Exponential Idle. -Disclaimer: The literature around (OL) L-system's symbols is generally not much -consistent. Therefore, the symbols used here may mean different things in other -implementations. One such example is that \ and / may be swapped; or that + -would turn the cursor clockwise instead of counter (as implemented here). -Another example would be that < and > are used instead of \ and /. - -The maths used in this theory do not resemble a paragon of correctness either, -particularly referencing the horrible butchery of quaternions, and all the -camera rotation slander in the world. In this theory, the vector is initially -heading in the X-axis, instead of the Y/Z-axis, which is more common in other -implementations of L-systems. I'm just a unit circle kind of person. - -If the X is the eyes of a laughing face, then the Y represents my waifu Ms. Y, -and the Z stands for Zombies. +Disclaimer: Differences between LSR and other L-system implementations all +around the web. +- / and \ may be swapped. +- + turns anti-clockwise, - turns clockwise. +- \ and / are used instead of < and >. +- Y-up is used here instead of Z-up. +- Quaternions are used instead of actually intuitive concepts such as matrices. +- Quaternions maths are absolutely butchered. (c) 2022 Temple of Pan (R) (TM) All rights reversed. */ @@ -33,6 +27,12 @@ import { Thickness } from '../api/ui/properties/Thickness'; import { TouchType } from '../api/ui/properties/TouchType'; import { Localization } from '../api/Localization'; import { MathExpression } from '../api/MathExpression'; +import { ClearButtonVisibility } from '../api/ui/properties/ClearButtonVisibility'; +import { LineBreakMode } from '../api/ui/properties/LineBreakMode'; +import { BigNumber } from '../api/BigNumber'; +import { Upgrade } from '../api/Upgrades'; +import { Button } from '../api/ui/Button'; +import { Frame } from '../api/ui/Frame'; var id = 'L_systems_renderer'; var getName = (language) => @@ -42,17 +42,14 @@ var getName = (language) => en: 'L-systems Renderer', }; - if(language in names) - return names[language]; - - return names.en; + return names[language] || names.en; } var getDescription = (language) => { let descs = { en: -`An educational tool that allows you to model plants and other fractal figures. +`An educational tool that allows you to model fractals figures and plants. Supported L-system features: - Stochastic (randomised) rules @@ -68,15 +65,12 @@ Warning: v0.20 might break your internal state. Be sure to back it up, and ` + `in case it's corrupted, please contact me.`, }; - if(language in descs) - return descs[language]; - - return descs.en; + return descs[language] || descs.en; } -var authors = 'propfeds#5988\n\nThanks to:\nSir Gilles-Philippe Paillé, ' + - 'for providing help with quaternions\nskyhigh173#3120, for ' + +var authors = 'propfeds\n\nThanks to:\nSir Gilles-Philippe Paillé, for ' + + 'providing help with quaternions\nskyhigh173#3120, for ' + 'suggesting clipboard and JSON internal state formatting'; -var version = 0.211; +var version = 1; let time = 0; let page = 0; @@ -86,24 +80,77 @@ let altTerEq = false; let tickDelayMode = false; let resetLvlOnConstruct = true; let measurePerformance = false; +let debugCamPath = false; let maxCharsPerTick = 5000; +let menuLang = Localization.language; let savedSystems = new Map(); -let savedModels = new Map(); -const DEFAULT_BUTTON_HEIGHT = ui.screenHeight * 0.055; +let getImageSize = (width) => +{ + if(width >= 1080) + return 48; + if(width >= 720) + return 36; + if(width >= 360) + return 24; + + return 20; +} + +let getBtnSize = (width) => +{ + if(width >= 1080) + return 96; + if(width >= 720) + return 72; + if(width >= 360) + return 48; + + return 40; +} + +let getMediumBtnSize = (width) => +{ + if(width >= 1080) + return 88; + if(width >= 720) + return 66; + if(width >= 360) + return 44; + + return 36; +} + +let getSmallBtnSize = (width) => +{ + if(width >= 1080) + return 80; + if(width >= 720) + return 60; + if(width >= 360) + return 40; + + return 32; +} + +const BUTTON_HEIGHT = getBtnSize(ui.screenWidth); +const SMALL_BUTTON_HEIGHT = getSmallBtnSize(ui.screenWidth); const ENTRY_CHAR_LIMIT = 5000; +const TRIM_SP = /\s+/g; +const BACKTRACK_LIST = new Set('+-&^\\/|[$T'); const locStrings = { en: { - versionName: 'v0.21.1', + versionName: 'v1.0', welcomeSystemName: 'Arrow', - welcomeSystemDesc: 'Welcome to L-systems Renderer!', + welcomeSystemDesc: 'Welcome to the L-systems Renderer!', equationOverlayLong: '{0} – {1}\n\n{2}\n\n{3}', equationOverlay: '{0}\n\n{1}', - rendererLoading: '\\begin{{matrix}}Loading...&\\text{{Lv. {0}}}&({1}\\text{{ chars}})\\end{{matrix}}', + rendererLoading: `\\begin{{matrix}}Loading...&\\text{{Lv. {0}}}&({1} +\\text{{ chars}})\\end{{matrix}}`, currencyTime: ' (elapsed)', @@ -113,15 +160,18 @@ const locStrings = varTsDesc: '\\text{{Tickspeed: }}{0}/\\text{{sec}}', upgResumeInfo: 'Resumes the last rendered system', - saPatienceTitle: 'Watching Grass Grow', + saPatienceTitle: 'You\'re watching grass grow.', saPatienceDesc: 'Let the renderer draw a 10-minute long figure or ' + 'playlist.', saPatienceHint: 'Be patient.', btnSave: 'Save', - btnClear: 'Clear', + btnClear: 'Clear All', btnDefault: '* Reset to Defaults', btnAdd: 'Add', + btnUp: '▲', + btnDown: '▼', + btnReroll: 'Reroll', btnConstruct: 'Construct', btnDelete: 'Delete', btnView: 'View', @@ -149,9 +199,10 @@ const locStrings = measurement: '{0}: max {1}ms, avg {2}ms over {3} ticks', rerollSeed: 'You are about to reroll the system\'s seed.', + resetRenderer: 'You are about to reset the renderer.', - menuSequence: 'Sequence Menu', - labelLevelSeq: 'Level {0}: ', + menuSequence: '{0} (Level {1})', + labelLevelSeq: 'Level {0}: {1} chars', labelChars: '({0} chars)', menuLSystem: 'L-system Menu', @@ -159,7 +210,8 @@ const locStrings = labelAngle: 'Turning angle (°): ', labelRules: 'Production rules: {0}', labelIgnored: 'Ignored symbols: ', - labelSeed: 'Seed (for stochastic systems): ', + labelTropism: 'Tropism (gravity): ', + labelSeed: 'Seed (≠ 0): ', menuRenderer: 'Renderer Menu', labelInitScale: '* Initial scale: ', @@ -171,12 +223,15 @@ const locStrings = labelFollowFactor: 'Follow factor (0-1): ', labelLoopMode: 'Looping mode: {0}', loopModes: ['Off', 'Level', 'Playlist'], - labelUpright: '* Upright x-axis: ', + labelUpright: '* Upright figure: ', labelBTTail: 'Draw tail end: ', - labelLoadModels: '* (Teaser) Load models: ', - labelQuickdraw: '* Quickdraw (unstable): ', + labelLoadModels: '* Load models: ', + labelQuickdraw: '* Quickdraw: ', labelQuickBT: '* Quick backtrack: ', labelHesitate: '* Stutter on backtrack: ', + labelHesitateApex: '* Stutter at apex: ', + labelHesitateFork: '* Stutter at fork: ', + labelOldTropism: '* Alternate tropism method: ', labelBTList: '* Backtrack list: ', labelRequireReset: '* Modifying this setting will require a reset.', @@ -186,7 +241,9 @@ const locStrings = labelApplyCamera: 'Applies static camera: ', menuClipboard: 'Clipboard Menu', - labelEntryCharLimit: 'Warning: This entry has been capped at {0} characters. Proceed with caution.', + labelTotalLength: 'Total length: {0}', + labelEntryCharLimit: `Warning: This entry has been capped at {0} ` + + `characters. Proceed with caution.`, menuNaming: 'Save System', labelName: 'Title: ', @@ -201,44 +258,46 @@ const locStrings = labelTerEq: 'Tertiary equation: {0}', terEqModes: ['Coordinates', 'Orientation'], labelMeasure: 'Measure performance: ', + debugCamPath: 'Debug camera path: ', labelMaxCharsPerTick: 'Maximum loaded chars/tick: ', labelInternalState: 'Internal state: ', menuManual: 'User Guide ({0}/{1})', menuTOC: 'Table of Contents', labelSource: 'Source: ', - manualSystemDesc: 'User guide, page {0}.', + manualSystemDesc: 'From the user guide, page {0}.', manual: [ { title: 'Introduction', contents: -`Welcome to the L-systems Renderer! This guide aims to help you understand ` + -`the basics of L-systems, as well as instructions on how to effectively use ` + -`this theory to construct and render them. +`Welcome to the Classic L-systems Renderer! This guide aims to help you ` + +`understand the basics of L-systems, as well as instructions on how to ` + +`effectively use this theory to construct and render them. + +Let's start discovering the wonders of L-systems (and the renderer). -Let's start discovering the wonders of L-systems.` +Notice: A gallery for L-systems has opened! Visit page 28 for details.` }, { title: 'Controls: Theory screen', contents: `The theory screen consists of the renderer and its controls. -Level: the system's iteration. Pressing + or - will grow/revert the system ` + -`respectively. +Level: the iteration/generation/stage of the system. Pressing + or - will ` + +`derive/revert the system. - Pressing the Level button will reveal all levels of the system. - Holding + or - will buy/refund levels in bulks of 10. Tickspeed: controls the renderer's drawing speed (up to 10 lines/sec, which ` + `produces less accurate lines). -- Pressing the Tickspeed button will toggle between the Tickspeed and Tick ` + +- Pressing the Tickspeed button will toggle between Tickspeed and Tick ` + `length modes. -- Holding - will create an 'anchor' on the current level then set it to 0, ` + -`pausing the renderer. Holding + afterwards will return the renderer to the ` + -`previously anchored speed. +- Holding - will create an 'anchor' on the current level before setting it ` + +`to 0, pausing the renderer. Holding + afterwards will return the renderer ` + +`to the previously anchored speed. -Reroll: located on the top right. Pressing this button will reroll the ` + -`system's seed (only applicable for stochastic systems). +Reset: located on the top right. Pressing this button will reset the renderer. Menu buttons: You pressed on one of them to get here, did you? - L-system menu: allows you to edit the currently displayed system. @@ -252,11 +311,14 @@ Menu buttons: You pressed on one of them to get here, did you? `Design your L-system using the L-systems menu. - Axiom: the system's starting sequence. -- Turning angle: the angle the turtle turns when the turtle turns (in degrees). +- Turning angle: the angle the turtle turns when turns the turtle (in degrees). - Production rules: an unlimited number of rules can be added using the ` + `'Add' button. - Ignored symbols: the turtle will stand still when encountering these symbols. -- Seed: sets the seed of a stochastic system. +- Tropism (gravity): determines the amount of gravity applied by the tropism ` + +`(T) command. +- Seed: determines the seed for a stochastic system. Can be manually set or ` + +`rerolled. Note: Any blank rules will be trimmed afterwards.` }, @@ -272,10 +334,10 @@ Camera options: - Camera mode: toggles between Fixed, Linear and Quadratic. The latter two ` + `modes follow the turtle. - Fixed camera centre: determines camera position in Fixed mode using a ` + -`formula, similar to figure scale. +`written formula similar to figure scale. - Follow factor: changes how quickly the camera follows the turtle. -- Upright x-axis: rotates figure by 90 degrees counter-clockwise around the ` + -`z-axis. +- Upright figure: rotates the figure by 90 degrees counter-clockwise around ` + +`the z-axis so that it heads upwards. Renderer logic: - Looping mode: the Level mode repeats a single level, while the Playlist ` + @@ -284,11 +346,10 @@ Renderer logic: `sequence. Advanced stroke options: -- Quickdraw (unstable): skips over straight consecutive segments. +- Quickdraw: skips over straight consecutive segments. - Quick backtrack: works similarly, but on the way back. -- Stutter on backtrack: pause for one tick after backtracking for more ` + -`accurate figures. -- Backtrack list: sets stopping symbols for backtracking.` +- Stutter at apex: pause for one tick at the tips of lines. +- Stutter at fork: pause for one tick after backtracking through branches.` }, { title: 'Saving and loading', @@ -356,15 +417,15 @@ Level 5: abaababa` `comprehend this crap though. Here are the basic symbols and their respective instructions: -F: moves turtle forward to draw a line of length 1 (usually). +F: moves turtle forward to draw a line of length 1. +: rotates turtle counter-clockwise by an angle. -: rotates turtle clockwise by an angle. Note: In the original grammar, the lower-case f is used to move the turtle ` + `forward without drawing anything, but that is simply impossible with this ` + `game's 3D graph. So in this theory, any non-reserved symbol will draw a ` + -`line. This includes both upper- and lower-case letters, and potentially ` + -`anything you can throw at it.` +`line. This includes both upper- and lower-case letters (except T), and ` + +`potentially anything you can throw at it.` }, { title: 'Example: The dragon curve', @@ -408,7 +469,7 @@ Scale: 2^lv Centre: (0.5*2^lv, sqrt(3)/4*2^lv, 0)` }, { - title: 'Stacking mechanism', + title: 'Branching mechanisms', contents: `Although numerous fractals can be created using only the basic symbols, ` + `when it comes to modelling branching structures such as trees, the turtle ` + @@ -420,8 +481,8 @@ Stack operations are represented with square brackets: [: records the turtle's position and facing onto a stack. ]: take the topmost element (position and facing) off the stack, and move ` + `the turtle there. -%: cuts off the remainder of its branch by searching for the closing bracket ` + -`] in the branch. +%: cuts off the remainder of a branch, by deleting every symbol up until ` + +`the closing bracket ] in the branch. Note: Due to the game's 3D graph only allowing one continuous path to be ` + `drawn, the turtle will not actually divide itself, but instead backtrack ` + @@ -444,7 +505,7 @@ Turning angle: 30° Applies static camera: Scale: 1.5*2^lv -Centre: (1.2*2^lv, 0, 0) +Centre: (0, 1.2*2^lv, 0) Upright` }, { @@ -465,7 +526,9 @@ When the system is grown, one of the possible derivations will be randomly ` + A system's seed can either be changed manually within the L-systems menu, or ` + `randomly reassigned using the 'Reroll' button on the top right corner of ` + -`the theory screen.` +`the theory screen. + +Note: setting the seed to 0 will disable the random generation.` }, { title: 'Example: Stochastic tree', @@ -479,7 +542,7 @@ Turning angle: 22.5° Applies static camera: Scale: 1.5*2^lv -Centre: (1.2*2^lv, 0, 0) +Centre: (0, 1.2*2^lv, 0) Upright` }, { @@ -503,17 +566,29 @@ Centre: (0, 0, 0)` contents: `Using a yaw-pitch-roll orientation system, we can also generate figures in 3D. +Counter-clockwise and clockwise respectively, + -: rotate turtle on the z-axis (yaw). & ^: rotate turtle on the y-axis (pitch). \\ /: rotate turtle on the x-axis (roll). -|: reverses turtle direction. + +|: reverses the turtle's direction. +T: applies a force of gravity (tropism) to the turtle's current heading, so ` + +`that it drops downward (with a positive tropism factor), or lifts upward ` + +`(with a negative tropism factor). The factor should be in the range from ` + +`-1 to 1. +$: rolls the turtle around its own axis, so that its up vector is closest to ` + +`absolute verticality i.e. the y-axis, and subsequently, its direction is ` + +`closest to lying on a horizontal plane. Note: In other L-system implementations, < and > may be used instead of \\ ` + `and / like in this theory. Note 2: Other L-system implementations may also start the turtle facing the ` + `y-axis or z-axis instead of the x-axis. To adopt those systems into LSR, ` + -`swap the axes around until the desired results are achieved.` +`swap the axes around until the desired results are achieved. + +Note 3: Other L-system implementations may swap counter-clockwise and ` + +`clockwise rotations.` }, { title: 'Example: Blackboard tree', @@ -532,7 +607,7 @@ Turning angle: 8° Applies static camera: Scale: 2*2^lv -Centre: (1.2*2^lv, 0, 0) +Centre: (0, 1.2*2^lv, 0) Upright`, source: 'https://www.bioquest.org/products/files/13157_Real-time%203D%20Plant%20Structure%20Modeling%20by%20L-System.pdf' }, @@ -565,7 +640,7 @@ Turning angle: 4° Applies static camera: (mathematically unproven) Scale: 3*1.3^lv -Centre: (1.8*1.3^lv, 0, 0) +Centre: (0, 1.8*1.3^lv, 0) Upright`, source: 'http://jobtalle.com/lindenmayer_systems.html' }, @@ -583,8 +658,9 @@ Upright`, Normal commands inside a polygon block will not draw lines, making it great ` + `for hiding away any scaffolding in the creation of models. -Note: Due to how the rendering engine works, the polygon tool in LSR works ` + -`a bit differently from that described in The Algorithmic Beauty of Plants. ` + + +Note: Due to how Exponential Idle's 3D graph works, the polygon tool in LSR ` + +`works differently from that described in The Algorithmic Beauty of Plants. ` + `Therefore, it is advised to make some adjustments when adopting schemes ` + `from the book into LSR.` }, @@ -602,7 +678,7 @@ Turning angle: 27° Applies static camera: Scale: lv -Centre: (lv/2-1, 0, 0) +Centre: (0, lv/2-1, 0) Upright` }, { @@ -613,32 +689,60 @@ Upright` `remains, as writing the model in a different rule will delay its drawing by ` + `one level. With a special kind of rule, we can assign dedicated models to ` + `each symbol to be drawn instantly. -To declare a model rule, attach a tilde in front of the symbol on the left side: +To declare a model rule, attach a ~ (tilde) in front of the symbol: ~{symbol} = {model} -To reference a model in another rule, attach a tilde in front of the symbol ` + -`in the same way it was declared. -Note: The symbol will not disappear from the rule after the model has been ` + -`drawn.` +To reference a model in another rule, attach a tilde in the same way it was ` + +`declared. The model will be represented as a temporary sequence that cannot ` + +`evolve, replacing the default action of drawing a straight line. +The tilde, and subsequently its model, will disappear in the following level. + +Note: This is unlike the incorporated surfaces described in the Algorithmic ` + +`Beauty of Plants, where the tilde does not need to be refreshed.` }, { title: 'Example: Lilac branch', contents: -`Ripped straight off of page 92 of The Algorithmic Beauty of Plants. But I ` + -`made the model myself. +`Ripped straight off of page 92 of The Algorithmic Beauty of Plants. +K represents the flower, and its model has to be refreshed every level with ` + +`the rule K = ~K. Axiom: A~K A = [--//~K][++//~K]I///A I = Fi i = Fj j = J[--FFA][++FFA] +K = ~K ~K = F[+++[--F+F]^^^[--F+F]^^^[--F+F]^^^[--F+F]] Turning angle: 30° Applies static camera: Scale: 3*lv -Centre: (1.5*lv, 0, 0) +Centre: (0, 1.5*lv, 0) Upright` + }, + { + title: 'Appendix: Summary of symbols', + contents: +`Any letter (except T): moves turtle forward to draw a line of length 1. ++ -: rotate turtle on the z-axis (yaw). +& ^: rotate turtle on the y-axis (pitch). +\\ /: rotate turtle on the x-axis (roll). + +|: reverses turtle direction. +T: applies tropism (gravity) to branch. +$: aligns turtle's up vector to vertical. + +[: pushes turtle state onto a stack. +]: pops the stack's topmost element onto the turtle. +%: cuts off the remainder of a branch. + +{: initiates polygon drawing mode. +.: sets a polygon vertex. +}: ends the polygon drawing mode. + +~: declares/references a symbol's model. +,: separates between stochastic derivations.` }, { title: 'Appendix: Advanced artistry in LSR', @@ -646,6 +750,11 @@ Upright` `Welcome to the LSR Art Academy. Thanks for finishing the manual, by the way! And today's class: Tick length. +For a background observation, Exponential Idle's 3D graph seems to be using ` + +`a Bézier-like spline with no locality. Therefore, it is not suitable for ` + +`drawing straight lines. However, this does not mean we cannot get anything ` + +`out of it, and today's class will demonstrate otherwise. + Now, while tickspeed might be more of a familiar concept to the idle ` + `fellows, in LSR it posesses a flaw: it is not consistent. For instance, at ` + `9 tickspeed, the renderer would skip one tick out of every 10, making the ` + @@ -661,21 +770,15 @@ Now, while tickspeed might be more of a familiar concept to the idle ` + `of leaves and flowers, this speed feels at home with plant modelling. It ` + `offers a good compromise between speed and precision, although even 0.1 ` + `would be too slow for large scale figures. -- 0.3 sec: with loose slanted lines, tick length 0.3 is generally is a solid ` + -`option for any figure requiring some playfulness. However, it is fairly ` + -`unknown that tick length 0.3 holds the most powerful secret in this whole ` + -`universe: it can truly create the straightest lines out of this family. As ` + -`always, some tricks are needed here: - + First, create an anchor at this speed by holding -. - + Switch back and forth between levels to reset the turtle. - + Activate the anchor by holding +, and marvel at the beauty of it all. -Note: This trick is not guaranteed to work every time, so it is advised to ` + -`try again multiple times. +- 0.3 sec: Tick length 0.3 holds the most powerful secret in this whole ` + +`universe: it can create the straightest lines out of this family. No ` + +`trickery needed! As the 3D graph seems to be running on a 3-tick cycle, ` + +`the sampled points line up precisely with the renderer's drawing. - 0.4 sec: this one can really spice the figure up by tying up cute knots ` + `between corners occasionally, mimicking leaf shapes on a tree. - 0.5 sec: with slight occasional overshoots, tick length 0.5 proves itself ` + `of use when it comes to bringing that rough sketch feeling to a figure. -- 0.6 sec and above: don't care, class dismissed.` +- 0.6 sec and above: I don't care, class dismissed.` }, { title: 'Advanced artistry in LSR (2)', @@ -691,14 +794,11 @@ Now, open your renderer menu textbook to the last section. There are about 4 ` + `comes to both precision and aesthetics. - Quick backtrack: this one's a reliable one, trading only a little beauty ` + `for some extra speed. -- Stutter on backtrack: now, this is what I mean when I say hesitation is ` + +- Stutter at apex/fork: now, this is what I mean when I say hesitation is ` + `not defeat. Pausing for even just one tick can give your figure just enough ` + `cohesion it really needs. To prove this, try loading the Arrow weed then ` + -`alternate between drawing with this option on and off, while on tick length ` + -`0.1, or 10 tickspeed. There will be a noticeable difference, even from afar. -- Backtrack list: usually, I would say that if you are here to draw ` + -`L-systems, I recommend not to edit this option, but for the brave and ` + -`worthy, you could create truly mesmerising results with this. +`alternate between drawing with these option on and off, at 0.1 tick length, ` + +`or 10 tickspeed. There will be a noticeable difference, even from afar. Class dismissed, and stay tuned for next week's lecture, on the Art of Looping!` }, @@ -733,9 +833,37 @@ Generally, in figures such as this or the Koch snowflake, it'd be better to ` + `due to the tail end being a backtrack itself, of course.` }, { - title: 'Appendix: Botched L-systems', + title: 'Gallery', + contents: +`Welcome to a L-systems gallery. Enjoy! + +Notice: The gallery is open for submission! +Mail me your own L-systems, so it can be included in the gallery. +Maybe over Discord. Reddit account. Arcane-mail logistics!` + }, + { + title: 'Lilac branch (Advanced)', contents: -`Here are the systems created for another theory of mine, Botched L-system.` +`A more complex version of the previous lilac branch in the Models section, ` + +`complete with detailed models and copious utilisation of tropism. + +Axiom: +S~A +S = FS +A = T[--//~K][++//~K]I///~A +~A = [+++~a~a~a~a] +~a = -{[^-F.][--FF.][&-F.].}+^^^ +K = ~K +~K = [FT[F]+++~k~k~k~k] +~k = -{[^--F.][F-^-F.][^--F|++^--F|+F+F.][-F+F.][&--F|++&--F|+F+F.][F-&-F.][&--F.].}+^^^ +I = Fi +i = Fj +j = J[--FF~A][++FF~A] +Turning angle: 30° +Tropism: 0.16 + +Applies static camera: +Scale: 2*lv+1 +Centre: (2*lv+1, lv/2+3/4, 0)` }, { title: 'Botched Cultivar FF', @@ -749,7 +877,7 @@ Turning angle: 15° Applies static camera: Scale: 2^lv -Centre: (2^lv, 0, 0) +Centre: (0, 2^lv, 0) Upright` }, { @@ -783,25 +911,18 @@ Turning angle: 22.5° Applies static camera: (mathematically unproven) Scale: 3^lv -Centre: (0.75*3^lv, -0.25*3^lv, 0) +Centre: (0.25*3^lv, 0.75*3^lv, 0) Upright` - }, - { - title: 'Appendix: LG', - contents: -`Here's to LG.` } ] } }; -let menuLang = Localization.language; /** * Returns a localised string. - * @param {string} name the internal name of the string. - * @returns {string} the string. + * @param {string} name the string's internal name. + * @returns {string} */ - let getLoc = (name, lang = menuLang) => { if(lang in locStrings && name in locStrings[lang]) @@ -810,12 +931,13 @@ let getLoc = (name, lang = menuLang) => if(name in locStrings.en) return locStrings.en[name]; - return `String not found: ${lang}.${name}`; + return `String missing: ${lang}.${name}`; } /** * Returns a string of a fixed decimal number, with a fairly uniform width. - * @returns {string} the string. + * @param {number} x the number. + * @returns {string} */ let getCoordString = (x) => x.toFixed(x >= -0.01 ? (x <= 9.999 ? 3 : (x <= 99.99 ? 2 : 1)) : @@ -823,10 +945,10 @@ let getCoordString = (x) => x.toFixed(x >= -0.01 ? ); /** - * Compares for every member of two sets. + * Compares equality for every member of two sets, disregarding order. * @param {Set} xs set 1. * @param {Set} ys set 2. - * @returns {boolean} whether two sets are the exact same (disregarding order). + * @returns {boolean} */ let eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x)); @@ -843,29 +965,25 @@ class LCG { /** * @type {number} the mod of this realm. - * @public but not really. */ this.m = 0x80000000; // 2**31; /** * @type {number} some constant - * @public but shouldn't be. */ this.a = 1103515245; /** * @type {number} some other constant. - * @public please leave me pretty be. */ this.c = 12345; /** * @type {number} the LCG's current state. - * @public honestly. */ this.state = seed % this.m; } /** * Returns a random integer within [0, 2^31). - * @returns {number} the next integer in the generator. + * @returns {number} */ get nextInt() { @@ -876,7 +994,7 @@ class LCG * Returns a random floating point number within [0, 1] or [0, 1). * @param {boolean} [includeEnd] (default: false) whether to include the * number 1 in the range. - * @returns {number} the floating point, corresponding to the next integer. + * @returns {number} */ nextFloat(includeEnd = false) { @@ -895,7 +1013,7 @@ class LCG * Returns a random integer within a range of [start, end). * @param {number} start the range's lower bound. * @param {number} end the range's upper bound, plus 1. - * @returns {number} the integer. + * @returns {number} */ nextRange(start, end) { @@ -906,7 +1024,85 @@ class LCG /** * Returns a random element from an array. * @param {any[]} array the array. - * @returns the element. + * @returns {any} + */ + choice(array) + { + return array[this.nextRange(0, array.length)]; + } +} + +/** + * Represents an instance of the Xorshift RNG. + */ +class Xorshift +{ + /** + * @constructor + * @param {number} seed must be initialized to non-zero. + */ + constructor(seed = 0) + { + this.x = seed; + this.y = 0; + this.z = 0; + this.w = 0; + for(let i = 0; i < 64; ++i) + this.nextInt; + } + /** + * Returns a random integer within [0, 2^31) probably. + * @returns {number} + */ + get nextInt() + { + let t = this.x ^ (this.x << 11); + this.x = this.y; + this.y = this.z; + this.z = this.w; + this.w ^= (this.w >> 19) ^ t ^ (t >> 8); + return this.w; + } + /** + * Returns a random floating point number within [0, 1). + * @returns {number} + */ + get nextFloat() + { + return (this.nextInt >>> 0) / ((1 << 30) * 2); + } + /** + * Returns a full random double floating point number using 2 rolls. + * @returns {number} + */ + get nextDouble() + { + let top, bottom, result; + do + { + top = this.nextInt >>> 10; + bottom = this.nextFloat; + result = (top + bottom) / (1 << 21); + } + while(result === 0); + return result; + } + /** + * Returns a random integer within a range of [start, end). + * @param {number} start the range's lower bound. + * @param {number} end the range's upper bound, plus 1. + * @returns {number} + */ + nextRange(start, end) + { + // [start, end) + let size = end - start; + return start + Math.floor(this.nextFloat * size); + } + /** + * Returns a random element from an array. + * @param {any[]} array the array. + * @returns {any} */ choice(array) { @@ -930,22 +1126,18 @@ class Quaternion { /** * @type {number} the real component. - * @public */ this.r = r; /** * @type {number} the imaginary i component. - * @public */ this.i = i; /** * @type {number} the imaginary j component. - * @public */ this.j = j; /** * @type {number} the imaginary k component. - * @public */ this.k = k; } @@ -954,7 +1146,7 @@ class Quaternion * Computes the sum of the current quaternion with another. Does not modify * the original quaternion. * @param {Quaternion} quat this other quaternion. - * @returns {Quaternion} the sum. + * @returns {Quaternion} */ add(quat) { @@ -969,7 +1161,7 @@ class Quaternion * Computes the product of the current quaternion with another. Does not * modify the original quaternion. * @param {Quaternion} quat this other quaternion. - * @returns {Quaternion} the product. + * @returns {Quaternion} */ mul(quat) { @@ -987,24 +1179,168 @@ class Quaternion * Computes the negation of a quaternion. The negation also acts as the * inverse if the quaternion's norm is 1, which is the case with rotation * quaternions. - * @returns {Quaternion} the negation. + * @returns {Quaternion} */ get neg() { return new Quaternion(this.r, -this.i, -this.j, -this.k); } /** - * Returns a rotation vector from the quaternion. - * @returns {Vector3} the rotation vector. + * Computes the norm of a quaternion. + * @returns {number} + */ + get norm() + { + return Math.sqrt(this.r ** 2 + this.i ** 2 + this.j ** 2 + this.k ** 2); + } + /** + * Normalises a quaternion. + * @returns {Quaternion} + */ + get normalise() + { + let n = this.norm; + return new Quaternion(this.r / n, this.i / n, this.j / n, this.k / n); + } + /** + * Returns a heading vector from the quaternion. + * @returns {Vector3} + */ + get headingVector() + { + let r = this.neg.mul(xUpQuat).mul(this); + return new Vector3(r.i, r.j, r.k); + } + /** + * Returns an up vector from the quaternion. + * @returns {Vector3} + */ + get upVector() + { + let r = this.neg.mul(yUpQuat).mul(this); + return new Vector3(r.i, r.j, r.k); + } + /** + * Returns a side vector (left or right?) from the quaternion. + * @returns {Vector3} */ - get rotVector() + get sideVector() { - let r = this.neg.mul(XAxisQuat).mul(this); + let r = this.neg.mul(zUpQuat).mul(this); return new Vector3(r.i, r.j, r.k); } + /** + * (Deprecated) Rotate from a heading vector to another. Inaccurate! + * @param {Vector3} src the current heading. + * @param {Vector3} dst the target heading. + * @returns {Quaternion} + */ + rotateFrom(src, dst) + { + let dp = src.x * dst.x + src.y * dst.y + + src.z * dst.z; + let rotAxis; + if(dp < -1 + 1e-8) + { + /* Edge case + If the two vectors are in opposite directions, just reverse. + */ + return zUpQuat.mul(this); + } + rotAxis = new Vector3( + src.y * dst.z - src.z * dst.y, + src.z * dst.x - src.x * dst.z, + src.x * dst.y - src.y * dst.x, + ); + let s = Math.sqrt((1 + dp) * 2); + // I forgore that our quaternions have to be all negative, dunnoe why + return this.mul(new Quaternion( + -s / 2, + rotAxis.x / s, + rotAxis.y / s, + rotAxis.z / s + )).normalise; + } + /** + * https://stackoverflow.com/questions/71518531/how-do-i-convert-a-direction-vector-to-a-quaternion + * (Deprecated) Applies a gravi-tropism vector to the quaternion. Inaccurat! + * @param {number} weight the vector's length (negative for upwards). + * @returns {Quaternion} + */ + applyTropismVector(weight = 0) + { + if(weight == 0) + return this; + + let curHead = this.headingVector; + let newHead = curHead - new Vector3(0, weight, 0); + let n = newHead.length; + if(n == 0) + return this; + newHead /= n; + let result = this.rotateFrom(curHead, newHead); + return result; + } + /** + * Applies a gravi-tropism vector to the quaternion. + * @param {number} weight the branch's susceptibility to bending. + * @returns {Quaternion} + */ + applyTropism(weight = 0) + { + if(weight == 0) + return this; + + // a = e * |HxT| (n) + let curHead = this.headingVector; + let rotAxis = new Vector3(curHead.z, 0, -curHead.x); + let n = rotAxis.length; + if(n == 0) + return this; + rotAxis /= n; + let a = weight * n / 2; + let s = Math.sin(a); + let c = Math.cos(a); + // I don't know why it works the opposite way this time + return this.mul(new Quaternion( + -c, + rotAxis.x * s, + rotAxis.y * s, + rotAxis.z * s + )).normalise; + } + /** + * https://gamedev.stackexchange.com/questions/198977/how-to-solve-for-the-angle-of-a-axis-angle-rotation-that-gets-me-closest-to-a-sp/199027#199027 + * Rolls the quaternion so that its up vector aligns with the earth. + * @returns {Quaternion} + */ + alignToVertical() + { + // L = V×H / |V×H| + let curHead = this.headingVector; + let curUp = this.upVector; + let side = new Vector3(curHead.z, 0, -curHead.x); + let n = side.length; + if(n == 0) + return this; + side /= n; + // U = HxL + let newUp = new Vector3( + curHead.y * side.z - curHead.z * side.y, + curHead.z * side.x - curHead.x * side.z, + curHead.x * side.y - curHead.y * side.x, + ); + let a = Math.atan2( + curUp.x * side.x + curUp.y * side.y + curUp.z * side.z, + curUp.x * newUp.x + curUp.y * newUp.y + newUp.z * newUp.z, + ) / 2; + let s = Math.sin(a); + let c = Math.cos(a); + return new Quaternion(-c, s, 0, 0).mul(this).normalise; + } /** * Returns the quaternion's string representation. - * @returns {string} the string. + * @returns {string} */ toString() { @@ -1021,41 +1357,60 @@ class LSystem * @constructor * @param {string} axiom the starting sequence. * @param {string[]} rules the production rules. - * @param {number} turnAngle (default: 30) the turning angle (in degrees). - * @param {number} seed (default: 0) the seed (for stochastic systems). + * @param {string} turnAngle the turning angle (in degrees). + * @param {number} seed the seed used for stochastic systems. + * @param {string} ignoreList a list of symbols to be ignored by the turtle. + * @param {string} tropism the tropism factor. */ constructor(axiom = '', rules = [], turnAngle = 0, seed = 0, - ignoreList = '', models = {}) + ignoreList = '', tropism = 0) { + /** + * @type {{ + * axiom: string, + * rules: string[], + * turnAngle: string, + * seed: number, + * ignoreList: string, + * tropism: string + * }} the user input in its original form. + */ this.userInput = { axiom: axiom, - rules: this.getPurged(rules), + rules: this.purgeEmpty(rules), turnAngle: turnAngle, seed: seed, ignoreList: ignoreList, - models: models + tropism: tropism }; /** - * @type {string[]} the production rules. - * @public + * @type {string} the starting sequence. + */ + this.axiom = axiom; + /** + * @type {Map} the production rules. */ this.rules = new Map(); - this.contextRules = new Map(); /** - * @type {string} a list of symbols ignored by the renderer. - * @public + * @type {set} a set of symbols ignored by the turtle. */ this.ignoreList = new Set(ignoreList); + /** + * @type {Map} the models to be used by the renderer. + */ this.models = new Map(); - for(let key in models) - this.models.set(key, models[key]); + // Rules processing. for(let i = 0; i < rules.length; ++i) { if(rules[i] !== '') { - let rs = rules[i].split('='); + let rs = rules[i].replace(TRIM_SP, '').split('='); + /* + Old rules format where rules without a derivation get added to + the ignore list, due to the old internal state's limitations. + */ if(rs.length < 2) { if(i == 0) @@ -1066,129 +1421,98 @@ class LSystem ]); continue; } - for(let i = 0; i < 2; ++i) - rs[i] = rs[i].trim(); let rder = rs[1].split(','); - if(rder.length == 1) + if(rder.length == 1) // Regular rule { if(rs[0].length == 1) - this.rules.set(rs[0], rs[1]); - else if(rs[0].length == 2 && rs[0][0] == '~') - this.models.set(rs[0][1], rs[1]); + { + let existingDer = this.rules.get(rs[0]); + if(!existingDer) + this.rules.set(rs[0], rs[1]); + else if(typeof existingDer === 'string') + this.rules.set(rs[0], [existingDer, rs[1]]); + else + this.rules.set(rs[0], [...existingDer, rs[1]]); + } + else if(rs[0].length == 2 && rs[0][0] == '~') // Model + { + let existingDer = this.models.get(rs[0][1]); + if(!existingDer) + this.models.set(rs[0][1], rs[1]); + else if(typeof existingDer === 'string') + this.models.set(rs[0][1], [existingDer, rs[1]]); + else + this.models.set(rs[0][1], [...existingDer, rs[1]]); + } } - else + else // Stochastic rule { - // Models can't have stochastic rules sadly, due to how - // derivations work. - for(let i = 0; i < rder.length; ++i) - rder[i] = rder[i].trim(); if(rs[0].length == 1) - this.rules.set(rs[0], rder); - else if(rs[0].length == 2 && rs[0][0] == '~') - this.models.set(rs[0][1], rder); + { + let existingDer = this.rules.get(rs[0]); + if(!existingDer) + this.rules.set(rs[0], rder); + else if(typeof existingDer === 'string') + this.rules.set(rs[0], [existingDer, rder]); + else + this.rules.set(rs[0], [...existingDer, rder]); + } + else if(rs[0].length == 2 && rs[0][0] == '~') // Model + { + let existingDer = this.models.get(rs[0][1]); + if(!existingDer) + this.models.set(rs[0][1], rder); + else if(typeof existingDer === 'string') + this.models.set(rs[0][1], [existingDer, rder]); + else + this.models.set(rs[0][1], [...existingDer, rder]); + } } } } /** - * @type {number} the seed (for stochastic systems). - * @public - */ - this.seed = seed; - /** - * @type {LCG} the LCG used for random number generation. - * @public not sure, ask Itsuki. - */ - this.random = new LCG(this.seed); - /** - * @type {string} the starting sequence. - * @public + * @type {Xorshift} the random number generator for this system. */ - this.axiom = this.getRecursiveModels(axiom).result; + this.RNG = new Xorshift(seed); /** - * @type {number} the turning angle (in degrees). - * @public + * @type {number} half the turning angle (in radians) for use in quats. */ - this.turnAngle = turnAngle; + this.halfAngle = MathExpression.parse(turnAngle.toString()).evaluate(). + toNumber() * Math.PI / 360; /** - * @type {number} half the turning angle (in radians). - * @public + * @type {Map} a map of rotation quaternions for + * quicker calculations. */ - this.halfAngle = this.turnAngle * Math.PI / 360; + this.rotations = new Map(); let s = Math.sin(this.halfAngle); let c = Math.cos(this.halfAngle); + this.rotations.set('+', new Quaternion(-c, 0, 0, s)); + this.rotations.set('-', new Quaternion(-c, 0, 0, -s)); + this.rotations.set('&', new Quaternion(-c, 0, s, 0)); + this.rotations.set('^', new Quaternion(-c, 0, -s, 0)); + this.rotations.set('\\', new Quaternion(-c, s, 0, 0)); + this.rotations.set('/', new Quaternion(-c, -s, 0, 0)); /** - * @type {Map} a map of rotation quaternions for - * quicker calculations. - * @public but shouldn't be. + * @type {number} the tropism factor. */ - this.rotations = new Map(); - this.rotations.set('+', new Quaternion(c, 0, 0, -s)); - this.rotations.set('-', new Quaternion(c, 0, 0, s)); - this.rotations.set('&', new Quaternion(c, 0, -s, 0)); - this.rotations.set('^', new Quaternion(c, 0, s, 0)); - this.rotations.set('\\', new Quaternion(c, -s, 0, 0)); - this.rotations.set('/', new Quaternion(c, s, 0, 0)); + this.tropism = MathExpression.parse(tropism.toString()).evaluate(). + toNumber(); } - rerollAxiom() - { - this.axiom = this.getRecursiveModels(this.userInput.axiom).result; - } - getRecursiveModels(sequence) - { - let result; - let count = 0; - if(typeof sequence === 'string') - { - result = ''; - for(let i = 0; i < sequence.length; ++i) - { - let deriv; - if(sequence[i] == '~' && this.models.has(sequence[i + 1])) - { - let r = this.getRecursiveModels( - this.models.get(sequence[i + 1])); - deriv = r.result; - count += r.count; - } - else - deriv = sequence[i]; - if(typeof deriv === 'string') - result += deriv; - else - result += deriv[this.random.nextRange(0, deriv.length)]; - - count += deriv.length; - } - } - else - { - result = []; - for(let i = 0; i < sequence.length; ++i) - { - let r = this.getRecursiveModels(sequence[i]); - result.push(r.result); - count += r.count; - } - } - return { - count: count, - result: result - }; - } /** - * Derive a sequence from the input string. - * @param {string} state the input string. - * @returns {string} the derivation. + * Derive a sequence from the input string. `next` denotes the starting + * position to be derived next tick. `result` contains the work completed + * for the current tick. + * @param {string} sequence the input string. + * @returns {{next: number, result: string}} */ derive(sequence, start = 0) { let result = ''; - let count = 0; for(let i = start; i < sequence.length; ++i) { - if(result.length + count > maxCharsPerTick) + if(result.length > maxCharsPerTick) { return { next: i, @@ -1218,26 +1542,17 @@ class LSystem else continue; } - else if(sequence[i] == '~' && this.models.has(sequence[i + 1])) - { - let r = this.getRecursiveModels( - this.models.get(sequence[i + 1])); - deriv = r.result; - count += r.count - r.result.length; - } + else if(sequence[i] == '~') + continue; else if(this.rules.has(sequence[i])) - { - let r = this.getRecursiveModels(this.rules.get(sequence[i])); - deriv = r.result; - count += r.count - r.result.length; - } + deriv = this.rules.get(sequence[i]); else deriv = sequence[i]; if(typeof deriv === 'string') result += deriv; else - result += deriv[this.random.nextRange(0, deriv.length)]; + result += deriv[this.RNG.nextRange(0, deriv.length)]; } return { next: 0, @@ -1245,16 +1560,20 @@ class LSystem }; } /** - * Sets the system's seed. + * (Deprecated) Sets the system's seed from the outside in. * @param {number} seed the seed. */ - set rerollSeed(seed) + set seed(seed) { - this.seed = seed; this.userInput.seed = seed; - this.random = new LCG(this.seed); + this.RNG = new Xorshift(this.seed); } - getPurged(rules) + /** + * Purge the rules of empty lines. + * @param {string[]} rules rules. + * @returns {string[]} + */ + purgeEmpty(rules) { let result = []; let idx = 0; @@ -1269,32 +1588,35 @@ class LSystem } return result; } + /** + * Returns a deep copy (hopefully) of the user input to prevent overwrites. + * @returns {{ + * axiom: string, + * rules: string[], + * turnAngle: string, + * seed: number, + * ignoreList: string, + * tropism: string + * }} + */ get object() { return { axiom: this.userInput.axiom, - rules: this.getPurged(this.userInput.rules), + rules: this.purgeEmpty(this.userInput.rules), turnAngle: this.userInput.turnAngle, seed: this.userInput.seed, ignoreList: this.userInput.ignoreList, - models: this.userInput.models + tropism: this.userInput.tropism }; } /** * Returns the system's string representation. - * @returns {string} the string. + * @returns {string} */ toString() { - let result = `${this.axiom} ${this.turnAngle} ${this.seed} ${[...this.ignoreList].join('')}`; - for(let [key, value] of this.rules) - { - if(typeof value === 'string') - result += ` ${key}=${value}`; - else - result += ` ${key}=${value.join(',')}`; - } - return result; + return JSON.stringify(this.object, null, 4); } } @@ -1306,157 +1628,190 @@ class Renderer /** * @constructor * @param {LSystem} system the L-system to be handled. - * @param {string} figureScale (default: 1) the zoom level expression. - * @param {boolean} cameraMode (default: 0) the camera mode. - * @param {number} camX (default: 0) the camera's x-axis centre. - * @param {number} camY (default: 0) the camera's y-axis centre. - * @param {number} camZ (default: 0) the camera's z-axis centre. - * @param {number} followFactor (default: 0.1; between 0 and 1) the - * camera's cursor-following speed. - * @param {number} loopMode (default: 0; between 0 and 2) the renderer's - * looping mode. - * @param {boolean} upright (default: false) whether to rotate the system - * around the z-axis by 90 degrees. - * @param {boolean} quickDraw (default: false) whether to skip through - * straight lines on the way forward. - * @param {boolean} quickBacktrack (default: false) whether to skip through - * straight lines on the way backward. - * @param {string} backtrackList (default: '+-&^\\/|[]') a list of symbols - * to act as stoppers for backtracking. + * @param {string} figureScale the zoom level expression. + * @param {boolean} cameraMode the camera mode. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. + * @param {number} followFactor the camera's cursor-following speed. + * @param {number} loopMode the renderer's looping mode. + * @param {boolean} upright whether to rotate the system around the z-axis + * by 90 degrees. + * @param {boolean} quickDraw whether to skip through straight lines on the + * way forward. + * @param {boolean} quickBacktrack whether to skip through straight lines on + * the way backward. + * @param {boolean} loadModels whether to load dedicated models for symbols. + * @param {boolean} backtrackTail whether to backtrack at the end of a loop. + * @param {boolean} hesitateApex whether to stutter for 1 tick at apices. + * @param {boolean} hesitateFork whether to stutter for 1 tick at forks. */ constructor(system, figureScale = 1, cameraMode = 0, camX = 0, camY = 0, camZ = 0, followFactor = 0.15, loopMode = 0, upright = false, - quickDraw = false, quickBacktrack = false, backtrackList = '+-&^\\/|[]', - loadModels = true, backtrackTail = false, hesitate = true) + quickDraw = false, quickBacktrack = false, loadModels = true, + backtrackTail = false, hesitateApex = true, hesitateFork = true) { /** * @type {LSystem} the L-system being handled. - * @public */ this.system = system; + /** + * @type {string} kept for comparison in the renderer menu. + */ this.figScaleStr = figureScale.toString(); + /** + * @type {MathExpression} the figure scale expression. + */ this.figScaleExpr = MathExpression.parse(this.figScaleStr); + /** + * @type {number} the calculated figure scale. + */ this.figureScale = 1; /** * @type {boolean} the camera mode. - * @public */ this.cameraMode = Math.round(Math.min(Math.max(cameraMode, 0), 2)); + /** + * @type {string} kept for comparison in the renderer menu. + */ this.camXStr = camX.toString(); + /** + * @type {string} kept for comparison in the renderer menu. + */ this.camYStr = camY.toString(); + /** + * @type {string} kept for comparison in the renderer menu. + */ this.camZStr = camZ.toString(); + /** + * @type {MathExpression} the camera x expression. + */ this.camXExpr = MathExpression.parse(this.camXStr); + /** + * @type {MathExpression} the camera y expression. + */ this.camYExpr = MathExpression.parse(this.camYStr); + /** + * @type {MathExpression} the camera z expression. + */ this.camZExpr = MathExpression.parse(this.camZStr); /** - * @type {Vector3} the static camera's coordinates. - * @public + * @type {Vector3} the calculated static camera coordinates. */ this.camCentre = new Vector3(0, 0, 0); /** * @type {number} the follow factor. - * @public */ this.followFactor = Math.min(Math.max(followFactor, 0), 1); /** * @type {number} the looping mode. - * @public */ this.loopMode = Math.round(Math.min(Math.max(loopMode, 0), 2)); /** * @type {boolean} the x-axis' orientation. - * @public */ this.upright = upright; /** * @type {boolean} whether to skip through straight lines on the way * forward. - * @public */ this.quickDraw = quickDraw; /** * @type {boolean} whether to skip through straight lines on the way * back. - * @public */ this.quickBacktrack = quickBacktrack; /** - * @type {string} a list of symbols to act as stoppers for backtracking. - * @public + * @type {boolean} whether to load models. */ - this.backtrackList = new Set(backtrackList); this.loadModels = loadModels; + /** + * @type {boolean} whether to backtrack at the end. + */ this.backtrackTail = backtrackTail; - this.hesitate = hesitate; /** - * @type {Vector3} the cursor's position. - * @public but shouldn't be. + * @type {boolean} whether to hesitate at apices. + */ + this.hesitateApex = hesitateApex; + /** + * @type {boolean} whether to hesitate at forks. + */ + this.hesitateFork = hesitateFork; + /** + * @type {Vector3} the turtle's position. */ this.state = new Vector3(0, 0, 0); /** - * @type {Quaternion} the cursor's orientation. - * @public stay away from me. + * @type {Quaternion} the turtle's orientation. */ - this.ori = new Quaternion(); + this.ori = this.upright ? uprightQuat : new Quaternion(); /** - * @type {string[]} stores the system's every level. - * @public don't touch me. + * @type {string[]} every level of the current system. */ this.levels = []; /** * @type {number} the current level (updates after buying the variable). - * @public don't modify this please. */ this.lv = -1; /** * @type {number} the maximum level loaded. - * @public don't mothify this either. */ this.loaded = -1; /** - * @type {number} the load target. - * @public don't. + * @type {number} the load target level. */ this.loadTarget = 0; /** * @type {[Vector3, Quaternion][]} stores cursor states for brackets. - * @public no. */ this.stack = []; /** * @type {number[]} stores the indices of the other stack. - * @public don't touch this. */ this.idxStack = []; + /** + * @type {string[]} keeps the currently rendered models. + */ + this.models = []; + /** + * @type {number[]} keeps the indices of the other stack. + */ + this.mdi = []; /** * @type {number} the current index of the sequence. - * @public don't know. */ this.i = 0; /** * @type {number} the elapsed time. - * @public */ this.elapsed = 0; + /** + * @type {number} the number of turns before the renderer starts working + * again. + */ + this.cooldown = 0; /** * @type {Vector3} the last tick's camera position. - * @public didn't tell you so. */ this.lastCamera = new Vector3(0, 0, 0); + /** + * @type {Vector3} the last tick's camera velocity. + */ this.lastCamVel = new Vector3(0, 0, 0); /** * @type {number} the next index to update for the current level. - * @public I told you so many times that you shouldn't access these. */ this.nextDeriveIdx = 0; + /** + * @type {number} how many nested polygons currently in (pls keep at 1). + */ this.polygonMode = 0; } /** * Updates the renderer's level. * @param {number} level the target level. - * @param {boolean} seedChanged (default: false) whether the seed has - * changed. + * @param {boolean} seedChanged whether the seed has changed. */ update(level, seedChanged = false) { @@ -1519,14 +1874,18 @@ class Renderer reset(clearGraph = true) { this.state = new Vector3(0, 0, 0); - this.ori = new Quaternion(); + this.ori = this.upright ? uprightQuat : new Quaternion(); this.stack = []; this.idxStack = []; this.i = 0; + this.models = []; + this.mdi = []; + this.cooldown = 0; this.polygonMode = 0; if(clearGraph) { this.elapsed = 0; + time = 0; theory.clearGraph(); } theory.invalidateTertiaryEquation(); @@ -1535,9 +1894,9 @@ class Renderer * Configures every parameter of the renderer, except the system. * @param {string} figureScale the zoom level expression. * @param {boolean} cameraMode the camera mode. - * @param {number} camX the camera's x-axis centre. - * @param {number} camY the camera's y-axis centre. - * @param {number} camZ the camera's z-axis centre. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. * @param {number} followFactor the camera's cursor-following speed. * @param {number} loopMode the renderer's looping mode. * @param {boolean} upright whether to rotate the system around the z-axis @@ -1546,17 +1905,21 @@ class Renderer * way forward. * @param {boolean} quickBacktrack whether to skip through straight lines * on the way backward. - * @param {string} backtrackList a list of symbols to act as stoppers for - * backtracking. + * @param {boolean} loadModels whether to load dedicated models for symbols. + * @param {boolean} backtrackTail whether to backtrack at the end of a loop. + * @param {boolean} hesitateApex whether to stutter for 1 tick at apices. + * @param {boolean} hesitateFork whether to stutter for 1 tick at forks. */ configure(figureScale, cameraMode, camX, camY, camZ, followFactor, - loopMode, upright, quickDraw, quickBacktrack, backtrackList, loadModels, - backtrackTail, hesitate) + loopMode, upright, quickDraw, quickBacktrack, loadModels, backtrackTail, + hesitateApex, hesitateFork) { let requireReset = (figureScale !== this.figScaleStr) || (upright != this.upright) || (quickDraw != this.quickDraw) || (quickBacktrack != this.quickBacktrack) || - (loadModels != this.loadModels) || (hesitate != this.hesitate); + (loadModels != this.loadModels) || + (hesitateApex != this.hesitateApex) || + (hesitateFork != this.hesitateFork); this.figScaleStr = figureScale.toString(); this.figScaleExpr = MathExpression.parse(this.figScaleStr); @@ -1582,25 +1945,20 @@ class Renderer this.upright = upright; this.quickDraw = quickDraw; this.quickBacktrack = quickBacktrack; - let btl = new Set(backtrackList); - if(!eqSet(btl, this.backtrackList)) - requireReset = true; - this.backtrackList = btl; this.loadModels = loadModels; this.backtrackTail = backtrackTail; - this.hesitate = hesitate; + this.hesitateApex = hesitateApex; + this.hesitateFork = hesitateFork; if(requireReset) this.reset(); - - return requireReset; } /** * Configures only the parameters related to the static camera mode. * @param {string} figureScale the zoom level expression. - * @param {number} camX the camera's x-axis centre. - * @param {number} camY the camera's y-axis centre. - * @param {number} camZ the camera's z-axis centre. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. * @param {boolean} upright whether to rotate the system around the z-axis * by 90 degrees. */ @@ -1636,7 +1994,7 @@ class Renderer * Applies a new L-system to the renderer. * @param {LSystem} system the new system. */ - set applySystem(system) + set constructSystem(system) { this.system = system; this.levels = []; @@ -1653,8 +2011,7 @@ class Renderer */ set seed(seed) { - this.system.rerollSeed = seed; - this.system.rerollAxiom(); + this.system.seed = seed; this.nextDeriveIdx = 0; this.loaded = -1; this.loadTarget = this.lv; @@ -1665,15 +2022,17 @@ class Renderer */ forward() { - this.state += this.ori.rotVector; + this.state += this.ori.headingVector; } /** * Ticks the clock. + * @param {number} dt the amount of time passed. */ tick(dt) { if(this.lv > this.loaded + 1 || - typeof this.levels[this.lv] == 'undefined') + typeof this.levels[this.lv] === 'undefined' || + this.levels[this.lv].length == 0) return; if(this.i >= this.levels[this.lv].length && this.loopMode == 0) @@ -1689,38 +2048,227 @@ class Renderer draw(level, onlyUpdate = false) { /* + Behold the broken monster patched by sheer duct tape. I can guarantee that because the game runs on one thread, the renderer - would always load faster than it draws. + would always load faster than it draws. Unless you make a rule that + spawns 10000 plus signs. Please don't do it. */ if(level > this.loaded) this.update(level); // You can't believe how many times I have to type this typeof clause. if(level > this.loaded + 1 || - typeof this.levels[this.lv] == 'undefined') + typeof this.levels[this.lv] === 'undefined') return; if(onlyUpdate) return; // This is to prevent the renderer from skipping the first point. - if(this.elapsed == 0) + if(this.elapsed <= 0.101) return; /* Don't worry, it'll not run forever. This is just to prevent the renderer from hesitating for 1 tick every loop. */ - let j, t; - for(j = 0; j < 2; ++j) + let j, t, moved; + let loopLimit = 2; // Shenanigans may arise with models? Try this + for(j = 0; j < loopLimit; ++j) { - for(; this.i < this.levels[this.lv].length; ++this.i) + if(this.cooldown > 0 && this.polygonMode <= 0) { - switch(this.levels[this.lv][this.i]) + --this.cooldown; + return; + } + + if(this.models.length > 0) + { + // Unreadable pile of shit + for(; this.mdi[this.mdi.length - 1] < + this.models[this.models.length - 1].length; + ++this.mdi[this.mdi.length - 1]) { - case ' ': - log('Blank space detected.') - break; + switch(this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + { + case ' ': + log('Blank space detected.') + break; + case '+': + this.ori = this.system.rotations.get('+').mul( + this.ori); + break; + case '-': + this.ori = this.system.rotations.get('-').mul( + this.ori); + break; + case '&': + this.ori = this.system.rotations.get('&').mul( + this.ori); + break; + case '^': + this.ori = this.system.rotations.get('^').mul( + this.ori); + break; + case '\\': + this.ori = this.system.rotations.get('\\').mul( + this.ori); + break; + case '/': + this.ori = this.system.rotations.get('/').mul( + this.ori); + break; + case '|': + this.ori = zUpQuat.mul(this.ori); + break; + case '$': + this.ori = this.ori.alignToVertical(); + break; + case 'T': + this.ori = this.ori.applyTropism( + this.system.tropism); + break; + case '~': + if(!this.system.models.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1])) + break; + + ++this.mdi[this.mdi.length - 1]; + this.models.push(this.system.models.get( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]])); + this.mdi.push(0); + return; + case '[': + this.idxStack.push(this.stack.length); + this.stack.push([this.state, this.ori]); + break; + case ']': + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + if(this.stack.length == 0) + { + log('You\'ve clearly made a bracket error.'); + break; + } + + moved = this.state !== + this.stack[this.stack.length - 1][0]; + + t = this.stack.pop(); + this.state = t[0]; + this.ori = t[1]; + if(this.stack.length == + this.idxStack[this.idxStack.length - 1]) + { + this.idxStack.pop(); + if(moved) + this.cooldown = 1; + if(this.hesitateFork && this.polygonMode <= 0) + { + ++this.mdi[this.mdi.length - 1]; + return; + } + else + { + break; + } + } + if(this.polygonMode <= 0) + return; + else + { + --this.mdi[this.mdi.length - 1]; + break; + } + case '%': + // Nothing to do here + break; + case '{': + ++this.polygonMode; + break; + case '}': + --this.polygonMode; + break; + case '.': + if(this.polygonMode <= 0) + log('You cannot register a vertex outside of ' + + 'polygon drawing.'); + else + ++this.mdi[this.mdi.length - 1]; + return; + default: + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + let ignored = this.system.ignoreList.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) || + this.loadModels && this.system.models.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]); + let breakAhead = BACKTRACK_LIST.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1]); + let btAhead = this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1] == ']' || + this.mdi[this.mdi.length - 1] == + this.models[this.models.length - 1].length - 1; + + if(this.hesitateApex && btAhead) + this.cooldown = 1; + + if(this.quickDraw && breakAhead) + this.cooldown = 1; + + moved = this.stack.length == 0 || + (this.stack.length > 0 && this.state !== + this.stack[this.stack.length - 1][0]); + + if(!this.quickBacktrack && moved && !ignored) + this.stack.push([this.state, this.ori]); + + if(!ignored) + this.forward(); + + if(this.quickBacktrack && breakAhead) + this.stack.push([this.state, this.ori]); + + if(this.quickDraw && !btAhead) + break; + else if(this.polygonMode <= 0) + { + ++this.mdi[this.mdi.length - 1]; + return; + } + else + break; + } + } + this.models.pop(); + this.mdi.pop(); + ++loopLimit; + // continue prevents the regular loop from running + continue; + } + for(; this.i < this.levels[this.lv].length; ++this.i) + { + // if(this.models.length > 0) + // break; + switch(this.levels[this.lv][this.i]) + { + case ' ': + log('Blank space detected.') + break; case '+': this.ori = this.system.rotations.get('+').mul(this.ori); break; @@ -1741,19 +2289,44 @@ class Renderer this.ori = this.system.rotations.get('/').mul(this.ori); break; case '|': - this.ori = ZAxisQuat.mul(this.ori); + this.ori = zUpQuat.mul(this.ori); + break; + case '$': + this.ori = this.ori.alignToVertical(); break; + case 'T': + this.ori = this.ori.applyTropism(this.system.tropism); + break; + case '~': + if(!this.loadModels || !this.system.models.has( + this.levels[this.lv][this.i + 1])) + break; + + ++this.i; + this.models.push(this.system.models.get( + this.levels[this.lv][this.i])); + this.mdi.push(0); + return; case '[': this.idxStack.push(this.stack.length); this.stack.push([this.state, this.ori]); break; case ']': + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + if(this.stack.length == 0) { log('You\'ve clearly made a bracket error.'); break; } + moved = this.state !== + this.stack[this.stack.length - 1][0]; + t = this.stack.pop(); this.state = t[0]; this.ori = t[1]; @@ -1761,13 +2334,17 @@ class Renderer this.idxStack[this.idxStack.length - 1]) { this.idxStack.pop(); - if(this.hesitate && this.polygonMode <= 0) + if(moved) + this.cooldown = 1; + if(this.hesitateFork && this.polygonMode <= 0) { ++this.i; return; } else + { break; + } } if(this.polygonMode <= 0) return; @@ -1787,37 +2364,46 @@ class Renderer break; case '.': if(this.polygonMode <= 0) - log('You\'re making a polygon outside of one?'); + log('You cannot register a vertex outside of ' + + 'polygon drawing.'); else ++this.i; - return; default: - let ignored = this.system.ignoreList.has( - this.levels[this.lv][this.i]); - - if(ignored) + if(this.cooldown > 0 && this.polygonMode <= 0) { - if(this.quickDraw && this.stack.length > 0 && - this.ori === this.stack[this.stack.length - 1][1]) - this.stack.push([this.state, this.ori]); - break; + --this.cooldown; + return; } - if(!this.quickBacktrack) + let ignored = this.system.ignoreList.has( + this.levels[this.lv][this.i]) || this.loadModels && + this.system.models.has(this.levels[this.lv][this.i]); + let breakAhead = BACKTRACK_LIST.has( + this.levels[this.lv][this.i + 1]); + let btAhead = this.levels[this.lv][this.i + 1] == ']' || + this.i == this.levels[this.lv].length - 1; + + if(this.hesitateApex && btAhead) + this.cooldown = 1; + + if(this.quickDraw && breakAhead) + this.cooldown = 1; + + moved = this.stack.length == 0 || + (this.stack.length > 0 && this.state !== + this.stack[this.stack.length - 1][0]); + + if(!this.quickBacktrack && moved && !ignored) this.stack.push([this.state, this.ori]); - this.forward(); + if(!ignored) + this.forward(); - let breakAhead = this.backtrackList.has( - this.levels[this.lv][this.i + 1]); if(this.quickBacktrack && breakAhead) this.stack.push([this.state, this.ori]); - - if(this.quickDraw && !breakAhead && - (this.quickBacktrack || this.stack.length > 0 && - this.ori === this.stack[this.stack.length - 1][1]) && - this.i < this.levels[this.lv].length - 1) + + if(this.quickDraw && !btAhead) break; else if(this.polygonMode <= 0) { @@ -1828,24 +2414,18 @@ class Renderer break; } } + // This is what the renderer will do at the end of a loop if(!this.backtrackTail || this.stack.length == 0) { - // log(this.stateString) switch(this.loopMode) { case 2: l.buy(1); - if(this.backtrackTail) - return; break; case 1: this.reset(false); - if(this.backtrackTail) - return; break; case 0: - if(this.backtrackTail) - this.state = new Vector3(0, 0, 0); return; } } @@ -1858,14 +2438,22 @@ class Renderer } } } + /** + * Return swizzled coordinates according to the in-game system. The game + * uses Android UI coordinates, which is X-right Y-down Z-face. + * @param {Vector3} coords the original coordinates. + * @returns {Vector3} + */ swizzle(coords) { - // The game uses left-handed Y-up, I mean Y-down coordinates. - if(this.upright) - return new Vector3(-coords.y, -coords.x, coords.z); - + // The game uses left-handed Y-up, aka Y-down coordinates. return new Vector3(coords.x, -coords.y, coords.z); } + /** + * Returns a variable's value for maths expressions. + * @param {string} v the variable's name. + * @returns {BigNumber} + */ getVariable(v) { switch(v) @@ -1876,7 +2464,7 @@ class Renderer } /** * Returns the camera centre's coordinates. - * @returns {Vector3} the coordinates. + * @returns {Vector3} */ get centre() { @@ -1886,8 +2474,8 @@ class Renderer return this.swizzle(-this.camCentre / this.figureScale); } /** - * Returns the cursor's coordinates. - * @returns {Vector3} the coordinates. + * Returns the turtle's coordinates. + * @returns {Vector3} */ get cursor() { @@ -1896,7 +2484,7 @@ class Renderer } /** * Returns the camera's coordinates. - * @returns {Vector3} the coordinates. + * @returns {Vector3} */ get camera() { @@ -1922,13 +2510,9 @@ class Renderer } } /** - * Returns the cursor's orientation. - * @returns {Quaternion} the orientation. + * Returns the static camera configuration. + * @returns {[string, string, string, string, boolean]} */ - get angles() - { - return this.ori; - } get staticCamera() { return [ @@ -1941,6 +2525,7 @@ class Renderer } /** * Returns the elapsed time. + * @returns {[number, number]} */ get elapsedTime() { @@ -1950,20 +2535,20 @@ class Renderer ]; } /** - * Returns the current progress on this level. - * @returns {number[]} the current progress in fractions. + * Returns the current progress on this level, in a fraction. + * @returns {[number, number]} */ get progressFrac() { return [this.i, this.levels[this.lv].length]; } /** - * Returns the current progress on this level. - * @returns {number} (between 0 and 100) the current progress. + * Returns the current progress on this level, in percent. + * @returns {number} */ get progressPercent() { - if(typeof this.levels[this.lv] == 'undefined') + if(typeof this.levels[this.lv] === 'undefined') return 0; let pf = this.progressFrac; @@ -1974,8 +2559,8 @@ class Renderer return result; } /** - * Returns the current progress as a string. - * @returns {string} the string. + * Returns the current progress fraction as a string. + * @returns {string} */ get progressString() { @@ -1984,75 +2569,146 @@ class Renderer } /** * Returns a loading message. - * @returns {string} the string. + * @returns {string} */ get loadingString() { - let len = typeof this.levels[this.loaded + 1] == 'undefined' ? 0 : + let len = typeof this.levels[this.loaded + 1] === 'undefined' ? 0 : this.levels[this.loaded + 1].length; return Localization.format(getLoc('rendererLoading'), this.loaded + 1, len); } /** * Returns the cursor's position as a string. - * @returns {string} the string. + * @returns {string} */ get stateString() { - if(typeof this.levels[this.lv] == 'undefined') + if(typeof this.levels[this.lv] === 'undefined') return this.loadingString; - return `\\begin{matrix}x=${getCoordString(this.state.x)},&y=${getCoordString(this.state.y)},&z=${getCoordString(this.state.z)},&${this.progressString}\\end{matrix}`; + return `\\begin{matrix}x=${getCoordString(this.state.x)},& + y=${getCoordString(this.state.y)},&z=${getCoordString(this.state.z)},& + ${this.progressString}\\end{matrix}`; } /** * Returns the cursor's orientation as a string. - * @returns {string} the string. + * @returns {string} */ get oriString() { - if(typeof this.levels[this.lv] == 'undefined') + if(typeof this.levels[this.lv] === 'undefined') return this.loadingString; - return `\\begin{matrix}q=${this.ori.toString()},&${this.progressString}\\end{matrix}`; + return `\\begin{matrix}q=${this.ori.toString()},&${this.progressString} + \\end{matrix}`; + } + /** + * Returns the object representation of the renderer. + * @returns {object} + */ + get object() + { + return { + figureScale: this.figScaleStr, + cameraMode: this.cameraMode, + camX: this.camXStr, + camY: this.camYStr, + camZ: this.camZStr, + followFactor: this.followFactor, + loopMode: this.loopMode, + upright: this.upright, + loadModels: this.loadModels, + quickDraw: this.quickDraw, + quickBacktrack: this.quickBacktrack, + backtrackTail: this.backtrackTail, + hesitateApex: this.hesitateApex, + hesitateFork: this.hesitateFork + } } /** * Returns the renderer's string representation. - * @returns {string} the string. + * @returns {string} */ toString() { - return`${this.figScaleStr} ${this.cameraMode} ${this.camXStr} ${this.camYStr} ${this.camZStr} ${this.followFactor} ${this.loopMode} ${this.upright ? 1 : 0} ${this.quickDraw ? 1 : 0} ${this.quickBacktrack ? 1 : 0} ${[...this.backtrackList].join('')} ${this.loadModels ? 1 : 0} ${this.backtrackTail ? 1 : 0} ${this.hesitate}`; + return JSON.stringify(this.object, null, 4); } } +/** + * Represents a bunch of buttons for variable controls. + */ class VariableControls { + /** + * @constructor + * @param {Upgrade} variable the variable being controlled. + * @param {boolean} useAnchor whether to use anchor controls. + * @param {number} quickbuyAmount the amount of levels to buy when held. + */ constructor(variable, useAnchor = false, quickbuyAmount = 10) { + /** + * @type {Upgrade} the variable being controlled. + */ this.variable = variable; + /** + * @type {Frame} the variable button. + */ this.varBtn = null; + /** + * @type {Frame} the refund button. + */ this.refundBtn = null; + /** + * @type {Frame} the buy button. + */ this.buyBtn = null; + /** + * @type {boolean} whether to use anchor controls. + */ this.useAnchor = useAnchor; + /** + * @type {number} the anchored variable level. + */ this.anchor = this.variable.level; + /** + * @type {number} whether the anchor is on. + */ this.anchorActive = false; + /** + * @type {number} the amount of levels to buy when held. + */ this.quickbuyAmount = quickbuyAmount; } + /** + * Updates all buttons, visually. + */ updateAllButtons() { this.updateDescription(); this.updateRefundButton(); this.updateBuyButton(); } + /** + * Updates the variable description written on the button's label. + */ updateDescription() { this.varBtn.content.text = this.variable.getDescription(); } - createVariableButton(callback = null, height = DEFAULT_BUTTON_HEIGHT) + /** + * Creates a variable button. + * @param {function(void): void} callback when pressed, calls this function. + * @param {number} height the button's height. + * @returns {Frame} + */ + createVariableButton(callback = null, height = BUTTON_HEIGHT) { - if(this.varBtn !== null) + if(this.varBtn) return this.varBtn; let frame = ui.createFrame @@ -2064,12 +2720,12 @@ class VariableControls content: ui.createLatexLabel ({ text: this.variable.getDescription(), - verticalOptions: LayoutOptions.CENTER, + verticalTextAlignment: TextAlignment.CENTER, textColor: Color.TEXT_MEDIUM }), borderColor: Color.TRANSPARENT }); - if(callback !== null) + if(callback) { frame.borderColor = Color.BORDER; frame.content.textColor = Color.TEXT; @@ -2098,6 +2754,9 @@ class VariableControls this.varBtn = frame; return this.varBtn; } + /** + * Updates the refund button, visually. + */ updateRefundButton() { this.refundBtn.borderColor = this.variable.level > 0 ? Color.BORDER : @@ -2105,18 +2764,17 @@ class VariableControls this.refundBtn.content.textColor = this.variable.level > 0 ? Color.TEXT : Color.TEXT_MEDIUM; } - createRefundButton(symbol = '-', height = DEFAULT_BUTTON_HEIGHT) + /** + * Creates a refund button. + * @param {string} symbol the button's label. + * @param {number} height the button's height. + * @returns {Frame} + */ + createRefundButton(symbol = '-', height = BUTTON_HEIGHT) { - if(this.refundBtn !== null) + if(this.refundBtn) return this.refundBtn; - // let bc = () => this.variable.level > 0 ? Color.BORDER : - // Color.TRANSPARENT; - // let tc = () => this.variable.level > 0 ? Color.TEXT : - // Color.TEXT_MEDIUM; - // let tcPressed = () => this.variable.level > 0 ? Color.TEXT_MEDIUM : - // Color.TEXT_DARK; - this.refundBtn = ui.createFrame ({ heightRequest: height, @@ -2126,8 +2784,8 @@ class VariableControls content: ui.createLatexLabel ({ text: symbol, - horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, textColor: this.variable.level > 0 ? Color.TEXT : Color.TEXT_MEDIUM }), @@ -2165,6 +2823,9 @@ class VariableControls }); return this.refundBtn; } + /** + * Updates the buy button, visually. + */ updateBuyButton() { this.buyBtn.borderColor = this.variable.level < this.variable.maxLevel ? @@ -2172,18 +2833,17 @@ class VariableControls this.buyBtn.content.textColor = this.variable.level < this.variable.maxLevel ? Color.TEXT : Color.TEXT_MEDIUM; } - createBuyButton(symbol = '+', height = DEFAULT_BUTTON_HEIGHT) + /** + * Creates a buy button. + * @param {string} symbol the button's label. + * @param {number} height the button's height. + * @returns {Frame} + */ + createBuyButton(symbol = '+', height = BUTTON_HEIGHT) { - if(this.buyBtn !== null) + if(this.buyBtn) return this.buyBtn; - // let bc = () => this.variable.level < this.variable.maxLevel ? - // Color.BORDER : Color.TRANSPARENT; - // let tc = () => this.variable.level < this.variable.maxLevel ? - // Color.TEXT : Color.TEXT_MEDIUM; - // let tcPressed = () => this.variable.level < this.variable.maxLevel ? - // Color.TEXT_MEDIUM : Color.TEXT_DARK; - this.buyBtn = ui.createFrame ({ heightRequest: height, @@ -2193,8 +2853,8 @@ class VariableControls content: ui.createLatexLabel ({ text: symbol, - horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, textColor: this.variable.level < this.variable.maxLevel ? Color.TEXT : Color.TEXT_MEDIUM }), @@ -2238,22 +2898,57 @@ class VariableControls } } +/** + * Measures performance for a piece of code. + */ class Measurer { + /** + * @constructor + * @param {string} title the measurement's title. + * @param {number} window the sample size. + */ constructor(title, window = 10) { + /** + * @type {string} the measurement's title. + */ this.title = title; + /** + * @type {number} the sample size. + */ this.window = window; + /** + * @type {number} the all-time sum. + */ this.sum = 0; + /** + * @type {number} the window sum. + */ this.windowSum = 0; + /** + * @type {number} the all-time maximum. + */ this.max = 0; + /** + * @type {number[]} recent records. + */ this.records = []; for(let i = 0; i < this.window; ++i) this.records[i] = 0; + /** + * @type {number} the elapsed time in ticks. + */ this.ticksPassed = 0; + /** + * @type {number} the most recent moment the function was stamped. + */ this.lastStamp = null; } - + + /** + * Resets the measurer. + */ reset() { this.sum = 0; @@ -2265,9 +2960,12 @@ class Measurer this.ticksPassed = 0; this.lastStamp = null; } + /** + * Stamps the measurer. + */ stamp() { - if(this.lastStamp === null) + if(!this.lastStamp) this.lastStamp = Date.now(); else { @@ -2282,14 +2980,26 @@ class Measurer ++this.ticksPassed; } } + /** + * Returns the window average. + * @returns {number} + */ get windowAvg() { return this.windowSum / Math.min(this.window, this.ticksPassed); } + /** + * Returns the all-time average. + * @returns {number} + */ get allTimeAvg() { return this.sum / this.ticksPassed; } + /** + * Returns the string for the window average. + * @returns {string} + */ get windowAvgString() { if(this.ticksPassed == 0) @@ -2302,6 +3012,10 @@ class Measurer getCoordString(this.max), getCoordString(this.windowAvg), Math.min(this.window, this.ticksPassed)); } + /** + * Returns the string for the all-time average. + * @returns {string} + */ get allTimeAvgString() { if(this.ticksPassed == 0) @@ -2316,20 +3030,18 @@ class Measurer } } -const XAxisQuat = new Quaternion(0, 1, 0, 0); -const ZAxisQuat = new Quaternion(0, 0, 0, 1); +// const sidewayQuat = new Quaternion(1, 0, 0, 0); +const uprightQuat = new Quaternion(-Math.sqrt(2)/2, 0, 0, Math.sqrt(2)/2); +const xUpQuat = new Quaternion(0, 1, 0, 0); +const yUpQuat = new Quaternion(0, 0, 1, 0); +const zUpQuat = new Quaternion(0, 0, 0, 1); let arrow = new LSystem('X', ['F=FF', 'X=F[+X][-X]FX'], 30); let renderer = new Renderer(arrow, '2^lv', 0, '2^lv'); -let globalSeed = new LCG(Date.now()); -let contentsTable = [1, 2, 3, 4, 5, 6, 7, 10, 12, 15, 19, 21, 23, 26]; +let globalRNG = new Xorshift(Date.now()); +let contentsTable = [0, 1, 2, 3, 4, 5, 6, 7, 10, 12, 15, 19, 21, 23, 24, 27]; let manualSystems = { - 11: - { - system: arrow, - config: ['1.5*2^lv', '1.2*2^lv', 0, 0, true] - }, 8: { system: new LSystem('FX', ['Y=-FX-Y', 'X=X+YF+'], 90), @@ -2340,13 +3052,18 @@ let manualSystems = system: new LSystem('X', ['X=+Y-X-Y+', 'Y=-X+Y+X-'], 60), config: ['2^lv', '0.5*2^lv', 'sqrt(3)/4*2^lv', 0, false] }, + 11: + { + system: arrow, + config: ['1.5*2^lv', 0, '1.2*2^lv', 0, true] + }, 13: { system: new LSystem('X', [ 'F=FF', 'X=F-[[X]+X]+F[+FX]-X,F+[[X]-X]-F[-FX]+X' ], 22.5), - config: ['1.5*2^lv', '1.2*2^lv', 0, 0, true] + config: ['1.5*2^lv', 0, '1.2*2^lv', 0, true] }, luckyFlower: { @@ -2357,7 +3074,7 @@ let manualSystems = 'R=+++I,++I,++++I', 'F=[---[I+I]--I+I][+++[I-I]++I-I]II' ], 12), - config: [6, 6, 0, 0, true] + config: [6, 0, 6, 0, true] }, 14: { @@ -2366,7 +3083,7 @@ let manualSystems = 'F=F[+i][-i]F', 'i=Ii,IIi' ], 60, 0, 'i'), - config: ["2*2^lv", 0, 0, 0, false] + config: ['2*2^lv', 0, 0, 0, false] }, 16: { @@ -2379,7 +3096,7 @@ let manualSystems = 'Y=Z-ZY+', 'Z=ZZ' ], 8), - config: ['2*2^lv', '1.2*2^lv', 0, 0, true] + config: ['2*2^lv', 0, '1.2*2^lv', 0, true] }, 17: { @@ -2396,7 +3113,7 @@ let manualSystems = 'C=[---------FF][+++++++++FF]B&&+C', 'D=[---------FF][+++++++++FF]B&&-D' ], 4), - config: ['3*1.3^lv', '1.8*1.3^lv', 0, 0, true] + config: ['3*1.3^lv', 0, '1.8*1.3^lv', 0, true] }, 20: { @@ -2405,7 +3122,7 @@ let manualSystems = 'B=[-B]C.', 'C=GC' ], 27), - config: ['lv', 'lv/2-1', 0, 0, true] + config: ['lv', 0, 'lv/2-1', 0, true] }, 22: { @@ -2414,28 +3131,45 @@ let manualSystems = 'I=Fi', 'i=Fj', 'j=J[--FFA][++FFA]', + 'K=~K', '~K=F[+++[--F+F]^^^[--F+F]^^^[--F+F]^^^[--F+F]]' ], 30), - config: ['3*lv', '1.5*lv', 0, 0, true] + config: ['3*lv', 0, '1.5*lv', 0, true] + }, + 28: + { + system: new LSystem('+S~A', [ + 'S=FS', + 'A=T[--//~K][++//~K]I///~A', + '~A=[+++~a~a~a~a]', + '~a=-{[^-F.][--FF.][&-F.].}+^^^', + 'K=~K', + '~K=[FT[F]+++~k~k~k~k]', + '~k=-{[^--F.][F-^-F.][^--F|++^--F|+F+F.][-F+F.][&--F|++&--F|+F+F.][F-&-F.][&--F.].}+^^^', + 'I=Fi', + 'i=Fj', + 'j=J[--FF~A][++FF~A]' + ], 30, 0, '', 0.16), + config: ['2*lv+1', '2*lv+1', 'lv/2+3/4', 0, false] }, - 27: + 29: { system: new LSystem('X', ['F=FF', 'X=F-[[X]+X]+F[-X]-X'], 15), - config: ['2^lv', '2^lv', 0, 0, true] + config: ['2^lv', 0, '2^lv', 0, true] }, - 28: + 30: { system: new LSystem('X', ['F=F[+F]XF', 'X=F-[[X]+X]+F[-FX]-X'], 27), config: ['1.5*2^lv', '0.225*2^lv', '-0.75*2^lv', 0, false] }, - 29: + 31: { system: new LSystem('X', [ 'E=XEXF-', 'F=FX+[E]X', 'X=F-[X+[X[++E]F]]+F[X+FX]-X' ], 22.5), - config: ['3^lv', '0.75*3^lv', '-0.25*3^lv', 0, true] + config: ['3^lv', '0.25*3^lv', '0.75*3^lv', 0, true] } }; let tmpSystem = null; @@ -2570,15 +3304,15 @@ var tick = (elapsedTime, multiplier) => } else { - renderer.draw(l.level, !timeCheck(elapsedTime)); renderer.tick(elapsedTime); + renderer.draw(l.level, !timeCheck(elapsedTime)); } if(measurePerformance) drawMeasurer.stamp(); let msTime = renderer.elapsedTime; - min.value = msTime[0] + msTime[1] / 100; + min.value = 1e-8 + msTime[0] + msTime[1] / 100; progress.value = renderer.progressPercent; theory.invalidateTertiaryEquation(); } @@ -2592,14 +3326,54 @@ var getEquationOverlay = () => let result = ui.createLatexLabel ({ text: overlayText, - margin: new Thickness(5, 4), + margin: new Thickness(8, 4), fontSize: 9, textColor: Color.TEXT_MEDIUM }); return result; } -let createButton = (label, callback, height = DEFAULT_BUTTON_HEIGHT) => +// var getCurrencyBarDelegate = () => +// { +// let stack = ui.createGrid +// ({ +// columnDefinitions: ['1*', '1*'], +// children: +// [ +// ui.createLatexLabel +// ({ +// column: 0, +// text: () => +// { +// let msTime = renderer.elapsedTime; +// return `${msTime[0] < 10 ? '0' : ''}${msTime[0]}:` + +// `${msTime[1] < 10 ? '0' : ''}${msTime[1].toFixed(1)} ` + +// `elapsed`; +// min.value = 1e-8 + msTime[0] + msTime[1] / 100; +// }, +// fontSize: 11, +// horizontalTextAlignment: TextAlignment.CENTER, +// verticalTextAlignment: TextAlignment.END +// }), +// ui.createLatexLabel +// ({ +// column: 1, +// text: () => `${renderer.progressPercent.toFixed(2)}\\%`, +// fontSize: 11, +// horizontalTextAlignment: TextAlignment.CENTER, +// verticalTextAlignment: TextAlignment.END +// }) +// ] +// }); +// return ui.createFrame +// ({ +// padding: new Thickness(0, 6), +// // margin: new Thickness(0, -1), +// content: stack +// }); +// } + +let createButton = (label, callback, height = BUTTON_HEIGHT) => { let frame = ui.createFrame ({ @@ -2610,7 +3384,7 @@ let createButton = (label, callback, height = DEFAULT_BUTTON_HEIGHT) => content: ui.createLatexLabel ({ text: label, - verticalOptions: LayoutOptions.CENTER, + verticalTextAlignment: TextAlignment.CENTER, textColor: Color.TEXT }), onTouched: (e) => @@ -2691,9 +3465,12 @@ var getUpgradeListDelegate = () => let resumeButton = createButton(Localization.format(getLoc('btnResume'), tmpSystemName), () => { - renderer.applySystem = tmpSystem; - tmpSystem = null; - }, ui.screenHeight * 0.05); + if(tmpSystem) + { + renderer.constructSystem = tmpSystem; + tmpSystem = null; + } + }, getMediumBtnSize(ui.screenWidth)); resumeButton.content.horizontalOptions = LayoutOptions.CENTER; resumeButton.isVisible = () => tmpSystem ? true : false; resumeButton.margin = new Thickness(0, 0, 0, 2); @@ -2712,8 +3489,8 @@ var getUpgradeListDelegate = () => rowSpacing: 6, rowDefinitions: [ - DEFAULT_BUTTON_HEIGHT, - DEFAULT_BUTTON_HEIGHT + BUTTON_HEIGHT, + BUTTON_HEIGHT ], columnDefinitions: ['50*', '50*'], children: @@ -2757,9 +3534,9 @@ var getUpgradeListDelegate = () => rowSpacing: 6, rowDefinitions: [ - DEFAULT_BUTTON_HEIGHT, - DEFAULT_BUTTON_HEIGHT, - DEFAULT_BUTTON_HEIGHT + BUTTON_HEIGHT, + BUTTON_HEIGHT, + BUTTON_HEIGHT ], columnDefinitions: ['50*', '50*'], children: @@ -2798,7 +3575,7 @@ let createConfigMenu = () => getLoc('camModes')[tmpCM]), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let CMSlider = ui.createSlider ({ @@ -2836,7 +3613,7 @@ let createConfigMenu = () => isVisible: tmpCM == 0, row: 2, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let camGrid = ui.createEntry ({ @@ -2874,8 +3651,8 @@ let createConfigMenu = () => text: getLoc('labelCamOffset'), row: 0, column: 0, - // horizontalOptions: LayoutOptions.END, - verticalOptions: LayoutOptions.CENTER + // horizontalTextAlignment: TextAlignment.END, + verticalTextAlignment: TextAlignment.CENTER }), CYEntry ] @@ -2898,7 +3675,7 @@ let createConfigMenu = () => text: getLoc('labelFollowFactor'), row: 2, column: 0, - verticalOptions: LayoutOptions.CENTER, + verticalTextAlignment: TextAlignment.CENTER, isVisible: tmpCM > 0 }); let FFEntry = ui.createEntry @@ -2939,7 +3716,7 @@ let createConfigMenu = () => getLoc('loopModes')[tmpLM]), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let LMSlider = ui.createSlider ({ @@ -2987,7 +3764,7 @@ let createConfigMenu = () => text: getLoc('labelLoadModels'), row: 2, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let modelSwitch = ui.createSwitch ({ @@ -3042,17 +3819,17 @@ let createConfigMenu = () => } } }); - let tmpHes = renderer.hesitate; - let hesLabel = ui.createLatexLabel + let tmpHesA = renderer.hesitateApex; + let hesALabel = ui.createLatexLabel ({ - text: getLoc('labelHesitate'), + text: getLoc('labelHesitateApex'), row: 2, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); - let hesSwitch = ui.createSwitch + let hesASwitch = ui.createSwitch ({ - isToggled: tmpHes, + isToggled: tmpHesA, row: 2, column: 1, horizontalOptions: LayoutOptions.END, @@ -3062,28 +3839,34 @@ let createConfigMenu = () => e.type == TouchType.LONGPRESS_RELEASED) { Sound.playClick(); - tmpHes = !tmpHes; - hesSwitch.isToggled = tmpHes; + tmpHesA = !tmpHesA; + hesASwitch.isToggled = tmpHesA; } } }); - let tmpEXB = [...renderer.backtrackList].join(''); - let EXBLabel = ui.createLatexLabel + let tmpHesN = renderer.hesitateFork; + let hesNLabel = ui.createLatexLabel ({ - text: getLoc('labelBTList'), + text: getLoc('labelHesitateFork'), row: 3, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); - let EXBEntry = ui.createEntry + let hesNSwitch = ui.createSwitch ({ - text: tmpEXB, + isToggled: tmpHesN, row: 3, column: 1, - horizontalTextAlignment: TextAlignment.END, - onTextChanged: (ot, nt) => + horizontalOptions: LayoutOptions.END, + onTouched: (e) => { - tmpEXB = nt; + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpHesN = !tmpHesN; + hesNSwitch.isToggled = tmpHesN; + } } }); @@ -3112,7 +3895,8 @@ let createConfigMenu = () => text: getLoc('labelFigScale'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), zoomEntry, CMLabel, @@ -3128,7 +3912,8 @@ let createConfigMenu = () => text: getLoc('labelUpright'), row: 4, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), uprightSwitch, ] @@ -3140,7 +3925,12 @@ let createConfigMenu = () => }), ui.createGrid ({ - rowDefinitions: [40, 40], + rowDefinitions: + [ + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT + ], columnDefinitions: ['70*', '30*'], children: [ @@ -3151,9 +3941,12 @@ let createConfigMenu = () => text: getLoc('labelBTTail'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), - tailSwitch + tailSwitch, + modelLabel, + modelSwitch ] }), ui.createBox @@ -3163,7 +3956,13 @@ let createConfigMenu = () => }), ui.createGrid ({ - // rowDefinitions: [40, 40, 40, 40, 40], + rowDefinitions: + [ + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT + ], columnDefinitions: ['70*', '30*'], children: [ @@ -3172,7 +3971,8 @@ let createConfigMenu = () => text: getLoc('labelQuickdraw'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), QDSwitch, ui.createLatexLabel @@ -3180,13 +3980,14 @@ let createConfigMenu = () => text: getLoc('labelQuickBT'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), QBSwitch, - hesLabel, - hesSwitch, - EXBLabel, - EXBEntry + hesALabel, + hesASwitch, + hesNLabel, + hesNSwitch ] }) ] @@ -3201,11 +4002,11 @@ let createConfigMenu = () => ({ text: getLoc('labelRequireReset'), margin: new Thickness(0, 0, 0, 4), - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), ui.createGrid ({ - minimumHeightRequest: 64, + minimumHeightRequest: BUTTON_HEIGHT, columnDefinitions: ['50*', '50*'], children: [ @@ -3217,10 +4018,9 @@ let createConfigMenu = () => onClicked: () => { Sound.playClick(); - let requireReset = renderer.configure(tmpZE, - tmpCM, tmpCX, tmpCY, tmpCZ, tmpFF, tmpLM, - tmpUpright, tmpQD, tmpQB, tmpEXB, tmpModel, - tmpTail, tmpHes); + renderer.configure(tmpZE, tmpCM, tmpCX, tmpCY, + tmpCZ, tmpFF, tmpLM, tmpUpright, tmpQD, tmpQB, + tmpModel, tmpTail, tmpHesA, tmpHesN); lvlControls.updateDescription(); menu.hide(); } @@ -3247,13 +4047,14 @@ let createConfigMenu = () => QDSwitch.isToggled = rx.quickDraw; tmpQB = rx.quickBacktrack; QBSwitch.isToggled = rx.quickBacktrack; - EXBEntry.text = [...rx.backtrackList].join(''); tmpModel = rx.loadModels; modelSwitch.isToggled = rx.loadModels; tmpTail = rx.backtrackTail; tailSwitch.isToggled = rx.backtrackTail; - tmpHes = rx.hesitate; - hesSwitch.isToggled = rx.hesitate; + tmpHesA = rx.hesitateApex; + hesASwitch.isToggled = rx.hesitateApex; + tmpHesN = rx.hesitateFork; + hesNSwitch.isToggled = rx.hesitateFork; lvlControls.updateDescription(); // menu.hide(); } @@ -3275,68 +4076,115 @@ let createSystemMenu = () => text: tmpAxiom, row: 0, column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpAxiom = nt; } }); - let tmpAngle = values.turnAngle; + let tmpAngle = values.turnAngle || '0'; let angleEntry = ui.createEntry ({ text: tmpAngle.toString(), - keyboard: Keyboard.NUMERIC, - row: 0, - column: 3, + row: 1, + column: 1, horizontalTextAlignment: TextAlignment.END, onTextChanged: (ot, nt) => { - tmpAngle = Number(nt); + tmpAngle = nt; } }); let tmpRules = values.rules; let ruleEntries = []; + let ruleMoveBtns = []; for(let i = 0; i < tmpRules.length; ++i) { ruleEntries.push(ui.createEntry ({ + row: i, + column: 0, text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpRules[i] = nt; } })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } } let rulesLabel = ui.createLatexLabel ({ text: Localization.format(getLoc('labelRules'), ruleEntries.length), - verticalOptions: LayoutOptions.CENTER, + verticalTextAlignment: TextAlignment.CENTER, margin: new Thickness(0, 12) }); - let ruleStack = ui.createStackLayout + let ruleStack = ui.createGrid ({ - children: ruleEntries + columnDefinitions: ['7*', '1*'], + children: [...ruleEntries, ...ruleMoveBtns] }); let addRuleButton = ui.createButton ({ text: getLoc('btnAdd'), row: 0, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); let i = ruleEntries.length; + tmpRules[i] = ''; ruleEntries.push(ui.createEntry ({ - text: '', + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpRules[i] = nt; } })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } rulesLabel.text = Localization.format(getLoc('labelRules'), ruleEntries.length); - ruleStack.children = ruleEntries; + ruleStack.children = [...ruleEntries, ...ruleMoveBtns]; } }); let tmpIgnore = values.ignoreList; @@ -3351,29 +4199,67 @@ let createSystemMenu = () => tmpIgnore = nt; } }); - let tmpSeed = values.seed; - let seedEntry = ui.createEntry + let tmpTropism = values.tropism || '0'; + let tropismEntry = ui.createEntry ({ - text: tmpSeed.toString(), - keyboard: Keyboard.NUMERIC, - row: 1, + text: tmpTropism.toString(), + row: 2, column: 1, horizontalTextAlignment: TextAlignment.END, onTextChanged: (ot, nt) => { - tmpSeed = Number(nt); + tmpTropism = nt; } }); - - let menu = ui.createPopup + let tmpSeed = values.seed || '0'; + let seedLabel = ui.createGrid ({ - title: getLoc('menuLSystem'), - isPeekable: true, - content: ui.createStackLayout - ({ - children: - [ - ui.createScrollView + row: 3, + column: 0, + columnDefinitions: ['40*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelSeed'), + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnReroll'), + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + seedEntry.text = globalRNG.nextInt.toString(); + } + }) + ] + }); + let seedEntry = ui.createEntry + ({ + text: tmpSeed.toString(), + keyboard: Keyboard.NUMERIC, + row: 3, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpSeed = Number(nt); + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuLSystem'), + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + ui.createScrollView ({ content: ui.createStackLayout ({ @@ -3381,7 +4267,7 @@ let createSystemMenu = () => [ ui.createGrid ({ - columnDefinitions: ['20*', '40*', '25*', '15*'], + columnDefinitions: ['20*', '80*'], children: [ ui.createLatexLabel @@ -3389,17 +4275,10 @@ let createSystemMenu = () => text: getLoc('labelAxiom'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), axiomEntry, - ui.createLatexLabel - ({ - text: getLoc('labelAngle'), - row: 0, - column: 2, - verticalOptions: LayoutOptions.CENTER - }), - angleEntry, ] }), ui.createGrid @@ -3422,16 +4301,29 @@ let createSystemMenu = () => text: getLoc('labelIgnored'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), ignoreEntry, ui.createLatexLabel ({ - text: getLoc('labelSeed'), + text: getLoc('labelAngle'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), + angleEntry, + ui.createLatexLabel + ({ + text: getLoc('labelTropism'), + row: 2, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + tropismEntry, + seedLabel, seedEntry ] }) @@ -3445,7 +4337,7 @@ let createSystemMenu = () => }), ui.createGrid ({ - minimumHeightRequest: 64, + minimumHeightRequest: BUTTON_HEIGHT, columnDefinitions: ['50*', '50*'], children: [ @@ -3457,8 +4349,9 @@ let createSystemMenu = () => onClicked: () => { Sound.playClick(); - renderer.applySystem = new LSystem(tmpAxiom, - tmpRules, tmpAngle, tmpSeed, tmpIgnore); + renderer.constructSystem = new LSystem(tmpAxiom, + tmpRules, tmpAngle, tmpSeed, tmpIgnore, + tmpTropism); if(tmpSystem) { tmpSystem = null; @@ -3485,6 +4378,7 @@ let createSystemMenu = () => getLoc('labelRules'), ruleEntries.length); ruleStack.children = ruleEntries; ignoreEntry.text = values.ignoreList; + tropismEntry.text = values.tropism.toString(); seedEntry.text = values.seed.toString(); } }) @@ -3504,6 +4398,7 @@ let createNamingMenu = () => text: tmpName, row: 0, column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpName = nt; @@ -3515,6 +4410,7 @@ let createNamingMenu = () => text: tmpDesc, row: 0, column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpDesc = nt; @@ -3532,7 +4428,7 @@ let createNamingMenu = () => text: key, row: i, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER })); let btnO = createOverwriteButton(key); btnO.row = i; @@ -3548,7 +4444,7 @@ let createNamingMenu = () => text: getLoc('btnOverwrite'), row: 0, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); @@ -3572,8 +4468,8 @@ let createNamingMenu = () => }); let systemGridScroll = ui.createScrollView ({ - heightRequest: () => Math.max(40, Math.min(ui.screenHeight * 0.2, - systemGrid.height)), + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.2, systemGrid.height)), content: systemGrid }); @@ -3594,7 +4490,7 @@ let createNamingMenu = () => text: getLoc('labelName'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), nameEntry ] @@ -3609,7 +4505,7 @@ let createNamingMenu = () => text: getLoc('labelDesc'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), descEntry ] @@ -3623,8 +4519,8 @@ let createNamingMenu = () => ({ text: Localization.format(getLoc('labelSavedSystems'), savedSystems.size), - // horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER, + // horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, margin: new Thickness(0, 12) }), systemGridScroll, @@ -3661,23 +4557,61 @@ let createNamingMenu = () => let createSystemClipboardMenu = (values) => { - let tmpSys = values; - let sysEntry = ui.createEntry + let totalLength = 0; + let tmpSys = []; + let sysEntries = []; + for(let i = 0; i * ENTRY_CHAR_LIMIT < values.length; ++i) + { + tmpSys.push(values.slice(i * ENTRY_CHAR_LIMIT, (i + 1) * + ENTRY_CHAR_LIMIT)); + sysEntries.push(ui.createEntry + ({ + text: tmpSys[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpSys[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + } + let lengthLabel = ui.createLatexLabel ({ - text: tmpSys, - onTextChanged: (ot, nt) => - { - tmpSys = nt; - warningEntry.isVisible = sysEntry.text.length >= ENTRY_CHAR_LIMIT; - } + text: Localization.format(getLoc('labelTotalLength'), totalLength), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) }); - let warningEntry = ui.createLatexLabel + let entryStack = ui.createStackLayout ({ - isVisible: sysEntry.text.length >= ENTRY_CHAR_LIMIT, - text: Localization.format(getLoc('labelEntryCharLimit'), - ENTRY_CHAR_LIMIT), - margin: new Thickness(0, 0, 0, 4), - verticalOptions: LayoutOptions.CENTER + children: sysEntries + }); + let addEntryButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = sysEntries.length; + tmpSys[i] = ''; + sysEntries.push(ui.createEntry + ({ + text: tmpSys[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpSys[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + entryStack.children = sysEntries; + } }); let menu = ui.createPopup @@ -3687,25 +4621,34 @@ let createSystemClipboardMenu = (values) => ({ children: [ - sysEntry, + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + lengthLabel, + addEntryButton + ] + }), + entryStack, ui.createBox ({ heightRequest: 1, margin: new Thickness(0, 6) }), - warningEntry, ui.createButton ({ text: getLoc('btnConstruct'), onClicked: () => { Sound.playClick(); - let sv = JSON.parse(tmpSys); + let sv = JSON.parse(tmpSys.join('')); tmpSystemName = sv.title; tmpSystemDesc = sv.desc; - renderer.applySystem = new LSystem(sv.system.axiom, + renderer.constructSystem = new LSystem(sv.system.axiom, sv.system.rules, sv.system.turnAngle, - sv.system.seed, sv.system.ignoreList); + sv.system.seed, sv.system.ignoreList, + sv.system.tropism); tmpSystem = null; if('config' in sv) renderer.configureStaticCamera(...sv.config); @@ -3718,58 +4661,7 @@ let createSystemClipboardMenu = (values) => return menu; } -let createStateClipboardMenu = (values) => -{ - let tmpState = values; - let sysEntry = ui.createEntry - ({ - text: tmpState, - onTextChanged: (ot, nt) => - { - tmpState = nt; - warningEntry.isVisible = sysEntry.text.length >= ENTRY_CHAR_LIMIT; - } - }); - let warningEntry = ui.createLatexLabel - ({ - isVisible: sysEntry.text.length >= ENTRY_CHAR_LIMIT, - text: Localization.format(getLoc('labelEntryCharLimit'), - ENTRY_CHAR_LIMIT), - margin: new Thickness(0, 0, 0, 4), - verticalOptions: LayoutOptions.CENTER - }); - - let menu = ui.createPopup - ({ - title: getLoc('menuClipboard'), - content: ui.createStackLayout - ({ - children: - [ - sysEntry, - ui.createBox - ({ - heightRequest: 1, - margin: new Thickness(0, 6) - }), - warningEntry, - ui.createButton - ({ - text: getLoc('btnImport'), - onClicked: () => - { - Sound.playClick(); - setInternalState(tmpState); - menu.hide(); - } - }) - ] - }) - }); - return menu; -} - -let createViewMenu = (title) => +let createViewMenu = (title, parentMenu) => { let systemObj = savedSystems.get(title); let values = systemObj.system; @@ -3799,7 +4691,7 @@ let createViewMenu = (title) => text: getLoc('labelCamCentre'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let camGrid = ui.createEntry ({ @@ -3824,8 +4716,8 @@ let createViewMenu = (title) => text: getLoc('labelCamOffset'), row: 0, column: 0, - // horizontalOptions: LayoutOptions.END, - verticalOptions: LayoutOptions.CENTER + // horizontalTextAlignment: TextAlignment.END, + verticalTextAlignment: TextAlignment.CENTER }), ui.createEntry ({ @@ -3875,70 +4767,117 @@ let createViewMenu = (title) => text: tmpAxiom, row: 0, column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpAxiom = nt; } }); - let tmpAngle = values.turnAngle; + let tmpAngle = values.turnAngle || '0'; let angleEntry = ui.createEntry ({ text: tmpAngle.toString(), - keyboard: Keyboard.NUMERIC, - row: 0, - column: 3, + row: 1, + column: 1, horizontalTextAlignment: TextAlignment.END, onTextChanged: (ot, nt) => { - tmpAngle = Number(nt); + tmpAngle = nt; } }); let tmpRules = []; for(let i = 0; i < values.rules.length; ++i) tmpRules[i] = values.rules[i]; let ruleEntries = []; + let ruleMoveBtns = []; for(let i = 0; i < tmpRules.length; ++i) { ruleEntries.push(ui.createEntry ({ + row: i, + column: 0, text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpRules[i] = nt; } })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } } let rulesLabel = ui.createLatexLabel ({ text: Localization.format(getLoc('labelRules'), ruleEntries.length), - verticalOptions: LayoutOptions.CENTER, + verticalTextAlignment: TextAlignment.CENTER, margin: new Thickness(0, 12) }); - let ruleStack = ui.createStackLayout + let ruleStack = ui.createGrid ({ - children: ruleEntries + columnDefinitions: ['7*', '1*'], + children: [...ruleEntries, ...ruleMoveBtns] }); let addRuleButton = ui.createButton ({ text: getLoc('btnAdd'), row: 0, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); let i = ruleEntries.length; + tmpRules[i] = ''; ruleEntries.push(ui.createEntry ({ - text: '', + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, onTextChanged: (ot, nt) => { tmpRules[i] = nt; } })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } rulesLabel.text = Localization.format(getLoc('labelRules'), ruleEntries.length); - ruleStack.children = ruleEntries; + ruleStack.children = [...ruleEntries, ...ruleMoveBtns]; } }); let tmpIgnore = values.ignoreList; @@ -3953,12 +4892,50 @@ let createViewMenu = (title) => tmpIgnore = nt; } }); - let tmpSeed = values.seed; + let tmpTropism = values.tropism || '0'; + let tropismEntry = ui.createEntry + ({ + text: tmpTropism.toString(), + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpTropism = nt; + } + }); + let tmpSeed = values.seed || '0'; + let seedLabel = ui.createGrid + ({ + row: 3, + column: 0, + columnDefinitions: ['40*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelSeed'), + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnReroll'), + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + seedEntry.text = globalRNG.nextInt.toString(); + } + }) + ] + }); let seedEntry = ui.createEntry ({ text: tmpSeed.toString(), keyboard: Keyboard.NUMERIC, - row: 1, + row: 3, column: 1, horizontalTextAlignment: TextAlignment.END, onTextChanged: (ot, nt) => @@ -3986,12 +4963,12 @@ let createViewMenu = (title) => ({ text: tmpDesc, margin: new Thickness(0, 6), - horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER }), ui.createGrid ({ - columnDefinitions: ['20*', '40*', '25*', '15*'], + columnDefinitions: ['20*', '80*'], children: [ ui.createLatexLabel @@ -3999,17 +4976,10 @@ let createViewMenu = (title) => text: getLoc('labelAxiom'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), axiomEntry, - ui.createLatexLabel - ({ - text: getLoc('labelAngle'), - row: 0, - column: 2, - verticalOptions: LayoutOptions.CENTER - }), - angleEntry ] }), ui.createGrid @@ -4032,16 +5002,29 @@ let createViewMenu = (title) => text: getLoc('labelIgnored'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), ignoreEntry, ui.createLatexLabel ({ - text: getLoc('labelSeed'), + text: getLoc('labelAngle'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), + angleEntry, + ui.createLatexLabel + ({ + text: getLoc('labelTropism'), + row: 2, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + tropismEntry, + seedLabel, seedEntry ] }), @@ -4053,9 +5036,10 @@ let createViewMenu = (title) => ui.createLatexLabel ({ text: getLoc('labelApplyCamera'), - // horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER, - margin: new Thickness(0, 12) + // horizontalTextAlignment: + // TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 9) }), ui.createGrid ({ @@ -4067,7 +5051,8 @@ let createViewMenu = (title) => text: getLoc('labelFigScale'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), zoomEntry, camLabel, @@ -4079,7 +5064,8 @@ let createViewMenu = (title) => text: getLoc('labelUpright'), row: 3, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: + TextAlignment.CENTER }), uprightSwitch ] @@ -4094,7 +5080,7 @@ let createViewMenu = (title) => }), ui.createGrid ({ - minimumHeightRequest: 64, + minimumHeightRequest: BUTTON_HEIGHT, columnDefinitions: ['30*', '30*', '30*'], children: [ @@ -4106,13 +5092,15 @@ let createViewMenu = (title) => onClicked: () => { Sound.playClick(); - renderer.applySystem = new LSystem(tmpAxiom, - tmpRules, tmpAngle, tmpSeed, tmpIgnore); + renderer.constructSystem = new LSystem(tmpAxiom, + tmpRules, tmpAngle, tmpSeed, tmpIgnore, + tmpTropism); tmpSystem = null; renderer.configureStaticCamera(tmpZE, tmpCX, tmpCY, tmpCZ, tmpUpright); tmpSystemName = title; tmpSystemDesc = tmpDesc; + parentMenu.hide(); menu.hide(); } }), @@ -4128,7 +5116,8 @@ let createViewMenu = (title) => { desc: tmpDesc, system: new LSystem(tmpAxiom, tmpRules, - tmpAngle, tmpSeed, tmpIgnore).object, + tmpAngle, tmpSeed, tmpIgnore, tmpTropism). + object, config: [tmpZE, tmpCX, tmpCY, tmpCZ, tmpUpright] }); @@ -4161,8 +5150,8 @@ let createSaveMenu = () => ({ text: Localization.format(getLoc('labelSavedSystems'), savedSystems.size), - // horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER, + // horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, margin: new Thickness(0, 12) }); let getSystemGrid = () => @@ -4176,7 +5165,7 @@ let createSaveMenu = () => text: key, row: i, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER })); let btn = createViewButton(key); btn.row = i; @@ -4195,11 +5184,11 @@ let createSaveMenu = () => text: getLoc('btnView'), row: 0, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); - let viewMenu = createViewMenu(title, systemGrid); + let viewMenu = createViewMenu(title, menu); viewMenu.onDisappearing = () => { systemGrid.children = getSystemGrid(); @@ -4217,8 +5206,8 @@ let createSaveMenu = () => }); let systemGridScroll = ui.createScrollView ({ - heightRequest: () => Math.max(40, Math.min(ui.screenHeight * 0.32, - systemGrid.height)), + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.32, systemGrid.height)), content: systemGrid }); let menu = ui.createPopup @@ -4238,14 +5227,14 @@ let createSaveMenu = () => text: getLoc('labelCurrentSystem'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), ui.createButton ({ text: getLoc('btnClipboard'), row: 0, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { let clipMenu = createSystemClipboardMenu( @@ -4264,7 +5253,7 @@ let createSaveMenu = () => text: getLoc('btnSave'), row: 0, column: 2, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); @@ -4300,8 +5289,8 @@ let createManualMenu = () => text: manualPages[page].title, margin: new Thickness(0, 4), heightRequest: 20, - horizontalOptions: LayoutOptions.CENTER, - verticalOptions: LayoutOptions.CENTER + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER }); let pageContents = ui.createLabel ({ @@ -4326,12 +5315,71 @@ let createManualMenu = () => text: getLoc('labelSource'), row: 0, column: 0, - horizontalOptions: LayoutOptions.END_AND_EXPAND, - verticalOptions: LayoutOptions.CENTER + horizontalOptions: LayoutOptions.END, + verticalTextAlignment: TextAlignment.CENTER }), sourceEntry ] }); + let prevButton = ui.createButton + ({ + text: getLoc('btnPrev'), + row: 0, + column: 0, + isVisible: page > 0, + onClicked: () => + { + Sound.playClick(); + if(page > 0) + setPage(page - 1); + } + }); + let constructButton = ui.createButton + ({ + text: getLoc('btnConstruct'), + row: 0, + column: 1, + isVisible: page in manualSystems, + onClicked: () => + { + Sound.playClick(); + let s = manualSystems[page]; + renderer.constructSystem = s.system; + tmpSystem = null; + if('config' in s) + renderer.configureStaticCamera(...s.config); + + tmpSystemName = manualPages[page].title; + tmpSystemDesc = Localization.format( + getLoc('manualSystemDesc'), page + 1); + menu.hide(); + } + }); + let tocButton = ui.createButton + ({ + text: getLoc('btnContents'), + row: 0, + column: 1, + isVisible: !(page in manualSystems), + onClicked: () => + { + Sound.playClick(); + TOCMenu.show(); + } + }); + let nextButton = ui.createButton + ({ + text: getLoc('btnNext'), + row: 0, + column: 2, + isVisible: page < manualPages.length - 1, + onClicked: () => + { + Sound.playClick(); + if(page < manualPages.length - 1) + setPage(page + 1); + } + }); let setPage = (p) => { page = p; @@ -4348,6 +5396,11 @@ let createManualMenu = () => sourceEntry.text = 'source' in manualPages[page] ? manualPages[page].source : ''; + + prevButton.isVisible = page > 0; + nextButton.isVisible = page < manualPages.length - 1; + constructButton.isVisible = page in manualSystems; + tocButton.isVisible = !(page in manualSystems); }; let getContentsTable = () => { @@ -4359,7 +5412,7 @@ let createManualMenu = () => text: manualPages[contentsTable[i]].title, row: i, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER })); children.push(ui.createButton ({ @@ -4367,7 +5420,7 @@ let createManualMenu = () => contentsTable[i] + 1), row: i, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { Sound.playClick(); @@ -4428,65 +5481,109 @@ let createManualMenu = () => columnDefinitions: ['30*', '30*', '30*'], children: [ - ui.createButton - ({ - text: getLoc('btnPrev'), - row: 0, - column: 0, - isVisible: () => page > 0, - onClicked: () => - { - Sound.playClick(); - if(page > 0) - setPage(page - 1); - } - }), - ui.createButton - ({ - text: getLoc('btnConstruct'), - row: 0, - column: 1, - isVisible: () => page in manualSystems, - onClicked: () => - { - Sound.playClick(); - let s = manualSystems[page]; - renderer.applySystem = s.system; - tmpSystem = null; - if('config' in s) - renderer.configureStaticCamera(...s.config); + prevButton, + constructButton, + tocButton, + nextButton + ] + }) + ] + }) + }); + return menu; +} - tmpSystemName = manualPages[page].title; - tmpSystemDesc = Localization.format( - getLoc('manualSystemDesc'), page + 1); - menu.hide(); - } - }), - ui.createButton - ({ - text: getLoc('btnContents'), - row: 0, - column: 1, - isVisible: () => !(page in manualSystems), - onClicked: () => - { - Sound.playClick(); - TOCMenu.show(); - } - }), - ui.createButton +let createSeqViewMenu = (level) => +{ + let pageTitle = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelLevelSeq'), level, + renderer.levels[level].length), + margin: new Thickness(0, 4), + heightRequest: 20, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + }); + let pageContents = ui.createLabel + ({ + fontFamily: FontFamily.CMU_REGULAR, + fontSize: 16, + text: renderer.levels[level], + lineBreakMode: LineBreakMode.CHARACTER_WRAP + }); + let prevButton = ui.createButton + ({ + text: getLoc('btnPrev'), + row: 0, + column: 0, + isVisible: level > 0, + onClicked: () => + { + Sound.playClick(); + if(level > 0) + setPage(level - 1); + } + }); + let nextButton = ui.createButton + ({ + text: getLoc('btnNext'), + row: 0, + column: 1, + isVisible: level < renderer.levels.length - 1, + onClicked: () => + { + Sound.playClick(); + if(level < renderer.levels.length - 1) + setPage(level + 1); + } + }); + let setPage = (p) => + { + level = p; + pageTitle.text = Localization.format(getLoc('labelLevelSeq'), level, + renderer.levels[level].length); + pageContents.text = renderer.levels[level]; + + prevButton.isVisible = level > 0; + nextButton.isVisible = level < renderer.levels.length - 1; + }; + + let menu = ui.createPopup + ({ + title: tmpSystemName, + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + pageTitle, + ui.createFrame + ({ + padding: new Thickness(8, 6), + heightRequest: ui.screenHeight * 0.28, + content: ui.createScrollView + ({ + content: ui.createStackLayout ({ - text: getLoc('btnNext'), - row: 0, - column: 2, - isVisible: () => page < manualPages.length - 1, - onClicked: () => - { - Sound.playClick(); - if(page < manualPages.length - 1) - setPage(page + 1); - } + children: + [ + pageContents + ] }) + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + columnDefinitions: ['50*', '50*'], + children: + [ + prevButton, + nextButton ] }) ] @@ -4502,54 +5599,127 @@ let createSequenceMenu = () => { tmpLvls.push(ui.createLatexLabel ({ - text: Localization.format(getLoc('labelLevelSeq'), i), + text: Localization.format(getLoc('labelLevelSeq'), i, + renderer.levels[i].length), row: i, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER })); - tmpLvls.push(ui.createGrid + tmpLvls.push(ui.createButton ({ - columnDefinitions: ['80*', 'auto'], + text: getLoc('btnView'), row: i, column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let viewMenu = createSeqViewMenu(i); + viewMenu.show(); + } + })); + } + let seqGrid = ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: tmpLvls + }); + + let menu = ui.createPopup + ({ + title: tmpSystemName, + content: ui.createStackLayout + ({ children: [ - ui.createEntry - ({ - text: renderer.levels[i], - row: 0, - column: 0 - }), - ui.createLatexLabel + ui.createScrollView ({ - text: Localization.format(getLoc('labelChars'), - renderer.levels[i].length), - row: 0, - column: 1, - horizontalOptions: LayoutOptions.END_AND_EXPAND, - verticalOptions: LayoutOptions.CENTER + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.36, seqGrid.height)), + content: seqGrid }) ] + }) + }); + return menu; +} + +let createStateClipboardMenu = (values) => +{ + let totalLength = 0; + let tmpState = []; + let stateEntries = []; + for(let i = 0; i * ENTRY_CHAR_LIMIT < values.length; ++i) + { + tmpState.push(values.slice(i * ENTRY_CHAR_LIMIT, (i + 1) * + ENTRY_CHAR_LIMIT)); + stateEntries.push(ui.createEntry + ({ + text: tmpState[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpState[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } })); } - let seqGrid = ui.createGrid + let lengthLabel = ui.createLatexLabel ({ - columnDefinitions: ['20*', '80*'], - children: tmpLvls + text: Localization.format(getLoc('labelTotalLength'), totalLength), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let entryStack = ui.createStackLayout + ({ + children: stateEntries + }); + let addEntryButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = stateEntries.length; + tmpState[i] = ''; + stateEntries.push(ui.createEntry + ({ + text: tmpState[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpState[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + entryStack.children = stateEntries; + } }); let menu = ui.createPopup ({ - title: getLoc('menuSequence'), + title: getLoc('menuClipboard'), content: ui.createStackLayout ({ children: [ - ui.createScrollView + ui.createGrid ({ - // heightRequest: ui.screenHeight * 0.3, - content: seqGrid + columnDefinitions: ['70*', '30*'], + children: + [ + lengthLabel, + addEntryButton + ] }), + entryStack, ui.createBox ({ heightRequest: 1, @@ -4557,10 +5727,11 @@ let createSequenceMenu = () => }), ui.createButton ({ - text: getLoc('btnClose'), + text: getLoc('btnImport'), onClicked: () => { Sound.playClick(); + setInternalState(tmpState.join('')); menu.hide(); } }) @@ -4615,7 +5786,7 @@ let createWorldMenu = () => getLoc('terEqModes')[Number(tmpAC)]), row: 2, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }); let ACSwitch = ui.createSwitch ({ @@ -4654,12 +5825,30 @@ let createWorldMenu = () => } } }); + let tmpDCP = debugCamPath; + let DCPSwitch = ui.createSwitch + ({ + isToggled: tmpDCP, + row: 4, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpDCP = !tmpDCP; + DCPSwitch.isToggled = tmpDCP; + } + } + }); let tmpMCPT = maxCharsPerTick; let MCPTEntry = ui.createEntry ({ text: tmpMCPT.toString(), keyboard: Keyboard.NUMERIC, - row: 4, + row: 5, column: 1, horizontalTextAlignment: TextAlignment.END, onTextChanged: (ot, nt) => @@ -4686,7 +5875,7 @@ let createWorldMenu = () => text: getLoc('labelOfflineReset'), row: 0, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), ODSwitch, ui.createLatexLabel @@ -4694,7 +5883,7 @@ let createWorldMenu = () => text: getLoc('labelResetLvl'), row: 1, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), RLSwitch, ACLabel, @@ -4704,30 +5893,38 @@ let createWorldMenu = () => text: getLoc('labelMeasure'), row: 3, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), MPSwitch, ui.createLatexLabel ({ - text: getLoc('labelMaxCharsPerTick'), + text: getLoc('debugCamPath'), row: 4, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER + }), + DCPSwitch, + ui.createLatexLabel + ({ + text: getLoc('labelMaxCharsPerTick'), + row: 5, + column: 0, + verticalTextAlignment: TextAlignment.CENTER }), MCPTEntry, ui.createLatexLabel ({ text: getLoc('labelInternalState'), - row: 5, + row: 6, column: 0, - verticalOptions: LayoutOptions.CENTER + verticalTextAlignment: TextAlignment.CENTER }), ui.createButton ({ text: getLoc('btnClipboard'), - row: 5, + row: 6, column: 1, - heightRequest: 40, + heightRequest: SMALL_BUTTON_HEIGHT, onClicked: () => { let clipMenu = createStateClipboardMenu( @@ -4757,6 +5954,9 @@ let createWorldMenu = () => camMeasurer.reset(); } measurePerformance = tmpMP; + if(tmpDCP != debugCamPath) + renderer.reset(); + debugCamPath = tmpDCP; maxCharsPerTick = tmpMCPT; menu.hide(); } @@ -4777,24 +5977,9 @@ var getInternalState = () => JSON.stringify tickDelayMode: tickDelayMode, resetLvlOnConstruct: resetLvlOnConstruct, measurePerformance: measurePerformance, + debugCamPath: debugCamPath, maxCharsPerTick: maxCharsPerTick, - renderer: - { - figureScale: renderer.figScaleStr, - cameraMode: renderer.cameraMode, - camX: renderer.camXStr, - camY: renderer.camYStr, - camZ: renderer.camZStr, - followFactor: renderer.followFactor, - loopMode: renderer.loopMode, - upright: renderer.upright, - loadModels: renderer.loadModels, - quickDraw: renderer.quickDraw, - quickBacktrack: renderer.quickBacktrack, - backtrackList: [...renderer.backtrackList].join(''), - backtrackTail: renderer.backtrackTail, - hesitate: renderer.hesitate - }, + renderer: renderer.object, system: tmpSystem ? { title: tmpSystemName, @@ -4811,6 +5996,9 @@ var getInternalState = () => JSON.stringify var setInternalState = (stateStr) => { + if(!stateStr) + return; + let values = stateStr.split('\n'); let worldValues = values[0].split(' '); @@ -4836,6 +6024,8 @@ var setInternalState = (stateStr) => resetLvlOnConstruct = state.resetLvlOnConstruct; if('measurePerformance' in state) measurePerformance = state.measurePerformance; + if('debugCamPath' in state) + debugCamPath = state.debugCamPath; if('maxCharsPerTick' in state) maxCharsPerTick = state.maxCharsPerTick; @@ -4844,7 +6034,8 @@ var setInternalState = (stateStr) => tmpSystemName = state.system.title; tmpSystemDesc = state.system.desc; tmpSystem = new LSystem(state.system.axiom, state.system.rules, - state.system.turnAngle, state.system.seed, state.system.ignoreList); + state.system.turnAngle, state.system.seed, state.system.ignoreList, + state.system.tropism); } if('renderer' in state) @@ -4854,8 +6045,8 @@ var setInternalState = (stateStr) => state.renderer.camZ, state.renderer.followFactor, state.renderer.loopMode, state.renderer.upright, state.renderer.quickDraw, state.renderer.quickBacktrack, - state.renderer.backtrackList, state.renderer.loadModels, - state.renderer.backtrackTail, state.renderer.hesitate); + state.renderer.loadModels, state.renderer.backtrackTail, + state.renderer.hesitateApex, state.renderer.hesitateFork); } else renderer = new Renderer(system); @@ -4987,9 +6178,9 @@ var setInternalState = (stateStr) => var canResetStage = () => true; -var getResetStageMessage = () => getLoc('rerollSeed'); +var getResetStageMessage = () => getLoc('resetRenderer'); -var resetStage = () => renderer.seed = globalSeed.nextInt; +var resetStage = () => renderer.reset(); var getTertiaryEquation = () => { @@ -4999,7 +6190,13 @@ var getTertiaryEquation = () => return renderer.stateString; } -var get3DGraphPoint = () => renderer.cursor; +var get3DGraphPoint = () => +{ + if(debugCamPath) + return -renderer.camera; + + return renderer.cursor; +} var get3DGraphTranslation = () => { diff --git a/parametric.js b/parametric.js new file mode 100644 index 0000000..c61b8b9 --- /dev/null +++ b/parametric.js @@ -0,0 +1,6841 @@ +import { FreeCost } from '../api/Costs'; +import { theory } from '../api/Theory'; +import { Utils } from '../api/Utils'; +import { Vector3 } from '../api/Vector3'; +import { ui } from '../api/ui/UI'; +import { Color } from '../api/ui/properties/Color'; +import { FontFamily } from '../api/ui/properties/FontFamily'; +import { Keyboard } from '../api/ui/properties/Keyboard'; +import { LayoutOptions } from '../api/ui/properties/LayoutOptions'; +import { TextAlignment } from '../api/ui/properties/TextAlignment'; +import { Thickness } from '../api/ui/properties/Thickness'; +import { TouchType } from '../api/ui/properties/TouchType'; +import { Localization } from '../api/Localization'; +import { MathExpression } from '../api/MathExpression'; +import { ClearButtonVisibility } from '../api/ui/properties/ClearButtonVisibility'; +import { LineBreakMode } from '../api/ui/properties/LineBreakMode'; +import { BigNumber } from '../api/BigNumber'; +import { Upgrade } from '../api/Upgrades'; +import { Button } from '../api/ui/Button'; +import { Frame } from '../api/ui/Frame'; + +var id = 'parametric_L_systems_renderer'; +var getName = (language) => +{ + let names = + { + en: 'Param. L-systems Renderer', + }; + + return names[language] || names.en; +} +var getDescription = (language) => +{ + let descs = + { + en: +`An educational tool that allows you to model plants and fractal figures. + +Supported L-system features: +- Parametric, context-sensitive (2L) systems +- Stochastic (randomised) rules +- 3D turtle controls +- Polygon modelling + +Other features: +- Can save a whole army of systems! +- Camera modes: static and turtle-following +- Drawing speed and advanced stroke options! + +Note: Systems from LSR can be ported to P-LSR with minimal changes. However, ` + +`the opposite is rarely true.`, + }; + + return descs[language] || descs.en; +} +var authors = 'propfeds\n\nThanks to:\nSir Gilles-Philippe Paillé, for ' + + 'providing help with quaternions\nskyhigh173#3120, for ' + + 'suggesting clipboard and JSON internal state formatting'; +var version = 0; + +let time = 0; +let page = 0; +let offlineReset = true; +let gameIsOffline = false; +let altTerEq = false; +let tickDelayMode = false; +let resetLvlOnConstruct = true; +let measurePerformance = false; +let debugCamPath = false; +let normaliseQuaternions = false; +let maxCharsPerTick = 500; +let menuLang = Localization.language; + +let savedSystems = new Map(); + +let getImageSize = (width) => +{ + if(width >= 1080) + return 48; + if(width >= 720) + return 36; + if(width >= 360) + return 24; + + return 20; +} + +let getBtnSize = (width) => +{ + if(width >= 1080) + return 96; + if(width >= 720) + return 72; + if(width >= 360) + return 48; + + return 40; +} + +let getMediumBtnSize = (width) => +{ + if(width >= 1080) + return 88; + if(width >= 720) + return 66; + if(width >= 360) + return 44; + + return 36; +} + +let getSmallBtnSize = (width) => +{ + if(width >= 1080) + return 80; + if(width >= 720) + return 60; + if(width >= 360) + return 40; + + return 32; +} + +const BUTTON_HEIGHT = getBtnSize(ui.screenWidth); +const SMALL_BUTTON_HEIGHT = getSmallBtnSize(ui.screenWidth); +const ENTRY_CHAR_LIMIT = 5000; +const TRIM_SP = /\s+/g; +const LS_RULE = /([^:]+)(:(.+))?=(.*)/; +// Context doesn't need to check for nested brackets! +const LS_CONTEXT = +/((.)(\(([^\)]+)\))?<)?((.)(\(([^\)]+)\))?)(>(.)(\(([^\)]+)\))?)?/; +const BACKTRACK_LIST = new Set('+-&^\\/|[$T'); + +const locStrings = +{ + en: + { + versionName: 'v1.0', + welcomeSystemName: 'Mistletoe', + welcomeSystemDesc: 'Welcome to the Parametric L-systems Renderer.', + equationOverlayLong: '{0} – {1}\n\n{2}\n\n{3}', + equationOverlay: '{0}\n\n{1}', + + rendererBuildingTree: `\\begin{{matrix}}Building\\enspace ancestree...& + \\text{{Stg. {0}}}&({1}\\text{{ chars}})\\end{{matrix}}`, + rendererDeriving: `\\begin{{matrix}}Deriving...&\\text{{Stg. {0}}}&({1} +\\text{{ chars}})\\end{{matrix}}`, + + currencyTime: ' (elapsed)', + + varLvDesc: '\\text{{Stage: }}{0}{1}', + varTdDesc: '\\text{{Tick length: }}{0}\\text{{ sec}}', + varTdDescInf: '\\text{{Tick length: }}\\infty', + varTsDesc: '\\text{{Tickspeed: }}{0}/\\text{{sec}}', + upgResumeInfo: 'Resumes the last rendered system', + + saPatienceTitle: 'You\'re watching grass grow.', + saPatienceDesc: 'Let the renderer draw a 10-minute long figure or ' + + 'playlist.', + saPatienceHint: 'Be patient.', + + btnSave: 'Save', + btnClear: 'Clear All', + btnDefault: '* Reset to Defaults', + btnVar: 'Variables', + btnAdd: 'Add', + btnUp: '▲', + btnDown: '▼', + btnReroll: 'Reroll', + btnConstruct: 'Construct', + btnDelete: 'Delete', + btnView: 'View', + btnClipboard: 'Clipboard', + btnOverwrite: 'Overwrite', + btnSaveCopy: 'Save as Copy', + btnSelect: 'Select', + btnSelected: '(Selected)', + btnPrev: 'Previous', + btnNext: 'Next', + btnClose: 'Close', + btnImport: 'Import', + btnContents: 'Table of\nContents', + btnPage: '{0}', + + btnMenuLSystem: 'L-system menu', + btnMenuRenderer: 'Renderer menu', + btnMenuSave: 'Save/load', + btnMenuTheory: 'Settings', + btnMenuManual: 'User guide', + btnResume: 'Resume – {0}', + btnStartMeasure: 'Measure performance', + btnEndMeasure: 'Stop measuring', + + measurement: '{0}: max {1}ms, avg {2}ms over {3} ticks', + + rerollSeed: 'You are about to reroll the system\'s seed.', + resetRenderer: 'You are about to reset the renderer.', + + menuSequence: '{0} (Stage {1})', + labelLevelSeq: 'Stage {0}: {1} chars', + labelLevelSeqLoading: 'Stage {0}: {1}/{2} chars', + labelChars: '({0} chars)', + + menuLSystem: 'L-system Menu', + labelAxiom: 'Axiom: ', + labelAngle: 'Turning angle (°): ', + labelRules: 'Production rules: {0}', + labelIgnored: 'Turtle-ignored: ', + labelCtxIgnored: 'Context-ignored: ', + labelTropism: 'Tropism (gravity): ', + labelSeed: 'Seed (≠ 0): ', + menuVariables: 'Define Variables', + labelVars: 'Variables: {0}', + + menuRenderer: 'Renderer Menu', + labelInitScale: '* Initial scale: ', + labelFigScale: '* Figure scale: ', + labelCamMode: 'Camera mode: {0}', + camModes: ['Fixed', 'Linear', 'Quadratic'], + labelCamCentre: 'Fixed camera centre (x,): ', + labelCamOffset: '... centre (y, z): ', + labelFollowFactor: 'Follow factor (0-1): ', + labelLoopMode: 'Looping mode: {0}', + loopModes: ['Off', 'Stage', 'Playlist'], + labelUpright: '* Upright figure: ', + labelBTTail: 'Draw tail end: ', + labelLoadModels: '* Load models: ', + labelQuickdraw: '* Quickdraw: ', + labelQuickBT: '* Quick backtrack: ', + labelHesitate: '* Stutter on backtrack: ', + labelHesitateApex: '* Stutter at apex: ', + labelHesitateFork: '* Stutter at fork: ', + labelOldTropism: '* Alternate tropism method: ', + labelBTList: '* Backtrack list: ', + labelRequireReset: '* Modifying this setting will require a reset.', + + menuSave: 'Save/Load Menu', + labelCurrentSystem: 'Current system: ', + labelSavedSystems: 'Saved systems: {0}', + labelApplyCamera: 'Applies static camera: ', + + menuClipboard: 'Clipboard Menu', + labelTotalLength: 'Total length: {0}', + labelEntryCharLimit: `Warning: This entry has been capped at {0} ` + + `characters. Proceed with caution.`, + + menuNaming: 'Save System', + labelName: 'Title: ', + defaultSystemName: 'Untitled L-system', + labelDesc: 'Description: ', + noDescription: 'No description.', + duplicateSuffix: ' (copy)', + + menuTheory: 'Theory Settings', + labelOfflineReset: 'Reset graph on tabbing in: ', + labelResetLvl: 'Reset to stage 0 on construction: ', + labelTerEq: 'Tertiary equation: {0}', + terEqModes: ['Coordinates', 'Orientation'], + labelMeasure: 'Measure performance: ', + debugCamPath: 'Debug camera path: ', + labelMaxCharsPerTick: 'Maximum loaded chars/tick: ', + labelInternalState: 'Internal state: ', + + menuManual: 'User Guide ({0}/{1})', + menuTOC: 'Table of Contents', + labelSource: 'Reference: ', + manualSystemDesc: 'From the user guide, page {0}.', + manual: + [ + { + title: 'Introduction', + contents: +`Welcome to the Parametric L-systems Renderer! This guide aims to help you ` + +`understand parametric L-systems in detail, as well as instructions on how ` + +`to effectively use this theory to construct and render them. Before using ` + +`the theory however, it is recommended to try out the Classic version first. + +Without further a due, let's start discovering the wonders of L-systems. + +Notice: A gallery for regular L-systems has opened! Visit that theory instead.` + }, + { + title: 'Differences between LSR versions', + contents: +`First of all, the terminology for Level has been changed to Stage. +That was the only major change. + +Anyway. In Parametric, as multiple rules can exist for one symbol, they are ` + +`checked from top to bottom when deriving, regardless of context or condition. +This is different from how it was defined in The Algorithmic Beauty of ` + +`Plants, where rules with a context are prioritised. Therefore, arrange your ` + +`rules accordingly. + +The syntax for multiple derivations on the same line has also changed from , ` + +`(comma) to ; (semi-colon), due to the introduction of parameters. + +Finally, declaring models now has a different syntax to align more with ` + +`context-sensitivity: +~ > {symbol} = {model} +Referencing a model in another rule still uses the old syntax: +~{symbol}` + }, + { + title: 'Context-sensitivity', + contents: +`One of the main weaknesses in the original L-system syntax comes from the ` + +`fact that each symbol has no way of interacting with other symbols. This ` + +`makes it unfit for applications of cellular automata or modelling forms of ` + +`communication between a plant's organs. + +Context-sensitive L-systems allow this to work, by letting a symbol see both ` + +`its ancestor (the symbol to its immediate left), and its child to the right ` + +`(children, if it opens up multiple branches). A context-sensitive rule goes ` + +`as follows: +{left} < {symbol} > {right} = {derivation} +The symbol will only derive according to this rule if its ancestor bears the ` + +`same symbol as {left}, and one of its children bears the same symbol as ` + +`{right}. + +Note: P-LSR uses a context-sensitive variant called 2L-systems, where the 2 ` + +`means that a context is determined by two symbols: an ancestor and a child. ` + +`Another variant, 1L-systems, only determines a context in one direction. + +Note 2: The list of context-ignored symbols can be configured in the ` + +`L-system menu. These symbols will be left out of the ancestree computation.` + }, + { + title: 'Example: Signal propagation', + contents: +`A simple signal sent by the concurrent youth. The signal, denoted by the ` + +`letter B, starts from the 🅱ase of the tree and travels to the to🅿️. + +Axiom: B[+AAA]A[-AA]A[+A]A +B0 = 0 +0<0>1 = 1[-F1F1] +0<1>0 = 1 +0<1>1 = 1 +1<0>0 = 0 +1<0>1 = 1F1 +1<1>0 = 1 +1<1>1 = 0 ++ = - +- = + +Turtle-ignored: 01 +Context-ignored: F+- +Turning angle: 22.5° + +Applies static camera: +Scale: lv+2 +Centre: (0, lv/2+1, 0) +Upright` + }, + { + title: 'Parametric L-systems: A primer', + contents: +`With many of the systems we have encountered until now, we know that their ` + +`size can grow quickly, even exponentially. One segment of a tree branch ` + +`could be sixteen Fs long, and its trunk 128 metres. As a result, the ` + +`L-system very quickly loses its readability. Another consequence, is that ` + +`we may not express any length precisely using only integers. +Then, a solution to that would be the ability to embed extra information ` + +`onto each symbol, perhaps to lengthen a segment to the desired ratio, or ` + +`turn around by a specific angle. And thus, Lindenmayer proposed the idea of ` + +`parametric L-systems. + +F(d): moves turtle forward by a distance d, and draw a line. +(Note: other letters move by 1 instead of taking the parameter.) ++-&^\\/(a): rotate turtle by an angle of a degrees. +T(g): applies a tropism force of g. +T(g, x, y, z): applies a tropism of g in the direction (x, y, z).` + }, + { + title: 'Example: The Koch curve', + contents: +`The Koch curve belongs to a family of self-similar fractals generated by ` + +`the iterated function systems (IFS) method. An IFS can construct a fractal ` + +`by recursively applying affine transformations (translation, rotation, ` + +`scaling) to an initial figure... an axiom! Many IFSs can be reconstructed ` + +`using parametric L-systems, as long as only lines are involved. + +Axiom: F(1) +F(d) = F(d/3)+F(d/3)-(120)F(d/3)+F(d/3) +Turning angle: 60° + +Applies static camera: +Scale: 1/2 +Centre: (1/2, sqrt(3)/12, 0)` + }, + { + title: 'Parametric 2L-systems', + contents: +`Beyond geometric applications, parametric L-systems allow individual ` + +`symbols to hold additional information such as its state of growth, elapsed ` + +`time, etc. They can be even peeked at in context rules! +The syntax for a parametric rule goes as follows: +{symbol}({param_0},...) : {condition*} = {derivation_0} : {probability**} ;... + +Examples: +I(t) : t>0 = FI(t-1) +A(t) : t>5 = B(t+1)CD(t^0.5, t-2) +Example including context: +A(x) < B(y) > C(z) : x+y+z>10 = E((x+y)/2)F((y+z)/2) + +Note: All arithmetic processing in parameters is done using the game's ` + +`MathExpression class (just like the formulae for Auto-prestige/supremacy). ` + +`As such, there are several unavailable expressions: +% (modulus) +== (equality) +true, false (keywords) +a ? b : c (conditional ternary) +Conversion (from boolean to number) +For more information, check the Math Expression manual in the Auto-prestige ` + +`settings. + +* When omitted, the condition is assumed to be always true. +** When omitted, the chance is assumed to be 100%.` + }, + { + title: 'Example: Stamp stick', + contents: +`Adopted from an L-system made in Houdini. The symbol J represents 4 organs ` + +`at the same time, with the 'type' parameter controlling which model to load: +type <= 0: leaves, +type <= 1: flower bud, +type <= 2: blooming flower, +type <= 3: closed flower, +and the 's' parameter controlling the model's size. + +Variables: b = 2.25 +The variable b controls how quickly the model type switches, with lower ` + +`values being faster. A lower value also decreases the organ density. + +Axiom: FA(1) +A(t): t<=5*b = F(2/b)//[+(24)&B(t)]//[&B(t)]//[&B(t)]A(t+1) +B(t) = ~J(0.15, t/b-2) +J(s, type) = ~J(s*0.75+0.25, type) +~>J(s, type): type<=0 = {[+(32)F(s).-TF(s)TF.-TF(s)..][-(32)F(s)+(16)[TF(s)TF.].]} +~>J(s, type): type<=1 = [F~p(s)/(60)~p(s)/(60)~p(s)] +~>p(s) = {[+F(s/2).--F(s/2).][-F(s/2).].} +~>J(s, type): type<=2 = [FT~k(s)/(72)~k(s)/(72)~k(s)/(72)~k(s)/(72)~k(s)] +~>k(s) = {[F(s).+(72)[&F(s-0.3).][F(s)..][^F(s-0.3).].]} +~>J(s, type): type<=3 = [FT~m(s)/(72)~m(s)/(72)~m(s)/(72)~m(s)/(72)~m(s)] +~>m(s) = {[+(24)F(s).-F(s/2)..].} +Turtle-ignored: A +Turning angle: 48° +Tropism: 0.16 + +Applies static camera: +Scale: 7 +Centre: (0, lv/2+1, 0) +Upright`, + source: 'https://www.houdinikitchen.net/2019/12/21/how-to-create-l-systems/' + }, + { + title: 'Example: Mistletoe', + contents: +`Welcome to the Parametric L-systems Renderer! + +Axiom: ++M(0) +M(t): t<2 = F~M(t+1) +M(t): t<3 = [&T$~M(t+1, 0)]/(120)[&T$M(t+1, 0)]/(120)[&T$M(t+1, 0)] +M(t, i): t<5 = F~M(t+1, i): 0.7-i; F~K(0)~M(t+1, i+0.3): 0.3+i +M(t, i): t>=5 = [&T~M(t-2, i+0.3)]/(180)[&TM(t-2, i+0.3)] +~> M(t): t<3 = [+(48)~L(t)]/(180)[+(48)~L(t)] +~> M(t, i) = [+(48)~L(2+0.4*t)]/(180)[+(48)~L(2+0.4*t)] +~> L(t) = {[+(16)TF(t/6).&(16)-(16)TF(t/10).-TF(t/8)..][-(16)TF(t/6)[&(16)+(16)TF(t/10).].]} +K(c)>M(t, i): c<3 = ~K(c+1): 0.7-t/10, ~B(0.3): 0.3+t/10 +K(t): t<3 = ~K(t+1): 0.3+t/10, : 0.7-t/10 +K(t): t>=3 = [&&\\~B(0.3)]/(120)[&&\\~B(0.24)]/(120)[&&~B(0.27)] +~> K(t) = [&&F(0.3+t/10)]/(120)[&&F(0.3+t/10)]/(120)[&&F(0.3+t/10)] +B(s) = ~B(s*0.9+0.1) +~> B(s) = {[-(67.5)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).]} +Turning angle: 32° +Seed: 1362151494 +Tropism: 0.16 + +Applies static camera: +Scale: 6 +Centre: (3, 3, 0)` + }, + { + title: 'Appendix: Model discussions', + contents: +`About the differences between LSR models (which disappear after one stage) ` + +`and The Algorithmic Beauty of Plants (which stick around). +The motive behind this mechanism in LSR, is that when a symbol moves to ` + +`another place, the original ~ (tilde) ends up referencing another symbol ` + +`entirely, which both wastes one space and can introduce errors by drawing ` + +`the wrong model. +Another reason is due to the fact that models in LSR consist of regular ` + +`symbols instead of being created using traditional techniques, but the ` + +`resulting models are not derivable, perhaps it is a more intuitive choice ` + +`to let them disappear. + +This design choice is not final, and if feedback can prove its ` + +`inconvenience, further considerations can be made: should it stick around ` + +`like defined in Abop? Should references be ditched entirely and models be ` + +`drawn automatically without having to reference? Will the syntax cease to ` + +`make sense?` + }, + { + title: 'Appendix: Credits', + contents: +`First of all, a biggest thanks goes out to Sir Gilles-Philippe Paillé of ` + +`Conic Games. Without his work on the game Exponential Idle, the foundation ` + +`for this theory, perhaps I would have never found myself writing this page ` + +`today. He, along with the people in the #custom-theories-dev channel, has ` + +`also helped me with numerous problems of custom theories (CT) development. +If by any chance you have not yet tried this game, I highly recommend it. ` + +`The download link is provided at the bottom of this page.`, + source: 'https://conicgames.github.io/exponentialidle' + }, + { + title: 'Credits (2)', + contents: +`The other massive thanks goes to Algorithmic Botany, the research team that ` + +`has been expanding Lindenmayer's work on L-systems. The site also hosts the ` + +`book 'The Algorithmic Beauty of Plants', which has been the primary source ` + +`of reference in the development of LSR, including the various syntax and ` + +`processing rules, as well as explanations to the scientific motives behind ` + +`the design of L-systems.`, + source: 'http://algorithmicbotany.org/' + } + ] + } +}; + +/** + * Returns a localised string. + * @param {string} name the string's internal name. + * @returns {string} + */ +let getLoc = (name, lang = menuLang) => +{ + if(lang in locStrings && name in locStrings[lang]) + return locStrings[lang][name]; + + if(name in locStrings.en) + return locStrings.en[name]; + + return `String missing: ${lang}.${name}`; +} + +/** + * Returns a string of a fixed decimal number, with a fairly uniform width. + * @param {number} x the number. + * @returns {string} + */ +let getCoordString = (x) => x.toFixed(x >= -0.01 ? + (x <= 9.999 ? 3 : (x <= 99.99 ? 2 : 1)) : + (x < -9.99 ? (x < -99.9 ? 0 : 1) : 2) +); + +/** + * Represents an instance of the Xorshift RNG. + */ +class Xorshift +{ + /** + * @constructor + * @param {number} seed must be initialized to non-zero. + */ + constructor(seed = 0) + { + this.x = seed; + this.y = 0; + this.z = 0; + this.w = 0; + for(let i = 0; i < 64; ++i) + this.nextInt; + } + /** + * Returns a random integer within [0, 2^31) probably. + * @returns {number} + */ + get nextInt() + { + let t = this.x ^ (this.x << 11); + this.x = this.y; + this.y = this.z; + this.z = this.w; + this.w ^= (this.w >> 19) ^ t ^ (t >> 8); + return this.w; + } + /** + * Returns a random floating point number within [0, 1). + * @returns {number} + */ + get nextFloat() + { + return (this.nextInt >>> 0) / ((1 << 30) * 2); + } + /** + * Returns a full random double floating point number using 2 rolls. + * @returns {number} + */ + get nextDouble() + { + let top, bottom, result; + do + { + top = this.nextInt >>> 10; + bottom = this.nextFloat; + result = (top + bottom) / (1 << 21); + } + while(result === 0); + return result; + } + /** + * Returns a random integer within a range of [start, end). + * @param {number} start the range's lower bound. + * @param {number} end the range's upper bound, plus 1. + * @returns {number} + */ + nextRange(start, end) + { + // [start, end) + let size = end - start; + return start + Math.floor(this.nextFloat * size); + } + /** + * Returns a random element from an array. + * @param {any[]} array the array. + * @returns {any} + */ + choice(array) + { + return array[this.nextRange(0, array.length)]; + } +} + +/** + * Represents one hell of a quaternion. + */ +class Quaternion +{ + /** + * @constructor + * @param {number} r (default: 1) the real component. + * @param {number} i (default: 0) the imaginary i component. + * @param {number} j (default: 0) the imaginary j component. + * @param {number} k (default: 0) the imaginary k component. + */ + constructor(r = 1, i = 0, j = 0, k = 0) + { + /** + * @type {number} the real component. + */ + this.r = r; + /** + * @type {number} the imaginary i component. + */ + this.i = i; + /** + * @type {number} the imaginary j component. + */ + this.j = j; + /** + * @type {number} the imaginary k component. + */ + this.k = k; + } + + /** + * Computes the sum of the current quaternion with another. Does not modify + * the original quaternion. + * @param {Quaternion} quat this other quaternion. + * @returns {Quaternion} + */ + add(quat) + { + return new Quaternion( + this.r + quat.r, + this.i + quat.i, + this.j + quat.j, + this.k + quat.k + ); + } + /** + * Computes the product of the current quaternion with another. Does not + * modify the original quaternion. + * @param {Quaternion} quat this other quaternion. + * @returns {Quaternion} + */ + mul(quat) + { + let t0 = this.r * quat.r - this.i * quat.i - + this.j * quat.j - this.k * quat.k; + let t1 = this.r * quat.i + this.i * quat.r + + this.j * quat.k - this.k * quat.j; + let t2 = this.r * quat.j - this.i * quat.k + + this.j * quat.r + this.k * quat.i; + let t3 = this.r * quat.k + this.i * quat.j - + this.j * quat.i + this.k * quat.r; + + let result = new Quaternion(t0, t1, t2, t3); + if(normaliseQuaternions) + return result.normalise; + else + return result; + } + /** + * Rotates the quaternion by some degrees. + * @param {number} degrees degrees. + * @param {string} symbol the corresponding symbol in L-system language. + */ + rotate(degrees = 0, symbol = '+') + { + if(degrees == 0) + return this; + + let halfAngle = degrees * Math.PI / 360; + let s = Math.sin(halfAngle); + let c = Math.cos(halfAngle); + let rotation; + switch(symbol) + { + case '+': + rotation = new Quaternion(-c, 0, 0, s); + break; + case '-': + rotation = new Quaternion(-c, 0, 0, -s); + break; + case '&': + rotation = new Quaternion(-c, 0, s, 0); + break; + case '^': + rotation = new Quaternion(-c, 0, -s, 0); + break; + case '\\': + rotation = new Quaternion(-c, s, 0, 0); + break; + case '/': + rotation = new Quaternion(-c, -s, 0, 0); + break; + default: + return this; + } + return rotation.mul(this); + } + /** + * Computes the negation of a quaternion. The negation also acts as the + * inverse if the quaternion's norm is 1, which is the case with rotation + * quaternions. + * @returns {Quaternion} + */ + get neg() + { + return new Quaternion(this.r, -this.i, -this.j, -this.k); + } + /** + * Computes the norm of a quaternion. + * @returns {number} + */ + get norm() + { + return Math.sqrt(this.r ** 2 + this.i ** 2 + this.j ** 2 + this.k ** 2); + } + /** + * Normalises a quaternion. + * @returns {Quaternion} + */ + get normalise() + { + let n = this.norm; + return new Quaternion(this.r / n, this.i / n, this.j / n, this.k / n); + } + /** + * Returns a heading vector from the quaternion. + * @returns {Vector3} + */ + get headingVector() + { + let r = this.neg.mul(xUpQuat).mul(this); + return new Vector3(r.i, r.j, r.k); + } + /** + * Returns an up vector from the quaternion. + * @returns {Vector3} + */ + get upVector() + { + let r = this.neg.mul(yUpQuat).mul(this); + return new Vector3(r.i, r.j, r.k); + } + /** + * Returns a side vector (left or right?) from the quaternion. + * @returns {Vector3} + */ + get sideVector() + { + let r = this.neg.mul(zUpQuat).mul(this); + return new Vector3(r.i, r.j, r.k); + } + /** + * (Deprecated) Rotate from a heading vector to another. Inaccurate! + * @param {Vector3} src the current heading. + * @param {Vector3} dst the target heading. + * @returns {Quaternion} + */ + rotateFrom(src, dst) + { + let dp = src.x * dst.x + src.y * dst.y + + src.z * dst.z; + let rotAxis; + if(dp < -1 + 1e-8) + { + /* Edge case + If the two vectors are in opposite directions, just reverse. + */ + return zUpQuat.mul(this); + } + rotAxis = new Vector3( + src.y * dst.z - src.z * dst.y, + src.z * dst.x - src.x * dst.z, + src.x * dst.y - src.y * dst.x, + ); + let s = Math.sqrt((1 + dp) * 2); + // I forgore that our quaternions have to be all negative, dunnoe why + return this.mul(new Quaternion( + -s / 2, + rotAxis.x / s, + rotAxis.y / s, + rotAxis.z / s + )); + } + /** + * https://stackoverflow.com/questions/71518531/how-do-i-convert-a-direction-vector-to-a-quaternion + * (Deprecated) Applies a gravi-tropism vector to the quaternion. + * @param {number} weight the vector's length (negative for upwards). + * @returns {Quaternion} + */ + applyTropismVector(weight = 0) + { + if(weight == 0) + return this; + + let curHead = this.headingVector; + let newHead = curHead - new Vector3(0, weight, 0); + let n = newHead.length; + if(n == 0) + return this; + newHead /= n; + let result = this.rotateFrom(curHead, newHead); + return result; + } + /** + * Applies a gravi-tropism vector to the quaternion. + * @param {number} weight the branch's susceptibility to bending. + * @param {number} x the tropism vector's x component. + * @param {number} y the tropism vector's y component. + * @param {number} z the tropism vector's z component. + * @returns {Quaternion} + */ + applyTropism(weight = 0, x = 0, y = -1, z = 0) + { + if(weight == 0) + return this; + + // a = e * |HxT| (n) + let curHead = this.headingVector; + let rotAxis = new Vector3( + curHead.y * z - curHead.z * y, + curHead.z * x - curHead.x * z, + curHead.x * y - curHead.y * x, + ); + let n = rotAxis.length; + if(n == 0) + return this; + rotAxis /= n; + let a = weight * n / 2; + let s = Math.sin(a); + let c = Math.cos(a); + // I don't know why it works the opposite way this time + return this.mul(new Quaternion( + -c, + rotAxis.x * s, + rotAxis.y * s, + rotAxis.z * s + )); + } + /** + * https://gamedev.stackexchange.com/questions/198977/how-to-solve-for-the-angle-of-a-axis-angle-rotation-that-gets-me-closest-to-a-sp/199027#199027 + * Rolls the quaternion so that its up vector aligns with the earth. + * @returns {Quaternion} + */ + alignToVertical() + { + // L = V×H / |V×H| + let curHead = this.headingVector; + let curUp = this.upVector; + let side = new Vector3(curHead.z, 0, -curHead.x); + let n = side.length; + if(n == 0) + return this; + side /= n; + // U = HxL + let newUp = new Vector3( + curHead.y * side.z - curHead.z * side.y, + curHead.z * side.x - curHead.x * side.z, + curHead.x * side.y - curHead.y * side.x, + ); + let a = Math.atan2( + curUp.x * side.x + curUp.y * side.y + curUp.z * side.z, + curUp.x * newUp.x + curUp.y * newUp.y + newUp.z * newUp.z, + ) / 2; + let s = Math.sin(a); + let c = Math.cos(a); + return new Quaternion(-c, s, 0, 0).mul(this); + } + /** + * Returns the quaternion's string representation. + * @returns {string} + */ + toString() + { + return `${getCoordString(this.r)} + ${getCoordString(this.i)}i + ${getCoordString(this.j)}j + ${getCoordString(this.k)}k`; + } +} + +/** + * Represents a parametric L-system. + */ +class LSystem +{ + /** + * @constructor + * @param {string} axiom the starting sequence. + * @param {string[]} rules the production rules. + * @param {string} turnAngle the turning angle (in degrees). + * @param {number} seed the seed used for stochastic systems. + * @param {string} ignoreList a list of symbols to be ignored by the turtle. + * @param {string} ctxIgnoreList a list of symbols ignored when deriving + * context. + * @param {string} tropism the tropism factor. + * @param {object} variables globally defined variables for the system. + */ + constructor(axiom = '', rules = [], turnAngle = 0, seed = 0, + ignoreList = '', ctxIgnoreList = '', tropism = 0, variables = {}) + { + // User input + this.userInput = + { + axiom: axiom, + rules: this.purgeEmpty(rules), + turnAngle: turnAngle, + seed: seed, + ignoreList: ignoreList, + ctxIgnoreList: ctxIgnoreList, + tropism: tropism, + variables: variables + }; + // I want to transfer them to a map to deep copy them. LS menu uses + // arrays so we're fine on that. + this.variables = new Map(); + for(let key in variables) + this.variables.set(key, MathExpression.parse(variables[key]). + evaluate()); + + let axiomMatches = this.parseSequence(axiom.replace(TRIM_SP, '')); + this.axiom = axiomMatches.result; + this.axiomParams = axiomMatches.params; + + // Manually calculate axiom parameters + for(let i = 0; i < this.axiomParams.length; ++i) + { + if(!this.axiomParams[i]) + continue; + + let params = this.axiomParams[i].split(','); + for(let j = 0; j < params.length; ++j) + params[j] = MathExpression.parse(params[j]).evaluate( + (v) => this.variables.get(v)); + this.axiomParams[i] = params; + // Maybe leave them at BigNumber? + } + + let ruleMatches = []; + for(let i = 0; i < this.userInput.rules.length; ++i) + { + ruleMatches.push([...this.userInput.rules[i].replace(TRIM_SP, ''). + match(LS_RULE)]); + // Indices 1, 3, 4 are context, condition, and all derivations + } + this.rules = new Map(); + this.models = new Map(); + for(let i = 0; i < ruleMatches.length; ++i) + { + // [i][1]: context + let contextMatch = [...ruleMatches[i][1].match(LS_CONTEXT)]; + // Indices 2, 4, 6, 8, 10, 12 are the symbols and parameters of + // left, middle, and right respectively + if(!contextMatch[6]) + continue; + + let tmpRule = {}; + let ruleParams = {}; + if(contextMatch[8]) + { + let params = contextMatch[8].split(','); + for(let j = 0; j < params.length; ++j) + ruleParams[params[j]] = ['m', j]; + } + tmpRule.left = contextMatch[2]; + if(tmpRule.left && contextMatch[4]) + { + let params = contextMatch[4].split(','); + for(let j = 0; j < params.length; ++j) + ruleParams[params[j]] = ['l', j]; + } + tmpRule.right = contextMatch[10]; + if(tmpRule.right && contextMatch[12]) + { + let params = contextMatch[12].split(','); + for(let j = 0; j < params.length; ++j) + { + ruleParams[params[j]] = ['r', j]; + } + } + tmpRule.params = ruleParams; + /* // O(1) lookup with O(n) memory, I think + ruleParams = { + 'a': ['m', 0], + 'b': ['l', 0], + 'c': ['r', 0], + 'd': ['r', 1] + }; + */ + tmpRule.paramMap = (v, l, m, r) => + { + let pos = tmpRule.params[v][0]; + let result = null; + switch(pos) + { + case 'm': + if(m) + { + result = m[tmpRule.params[v][1]]; + break; + } + case 'l': + if(l) + { + result = l[tmpRule.params[v][1]]; + break; + } + case 'r': + if(r) + { + result = r[tmpRule.params[v][1]]; + break; + } + } + // log(`${v} = ${result}`); + return result; + // MathExpression eval: (v) => rule.paramMap(v, params[l], ...) + } + + // [i][3]: condition + if(ruleMatches[i][3]) + tmpRule.condition = MathExpression.parse(ruleMatches[i][3]); + else + tmpRule.condition = MathExpression.parse('1'); + + // [i][4]: everything else + // Doing just comma instead of semi-colon will ruin the parameters! + let tmpRuleMatches = ruleMatches[i][4].split(';'); + for(let j = 0; j < tmpRuleMatches.length; ++j) + { + if(typeof tmpRuleMatches[j] === 'undefined') + continue; + + tmpRuleMatches[j] = tmpRuleMatches[j].split(':'); + let tmpDeriv = this.parseSequence(tmpRuleMatches[j][0]); + let derivParams = tmpDeriv.params; + for(let k = 0; k < derivParams.length; ++k) + { + if(!derivParams[k]) + continue; + + let params = derivParams[k].split(','); + for(let l = 0; l < params.length; ++l) + params[l] = MathExpression.parse(params[l]); + + derivParams[k] = params; + } + if(typeof tmpRule.derivations === 'string') + { + tmpRule.derivations = [tmpRule.derivations, + tmpDeriv.result]; + tmpRule.parameters = [tmpRule.parameters, derivParams]; + if(tmpRuleMatches[j][1]) + tmpRule.chances = [tmpRule.chances, + MathExpression.parse(tmpRuleMatches[j][1])]; + else + tmpRule.chances = [tmpRule.chances, + MathExpression.parse('1')]; + } + else if(!tmpRule.derivations) + { + tmpRule.derivations = tmpDeriv.result; + tmpRule.parameters = derivParams; + if(tmpRuleMatches[j][1]) + tmpRule.chances = MathExpression.parse( + tmpRuleMatches[j][1]); + else + tmpRule.chances = MathExpression.parse('1'); + } + else // Already an array + { + tmpRule.derivations.push(tmpDeriv.result); + tmpRule.parameters.push(derivParams); + if(tmpRuleMatches[j][1]) + tmpRule.chances.push(MathExpression.parse( + tmpRuleMatches[j][1])); + else + tmpRule.chances.push(MathExpression.parse('1')); + } + } + + // Finally, push rule + if(contextMatch[6] == '~') + { + if(!this.models.has(contextMatch[10])) + this.models.set(contextMatch[10], []); + this.models.get(contextMatch[10]).push(tmpRule); + } + else + { + if(!this.rules.has(contextMatch[6])) + this.rules.set(contextMatch[6], []); + this.rules.get(contextMatch[6]).push(tmpRule); + } + } + + this.ignoreList = new Set(ignoreList); + this.ctxIgnoreList = new Set(ctxIgnoreList); + + this.RNG = new Xorshift(seed); + this.halfAngle = MathExpression.parse(turnAngle.toString()).evaluate( + (v) => this.variables.get(v)). + toNumber() * Math.PI / 360; + + this.rotations = new Map(); + let s = Math.sin(this.halfAngle); + let c = Math.cos(this.halfAngle); + this.rotations.set('+', new Quaternion(-c, 0, 0, s)); + this.rotations.set('-', new Quaternion(-c, 0, 0, -s)); + this.rotations.set('&', new Quaternion(-c, 0, s, 0)); + this.rotations.set('^', new Quaternion(-c, 0, -s, 0)); + this.rotations.set('\\', new Quaternion(-c, s, 0, 0)); + this.rotations.set('/', new Quaternion(-c, -s, 0, 0)); + + this.tropism = MathExpression.parse(tropism.toString()).evaluate( + (v) => this.variables.get(v)). + toNumber(); + } + + /** + * Parse a sequence to return one array of characters and one of parameters. + * Is only used when initialising the L-system. + * @param {string} sequence the sequence to be parsed. + * @returns {object} + */ + parseSequence(sequence) + { + let result = ''; + let resultParams = []; + let bracketLvl = 0; + let start = null; + for(let i = 0; i < sequence.length; ++i) + { + switch(sequence[i]) + { + case ' ': + log('Blank space detected.') + break; + case '(': + ++bracketLvl; + if(bracketLvl == 1) + start = i + 1; + break; + case ')': + if(!bracketLvl) + { + log('You\'ve clearly made a bracket error.'); + break; + } + --bracketLvl; + if(!bracketLvl) + resultParams.push(sequence.slice(start, i)); + break; + default: + if(bracketLvl) + break; + + result += sequence[i]; + if(sequence[i + 1] != '(') + resultParams.push(null); + break; + } + } + return { + result: result, + params: resultParams + }; + // Tested this out on Chrome console, it worked. + } + /** + * Returns and ancestree and a child tree for a sequence. + * @param {string} sequence the sequence. + * @returns {object} + */ + getAncestree(sequence, task = {}) + { + // Scanning behaviour should be very similar to renderer drawing. + let tmpStack = task.stack || []; + let tmpIdxStack = task.idxStack || []; + let tmpAncestors = task.ancestors || []; + let tmpChildren = task.children || []; + let i = task.start || 0; + for(; i < sequence.length; ++i) + { + if(i - task.start > maxCharsPerTick) + { + return { + start: i, + stack: tmpStack, + idxStack: tmpIdxStack, + ancestors: tmpAncestors, + children: tmpChildren + }; + } + switch(sequence[i]) + { + case ' ': + log('Blank space detected.') + break; + case '[': + tmpIdxStack.push(tmpStack.length); + break; + case ']': + if(tmpStack.length == 0) + { + log('You\'ve clearly made a bracket error.'); + break; + } + while(tmpStack.length > tmpIdxStack[tmpIdxStack.length - 1]) + tmpStack.pop(); + + tmpIdxStack.pop(); + break; + default: + let ignored = this.ctxIgnoreList.has(sequence[i]); + if(ignored) + break; + + if(tmpStack.length > 0) + { + let ancIdx = tmpStack[tmpStack.length - 1]; + tmpAncestors[i] = ancIdx; + if(typeof tmpChildren[ancIdx] === 'undefined') + tmpChildren[ancIdx] = []; + tmpChildren[ancIdx].push(i); + } + + tmpStack.push(i); + break; + } + } + return { + start: 0, + stack: tmpStack, + idxStack: tmpIdxStack, + ancestors: tmpAncestors, + children: tmpChildren + }; + } + + /** + * Derive a sequence from the input string. `next` denotes the starting + * position to be derived next tick. `result` contains the work completed + * for the current tick. + * @param {string} sequence the input string. + * @returns {{start: number, result: string}} + */ + derive(sequence, seqParams, ancestors, children, task = {}) + { + let result = task.derivation || ''; + let resultParams = task.parameters || []; + let i = task.start || 0; + let charCount = 0; + for(; i < sequence.length; ++i) + { + if(charCount > maxCharsPerTick) + { + return { + start: i, + charCount: charCount, + derivation: result, + parameters: resultParams + }; + } + if(sequence[i] == '%') + { + let branchLvl = 0; + for(; i < sequence.length; ++i) + { + switch(sequence[i]) + { + case '[': + ++branchLvl; + break; + case ']': + --branchLvl; + break; + } + if(branchLvl < 0) + break; + } + if(sequence[i] == ']') + { + result += sequence[i]; + ++charCount; + resultParams.push(null); + } + else + continue; + } + else if(sequence[i] == '~') + continue; + else if(this.rules.has(sequence[i])) + { + let tmpRules = this.rules.get(sequence[i]); + let ruleChoice = -1; + for(let j = 0; j < tmpRules.length; ++j) + { + // Left and right first + if(tmpRules[j].left && tmpRules[j].left != + sequence[ancestors[i]]) + continue; + + let right = -1; + if(tmpRules[j].right) + { + if(children[i]) + { + for(let k = 0; k < children[i].length; ++k) + { + if(tmpRules[j].right == sequence[children[i][ + k]]) + { + right = children[i][k]; + break; + } + } + } + if(right == -1) + continue; + } + + let tmpParamMap = (v) => this.variables.get(v) || + tmpRules[j].paramMap(v, seqParams[ancestors[i]], + seqParams[i], seqParams[right]); + // Next up is the condition + if(tmpRules[j].condition.evaluate(tmpParamMap) == + BigNumber.ZERO) + continue; + + if(typeof tmpRules[j].derivations === 'string') + { + result += tmpRules[j].derivations; + charCount += tmpRules[j].derivations.length; + if(tmpRules[j].parameters) + { + for(let k = 0; k < tmpRules[j].parameters.length; + ++k) + { + let derivPi = null; + if(tmpRules[j].parameters[k]) + { + for(let l = 0; l < tmpRules[j].parameters[ + k].length; ++l) + { + if(tmpRules[j].parameters[k][l]) + { + if(!derivPi) + derivPi = []; + derivPi.push(tmpRules[j]. + parameters[k][l].evaluate( + tmpParamMap)); + } + } + } + resultParams.push(derivPi); + } + } + ruleChoice = j; + break; + } + else // Stochastic time + { + let roll = this.RNG.nextFloat; + let chanceSum = 0; + let choice = -1; + for(let k = 0; k < tmpRules[j].derivations.length; ++k) + { + // Example + // roll: 0.50 + // chance 1: 0.00 - 0.49 + // sum after 1: 0.50 + // chance 2: 0.50 - 0.99 + // sum after 2: 1 (select!) + chanceSum += tmpRules[j].chances[k].evaluate( + tmpParamMap); + if(chanceSum > roll) // select this + { + choice = k; + result += tmpRules[j].derivations[k]; + charCount += tmpRules[j].derivations[k].length; + if(tmpRules[j].parameters[k]) + { + for(let l = 0; l < tmpRules[j]. + parameters[k].length; ++l) + { + let derivPi = null; + if(tmpRules[j].parameters[k][l]) + { + for(let m = 0; m < tmpRules[j]. + parameters[k][l].length; ++m) + { + if(tmpRules[j]. + parameters[k][l][m]) + { + if(!derivPi) + derivPi = []; + derivPi.push(tmpRules[j]. + parameters[k][l][m]. + evaluate(tmpParamMap)); + } + } + } + resultParams.push(derivPi); + } + } + break; + } + } + // log(`roll = ${roll} choice = ${choice}`) + if(choice == -1) + continue; + ruleChoice = j; + break; + } + } + if(ruleChoice == -1) + { + result += sequence[i]; + ++charCount; + resultParams.push(...[seqParams[i]]); + } + } + else + { + result += sequence[i]; + ++charCount; + resultParams.push(...[seqParams[i]]); + } + } + return { + start: 0, + charCount: charCount, + derivation: result, + parameters: resultParams + }; + } + + deriveModel(symbol, params) + { + let result = ''; + let resultParams = []; + if(this.models.has(symbol)) + { + let tmpRules = this.models.get(symbol); + for(let j = 0; j < tmpRules.length; ++j) + { + let tmpParamMap = (v) => this.variables.get(v) || + tmpRules[j].paramMap(v, null, null, params); + // Next up is the condition + if(tmpRules[j].condition.evaluate(tmpParamMap) == + BigNumber.ZERO) + continue; + + if(typeof tmpRules[j].derivations === 'string') + { + result = tmpRules[j].derivations; + if(tmpRules[j].parameters) + { + for(let k = 0; k < tmpRules[j].parameters.length; + ++k) + { + let derivPi = null; + if(tmpRules[j].parameters[k]) + { + for(let l = 0; l < tmpRules[j].parameters[k]. + length; ++l) + { + if(tmpRules[j].parameters[k][l]) + { + if(!derivPi) + derivPi = []; + derivPi.push(tmpRules[j].parameters[k][ + l].evaluate(tmpParamMap)); + } + } + } + resultParams.push(derivPi); + } + } + break; + } + else // Stochastic time + { + // Models can be drawn any time, thus, the RNG should be + // separate from actual rule processing. + let roll = globalRNG.nextFloat; + let chanceSum = 0; + let choice = -1; + for(let k = 0; k < tmpRules[j].derivations.length; ++k) + { + // Example + // roll: 0.50 + // chance 1: 0.00 - 0.49 + // sum after 1: 0.50 + // chance 2: 0.50 - 0.99 + // sum after 2: 1 (select!) + chanceSum += tmpRules[j].chances[k].evaluate( + tmpParamMap); + if(chanceSum > roll) // select this + { + choice = k; + result = tmpRules[j].derivations[k]; + if(tmpRules[j].parameters[k]) + { + for(let l = 0; l < tmpRules[j]. + parameters[k].length; ++l) + { + let derivPi = null; + if(tmpRules[j].parameters[k][l]) + { + for(let m = 0; m < tmpRules[j]. + parameters[k][l].length; ++m) + { + if(tmpRules[j]. + parameters[k][l][m]) + { + if(!derivPi) + derivPi = []; + derivPi.push(tmpRules[j]. + parameters[k][l][m]. + evaluate(tmpParamMap)); + } + } + } + resultParams.push(derivPi); + } + } + break; + } + } + // log(`roll = ${roll} choice = ${choice}`) + if(choice == -1) + continue; + break; + } + } + } + return { + result: result, + params: resultParams + }; + } + + reconstruct(sequence, params, task = {}) + { + let result = task.result || ''; + let i = task.start || 0; + for(; i < sequence.length; ++i) + { + if((i - task.start) * (task.start + 1) > maxCharsPerTick) + { + return { + start: i, + result: result + } + } + result += sequence[i]; + if(params[i]) + result += `(${params[i].join(', ')})`; + } + return { + start: 0, + result: result + }; + } + /** + * Purge the rules of empty lines. + * @param {string[]} rules rules. + * @returns {string[]} + */ + purgeEmpty(rules) + { + let result = []; + let idx = 0; + for(let i = 0; i < rules.length; ++i) + { + // I hope this deep-copies + if(rules[i]) + { + result[idx] = rules[i]; + ++idx; + } + } + return result; + } + /** + * Returns a deep copy (hopefully) of the user input to prevent overwrites. + * @returns {{ + * axiom: string, + * rules: string[], + * turnAngle: string, + * seed: number, + * ignoreList: string, + * ctxIgnoreList: string, + * tropism: string, + * variables: object + * }} + */ + get object() + { + return { + axiom: this.userInput.axiom, + rules: this.purgeEmpty(this.userInput.rules), + turnAngle: this.userInput.turnAngle, + seed: this.userInput.seed, + ignoreList: this.userInput.ignoreList, + ctxIgnoreList: this.userInput.ctxIgnoreList, + tropism: this.userInput.tropism, + variables: this.userInput.variables + }; + } + /** + * Returns the system's string representation. + * @returns {string} + */ + toString() + { + return JSON.stringify(this.object, null, 4); + } +} + +/** + * The renderer handles all logic for drawing the L-system. + */ +class Renderer +{ + /** + * @constructor + * @param {LSystem} system the L-system to be handled. + * @param {string} figureScale the zoom level expression. + * @param {boolean} cameraMode the camera mode. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. + * @param {number} followFactor the camera's cursor-following speed. + * @param {number} loopMode the renderer's looping mode. + * @param {boolean} upright whether to rotate the system around the z-axis + * by 90 degrees. + * @param {boolean} quickDraw whether to skip through straight lines on the + * way forward. + * @param {boolean} quickBacktrack whether to skip through straight lines on + * the way backward. + * @param {boolean} loadModels whether to load dedicated models for symbols. + * @param {boolean} backtrackTail whether to backtrack at the end of a loop. + * @param {boolean} hesitateApex whether to stutter for 1 tick at apices. + * @param {boolean} hesitateFork whether to stutter for 1 tick at forks. + */ + constructor(system, figureScale = 1, cameraMode = 0, camX = 0, camY = 0, + camZ = 0, followFactor = 0.15, loopMode = 0, upright = false, + quickDraw = false, quickBacktrack = false, loadModels = true, + backtrackTail = false, hesitateApex = true, hesitateFork = true) + { + /** + * @type {LSystem} the L-system being handled. + */ + this.system = system; + /** + * @type {string} kept for comparison in the renderer menu. + */ + this.figScaleStr = figureScale.toString(); + /** + * @type {MathExpression} the figure scale expression. + */ + this.figScaleExpr = MathExpression.parse(this.figScaleStr); + /** + * @type {number} the calculated figure scale. + */ + this.figureScale = 1; + /** + * @type {boolean} the camera mode. + */ + this.cameraMode = Math.round(Math.min(Math.max(cameraMode, 0), 2)); + /** + * @type {string} kept for comparison in the renderer menu. + */ + this.camXStr = camX.toString(); + /** + * @type {string} kept for comparison in the renderer menu. + */ + this.camYStr = camY.toString(); + /** + * @type {string} kept for comparison in the renderer menu. + */ + this.camZStr = camZ.toString(); + /** + * @type {MathExpression} the camera x expression. + */ + this.camXExpr = MathExpression.parse(this.camXStr); + /** + * @type {MathExpression} the camera y expression. + */ + this.camYExpr = MathExpression.parse(this.camYStr); + /** + * @type {MathExpression} the camera z expression. + */ + this.camZExpr = MathExpression.parse(this.camZStr); + /** + * @type {Vector3} the calculated static camera coordinates. + */ + this.camCentre = new Vector3(0, 0, 0); + /** + * @type {number} the follow factor. + */ + this.followFactor = Math.min(Math.max(followFactor, 0), 1); + /** + * @type {number} the looping mode. + */ + this.loopMode = Math.round(Math.min(Math.max(loopMode, 0), 2)); + /** + * @type {boolean} the x-axis' orientation. + */ + this.upright = upright; + /** + * @type {boolean} whether to skip through straight lines on the way + * forward. + */ + this.quickDraw = quickDraw; + /** + * @type {boolean} whether to skip through straight lines on the way + * back. + */ + this.quickBacktrack = quickBacktrack; + /** + * @type {boolean} whether to load models. + */ + this.loadModels = loadModels; + /** + * @type {boolean} whether to backtrack at the end. + */ + this.backtrackTail = backtrackTail; + /** + * @type {boolean} whether to hesitate at apices. + */ + this.hesitateApex = hesitateApex; + /** + * @type {boolean} whether to hesitate at forks. + */ + this.hesitateFork = hesitateFork; + /** + * @type {Vector3} the turtle's position. + */ + this.state = new Vector3(0, 0, 0); + /** + * @type {Quaternion} the turtle's orientation. + */ + this.ori = this.upright ? uprightQuat : new Quaternion(); + /** + * @type {string[]} every level of the current system. + */ + this.levels = []; + this.levelParams = []; + /** + * @type {number} the current level (updates after buying the variable). + */ + this.lv = -1; + /** + * @type {number} the maximum level loaded. + */ + this.loaded = -1; + /** + * @type {number} the load target level. + */ + this.loadTarget = 0; + /** + * @type {[Vector3, Quaternion][]} stores cursor states for brackets. + */ + this.stack = []; + /** + * @type {number[]} stores the indices of the other stack. + */ + this.idxStack = []; + /** + * @type {string[]} keeps the currently rendered models. + */ + this.models = []; + this.modelParams = []; + /** + * @type {number[]} keeps the indices of the other stack. + */ + this.mdi = []; + /** + * @type {number} the current index of the sequence. + */ + this.i = 0; + /** + * @type {number} the elapsed time. + */ + this.elapsed = 0; + /** + * @type {number} the number of turns before the renderer starts working + * again. + */ + this.cooldown = 0; + /** + * @type {Vector3} the last tick's camera position. + */ + this.lastCamera = new Vector3(0, 0, 0); + /** + * @type {Vector3} the last tick's camera velocity. + */ + this.lastCamVel = new Vector3(0, 0, 0); + /** + * @type {object} the current ancestree task. + */ + this.ancestreeTask = + { + start: 0 + }; + /** + * @type {object} the current derivation task. + */ + this.deriveTask = + { + start: 0 + }; + /** + * @type {number} how many nested polygons currently in (pls keep at 1). + */ + this.polygonMode = 0; + } + + /** + * Updates the renderer's level. + * @param {number} level the target level. + * @param {boolean} seedChanged whether the seed has changed. + */ + update(level, seedChanged = false) + { + let clearGraph = this.loopMode != 2 || level < this.lv || seedChanged; + + if(this.lv != level) + { + this.reset(clearGraph); + this.lv = level; + this.figureScale = this.figScaleExpr.evaluate( + v => this.getVariable(v)).toNumber(); + if(this.figureScale == 0) + this.figureScale = 1; + this.camCentre = new Vector3 + ( + this.camXExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camYExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camZExpr.evaluate(v => this.getVariable(v)).toNumber() + ); + } + + this.loadTarget = Math.max(level, this.loadTarget); + + let charCount = 0; + for(let i = this.loaded + 1; i <= this.loadTarget; ++i) + { + // Threshold to prevent maximum statements error + if(charCount > maxCharsPerTick) + return; + + if(i == 0 && !('derivation' in this.deriveTask)) + { + this.deriveTask = + { + start: 0, + derivation: this.system.axiom, + parameters: this.system.axiomParams + }; + charCount += this.system.axiom.length; + } + else + { + // If no ancestor (next is obviously 0): do work + // If no ancestor and next isn't 0: panik + // If has ancestor and next is 0: move to derive work + // If has ancestor and next isn't 0: do work + + while(!('ancestors' in this.ancestreeTask) || + ('ancestors' in this.ancestreeTask && this.ancestreeTask.start)) + { + let at = this.system.getAncestree(this.levels[i - 1], + this.ancestreeTask); + charCount += (at.start - this.ancestreeTask.start); + this.ancestreeTask = at; + if(charCount > maxCharsPerTick) + return; + } + // TODO: convert derivation to new tasking model + if(!('derivation' in this.deriveTask) || + ('derivation' in this.deriveTask && this.deriveTask.start)) + { + let ret = this.system.derive(this.levels[i - 1], + this.levelParams[i - 1], this.ancestreeTask.ancestors, + this.ancestreeTask.children, this.deriveTask); + charCount += ret.charCount; + this.deriveTask = ret; + } + } + this.levels[i] = this.deriveTask.derivation; + this.levelParams[i] = this.deriveTask.parameters; + if(!this.deriveTask.start) + { + ++this.loaded; + this.ancestreeTask = + { + start: 0 + }; + this.deriveTask = + { + start: 0 + }; + } + else + return; + } + this.reset(clearGraph); + } + /** + * Resets the renderer. + * @param {boolean} clearGraph whether to clear the graph. + */ + reset(clearGraph = true) + { + this.state = new Vector3(0, 0, 0); + this.ori = this.upright ? uprightQuat : new Quaternion(); + this.stack = []; + this.idxStack = []; + this.i = 0; + this.models = []; + this.modelParams = []; + this.mdi = []; + this.cooldown = 0; + this.polygonMode = 0; + if(clearGraph) + { + this.elapsed = 0; + time = 0; + theory.clearGraph(); + } + theory.invalidateTertiaryEquation(); + } + /** + * Configures every parameter of the renderer, except the system. + * @param {string} figureScale the zoom level expression. + * @param {boolean} cameraMode the camera mode. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. + * @param {number} followFactor the camera's cursor-following speed. + * @param {number} loopMode the renderer's looping mode. + * @param {boolean} upright whether to rotate the system around the z-axis + * by 90 degrees. + * @param {boolean} quickDraw whether to skip through straight lines on the + * way forward. + * @param {boolean} quickBacktrack whether to skip through straight lines + * on the way backward. + * @param {boolean} loadModels whether to load dedicated models for symbols. + * @param {boolean} backtrackTail whether to backtrack at the end of a loop. + * @param {boolean} hesitateApex whether to stutter for 1 tick at apices. + * @param {boolean} hesitateFork whether to stutter for 1 tick at forks. + */ + configure(figureScale, cameraMode, camX, camY, camZ, followFactor, + loopMode, upright, quickDraw, quickBacktrack, loadModels, backtrackTail, + hesitateApex, hesitateFork) + { + let requireReset = (figureScale !== this.figScaleStr) || + (upright != this.upright) || (quickDraw != this.quickDraw) || + (quickBacktrack != this.quickBacktrack) || + (loadModels != this.loadModels) || + (hesitateApex != this.hesitateApex) || + (hesitateFork != this.hesitateFork); + + this.figScaleStr = figureScale.toString(); + this.figScaleExpr = MathExpression.parse(this.figScaleStr); + this.figureScale = this.figScaleExpr.evaluate( + v => this.getVariable(v)).toNumber(); + if(this.figureScale == 0) + this.figureScale = 1; + this.cameraMode = cameraMode; + this.camXStr = camX.toString(); + this.camYStr = camY.toString(); + this.camZStr = camZ.toString(); + this.camXExpr = MathExpression.parse(this.camXStr); + this.camYExpr = MathExpression.parse(this.camYStr); + this.camZExpr = MathExpression.parse(this.camZStr); + this.camCentre = new Vector3 + ( + this.camXExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camYExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camZExpr.evaluate(v => this.getVariable(v)).toNumber() + ); + this.followFactor = followFactor; + this.loopMode = loopMode; + this.upright = upright; + this.quickDraw = quickDraw; + this.quickBacktrack = quickBacktrack; + this.loadModels = loadModels; + this.backtrackTail = backtrackTail; + this.hesitateApex = hesitateApex; + this.hesitateFork = hesitateFork; + + if(requireReset) + this.reset(); + } + /** + * Configures only the parameters related to the static camera mode. + * @param {string} figureScale the zoom level expression. + * @param {string} camX the camera's x-axis centre. + * @param {string} camY the camera's y-axis centre. + * @param {string} camZ the camera's z-axis centre. + * @param {boolean} upright whether to rotate the system around the z-axis + * by 90 degrees. + */ + configureStaticCamera(figureScale, camX, camY, camZ, upright) + { + let requireReset = (figureScale !== this.figScaleStr) || + (upright != this.upright); + + this.figScaleStr = figureScale.toString(); + this.figScaleExpr = MathExpression.parse(this.figScaleStr); + this.figureScale = this.figScaleExpr.evaluate( + v => this.getVariable(v)).toNumber(); + if(this.figureScale == 0) + this.figureScale = 1; + this.camXStr = camX.toString(); + this.camYStr = camY.toString(); + this.camZStr = camZ.toString(); + this.camXExpr = MathExpression.parse(this.camXStr); + this.camYExpr = MathExpression.parse(this.camYStr); + this.camZExpr = MathExpression.parse(this.camZStr); + this.camCentre = new Vector3 + ( + this.camXExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camYExpr.evaluate(v => this.getVariable(v)).toNumber(), + this.camZExpr.evaluate(v => this.getVariable(v)).toNumber() + ); + this.upright = upright; + + if(requireReset) + this.reset(); + } + /** + * Applies a new L-system to the renderer. + * @param {LSystem} system the new system. + */ + set constructSystem(system) + { + this.system = system; + this.levels = []; + this.levelParams = []; + this.ancestreeTask = + { + start: 0 + }; + this.deriveTask = + { + start: 0 + }; + this.loaded = -1; + this.loadTarget = 0; + if(resetLvlOnConstruct) + l.level = 0; + this.update(l.level); + } + /** + * Sets the seed of the current system. + * @param {number} seed the seed. + */ + set seed(seed) + { + this.system.seed = seed; + this.ancestreeTask = + { + start: 0 + }; + this.deriveTask = + { + start: 0 + }; + this.loaded = -1; + this.loadTarget = this.lv; + this.update(this.lv, true); + } + /** + * Moves the cursor forward. + */ + forward(distance = 1) + { + this.state += this.ori.headingVector * distance; + } + /** + * Ticks the clock. + * @param {number} dt the amount of time passed. + */ + tick(dt) + { + if(this.lv > this.loaded + 1 || + typeof this.levels[this.lv] === 'undefined' || + this.levels[this.lv].length == 0) + return; + + if(this.i >= this.levels[this.lv].length && this.loopMode == 0) + if(!this.backtrackTail || this.stack.length == 0) + return; + + this.elapsed += dt; + } + /** + * Computes the next cursor position internally. + * @param {number} level the level to be drawn. + */ + draw(level, onlyUpdate = false) + { + /* + Behold the broken monster patched by sheer duct tape. + I can guarantee that because the game runs on one thread, the renderer + would always load faster than it draws. Unless you make a rule that + spawns 10000 plus signs. Please don't do it. + */ + if(level > this.loaded) + this.update(level); + + // You can't believe how many times I have to type this typeof clause. + if(level > this.loaded + 1 || + typeof this.levels[this.lv] === 'undefined') + return; + + if(onlyUpdate) + return; + + // This is to prevent the renderer from skipping the first point. + if(this.elapsed <= 0.101) + return; + + /* + Don't worry, it'll not run forever. This is just to prevent the renderer + from hesitating for 1 tick every loop. + */ + let j, t, moved; + let loopLimit = 2; // Shenanigans may arise with models? Try this + for(j = 0; j < loopLimit; ++j) + { + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + if(this.models.length > 0) + { + // Unreadable pile of shit + for(; this.mdi[this.mdi.length - 1] < + this.models[this.models.length - 1].length; + ++this.mdi[this.mdi.length - 1]) + { + switch(this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + { + case ' ': + log('Blank space detected.') + break; + case '+': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '+'); + else + this.ori = this.system.rotations.get('+').mul( + this.ori); + break; + case '-': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '-'); + else + this.ori = this.system.rotations.get('-').mul( + this.ori); + break; + case '&': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '&'); + else + this.ori = this.system.rotations.get('&').mul( + this.ori); + break; + case '^': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '^'); + else + this.ori = this.system.rotations.get('^').mul( + this.ori); + break; + case '\\': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '\\'); + else + this.ori = this.system.rotations.get('\\').mul( + this.ori); + break; + case '/': + if(this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + this.ori = this.ori.rotate(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][0].toNumber(), + '/'); + else + this.ori = this.system.rotations.get('/').mul( + this.ori); + break; + case '|': + this.ori = zUpQuat.mul(this.ori); + break; + case '$': + this.ori = this.ori.alignToVertical(); + break; + case 'T': + let args = this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]; + if(args) + { + if(args.length >= 4) + this.ori = this.ori.applyTropism( + args[0].toNumber(), + args[1].toNumber(), + args[2].toNumber(), + args[3].toNumber()); + else + this.ori = this.ori.applyTropism( + args[0].toNumber()); + } + else + this.ori = this.ori.applyTropism( + this.system.tropism); + break; + case '~': + if(!this.system.models.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1])) + break; + + ++this.mdi[this.mdi.length - 1]; + let model = this.system.deriveModel(this.models[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]], this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]]); + + this.models.push(model.result); + this.modelParams.push(model.params); + this.mdi.push(0); + return; + case '[': + this.idxStack.push(this.stack.length); + this.stack.push([this.state, this.ori]); + break; + case ']': + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + if(this.stack.length == 0) + { + log('You\'ve clearly made a bracket error.'); + break; + } + + moved = this.state !== + this.stack[this.stack.length - 1][0]; + + t = this.stack.pop(); + this.state = t[0]; + this.ori = t[1]; + if(this.stack.length == + this.idxStack[this.idxStack.length - 1]) + { + this.idxStack.pop(); + if(moved) + this.cooldown = 1; + if(this.hesitateFork && this.polygonMode <= 0) + { + ++this.mdi[this.mdi.length - 1]; + return; + } + else + { + break; + } + } + if(this.polygonMode <= 0) + return; + else + { + --this.mdi[this.mdi.length - 1]; + break; + } + case '%': + // Nothing to do here + break; + case '{': + ++this.polygonMode; + break; + case '}': + --this.polygonMode; + break; + case '.': + if(this.polygonMode <= 0) + log('You cannot register a vertex outside of ' + + 'polygon drawing.'); + else + ++this.mdi[this.mdi.length - 1]; + return; + default: + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + let ignored = this.system.ignoreList.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) || + this.loadModels && this.system.models.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]); + let breakAhead = BACKTRACK_LIST.has( + this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1]); + let btAhead = this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1] + 1] == ']' || + this.mdi[this.mdi.length - 1] == + this.models[this.models.length - 1].length - 1; + + if(this.hesitateApex && btAhead) + this.cooldown = 1; + + if(this.quickDraw && breakAhead) + this.cooldown = 1; + + moved = this.stack.length == 0 || + (this.stack.length > 0 && this.state !== + this.stack[this.stack.length - 1][0]); + + if(!this.quickBacktrack && moved && !ignored) + this.stack.push([this.state, this.ori]); + + if(!ignored) + { + if(this.models[this.models.length - 1][ + this.mdi[this.mdi.length - 1]] == 'F' && + this.modelParams[this.models.length - 1][ + this.mdi[this.mdi.length - 1]]) + { + this.forward(this.modelParams[ + this.models.length - 1][ + this.mdi[this.mdi.length - 1]][ + 0].toNumber()); + } + else + this.forward(); + } + + if(this.quickBacktrack && breakAhead) + this.stack.push([this.state, this.ori]); + + if(this.quickDraw && !btAhead) + break; + else if(this.polygonMode <= 0) + { + ++this.mdi[this.mdi.length - 1]; + return; + } + else + break; + } + } + this.models.pop(); + this.modelParams.pop(); + this.mdi.pop(); + ++loopLimit; + // continue prevents the regular loop from running + continue; + } + for(; this.i < this.levels[this.lv].length; ++this.i) + { + // if(this.models.length > 0) + // break; + switch(this.levels[this.lv][this.i]) + { + case ' ': + log('Blank space detected.') + break; + case '+': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '+'); + else + this.ori = this.system.rotations.get('+').mul( + this.ori); + break; + case '-': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '-'); + else + this.ori = this.system.rotations.get('-').mul( + this.ori); + break; + case '&': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '&'); + else + this.ori = this.system.rotations.get('&').mul( + this.ori); + break; + case '^': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '^'); + else + this.ori = this.system.rotations.get('^').mul( + this.ori); + break; + case '\\': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '\\'); + else + this.ori = this.system.rotations.get('\\').mul( + this.ori); + break; + case '/': + if(this.levelParams[this.lv][this.i]) + this.ori = this.ori.rotate(this.levelParams[ + this.lv][this.i][0].toNumber(), '/'); + else + this.ori = this.system.rotations.get('/').mul( + this.ori); + break; + case '|': + this.ori = zUpQuat.mul(this.ori); + break; + case '$': + this.ori = this.ori.alignToVertical(); + break; + case 'T': + let args = this.levelParams[this.lv][this.i]; + if(args) + { + if(args.length >= 4) + this.ori = this.ori.applyTropism( + args[0].toNumber(), + args[1].toNumber(), + args[2].toNumber(), + args[3].toNumber()); + else + this.ori = this.ori.applyTropism( + args[0].toNumber()); + } + else + this.ori = this.ori.applyTropism( + this.system.tropism); + break; + case '~': + if(!this.loadModels || !this.system.models.has( + this.levels[this.lv][this.i + 1])) + break; + + ++this.i; + let model = this.system.deriveModel(this.levels[ + this.lv][this.i], this.levelParams[this.lv][this.i]); + + this.models.push(model.result); + this.modelParams.push(model.params); + this.mdi.push(0); + return; + case '[': + this.idxStack.push(this.stack.length); + this.stack.push([this.state, this.ori]); + break; + case ']': + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + if(this.stack.length == 0) + { + log('You\'ve clearly made a bracket error.'); + break; + } + + moved = this.state !== + this.stack[this.stack.length - 1][0]; + + t = this.stack.pop(); + this.state = t[0]; + this.ori = t[1]; + if(this.stack.length == + this.idxStack[this.idxStack.length - 1]) + { + this.idxStack.pop(); + if(moved) + this.cooldown = 1; + if(this.hesitateFork && this.polygonMode <= 0) + { + ++this.i; + return; + } + else + { + break; + } + } + if(this.polygonMode <= 0) + return; + else + { + --this.i; + break; + } + case '%': + // Nothing to do here, all handled by LSystem derivation + break; + case '{': + ++this.polygonMode; + break; + case '}': + --this.polygonMode; + break; + case '.': + if(this.polygonMode <= 0) + log('You cannot register a vertex outside of ' + + 'polygon drawing.'); + else + ++this.i; + return; + default: + if(this.cooldown > 0 && this.polygonMode <= 0) + { + --this.cooldown; + return; + } + + let ignored = this.system.ignoreList.has( + this.levels[this.lv][this.i]) || this.loadModels && + this.system.models.has(this.levels[this.lv][this.i]); + let breakAhead = BACKTRACK_LIST.has( + this.levels[this.lv][this.i + 1]); + let btAhead = this.levels[this.lv][this.i + 1] == ']' || + this.i == this.levels[this.lv].length - 1; + + if(this.hesitateApex && btAhead) + this.cooldown = 1; + + if(this.quickDraw && breakAhead) + this.cooldown = 1; + + moved = this.stack.length == 0 || + (this.stack.length > 0 && this.state !== + this.stack[this.stack.length - 1][0]); + + if(!this.quickBacktrack && moved && !ignored) + this.stack.push([this.state, this.ori]); + + if(!ignored) + { + if(this.levels[this.lv][this.i] == 'F' && + this.levelParams[this.lv][this.i]) + { + this.forward(this.levelParams[this.lv][this.i][ + 0].toNumber()); + } + else + this.forward(); + } + + if(this.quickBacktrack && breakAhead) + this.stack.push([this.state, this.ori]); + + if(this.quickDraw && !btAhead) + break; + else if(this.polygonMode <= 0) + { + ++this.i; + return; + } + else + break; + } + } + // This is what the renderer will do at the end of a loop + if(!this.backtrackTail || this.stack.length == 0) + { + switch(this.loopMode) + { + case 2: + l.buy(1); + break; + case 1: + this.reset(false); + break; + case 0: + return; + } + } + else + { + let t = this.stack.pop(); + this.state = t[0]; + this.ori = t[1]; + return; + } + } + } + /** + * Return swizzled coordinates according to the in-game system. The game + * uses Android UI coordinates, which is X-right Y-down Z-face. + * @param {Vector3} coords the original coordinates. + * @returns {Vector3} + */ + swizzle(coords) + { + // The game uses left-handed Y-up, aka Y-down coordinates. + return new Vector3(coords.x, -coords.y, coords.z); + } + /** + * Returns a variable's value for maths expressions. + * @param {string} v the variable's name. + * @returns {BigNumber} + */ + getVariable(v) + { + switch(v) + { + case 'lv': return BigNumber.from(this.lv); + } + return null; + } + /** + * Returns the camera centre's coordinates. + * @returns {Vector3} + */ + get centre() + { + if(this.cameraMode) + return -this.cursor; + + return this.swizzle(-this.camCentre / this.figureScale); + } + /** + * Returns the turtle's coordinates. + * @returns {Vector3} + */ + get cursor() + { + let coords = this.state / this.figureScale; + return this.swizzle(coords); + } + /** + * Returns the camera's coordinates. + * @returns {Vector3} + */ + get camera() + { + let newCamera; + switch(this.cameraMode) + { + case 2: + // I accidentally discovered Bézier curves unknowingly. + let dist = this.centre - this.lastCamera; + newCamera = this.lastCamera + dist * this.followFactor ** 2 + + this.lastCamVel * (1 - this.followFactor) ** 2; + this.lastCamVel = newCamera - this.lastCamera; + this.lastCamera = newCamera; + return newCamera; + case 1: + newCamera = this.centre * this.followFactor + + this.lastCamera * (1 - this.followFactor); + this.lastCamVel = newCamera - this.lastCamera; + this.lastCamera = newCamera; + return newCamera; + case 0: + return this.centre; + } + } + /** + * Returns the static camera configuration. + * @returns {[string, string, string, string, boolean]} + */ + get staticCamera() + { + return [ + this.figScaleStr, + this.camXStr, + this.camYStr, + this.camZStr, + this.upright + ]; + } + /** + * Returns the elapsed time. + * @returns {[number, number]} + */ + get elapsedTime() + { + return [ + Math.floor(this.elapsed / 60), + this.elapsed % 60 + ]; + } + /** + * Returns the current progress on this level, in a fraction. + * @returns {[number, number]} + */ + get progressFrac() + { + return [this.i, this.levels[this.lv].length]; + } + /** + * Returns the current progress on this level, in percent. + * @returns {number} + */ + get progressPercent() + { + if(typeof this.levels[this.lv] === 'undefined') + return 0; + + let pf = this.progressFrac; + let result = pf[0] * 100 / pf[1]; + if(isNaN(result)) + result = 0; + + return result; + } + /** + * Returns the current progress fraction as a string. + * @returns {string} + */ + get progressString() + { + let pf = this.progressFrac; + return `i=${pf[0]}/${pf[1]}`; + } + /** + * Returns a loading message. + * @returns {string} + */ + get loadingString() + { + if(typeof this.levels[this.loaded + 1] === 'undefined') + return Localization.format(getLoc('rendererBuildingTree'), + this.loaded + 1, this.ancestreeTask.start); + return Localization.format(getLoc('rendererDeriving'), this.loaded + 1, + this.levels[this.loaded + 1].length); + } + /** + * Returns the cursor's position as a string. + * @returns {string} + */ + get stateString() + { + if(typeof this.levels[this.lv] === 'undefined') + return this.loadingString; + + return `\\begin{matrix}x=${getCoordString(this.state.x)},& + y=${getCoordString(this.state.y)},&z=${getCoordString(this.state.z)},& + ${this.progressString}\\end{matrix}`; + } + /** + * Returns the cursor's orientation as a string. + * @returns {string} + */ + get oriString() + { + if(typeof this.levels[this.lv] === 'undefined') + return this.loadingString; + + return `\\begin{matrix}q=${this.ori.toString()},&${this.progressString} + \\end{matrix}`; + } + /** + * Returns the object representation of the renderer. + * @returns {object} + */ + get object() + { + return { + figureScale: this.figScaleStr, + cameraMode: this.cameraMode, + camX: this.camXStr, + camY: this.camYStr, + camZ: this.camZStr, + followFactor: this.followFactor, + loopMode: this.loopMode, + upright: this.upright, + loadModels: this.loadModels, + quickDraw: this.quickDraw, + quickBacktrack: this.quickBacktrack, + backtrackTail: this.backtrackTail, + hesitateApex: this.hesitateApex, + hesitateFork: this.hesitateFork + } + } + /** + * Returns the renderer's string representation. + * @returns {string} + */ + toString() + { + return JSON.stringify(this.object, null, 4); + } +} + +/** + * Represents a bunch of buttons for variable controls. + */ +class VariableControls +{ + /** + * @constructor + * @param {Upgrade} variable the variable being controlled. + * @param {boolean} useAnchor whether to use anchor controls. + * @param {number} quickbuyAmount the amount of levels to buy when held. + */ + constructor(variable, useAnchor = false, quickbuyAmount = 10) + { + /** + * @type {Upgrade} the variable being controlled. + */ + this.variable = variable; + /** + * @type {Frame} the variable button. + */ + this.varBtn = null; + /** + * @type {Frame} the refund button. + */ + this.refundBtn = null; + /** + * @type {Frame} the buy button. + */ + this.buyBtn = null; + + /** + * @type {boolean} whether to use anchor controls. + */ + this.useAnchor = useAnchor; + /** + * @type {number} the anchored variable level. + */ + this.anchor = this.variable.level; + /** + * @type {number} whether the anchor is on. + */ + this.anchorActive = false; + /** + * @type {number} the amount of levels to buy when held. + */ + this.quickbuyAmount = quickbuyAmount; + } + + /** + * Updates all buttons, visually. + */ + updateAllButtons() + { + this.updateDescription(); + this.updateRefundButton(); + this.updateBuyButton(); + } + /** + * Updates the variable description written on the button's label. + */ + updateDescription() + { + this.varBtn.content.text = this.variable.getDescription(); + } + /** + * Creates a variable button. + * @param {function(void): void} callback when pressed, calls this function. + * @param {number} height the button's height. + * @returns {Frame} + */ + createVariableButton(callback = null, height = BUTTON_HEIGHT) + { + if(this.varBtn) + return this.varBtn; + + let frame = ui.createFrame + ({ + heightRequest: height, + cornerRadius: 1, + padding: new Thickness(10, 2), + verticalOptions: LayoutOptions.CENTER, + content: ui.createLatexLabel + ({ + text: this.variable.getDescription(), + verticalTextAlignment: TextAlignment.CENTER, + textColor: Color.TEXT_MEDIUM + }), + borderColor: Color.TRANSPARENT + }); + if(callback) + { + frame.borderColor = Color.BORDER; + frame.content.textColor = Color.TEXT; + frame.onTouched = (e) => + { + if(e.type == TouchType.PRESSED) + { + frame.borderColor = Color.TRANSPARENT; + frame.content.textColor = Color.TEXT_MEDIUM; + } + else if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + frame.borderColor = Color.BORDER; + frame.content.textColor = Color.TEXT; + callback(); + } + else if(e.type == TouchType.CANCELLED) + { + frame.borderColor = Color.BORDER; + frame.content.textColor = Color.TEXT + } + } + } + this.varBtn = frame; + return this.varBtn; + } + /** + * Updates the refund button, visually. + */ + updateRefundButton() + { + this.refundBtn.borderColor = this.variable.level > 0 ? Color.BORDER : + Color.TRANSPARENT; + this.refundBtn.content.textColor = this.variable.level > 0 ? + Color.TEXT : Color.TEXT_MEDIUM; + } + /** + * Creates a refund button. + * @param {string} symbol the button's label. + * @param {number} height the button's height. + * @returns {Frame} + */ + createRefundButton(symbol = '-', height = BUTTON_HEIGHT) + { + if(this.refundBtn) + return this.refundBtn; + + this.refundBtn = ui.createFrame + ({ + heightRequest: height, + cornerRadius: 1, + padding: new Thickness(10, 2), + verticalOptions: LayoutOptions.CENTER, + content: ui.createLatexLabel + ({ + text: symbol, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + textColor: this.variable.level > 0 ? Color.TEXT : + Color.TEXT_MEDIUM + }), + onTouched: (e) => + { + if(e.type == TouchType.PRESSED) + { + this.refundBtn.borderColor = Color.TRANSPARENT; + this.refundBtn.content.textColor = this.variable.level > 0 ? + Color.TEXT_MEDIUM : Color.TEXT_DARK; + } + else if(e.type == TouchType.SHORTPRESS_RELEASED) + { + Sound.playClick(); + this.variable.refund(1); + } + else if(e.type == TouchType.LONGPRESS) + { + Sound.playClick(); + if(this.useAnchor) + { + this.anchorActive = true; + if(this.variable.level > 0) + this.anchor = this.variable.level; + } + this.variable.refund(this.quickbuyAmount); + } + else if(e.type == TouchType.CANCELLED) + { + this.updateRefundButton(); + } + }, + borderColor: this.variable.level > 0 ? Color.BORDER : + Color.TRANSPARENT + }); + return this.refundBtn; + } + /** + * Updates the buy button, visually. + */ + updateBuyButton() + { + this.buyBtn.borderColor = this.variable.level < this.variable.maxLevel ? + Color.BORDER : Color.TRANSPARENT; + this.buyBtn.content.textColor = this.variable.level < + this.variable.maxLevel ? Color.TEXT : Color.TEXT_MEDIUM; + } + /** + * Creates a buy button. + * @param {string} symbol the button's label. + * @param {number} height the button's height. + * @returns {Frame} + */ + createBuyButton(symbol = '+', height = BUTTON_HEIGHT) + { + if(this.buyBtn) + return this.buyBtn; + + this.buyBtn = ui.createFrame + ({ + heightRequest: height, + cornerRadius: 1, + padding: new Thickness(10, 2), + verticalOptions: LayoutOptions.CENTER, + content: ui.createLatexLabel + ({ + text: symbol, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + textColor: this.variable.level < this.variable.maxLevel ? + Color.TEXT : Color.TEXT_MEDIUM + }), + onTouched: (e) => + { + if(e.type == TouchType.PRESSED) + { + this.buyBtn.borderColor = Color.TRANSPARENT; + this.buyBtn.content.textColor = this.variable.level < + this.variable.maxLevel ? Color.TEXT_MEDIUM : + Color.TEXT_DARK; + } + else if(e.type == TouchType.SHORTPRESS_RELEASED) + { + Sound.playClick(); + this.variable.buy(1); + } + else if(e.type == TouchType.LONGPRESS) + { + Sound.playClick(); + let q = this.quickbuyAmount; + if(this.useAnchor && this.anchorActive) + { + q = Math.min(q, this.anchor - this.variable.level); + if(q == 0) + q = this.quickbuyAmount; + this.anchorActive = false; + } + for(let i = 0; i < q; ++i) + this.variable.buy(1); + } + else if(e.type == TouchType.CANCELLED) + { + this.updateBuyButton(); + } + }, + borderColor: this.variable.level < this.variable.maxLevel ? + Color.BORDER : Color.TRANSPARENT + }); + return this.buyBtn; + } +} + +/** + * Measures performance for a piece of code. + */ +class Measurer +{ + /** + * @constructor + * @param {string} title the measurement's title. + * @param {number} window the sample size. + */ + constructor(title, window = 10) + { + /** + * @type {string} the measurement's title. + */ + this.title = title; + /** + * @type {number} the sample size. + */ + this.window = window; + /** + * @type {number} the all-time sum. + */ + this.sum = 0; + /** + * @type {number} the window sum. + */ + this.windowSum = 0; + /** + * @type {number} the all-time maximum. + */ + this.max = 0; + /** + * @type {number[]} recent records. + */ + this.records = []; + for(let i = 0; i < this.window; ++i) + this.records[i] = 0; + /** + * @type {number} the elapsed time in ticks. + */ + this.ticksPassed = 0; + /** + * @type {number} the most recent moment the function was stamped. + */ + this.lastStamp = null; + } + + /** + * Resets the measurer. + */ + reset() + { + this.sum = 0; + this.windowSum = 0; + this.max = 0; + this.records = []; + for(let i = 0; i < this.window; ++i) + this.records[i] = 0; + this.ticksPassed = 0; + this.lastStamp = null; + } + /** + * Stamps the measurer. + */ + stamp() + { + if(!this.lastStamp) + this.lastStamp = Date.now(); + else + { + let closingStamp = Date.now(); + let i = this.ticksPassed % this.window; + this.windowSum -= this.records[i]; + this.records[i] = closingStamp - this.lastStamp; + this.windowSum += this.records[i]; + this.sum += this.records[i]; + if(this.records[i] > this.max) + { + this.max = this.records[i]; + // log(`Boom! ${this.title} took ${this.records[i]}ms.`) + } + this.lastStamp = null; + ++this.ticksPassed; + } + } + /** + * Returns the window average. + * @returns {number} + */ + get windowAvg() + { + return this.windowSum / Math.min(this.window, this.ticksPassed); + } + /** + * Returns the all-time average. + * @returns {number} + */ + get allTimeAvg() + { + return this.sum / this.ticksPassed; + } + /** + * Returns the string for the window average. + * @returns {string} + */ + get windowAvgString() + { + if(this.ticksPassed == 0) + return ''; + + if(!measurePerformance) + return ''; + + return Localization.format(getLoc('measurement'), this.title, + getCoordString(this.max), getCoordString(this.windowAvg), + Math.min(this.window, this.ticksPassed)); + } + /** + * Returns the string for the all-time average. + * @returns {string} + */ + get allTimeAvgString() + { + if(this.ticksPassed == 0) + return ''; + + if(!measurePerformance) + return ''; + + return Localization.format(getLoc('measurement'), this.title, + getCoordString(this.max), getCoordString(this.allTimeAvg), + this.ticksPassed); + } +} + +// const sidewayQuat = new Quaternion(1, 0, 0, 0); +const uprightQuat = new Quaternion(-Math.sqrt(2)/2, 0, 0, Math.sqrt(2)/2); +const xUpQuat = new Quaternion(0, 1, 0, 0); +const yUpQuat = new Quaternion(0, 0, 1, 0); +const zUpQuat = new Quaternion(0, 0, 0, 1); + +let toe = new LSystem('++M(0)', [ + 'M(t): t<2 = F~M(t+1)', + 'M(t): t<3 = [&T$~M(t+1, 0)]/(120)[&T$M(t+1, 0)]/(120)[&T$M(t+1, 0)]', + 'M(t, i): t<5 = F~M(t+1, i): 0.7-i; F~K(0)~M(t+1, i+0.3): 0.3+i', + 'M(t, i): t>=5 = [&T~M(t-2, i+0.3)]/(180)[&TM(t-2, i+0.3)]', + '~> M(t): t<3 = [+(48)~L(t)]/(180)[+(48)~L(t)]', + '~> M(t, i) = [+(48)~L(2+0.4*t)]/(180)[+(48)~L(2+0.4*t)]', + '~> L(t) = {[+(16)TF(t/6).&(16)-(16)TF(t/10).-TF(t/8)..][-(16)TF(t/6)[&(16)+(16)TF(t/10).].]}', + 'K(c)>M(t, i): c<3 = ~K(c+1): 0.7-t/10, ~B(0.3): 0.3+t/10', + 'K(t): t<3 = ~K(t+1): 0.3+t/10, : 0.7-t/10', + 'K(t): t>=3 = [&&\\~B(0.3)]/(120)[&&\\~B(0.24)]/(120)[&&~B(0.27)]', + '~> K(t) = [&&F(0.3+t/10)]/(120)[&&F(0.3+t/10)]/(120)[&&F(0.3+t/10)]', + 'B(s) = ~B(s*0.9+0.1)', + '~> B(s) = {[-(67.5)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).+(45)F(s).]}', +], '32', 157120112, '', 'F+-&^/\\~{}', 0.16, {}); +let renderer = new Renderer(toe, 6, 0, 3, 3, 0); +let globalRNG = new Xorshift(Date.now()); +let contentsTable = [0, 1, 2, 5, 7, 10, 11]; +let manualSystems = +{ + 3: + { + system: new LSystem('B[+AAA]A[-AA]A[+A]A', [ + 'B0=0', + '0<0>1=1[-F1F1]', + '0<1>0=1', + '0<1>1=1', + '1<0>0=0', + '1<0>1=1F1', + '1<1>0=1', + '1<1>1=0', + '+=-', + '-=+' + ], 22.5, 0, '01', 'F+-'), + config: ['lv+2', 0, 'lv/2+1', 0, true] + }, + 6: + { + system: new LSystem('F(1)', [ + 'F(d)=F(d/3)+F(d/3)-(120)F(d/3)+F(d/3)' + ], 60), + config: ['1/2', '1/2', 'sqrt(3)/12', 0, false] + }, + 8: + { + system: new LSystem('FA(1)', [ + 'A(t): t<=5*b = F(2/b)//[+(24)&B(t)]//[&B(t)]//[&B(t)]A(t+1)', + 'B(t) = ~J(0.15, t/b-2)', + 'J(s, type) = ~J(s*0.75+0.25, type)', + '~>J(s, type): type<=0 = {[+(32)F(s).-TF(s)TF.-TF(s)..][-(32)F(s)+(16)[TF(s)TF.].]}', + '~>J(s, type): type<=1 = [F~p(s)/(60)~p(s)/(60)~p(s)]', + '~>p(s) = {[+F(s/2).--F(s/2).][-F(s/2).].}', + '~>J(s, type): type<=2 = [FT~k(s)/(72)~k(s)/(72)~k(s)/(72)~k(s)/(72)~k(s)]', + '~>k(s) = {[F(s).+(72)[&F(s-0.3).][F(s)..][^F(s-0.3).].]}', + '~>J(s, type): type<=3 = [FT~m(s)/(72)~m(s)/(72)~m(s)/(72)~m(s)/(72)~m(s)]', + '~>m(s) = {[+(24)F(s).-F(s/2)..].}' + ], '48', 0, 'A', '', 0.16, { + 'b': '2.25' + }), + config: ['7', '0', 'lv/2+1', '0', true] + }, + 9: + { + system: toe, + config: ['6', '3', '3', '0', false] + } +}; +let tmpSystem = null; +let tmpSystemName = getLoc('welcomeSystemName'); +let tmpSystemDesc = getLoc('welcomeSystemDesc'); + +var l, ts; +// Variable controls +let lvlControls, tsControls; + +// Measure drawing performance +let drawMeasurer = new Measurer('renderer.draw()', 30); +let camMeasurer = new Measurer('renderer.camera', 30); + +var init = () => +{ + min = theory.createCurrency(getLoc('currencyTime')); + progress = theory.createCurrency('%'); + + // l (STAGE) + { + let getDesc = (level) => Localization.format(getLoc('varLvDesc'), + level.toString(), renderer.loopMode == 2 ? '+' : ''); + let getInfo = (level) => `\\text{Stg. }${level.toString()}`; + l = theory.createUpgrade(0, progress, new FreeCost); + l.getDescription = (_) => Utils.getMath(getDesc(l.level)); + l.getInfo = (amount) => Utils.getMathTo(getInfo(l.level), + getInfo(l.level + amount)); + l.canBeRefunded = (_) => true; + l.boughtOrRefunded = (_) => + { + lvlControls.updateAllButtons(); + renderer.update(l.level); + }; + lvlControls = new VariableControls(l); + } + // ts (Tickspeed) + { + let getDesc = (level) => + { + if(tickDelayMode) + { + if(level == 0) + return getLoc('varTdDescInf'); + return Localization.format(getLoc('varTdDesc'), + (level / 10).toString()); + } + return Localization.format(getLoc('varTsDesc'), level.toString()); + }; + let getInfo = (level) => `\\text{Ts=}${level.toString()}/s`; + ts = theory.createUpgrade(1, progress, new FreeCost); + ts.getDescription = (_) => Utils.getMath(getDesc(ts.level)); + ts.getInfo = (amount) => Utils.getMathTo(getInfo(ts.level), + getInfo(ts.level + amount)); + ts.maxLevel = 10; + ts.canBeRefunded = (_) => true; + ts.boughtOrRefunded = (_) => + { + tsControls.updateAllButtons(); + time = 0; + }; + tsControls = new VariableControls(ts, true); + } + + theory.createSecretAchievement(0, null, + getLoc('saPatienceTitle'), + getLoc('saPatienceDesc'), + getLoc('saPatienceHint'), + () => min.value > 9.6 + ); +} + +var alwaysShowRefundButtons = () => true; + +let timeCheck = (elapsedTime) => +{ + let timeLimit; + if(tickDelayMode) + { + time += 1; + timeLimit = ts.level; + } + else + { + time += elapsedTime; + timeLimit = 1 / ts.level; + } + if(time >= timeLimit - 1e-8) + { + time -= timeLimit; + return true; + } + return false; +} + +var tick = (elapsedTime, multiplier) => +{ + if(game.isCalculatingOfflineProgress) + { + gameIsOffline = true; + return; + } + else if(gameIsOffline) + { + // Triggers only once when reloading + if(offlineReset) + renderer.reset(); + gameIsOffline = false; + } + + if(measurePerformance) + drawMeasurer.stamp(); + + if(ts.level == 0) + { + // Keep updating even when paused + renderer.draw(l.level, true); + } + else + { + renderer.tick(elapsedTime); + renderer.draw(l.level, !timeCheck(elapsedTime)); + } + + if(measurePerformance) + drawMeasurer.stamp(); + + let msTime = renderer.elapsedTime; + min.value = 1e-8 + msTime[0] + msTime[1] / 100; + progress.value = renderer.progressPercent; + theory.invalidateTertiaryEquation(); +} + +var getEquationOverlay = () => +{ + let overlayText = () => Localization.format(getLoc('equationOverlayLong'), + getLoc('versionName'), tmpSystemName, drawMeasurer.windowAvgString, + camMeasurer.windowAvgString); + + let result = ui.createLatexLabel + ({ + text: overlayText, + margin: new Thickness(8, 4), + fontSize: 9, + textColor: Color.TEXT_MEDIUM + }); + return result; +} + +let createButton = (label, callback, height = BUTTON_HEIGHT) => +{ + let frame = ui.createFrame + ({ + heightRequest: height, + cornerRadius: 1, + padding: new Thickness(10, 2), + verticalOptions: LayoutOptions.CENTER, + content: ui.createLatexLabel + ({ + text: label, + verticalTextAlignment: TextAlignment.CENTER, + textColor: Color.TEXT + }), + onTouched: (e) => + { + if(e.type == TouchType.PRESSED) + { + frame.borderColor = Color.TRANSPARENT; + frame.content.textColor = Color.TEXT_MEDIUM; + } + else if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + frame.borderColor = Color.BORDER; + frame.content.textColor = Color.TEXT; + callback(); + } + else if(e.type == TouchType.CANCELLED) + { + frame.borderColor = Color.BORDER; + frame.content.textColor = Color.TEXT; + } + }, + borderColor: Color.BORDER + }); + return frame; +} + +var getUpgradeListDelegate = () => +{ + let openSeqMenu = () => + { + let menu = createSequenceMenu(); + menu.show(); + }; + let lvlButton = lvlControls.createVariableButton(openSeqMenu); + lvlButton.row = 0; + lvlButton.column = 0; + let lvlRefund = lvlControls.createRefundButton('–'); + lvlRefund.column = 0; + let lvlBuy = lvlControls.createBuyButton(); + lvlBuy.column = 1; + + let toggleTDM = () => + { + tickDelayMode = !tickDelayMode; + tsControls.updateDescription(); + time = 0; + }; + let tsButton = tsControls.createVariableButton(toggleTDM); + tsButton.row = 1; + tsButton.column = 0; + let tsRefund = tsControls.createRefundButton('–'); + tsRefund.column = 0; + let tsBuy = tsControls.createBuyButton(); + tsBuy.column = 1; + + let sysButton = createButton(getLoc('btnMenuLSystem'), () => + createSystemMenu().show()); + sysButton.row = 0; + sysButton.column = 0; + let cfgButton = createButton(getLoc('btnMenuRenderer'), () => + createConfigMenu().show()); + cfgButton.row = 0; + cfgButton.column = 1; + let slButton = createButton(getLoc('btnMenuSave'), () => + createSaveMenu().show()); + slButton.row = 1; + slButton.column = 0; + let manualButton = createButton(getLoc('btnMenuManual'), () => + createManualMenu().show()); + manualButton.row = 1; + manualButton.column = 1; + let theoryButton = createButton(getLoc('btnMenuTheory'), () => + createWorldMenu().show()); + theoryButton.row = 2; + theoryButton.column = 0; + let resumeButton = createButton(Localization.format(getLoc('btnResume'), + tmpSystemName), () => + { + if(tmpSystem) + { + renderer.constructSystem = tmpSystem; + tmpSystem = null; + } + }, getMediumBtnSize(ui.screenWidth)); + resumeButton.content.horizontalOptions = LayoutOptions.CENTER; + resumeButton.isVisible = () => tmpSystem ? true : false; + resumeButton.margin = new Thickness(0, 0, 0, 2); + + let stack = ui.createScrollView + ({ + padding: new Thickness(6, 8), + content: ui.createStackLayout + ({ + children: + [ + resumeButton, + ui.createGrid + ({ + columnSpacing: 8, + rowSpacing: 6, + rowDefinitions: + [ + BUTTON_HEIGHT, + BUTTON_HEIGHT + ], + columnDefinitions: ['50*', '50*'], + children: + [ + lvlButton, + ui.createGrid + ({ + row: 0, + column: 1, + columnSpacing: 7, + columnDefinitions: ['50*', '50*'], + children: + [ + lvlRefund, + lvlBuy + ] + }), + tsButton, + ui.createGrid + ({ + row: 1, + column: 1, + columnSpacing: 7, + columnDefinitions: ['50*', '50*'], + children: + [ + tsRefund, + tsBuy + ] + }) + ] + }), + ui.createBox + ({ + heightRequest: 0, + // margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + columnSpacing: 8, + rowSpacing: 6, + rowDefinitions: + [ + BUTTON_HEIGHT, + BUTTON_HEIGHT, + BUTTON_HEIGHT + ], + columnDefinitions: ['50*', '50*'], + children: + [ + sysButton, + cfgButton, + slButton, + manualButton, + theoryButton + ] + }) + ] + }) + }); + return stack; +} + +let createConfigMenu = () => +{ + let tmpZE = renderer.figScaleStr; + let zoomEntry = ui.createEntry + ({ + text: tmpZE, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpZE = nt; + } + }); + let tmpCM = renderer.cameraMode; + let CMLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelCamMode'), + getLoc('camModes')[tmpCM]), + row: 1, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let CMSlider = ui.createSlider + ({ + row: 1, + column: 1, + minimum: 0, + maximum: 2, + value: tmpCM, + onValueChanged: () => + { + tmpCM = Math.round(CMSlider.value); + CMSlider.isToggled = tmpCM > 0; + camLabel.isVisible = tmpCM == 0; + camGrid.isVisible = tmpCM == 0; + camOffLabel.isVisible = tmpCM == 0; + camOffGrid.isVisible = tmpCM == 0; + FFLabel.isVisible = tmpCM > 0; + FFEntry.isVisible = tmpCM > 0; + CMLabel.text = Localization.format(getLoc('labelCamMode'), + getLoc('camModes')[tmpCM]); + + }, + onDragCompleted: () => + { + Sound.playClick(); + CMSlider.value = tmpCM; + } + }); + let tmpCX = renderer.camXStr; + let tmpCY = renderer.camYStr; + let tmpCZ = renderer.camZStr; + let camLabel = ui.createLatexLabel + ({ + text: getLoc('labelCamCentre'), + isVisible: tmpCM == 0, + row: 2, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let camGrid = ui.createEntry + ({ + text: tmpCX, + isVisible: tmpCM == 0, + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCX = nt; + } + }); + let CYEntry = ui.createEntry + ({ + text: tmpCY, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCY = nt; + } + }); + let camOffLabel = ui.createGrid + ({ + row: 3, + column: 0, + columnDefinitions: ['40*', '30*'], + isVisible: tmpCM == 0, + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelCamOffset'), + row: 0, + column: 0, + // horizontalTextAlignment: TextAlignment.END, + verticalTextAlignment: TextAlignment.CENTER + }), + CYEntry + ] + }); + let camOffGrid = ui.createEntry + ({ + text: tmpCZ, + isVisible: tmpCM == 0, + row: 3, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCZ = nt; + } + }); + let tmpFF = renderer.followFactor; + let FFLabel = ui.createLatexLabel + ({ + text: getLoc('labelFollowFactor'), + row: 2, + column: 0, + verticalTextAlignment: TextAlignment.CENTER, + isVisible: tmpCM > 0 + }); + let FFEntry = ui.createEntry + ({ + text: tmpFF.toString(), + keyboard: Keyboard.NUMERIC, + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + isVisible: tmpCM > 0, + onTextChanged: (ot, nt) => + { + tmpFF = Number(nt); + } + }); + let tmpUpright = renderer.upright; + let uprightSwitch = ui.createSwitch + ({ + isToggled: tmpUpright, + row: 4, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpUpright = !tmpUpright; + uprightSwitch.isToggled = tmpUpright; + } + } + }); + let tmpLM = renderer.loopMode; + let LMLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelLoopMode'), + getLoc('loopModes')[tmpLM]), + row: 0, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let LMSlider = ui.createSlider + ({ + row: 0, + column: 1, + minimum: 0, + maximum: 2, + value: tmpLM, + // minimumTrackColor: Color.BORDER, + // maximumTrackColor: Color.TRANSPARENT, + // thumbImageSource: ImageSource.UPGRADES, + onValueChanged: () => + { + tmpLM = Math.round(LMSlider.value); + LMLabel.text = Localization.format(getLoc('labelLoopMode'), + getLoc('loopModes')[tmpLM]); + }, + onDragCompleted: () => + { + Sound.playClick(); + LMSlider.value = tmpLM; + } + }); + let tmpTail = renderer.backtrackTail; + let tailSwitch = ui.createSwitch + ({ + isToggled: tmpTail, + row: 1, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpTail = !tmpTail; + tailSwitch.isToggled = tmpTail; + } + } + }); + let tmpModel = renderer.loadModels; + let modelLabel = ui.createLatexLabel + ({ + text: getLoc('labelLoadModels'), + row: 2, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let modelSwitch = ui.createSwitch + ({ + isToggled: tmpModel, + row: 2, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpModel = !tmpModel; + modelSwitch.isToggled = tmpModel; + } + } + }); + let tmpQD = renderer.quickDraw; + let QDSwitch = ui.createSwitch + ({ + isToggled: tmpQD, + row: 0, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpQD = !tmpQD; + QDSwitch.isToggled = tmpQD; + } + } + }); + let tmpQB = renderer.quickBacktrack; + let QBSwitch = ui.createSwitch + ({ + isToggled: tmpQB, + row: 1, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpQB = !tmpQB; + QBSwitch.isToggled = tmpQB; + } + } + }); + let tmpHesA = renderer.hesitateApex; + let hesALabel = ui.createLatexLabel + ({ + text: getLoc('labelHesitateApex'), + row: 2, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let hesASwitch = ui.createSwitch + ({ + isToggled: tmpHesA, + row: 2, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpHesA = !tmpHesA; + hesASwitch.isToggled = tmpHesA; + } + } + }); + let tmpHesN = renderer.hesitateFork; + let hesNLabel = ui.createLatexLabel + ({ + text: getLoc('labelHesitateFork'), + row: 3, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let hesNSwitch = ui.createSwitch + ({ + isToggled: tmpHesN, + row: 3, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpHesN = !tmpHesN; + hesNSwitch.isToggled = tmpHesN; + } + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuRenderer'), + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + ui.createScrollView + ({ + heightRequest: ui.screenHeight * 0.36, + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelFigScale'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + zoomEntry, + CMLabel, + CMSlider, + camLabel, + camGrid, + camOffLabel, + camOffGrid, + FFLabel, + FFEntry, + ui.createLatexLabel + ({ + text: getLoc('labelUpright'), + row: 4, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + uprightSwitch, + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + rowDefinitions: + [ + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT + ], + columnDefinitions: ['70*', '30*'], + children: + [ + LMLabel, + LMSlider, + ui.createLatexLabel + ({ + text: getLoc('labelBTTail'), + row: 1, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + tailSwitch, + modelLabel, + modelSwitch + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + rowDefinitions: + [ + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT, + SMALL_BUTTON_HEIGHT + ], + columnDefinitions: ['70*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelQuickdraw'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + QDSwitch, + ui.createLatexLabel + ({ + text: getLoc('labelQuickBT'), + row: 1, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + QBSwitch, + hesALabel, + hesASwitch, + hesNLabel, + hesNSwitch + ] + }) + ] + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createLatexLabel + ({ + text: getLoc('labelRequireReset'), + margin: new Thickness(0, 0, 0, 4), + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createGrid + ({ + minimumHeightRequest: BUTTON_HEIGHT, + columnDefinitions: ['50*', '50*'], + children: + [ + ui.createButton + ({ + text: getLoc('btnSave'), + row: 0, + column: 0, + onClicked: () => + { + Sound.playClick(); + renderer.configure(tmpZE, tmpCM, tmpCX, tmpCY, + tmpCZ, tmpFF, tmpLM, tmpUpright, tmpQD, tmpQB, + tmpModel, tmpTail, tmpHesA, tmpHesN); + lvlControls.updateDescription(); + menu.hide(); + } + }), + ui.createButton + ({ + text: getLoc('btnDefault'), + row: 0, + column: 1, + onClicked: () => + { + Sound.playClick(); + let rx = new Renderer(); + zoomEntry.text = rx.figScaleStr; + CMSlider.value = rx.cameraMode; + camGrid.text = rx.camXStr; + CYEntry.text = rx.camYStr; + camOffGrid.text = rx.camZStr; + FFEntry.text = rx.followFactor.toString(); + LMSlider.value = rx.loopMode; + tmpUpright = rx.upright; + uprightSwitch.isToggled = rx.upright; + tmpQD = rx.quickDraw; + QDSwitch.isToggled = rx.quickDraw; + tmpQB = rx.quickBacktrack; + QBSwitch.isToggled = rx.quickBacktrack; + tmpModel = rx.loadModels; + modelSwitch.isToggled = rx.loadModels; + tmpTail = rx.backtrackTail; + tailSwitch.isToggled = rx.backtrackTail; + tmpHesA = rx.hesitateApex; + hesASwitch.isToggled = rx.hesitateApex; + tmpHesN = rx.hesitateFork; + hesNSwitch.isToggled = rx.hesitateFork; + lvlControls.updateDescription(); + // menu.hide(); + } + }) + ] + }) + ] + }) + }) + return menu; +} + +let createVariableMenu = (variables) => +{ + // Q: Does Object.entries mean that its contents are references, and + // therefore overwritable from afar? + let tmpVars = []; + let varEntries = []; + for(let i = 0; i < variables.length; ++i) + { + // I'm just making sure these are deep copied + tmpVars[i] = []; + tmpVars[i][0] = variables[i][0]; + tmpVars[i][1] = variables[i][1]; + + varEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpVars[i][0], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpVars[i][0] = nt; + } + })); + varEntries.push(ui.createLatexLabel + ({ + text: '=', + row: i, + column: 1, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + })); + varEntries.push(ui.createEntry + ({ + row: i, + column: 2, + text: tmpVars[i][1], + horizontalTextAlignment: TextAlignment.END, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpVars[i][1] = nt; + } + })); + } + let varsLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelVars'), tmpVars.length), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let varStack = ui.createGrid + ({ + columnDefinitions: ['50*', '20*', '30*'], + children: varEntries + }); + let addVarButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = tmpVars.length; + + tmpVars[i] = []; + tmpVars[i][0] = ''; + tmpVars[i][1] = ''; + + varEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpVars[i][0], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpVars[i][0] = nt; + } + })); + varEntries.push(ui.createLatexLabel + ({ + text: '=', + row: i, + column: 1, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + })); + varEntries.push(ui.createEntry + ({ + row: i, + column: 2, + text: tmpVars[i][1], + horizontalTextAlignment: TextAlignment.END, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpVars[i][1] = nt; + } + })); + varsLabel.text = Localization.format(getLoc('labelVars'), + tmpVars.length); + varStack.children = varEntries; + } + }); + let menu = ui.createPopup + ({ + title: getLoc('menuVariables'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + varsLabel, + addVarButton + ] + }), + ui.createScrollView + ({ + content: varStack + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createButton + ({ + text: getLoc('btnSave'), + onClicked: () => + { + Sound.playClick(); + // This will telepathically clear the variables outside + variables.length = 0; + for(let i = 0; i < tmpVars.length; ++i) + if(tmpVars[i][0] && tmpVars[i][1]) + variables.push(tmpVars[i]); + menu.hide(); + } + }), + ] + }) + }); + return menu; +} + +let createSystemMenu = () => +{ + let values = renderer.system.object; + let tmpAxiom = values.axiom; + let axiomEntry = ui.createEntry + ({ + text: tmpAxiom, + row: 0, + column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpAxiom = nt; + } + }); + let tmpVars = Object.entries(values.variables); + let varButton = ui.createButton + ({ + text: getLoc('btnVar'), + row: 0, + column: 2, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let varMenu = createVariableMenu(tmpVars); + varMenu.show(); + } + }); + let tmpRules = values.rules; + let ruleEntries = []; + let ruleMoveBtns = []; + for(let i = 0; i < tmpRules.length; ++i) + { + ruleEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpRules[i] = nt; + } + })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } + } + let rulesLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelRules'), ruleEntries.length), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let ruleStack = ui.createGrid + ({ + columnDefinitions: ['7*', '1*'], + children: [...ruleEntries, ...ruleMoveBtns] + }); + let addRuleButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = ruleEntries.length; + tmpRules[i] = ''; + ruleEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpRules[i] = nt; + } + })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } + rulesLabel.text = Localization.format(getLoc('labelRules'), + ruleEntries.length); + ruleStack.children = [...ruleEntries, ...ruleMoveBtns]; + } + }); + let tmpIgnore = values.ignoreList || ''; + let ignoreEntry = ui.createEntry + ({ + text: tmpIgnore, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpIgnore = nt; + } + }); + let tmpCI = values.ctxIgnoreList || ''; + let CIEntry = ui.createEntry + ({ + text: tmpCI, + row: 1, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCI = nt; + } + }); + let tmpAngle = values.turnAngle || '0'; + let angleEntry = ui.createEntry + ({ + text: tmpAngle.toString(), + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpAngle = nt; + } + }); + let tmpTropism = values.tropism || '0'; + let tropismEntry = ui.createEntry + ({ + text: tmpTropism.toString(), + row: 3, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpTropism = nt; + } + }); + let tmpSeed = values.seed || '0'; + let seedLabel = ui.createGrid + ({ + row: 4, + column: 0, + columnDefinitions: ['40*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelSeed'), + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnReroll'), + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + seedEntry.text = globalRNG.nextInt.toString(); + } + }) + ] + }); + let seedEntry = ui.createEntry + ({ + text: tmpSeed.toString(), + keyboard: Keyboard.NUMERIC, + row: 4, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpSeed = Number(nt); + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuLSystem'), + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + ui.createScrollView + ({ + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['20*', '50*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelAxiom'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + axiomEntry, + varButton + ] + }), + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + rulesLabel, + addRuleButton + ] + }), + ruleStack, + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelIgnored'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + ignoreEntry, + ui.createLatexLabel + ({ + text: getLoc('labelCtxIgnored'), + row: 1, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + CIEntry, + ui.createLatexLabel + ({ + text: getLoc('labelAngle'), + row: 2, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + angleEntry, + ui.createLatexLabel + ({ + text: getLoc('labelTropism'), + row: 3, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + tropismEntry, + seedLabel, + seedEntry + ] + }) + ] + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + minimumHeightRequest: BUTTON_HEIGHT, + columnDefinitions: ['50*', '50*'], + children: + [ + ui.createButton + ({ + text: getLoc('btnConstruct'), + row: 0, + column: 0, + onClicked: () => + { + Sound.playClick(); + renderer.constructSystem = new LSystem(tmpAxiom, + tmpRules, tmpAngle, tmpSeed, tmpIgnore, tmpCI, + tmpTropism, Object.fromEntries(tmpVars)); + if(tmpSystem) + { + tmpSystem = null; + tmpSystemName = getLoc('defaultSystemName'); + tmpSystemDesc = getLoc('noDescription'); + } + menu.hide(); + } + }), + ui.createButton + ({ + text: getLoc('btnClear'), + row: 0, + column: 1, + onClicked: () => + { + Sound.playClick(); + let values = new LSystem().object; + axiomEntry.text = values.axiom; + angleEntry.text = values.turnAngle.toString(); + tmpVars = Object.entries(values.variables); + tmpRules = values.rules; + ruleEntries = []; + rulesLabel.text = Localization.format( + getLoc('labelRules'), ruleEntries.length); + ruleStack.children = ruleEntries; + ignoreEntry.text = values.ignoreList; + CIEntry.text = values.ctxIgnoreList; + tropismEntry.text = values.tropism.toString(); + seedEntry.text = values.seed.toString(); + } + }) + ] + }) + ] + }) + }); + return menu; +} + +let createNamingMenu = () => +{ + let tmpName = tmpSystemName; + let nameEntry = ui.createEntry + ({ + text: tmpName, + row: 0, + column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpName = nt; + } + }); + let tmpDesc = tmpSystemDesc; + let descEntry = ui.createEntry + ({ + text: tmpDesc, + row: 0, + column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpDesc = nt; + } + }); + + let getSystemGrid = () => + { + let children = []; + let i = 0; + for(let [key, value] of savedSystems) + { + children.push(ui.createLatexLabel + ({ + text: key, + row: i, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + })); + let btnO = createOverwriteButton(key); + btnO.row = i; + children.push(btnO); + ++i; + } + return children; + }; + let createOverwriteButton = (title) => + { + let btn = ui.createButton + ({ + text: getLoc('btnOverwrite'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + savedSystems.set(title, { + desc: savedSystems.get(title).desc, + system: renderer.system.object, + config: renderer.staticCamera + }); + tmpSystemName = title; + tmpSystemDesc = savedSystems.get(title).desc; + menu.hide(); + } + }); + return btn; + }; + let systemGrid = ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + verticalOptions: LayoutOptions.START, + children: getSystemGrid() + }); + let systemGridScroll = ui.createScrollView + ({ + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.2, systemGrid.height)), + content: systemGrid + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuNaming'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['30*', '70*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelName'), + row: 0, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + nameEntry + ] + }), + ui.createGrid + ({ + columnDefinitions: ['30*', '70*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelDesc'), + row: 0, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + descEntry + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelSavedSystems'), + savedSystems.size), + // horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }), + systemGridScroll, + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createButton + ({ + text: getLoc('btnSave'), + row: 0, + column: 1, + onClicked: () => + { + Sound.playClick(); + while(savedSystems.has(tmpName)) + tmpName += getLoc('duplicateSuffix'); + savedSystems.set(tmpName, { + desc: tmpDesc, + system: renderer.system.object, + config: renderer.staticCamera + }); + tmpSystemName = tmpName; + tmpSystemDesc = tmpDesc; + menu.hide(); + } + }) + ] + }) + }); + return menu; +} + +let createSystemClipboardMenu = (values) => +{ + let totalLength = 0; + let tmpSys = []; + let sysEntries = []; + for(let i = 0; i * ENTRY_CHAR_LIMIT < values.length; ++i) + { + tmpSys.push(values.slice(i * ENTRY_CHAR_LIMIT, (i + 1) * + ENTRY_CHAR_LIMIT)); + sysEntries.push(ui.createEntry + ({ + text: tmpSys[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpSys[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + } + let lengthLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelTotalLength'), totalLength), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let entryStack = ui.createStackLayout + ({ + children: sysEntries + }); + let addEntryButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = sysEntries.length; + tmpSys[i] = ''; + sysEntries.push(ui.createEntry + ({ + text: tmpSys[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpSys[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + entryStack.children = sysEntries; + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuClipboard'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + lengthLabel, + addEntryButton + ] + }), + entryStack, + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createButton + ({ + text: getLoc('btnConstruct'), + onClicked: () => + { + Sound.playClick(); + let sv = JSON.parse(tmpSys.join('')); + tmpSystemName = sv.title; + tmpSystemDesc = sv.desc; + renderer.constructSystem = new LSystem(sv.system.axiom, + sv.system.rules, sv.system.turnAngle, + sv.system.seed, sv.system.ignoreList, + sv.system.ctxIgnoreList, sv.system.tropism, + sv.system.variables); + tmpSystem = null; + if('config' in sv) + renderer.configureStaticCamera(...sv.config); + menu.hide(); + } + }) + ] + }) + }); + return menu; +} + +let createViewMenu = (title, parentMenu) => +{ + let systemObj = savedSystems.get(title); + let values = systemObj.system; + let tmpDesc = systemObj.desc; + if(!tmpDesc) + tmpDesc = getLoc('noDescription'); + let rendererValues = systemObj.config; + let tmpZE = rendererValues[0]; + let tmpCX = rendererValues[1]; + let tmpCY = rendererValues[2]; + let tmpCZ = rendererValues[3]; + let tmpUpright = rendererValues[4]; + + let zoomEntry = ui.createEntry + ({ + text: tmpZE, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpZE = nt; + } + }); + let camLabel = ui.createLatexLabel + ({ + text: getLoc('labelCamCentre'), + row: 1, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let camGrid = ui.createEntry + ({ + text: tmpCX, + row: 1, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCX = nt; + } + }); + let camOffLabel = ui.createGrid + ({ + row: 2, + column: 0, + columnDefinitions: ['40*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelCamOffset'), + row: 0, + column: 0, + // horizontalTextAlignment: TextAlignment.END, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createEntry + ({ + text: tmpCY, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCY = nt; + } + }) + ] + }); + let camOffGrid = ui.createEntry + ({ + text: tmpCZ, + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCZ = nt; + } + }); + let uprightSwitch = ui.createSwitch + ({ + isToggled: tmpUpright, + row: 3, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpUpright = !tmpUpright; + uprightSwitch.isToggled = tmpUpright; + } + } + }); + + let tmpAxiom = values.axiom; + let axiomEntry = ui.createEntry + ({ + text: tmpAxiom, + row: 0, + column: 1, + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpAxiom = nt; + } + }); + let tmpVars = Object.entries(values.variables); + let varButton = ui.createButton + ({ + text: getLoc('btnVar'), + row: 0, + column: 2, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let varMenu = createVariableMenu(tmpVars); + varMenu.show(); + } + }); + let tmpRules = []; + for(let i = 0; i < values.rules.length; ++i) + tmpRules[i] = values.rules[i]; + let ruleEntries = []; + let ruleMoveBtns = []; + for(let i = 0; i < tmpRules.length; ++i) + { + ruleEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpRules[i] = nt; + } + })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } + } + let rulesLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelRules'), ruleEntries.length), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let ruleStack = ui.createGrid + ({ + columnDefinitions: ['7*', '1*'], + children: [...ruleEntries, ...ruleMoveBtns] + }); + let addRuleButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = ruleEntries.length; + tmpRules[i] = ''; + ruleEntries.push(ui.createEntry + ({ + row: i, + column: 0, + text: tmpRules[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpRules[i] = nt; + } + })); + if(i) + { + ruleMoveBtns.push(ui.createButton + ({ + row: i, + column: 1, + text: getLoc('btnUp'), + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let tmpRule = tmpRules[i]; + tmpRules[i] = tmpRules[i - 1]; + tmpRules[i - 1] = tmpRule; + ruleEntries[i - 1].text = tmpRules[i - 1]; + ruleEntries[i].text = tmpRules[i]; + } + })); + } + rulesLabel.text = Localization.format(getLoc('labelRules'), + ruleEntries.length); + ruleStack.children = [...ruleEntries, ...ruleMoveBtns]; + } + }); + let tmpIgnore = values.ignoreList || ''; + let ignoreEntry = ui.createEntry + ({ + text: tmpIgnore, + row: 0, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpIgnore = nt; + } + }); + let tmpCI = values.ctxIgnoreList || ''; + let CIEntry = ui.createEntry + ({ + text: tmpCI, + row: 1, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpCI = nt; + } + }); + let tmpAngle = values.turnAngle || '0'; + let angleEntry = ui.createEntry + ({ + text: tmpAngle.toString(), + row: 2, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpAngle = nt; + } + }); + let tmpTropism = values.tropism || '0'; + let tropismEntry = ui.createEntry + ({ + text: tmpTropism.toString(), + row: 3, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpTropism = nt; + } + }); + let tmpSeed = values.seed || '0'; + let seedLabel = ui.createGrid + ({ + row: 4, + column: 0, + columnDefinitions: ['40*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelSeed'), + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnReroll'), + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + seedEntry.text = globalRNG.nextInt.toString(); + } + }) + ] + }); + let seedEntry = ui.createEntry + ({ + text: tmpSeed.toString(), + keyboard: Keyboard.NUMERIC, + row: 4, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpSeed = Number(nt); + } + }); + + let menu = ui.createPopup + ({ + title: title, + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + ui.createScrollView + ({ + // heightRequest: ui.screenHeight * 0.32, + content: ui.createStackLayout + ({ + children: + [ + ui.createLatexLabel + ({ + text: tmpDesc, + margin: new Thickness(0, 6), + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createGrid + ({ + columnDefinitions: ['20*', '50*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelAxiom'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + axiomEntry, + varButton + ] + }), + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + rulesLabel, + addRuleButton + ] + }), + ruleStack, + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelIgnored'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + ignoreEntry, + ui.createLatexLabel + ({ + text: getLoc('labelCtxIgnored'), + row: 1, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + CIEntry, + ui.createLatexLabel + ({ + text: getLoc('labelAngle'), + row: 2, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + angleEntry, + ui.createLatexLabel + ({ + text: getLoc('labelTropism'), + row: 3, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + tropismEntry, + seedLabel, + seedEntry + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createLatexLabel + ({ + text: getLoc('labelApplyCamera'), + // horizontalTextAlignment: + // TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 9) + }), + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelFigScale'), + row: 0, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + zoomEntry, + camLabel, + camGrid, + camOffLabel, + camOffGrid, + ui.createLatexLabel + ({ + text: getLoc('labelUpright'), + row: 3, + column: 0, + verticalTextAlignment: + TextAlignment.CENTER + }), + uprightSwitch + ] + }) + ] + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + minimumHeightRequest: BUTTON_HEIGHT, + columnDefinitions: ['30*', '30*', '30*'], + children: + [ + ui.createButton + ({ + text: getLoc('btnConstruct'), + row: 0, + column: 0, + onClicked: () => + { + Sound.playClick(); + renderer.constructSystem = new LSystem(tmpAxiom, + tmpRules, tmpAngle, tmpSeed, tmpIgnore, tmpCI, + tmpTropism, Object.fromEntries(tmpVars)); + tmpSystem = null; + renderer.configureStaticCamera(tmpZE, tmpCX, + tmpCY, tmpCZ, tmpUpright); + tmpSystemName = title; + tmpSystemDesc = tmpDesc; + parentMenu.hide(); + menu.hide(); + } + }), + ui.createButton + ({ + text: getLoc('btnSave'), + row: 0, + column: 1, + onClicked: () => + { + Sound.playClick(); + savedSystems.set(title, + { + desc: tmpDesc, + system: new LSystem(tmpAxiom, tmpRules, + tmpAngle, tmpSeed, tmpIgnore, tmpCI, + tmpTropism, Object.fromEntries(tmpVars)). + object, + config: [tmpZE, tmpCX, tmpCY, tmpCZ, + tmpUpright] + }); + // menu.hide(); + } + }), + ui.createButton + ({ + text: getLoc('btnDelete'), + row: 0, + column: 2, + onClicked: () => + { + Sound.playClick(); + savedSystems.delete(title); + menu.hide(); + } + }) + ] + }) + ] + }) + }); + return menu; +} + +let createSaveMenu = () => +{ + let savedSystemsLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelSavedSystems'), + savedSystems.size), + // horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let getSystemGrid = () => + { + let children = []; + let i = 0; + for(let [key, value] of savedSystems) + { + children.push(ui.createLatexLabel + ({ + text: key, + row: i, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + })); + let btn = createViewButton(key); + btn.row = i; + btn.column = 1; + children.push(btn); + ++i; + } + savedSystemsLabel.text = Localization.format( + getLoc('labelSavedSystems'), savedSystems.size); + return children; + }; + let createViewButton = (title) => + { + let btn = ui.createButton + ({ + text: getLoc('btnView'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let viewMenu = createViewMenu(title, menu); + viewMenu.onDisappearing = () => + { + systemGrid.children = getSystemGrid(); + }; + viewMenu.show(); + } + }); + return btn; + }; + let systemGrid = ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + verticalOptions: LayoutOptions.START, + children: getSystemGrid() + }); + let systemGridScroll = ui.createScrollView + ({ + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.32, systemGrid.height)), + content: systemGrid + }); + let menu = ui.createPopup + ({ + title: getLoc('menuSave'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['40*', '30*', '30*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelCurrentSystem'), + row: 0, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnClipboard'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + let clipMenu = createSystemClipboardMenu( + JSON.stringify( + { + title: tmpSystemName, + desc: tmpSystemDesc, + system: renderer.system.object, + config: renderer.staticCamera + })); + clipMenu.show(); + } + }), + ui.createButton + ({ + text: getLoc('btnSave'), + row: 0, + column: 2, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let namingMenu = createNamingMenu(); + namingMenu.onDisappearing = () => + { + systemGrid.children = getSystemGrid(); + }; + namingMenu.show(); + } + }) + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + savedSystemsLabel, + systemGridScroll + ] + }) + }); + return menu; +} + +let createManualMenu = () => +{ + let manualPages = getLoc('manual'); + + let pageTitle = ui.createLatexLabel + ({ + text: manualPages[page].title, + margin: new Thickness(0, 4), + heightRequest: 20, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + }); + let pageContents = ui.createLabel + ({ + fontFamily: FontFamily.CMU_REGULAR, + fontSize: 16, + text: manualPages[page].contents + }); + let sourceEntry = ui.createEntry + ({ + row: 0, + column: 1, + text: 'source' in manualPages[page] ? manualPages[page].source : '' + }); + let sourceGrid = ui.createGrid + ({ + isVisible: 'source' in manualPages[page], + columnDefinitions: ['auto', '1*'], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelSource'), + row: 0, + column: 0, + horizontalTextAlignment: TextAlignment.START, + verticalTextAlignment: TextAlignment.CENTER + }), + sourceEntry + ] + }); + let prevButton = ui.createButton + ({ + text: getLoc('btnPrev'), + row: 0, + column: 0, + isVisible: page > 0, + onClicked: () => + { + Sound.playClick(); + if(page > 0) + setPage(page - 1); + } + }); + let constructButton = ui.createButton + ({ + text: getLoc('btnConstruct'), + row: 0, + column: 1, + isVisible: page in manualSystems, + onClicked: () => + { + Sound.playClick(); + let s = manualSystems[page]; + renderer.constructSystem = s.system; + tmpSystem = null; + if('config' in s) + renderer.configureStaticCamera(...s.config); + + tmpSystemName = manualPages[page].title; + tmpSystemDesc = Localization.format( + getLoc('manualSystemDesc'), page + 1); + menu.hide(); + } + }); + let tocButton = ui.createButton + ({ + text: getLoc('btnContents'), + row: 0, + column: 1, + isVisible: !(page in manualSystems), + onClicked: () => + { + Sound.playClick(); + TOCMenu.show(); + } + }); + let nextButton = ui.createButton + ({ + text: getLoc('btnNext'), + row: 0, + column: 2, + isVisible: page < manualPages.length - 1, + onClicked: () => + { + Sound.playClick(); + if(page < manualPages.length - 1) + setPage(page + 1); + } + }); + let setPage = (p) => + { + page = p; + menu.title = Localization.format( + getLoc('menuManual'), page + 1, + getLoc('manual').length + ); + pageTitle.text = manualPages[page].title; + pageContents.text = + manualPages[page].contents; + + sourceGrid.isVisible = 'source' in + manualPages[page]; + sourceEntry.text = 'source' in + manualPages[page] ? + manualPages[page].source : ''; + + prevButton.isVisible = page > 0; + nextButton.isVisible = page < manualPages.length - 1; + constructButton.isVisible = page in manualSystems; + tocButton.isVisible = !(page in manualSystems); + }; + let getContentsTable = () => + { + let children = []; + for(let i = 0; i < contentsTable.length; ++i) + { + children.push(ui.createLatexLabel + ({ + text: manualPages[contentsTable[i]].title, + row: i, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + })); + children.push(ui.createButton + ({ + text: Localization.format(getLoc('btnPage'), + contentsTable[i] + 1), + row: i, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + setPage(contentsTable[i]); + TOCMenu.hide(); + } + })); + } + return children; + }; + let TOCMenu = ui.createPopup + ({ + title: getLoc('menuTOC'), + content: ui.createScrollView + ({ + heightRequest: ui.screenHeight * 0.36, + content: ui.createGrid + ({ + columnDefinitions: ['80*', '20*'], + children: getContentsTable() + }) + }) + }); + + let menu = ui.createPopup + ({ + title: Localization.format(getLoc('menuManual'), page + 1, + getLoc('manual').length), + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + pageTitle, + ui.createFrame + ({ + padding: new Thickness(8, 6), + heightRequest: ui.screenHeight * 0.32, + content: ui.createScrollView + ({ + content: ui.createStackLayout + ({ + children: + [ + pageContents, + sourceGrid + ] + }) + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + columnDefinitions: ['30*', '30*', '30*'], + children: + [ + prevButton, + constructButton, + tocButton, + nextButton + ] + }) + ] + }) + }); + return menu; +} + +let createSeqViewMenu = (level) => +{ + let reconstructionTask = + { + start: 0 + }; + let updateReconstruction = () => + { + if(!('result' in reconstructionTask) || + ('result' in reconstructionTask && reconstructionTask.start)) + { + reconstructionTask = renderer.system.reconstruct( + renderer.levels[level], renderer.levelParams[level], + reconstructionTask); + } + return reconstructionTask.result; + } + let pageTitle = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelLevelSeq'), level, + renderer.levels[level].length), + margin: new Thickness(0, 4), + heightRequest: 20, + horizontalTextAlignment: TextAlignment.CENTER, + verticalTextAlignment: TextAlignment.CENTER + }); + let pageContents = ui.createLabel + ({ + fontFamily: FontFamily.CMU_REGULAR, + fontSize: 16, + text: () => updateReconstruction(), + lineBreakMode: LineBreakMode.CHARACTER_WRAP + }); + let prevButton = ui.createButton + ({ + text: getLoc('btnPrev'), + row: 0, + column: 0, + isVisible: level > 0, + onClicked: () => + { + Sound.playClick(); + if(level > 0) + setPage(level - 1); + } + }); + let nextButton = ui.createButton + ({ + text: getLoc('btnNext'), + row: 0, + column: 1, + isVisible: level < renderer.levels.length - 1, + onClicked: () => + { + Sound.playClick(); + if(level < renderer.levels.length - 1) + setPage(level + 1); + } + }); + let setPage = (p) => + { + level = p; + pageTitle.text = Localization.format(getLoc('labelLevelSeq'), level, + renderer.levels[level].length); + reconstructionTask = + { + start: 0 + }; + + prevButton.isVisible = level > 0; + nextButton.isVisible = level < renderer.levels.length - 1; + }; + + let menu = ui.createPopup + ({ + title: tmpSystemName, + isPeekable: true, + content: ui.createStackLayout + ({ + children: + [ + pageTitle, + ui.createFrame + ({ + padding: new Thickness(8, 6), + heightRequest: ui.screenHeight * 0.28, + content: ui.createScrollView + ({ + content: ui.createStackLayout + ({ + children: + [ + pageContents + ] + }) + }) + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createGrid + ({ + columnDefinitions: ['50*', '50*'], + children: + [ + prevButton, + nextButton + ] + }) + ] + }) + }); + return menu; +} + +let createSequenceMenu = () => +{ + let tmpLvls = []; + for(let i = 0; i < renderer.levels.length; ++i) + { + tmpLvls.push(ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelLevelSeq'), i, + renderer.levels[i].length), + row: i, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + })); + tmpLvls.push(ui.createButton + ({ + text: getLoc('btnView'), + row: i, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let viewMenu = createSeqViewMenu(i); + viewMenu.show(); + } + })); + } + let seqGrid = ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: tmpLvls + }); + + let menu = ui.createPopup + ({ + title: tmpSystemName, + content: ui.createStackLayout + ({ + children: + [ + ui.createScrollView + ({ + heightRequest: () => Math.max(SMALL_BUTTON_HEIGHT, + Math.min(ui.screenHeight * 0.36, seqGrid.height)), + content: seqGrid + }) + ] + }) + }); + return menu; +} + +let createStateClipboardMenu = (values) => +{ + let totalLength = 0; + let tmpState = []; + let stateEntries = []; + for(let i = 0; i * ENTRY_CHAR_LIMIT < values.length; ++i) + { + tmpState.push(values.slice(i * ENTRY_CHAR_LIMIT, (i + 1) * + ENTRY_CHAR_LIMIT)); + stateEntries.push(ui.createEntry + ({ + text: tmpState[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpState[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + } + let lengthLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelTotalLength'), totalLength), + verticalTextAlignment: TextAlignment.CENTER, + margin: new Thickness(0, 12) + }); + let entryStack = ui.createStackLayout + ({ + children: stateEntries + }); + let addEntryButton = ui.createButton + ({ + text: getLoc('btnAdd'), + row: 0, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + Sound.playClick(); + let i = stateEntries.length; + tmpState[i] = ''; + stateEntries.push(ui.createEntry + ({ + text: tmpState[i], + clearButtonVisibility: ClearButtonVisibility.WHILE_EDITING, + onTextChanged: (ot, nt) => + { + tmpState[i] = nt; + totalLength += (nt ? nt.length : 0) - (ot ? ot.length : 0); + lengthLabel.text = Localization.format(getLoc( + 'labelTotalLength'), totalLength); + } + })); + entryStack.children = stateEntries; + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuClipboard'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + children: + [ + lengthLabel, + addEntryButton + ] + }), + entryStack, + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createButton + ({ + text: getLoc('btnImport'), + onClicked: () => + { + Sound.playClick(); + setInternalState(tmpState.join('')); + menu.hide(); + } + }) + ] + }) + }); + return menu; +} + +let createWorldMenu = () => +{ + let tmpOD = offlineReset; + let ODSwitch = ui.createSwitch + ({ + isToggled: tmpOD, + row: 0, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpOD = !tmpOD; + ODSwitch.isToggled = tmpOD; + } + } + }); + let tmpRL = resetLvlOnConstruct; + let RLSwitch = ui.createSwitch + ({ + isToggled: tmpRL, + row: 1, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpRL = !tmpRL; + RLSwitch.isToggled = tmpRL; + } + } + }); + let tmpAC = altTerEq; + let ACLabel = ui.createLatexLabel + ({ + text: Localization.format(getLoc('labelTerEq'), + getLoc('terEqModes')[Number(tmpAC)]), + row: 2, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }); + let ACSwitch = ui.createSwitch + ({ + isToggled: tmpAC, + row: 2, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpAC = !tmpAC; + ACSwitch.isToggled = tmpAC; + ACLabel.text = Localization.format(getLoc('labelTerEq'), + getLoc('terEqModes')[Number(tmpAC)]); + } + } + }); + let tmpMP = measurePerformance; + let MPSwitch = ui.createSwitch + ({ + isToggled: tmpMP, + row: 3, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpMP = !tmpMP; + MPSwitch.isToggled = tmpMP; + } + } + }); + let tmpDCP = debugCamPath; + let DCPSwitch = ui.createSwitch + ({ + isToggled: tmpDCP, + row: 4, + column: 1, + horizontalOptions: LayoutOptions.END, + onTouched: (e) => + { + if(e.type == TouchType.SHORTPRESS_RELEASED || + e.type == TouchType.LONGPRESS_RELEASED) + { + Sound.playClick(); + tmpDCP = !tmpDCP; + DCPSwitch.isToggled = tmpDCP; + } + } + }); + let tmpMCPT = maxCharsPerTick; + let MCPTEntry = ui.createEntry + ({ + text: tmpMCPT.toString(), + keyboard: Keyboard.NUMERIC, + row: 5, + column: 1, + horizontalTextAlignment: TextAlignment.END, + onTextChanged: (ot, nt) => + { + tmpMCPT = Number(nt); + } + }); + + let menu = ui.createPopup + ({ + title: getLoc('menuTheory'), + content: ui.createStackLayout + ({ + children: + [ + ui.createGrid + ({ + columnDefinitions: ['70*', '30*'], + // rowDefinitions: [40, 40, 40, 40], + children: + [ + ui.createLatexLabel + ({ + text: getLoc('labelOfflineReset'), + row: 0, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ODSwitch, + ui.createLatexLabel + ({ + text: getLoc('labelResetLvl'), + row: 1, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + RLSwitch, + ACLabel, + ACSwitch, + ui.createLatexLabel + ({ + text: getLoc('labelMeasure'), + row: 3, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + MPSwitch, + ui.createLatexLabel + ({ + text: getLoc('debugCamPath'), + row: 4, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + DCPSwitch, + ui.createLatexLabel + ({ + text: getLoc('labelMaxCharsPerTick'), + row: 5, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + MCPTEntry, + ui.createLatexLabel + ({ + text: getLoc('labelInternalState'), + row: 6, + column: 0, + verticalTextAlignment: TextAlignment.CENTER + }), + ui.createButton + ({ + text: getLoc('btnClipboard'), + row: 6, + column: 1, + heightRequest: SMALL_BUTTON_HEIGHT, + onClicked: () => + { + let clipMenu = createStateClipboardMenu( + getInternalState()); + clipMenu.show(); + } + }), + ] + }), + ui.createBox + ({ + heightRequest: 1, + margin: new Thickness(0, 6) + }), + ui.createButton + ({ + text: getLoc('btnSave'), + onClicked: () => + { + Sound.playClick(); + offlineReset = tmpOD; + resetLvlOnConstruct = tmpRL; + altTerEq = tmpAC; + if(tmpMP != measurePerformance && tmpMP) + { + drawMeasurer.reset(); + camMeasurer.reset(); + } + measurePerformance = tmpMP; + if(tmpDCP != debugCamPath) + renderer.reset(); + debugCamPath = tmpDCP; + maxCharsPerTick = tmpMCPT; + menu.hide(); + } + }) + ] + }) + }); + return menu; +} + +var getInternalState = () => JSON.stringify +({ + version: version, + time: time, + page: page, + offlineReset: offlineReset, + altTerEq: altTerEq, + tickDelayMode: tickDelayMode, + resetLvlOnConstruct: resetLvlOnConstruct, + measurePerformance: measurePerformance, + debugCamPath: debugCamPath, + maxCharsPerTick: maxCharsPerTick, + renderer: renderer.object, + system: tmpSystem ? + { + title: tmpSystemName, + desc: tmpSystemDesc, + ...tmpSystem.object + } : + { + title: tmpSystemName, + desc: tmpSystemDesc, + ...renderer.system.object + }, + savedSystems: Object.fromEntries(savedSystems) +}); + +var setInternalState = (stateStr) => +{ + if(!stateStr) + return; + + let values = stateStr.split('\n'); + + let worldValues = values[0].split(' '); + let stateVersion = 0; + if(worldValues.length > 0) + stateVersion = Number(worldValues[0]); + + if(isNaN(stateVersion)) + { + let state = JSON.parse(stateStr); + log(`Loading JSON state (version: ${state.version})`); + if('time' in state) + time = state.time; + if('page' in state) + page = state.page; + if('offlineReset' in state) + offlineReset = state.offlineReset; + if('altTerEq' in state) + altTerEq = state.altTerEq; + if('tickDelayMode' in state) + tickDelayMode = state.tickDelayMode; + if('resetLvlOnConstruct' in state) + resetLvlOnConstruct = state.resetLvlOnConstruct; + if('measurePerformance' in state) + measurePerformance = state.measurePerformance; + if('debugCamPath' in state) + debugCamPath = state.debugCamPath; + if('maxCharsPerTick' in state) + maxCharsPerTick = state.maxCharsPerTick; + + if('system' in state) + { + tmpSystemName = state.system.title; + tmpSystemDesc = state.system.desc; + tmpSystem = new LSystem(state.system.axiom, state.system.rules, + state.system.turnAngle, state.system.seed, state.system.ignoreList, + state.system.ctxIgnoreList, state.system.tropism, + state.system.variables); + } + + if('renderer' in state) + { + renderer = new Renderer(new LSystem(), state.renderer.figureScale, + state.renderer.cameraMode, state.renderer.camX, state.renderer.camY, + state.renderer.camZ, state.renderer.followFactor, + state.renderer.loopMode, state.renderer.upright, + state.renderer.quickDraw, state.renderer.quickBacktrack, + state.renderer.loadModels, state.renderer.backtrackTail, + state.renderer.hesitateApex, state.renderer.hesitateFork); + } + else + renderer = new Renderer(system); + + if('savedSystems' in state) + savedSystems = new Map(Object.entries(state.savedSystems)); + } + // Doesn't even need checking the version number; if it appears at all then + // it's definitely written before switching to JSON + else + { + log(`Loading space-separated state (version: ${stateVersion})`); + if(worldValues.length > 1) + time = Number(worldValues[1]); + if(worldValues.length > 2) + page = Number(worldValues[2]); + if(worldValues.length > 3) + offlineReset = Boolean(Number(worldValues[3])); + if(worldValues.length > 4) + altTerEq = Boolean(Number(worldValues[4])); + if(worldValues.length > 5) + tickDelayMode = Boolean(Number(worldValues[5])); + if(worldValues.length > 6) + resetLvlOnConstruct = Boolean(Number(worldValues[6])); + let noofSystems = 0; + if(worldValues.length > 7) + noofSystems = Number(worldValues[7]); + + if(values.length > 1) + { + let rv = values[1].split(' '); + if(rv.length > 2) + rv[2] = Number(rv[2]); // cameraMode + if(rv.length > 6) + rv[6] = Number(rv[6]); + if(rv.length > 7) + rv[7] = Number(rv[7]); + if(rv.length > 8) + rv[8] = Boolean(Number(rv[8])); + if(rv.length > 9) + rv[9] = Boolean(Number(rv[9])); + if(rv.length > 10) + rv[10] = Boolean(Number(rv[10])); + if(rv.length > 12) // camera offset + rv[3] = `${rv[3]}*${rv[0]}*${rv[1]}^lv+${rv[12]}`; + if(rv.length > 13) + rv[4] = `${rv[4]}*${rv[0]}*${rv[1]}^lv+${rv[13]}`; + if(rv.length > 14) + rv[5] = `${rv[5]}*${rv[0]}*${rv[1]}^lv+${rv[14]}`; + rv[1] = `${rv[0]}*${rv[1]}^lv`; + if(rv.length > 15) + rv[12] = Boolean(Number(rv[15])); + + for(let i = 13; i < rv.length; ++i) + rv[i] = undefined; + + if(stateVersion < 0.2) + { + if(values.length > 2) + { + let systemValues = values[2].split(' '); + let system = new LSystem(systemValues[0], + systemValues.slice(3), Number(systemValues[1]), + Number(systemValues[2])); + renderer = new Renderer(system, ...rv.slice(1)); + } + else + renderer = new Renderer(new LSystem(), ...rv.slice(1)); + } + else + { + if(values.length > 2) + tmpSystemName = values[2]; + if(values.length > 3) + tmpSystemDesc = values[3]; + if(values.length > 4) + { + let systemValues = values[4].split(' '); + let system = new LSystem(systemValues[0], + systemValues.slice(3), Number(systemValues[1]), + Number(systemValues[2])); + renderer = new Renderer(system, ...rv.slice(1)); + } + else + renderer = new Renderer(new LSystem(), ...rv.slice(1)); + } + } + + if(stateVersion < 0.2) + { + // Load everything. + for(let i = 0; 4 + i * 2 < values.length; ++i) + savedSystems.set(values[3 + i * 2], + { + desc: getLoc('noDescription'), + system: values[4 + i * 2], + config: ['1', '0', '0', '0', false] + }); + } + else + { + for(let i = 0; i < noofSystems; ++i) + { + let rv = values[9 + i * 5].split(' '); + if(rv.length > 5) + rv[2] = `${rv[2]}*${rv[0]}*${rv[1]}^lv+${rv[5]}`; + if(rv.length > 6) + rv[3] = `${rv[3]}*${rv[0]}*${rv[1]}^lv+${rv[6]}`; + if(rv.length > 7) + { + rv[4] = `${rv[4]}*${rv[0]}*${rv[1]}^lv+${rv[7]}`; + rv[1] = `${rv[0]}*${rv[1]}^lv`; + } + if(rv.length > 8) + rv[5] = Boolean(Number(rv[8])); + + for(let i = 6; i < rv.length; ++i) + rv[i] = undefined; + + savedSystems.set(values[6 + i * 5], { + desc: values[7 + i * 5], + system: values[8 + i * 5], + config: rv.slice(1) + }); + } + } + } +} + +var canResetStage = () => true; + +var getResetStageMessage = () => getLoc('resetRenderer'); + +var resetStage = () => renderer.reset(); + +var getTertiaryEquation = () => +{ + if(altTerEq) + return renderer.oriString; + + return renderer.stateString; +} + +var get3DGraphPoint = () => +{ + if(debugCamPath) + return -renderer.camera; + + return renderer.cursor; +} + +var get3DGraphTranslation = () => +{ + if(measurePerformance) + camMeasurer.stamp(); + + let result = renderer.camera; + + if(measurePerformance) + camMeasurer.stamp(); + + return result; +} + +init(); + +let testDerive = () => +{ + let a = new LSystem('[+(30)G]F/(180)A(2)', [ + 'A(t):t<=5=[+(30)G]F/(180)A(t+2):0.5,[-(30)G]F\\(180)A(t+2):0.4,:0,C:0' + ], 30, 1); + // A(0) + let a0 = 'A'; + let a0p = [[BigNumber.ZERO]]; + let at0 = a.getAncestree(a0); + let tmpDeriv = a.derive(a0, a0p, at0.ancestors, at0.children, 0); + let a1 = tmpDeriv.result; + let a1p = tmpDeriv.params; + log(a.reconstruct(a1, a1p)); + + let at1 = a.getAncestree(a1); + tmpDeriv = a.derive(a1, a1p, at1.ancestors, at1.children, 0); + let a2 = tmpDeriv.result; + let a2p = tmpDeriv.params; + log(a.reconstruct(a2, a2p)); + + let b0 = 'X'; + let b0p = [null]; + let bt0 = arrow.getAncestree(b0); + tmpDeriv = arrow.derive(b0, b0p, bt0.ancestors, bt0.children, 0); + let b1 = tmpDeriv.result; + let b1p = tmpDeriv.params; + log(arrow.reconstruct(b1, b1p)); + + let bt1 = arrow.getAncestree(b1); + tmpDeriv = arrow.derive(b1, b1p, bt1.ancestors, bt1.children, 0); + let b2 = tmpDeriv.result; + let b2p = tmpDeriv.params; + log(arrow.reconstruct(b2, b2p)); +} + +let testQuaternion = () => +{ + // Normalisation = off + let a = new Quaternion().rotate(60, '+').rotate(60, '+'); + let b = new Quaternion().rotate(120, '+'); + // Round 1 results: + // [10:56:40] 60 + 60: + // 0.500 + 0.000i + 0.000j + -0.87k + // (-0.5, 0.866025403784439, 0) + // [10:56:40] 120: + // -0.50 + 0.000i + 0.000j + 0.866k + // (-0.5, 0.866025403784439, 0) + a = a.rotate(60, '+').rotate(60, '+'); + b = b.rotate(120, '+'); + // Round 2 results: + // [10:58:17] 60 + 60: + // -0.50 + 0.000i + 0.000j + -0.87k + // (-0.500000000000001, -0.866025403784438, 0) + // [10:58:17] 120: + // -0.50 + 0.000i + 0.000j + -0.87k + // (-0.500000000000001, -0.866025403784438, 0) + a = a.rotate(60, '+').rotate(60, '+'); + b = b.rotate(120, '+'); + // Round 3 results: + // [10:59:03] 60 + 60: + // -1.00 + 0.000i + 0.000j + 0.000k + // (1, -1.11022302462516E-15, 0) + // [10:59:03] 120: + // 1.000 + 0.000i + 0.000j + 0.000k + // (1, -7.7715611723761E-16, 0) + a = a.rotate(60, '+').rotate(60, '+'); + b = b.rotate(120, '+'); + // Round 4 results: + // [11:00:54] 60 + 60: + // -0.50 + 0.000i + 0.000j + 0.866k + // (-0.499999999999999, 0.866025403784439, 0) + // [11:00:54] 120: + // -0.50 + 0.000i + 0.000j + 0.866k + // (-0.499999999999999, 0.866025403784439, 0) + a = a.rotate(60, '+').rotate(60, '+'); + b = b.rotate(120, '+'); + // Round 5 results: + // [11:01:45] 60 + 60: + // 0.500 + 0.000i + 0.000j + 0.866k + // (-0.500000000000002, -0.866025403784438, 0) + // [11:01:45] 120: + // -0.50 + 0.000i + 0.000j + -0.87k + // (-0.500000000000001, -0.866025403784438, 0) + a = a.rotate(60, '+').rotate(60, '+'); + b = b.rotate(120, '+'); + // Round 6 results: + // [11:02:19] 60 + 60: + // 1.000 + 0.000i + 0.000j + 0.000k + // (1, -2.22044604925031E-15, 0) + // [11:02:19] 120: + // 1.000 + 0.000i + 0.000j + 0.000k + // (1, -1.77635683940025E-15, 0) + + log(`60 + 60:\n${a.toString()}\n${a.headingVector.toString()}`); + log(`120:\n${b.toString()}\n${b.headingVector.toString()}`); +} + +let testQuaternion2 = () => +{ + // Normalisation = off + let a = new Quaternion().rotate(60, '+').rotate(60, '+').rotate(120, '-'); + // Round 1 results: + // [11:05:41] 60 + 60 - 120: + // -1.00 + 0.000i + 0.000j + 0.000k + // (1, -1.11022302462516E-16, 0) + a = a.rotate(60, '+').rotate(60, '+').rotate(120, '-'); + // Round 2 results: + // [11:07:00] 60 + 60 - 120: + // 1.000 + 0.000i + 0.000j + 0.000k + // (1, -1.11022302462516E-16, 0) + a = a.rotate(60, '+').rotate(60, '+').rotate(120, '-'); + // Round 3 results: + // [11:07:31] 60 + 60 - 120: + // -1.00 + 0.000i + 0.000j + 0.000k + // (1, -1.11022302462516E-16, 0) + log(`60 + 60 - 120:\n${a.toString()}\n${a.headingVector.toString()}`); + // Results are exact same if replaced with BigNumber.from(120).toNumber(). +} + +let testQuaternion3 = () => +{ + // Normalisation = off + let a = new Quaternion().rotate(60, '+').rotate(120, '-').rotate(60, '+'); + for(let i = 0; i < 1000; ++i) + a = a.rotate(60, '+').rotate(120, '-').rotate(60, '+'); + + // [11:12:20] 60 - 120 + 60: + // -1.00 + 0.000i + 0.000j + 0.000k + // (1, -2.22044604925031E-16, 0) + log(`60 - 120 + 60:\n${a.toString()}\n${a.headingVector.toString()}`); +} + +let testVector = () => +{ + // Normalisation = off + let state = new Vector3(0, 0, 0); + let ori = new Quaternion(); + for(let i = 0; i < 180; ++i) + { + state += ori.headingVector; + ori = ori.rotate(120, '+'); + state += ori.headingVector; + ori = ori.rotate(120, '-'); + state += ori.headingVector; + ori = ori.rotate(120, '-'); + state += ori.headingVector; + ori = ori.rotate(120, '+'); + state += ori.headingVector; + ori = ori.rotate(120, '+'); + state += ori.headingVector; + state += ori.headingVector; + ori = ori.rotate(120, '+'); + state += ori.headingVector; + state += ori.headingVector; + ori = ori.rotate(120, '+'); + if(i % 10 == 9) + log(`Error ${i}: ${state.x} ${state.y}`); + } + // [11:30:13] Error 9: -8.881784197001252e-16 6.8833827526759706e-15 + // [11:30:14] Error 19: -1.7763568394002505e-15 1.2656542480726785e-14 + // [11:30:14] Error 29: 1.7763568394002505e-15 1.887379141862766e-14 + // [11:30:14] Error 39: 3.9968028886505635e-15 2.55351295663786e-14 + // [11:30:14] Error 49: 4.884981308350689e-15 3.2862601528904634e-14 + // [11:30:14] Error 59: 7.327471962526033e-15 3.907985046680551e-14 + // [11:30:14] Error 69: 8.881784197001252e-15 4.707345624410664e-14 + // [11:30:14] Error 79: 1.199040866595169e-14 5.484501741648273e-14 + // [11:30:14] Error 89: 1.3322676295501878e-14 6.150635556423367e-14 + // [11:30:14] Error 99: 1.4654943925052066e-14 6.816769371198461e-14 + // [11:30:14] Error 109: 1.6431300764452317e-14 7.527312106958561e-14 + // [11:30:14] Error 119: 1.8207657603852567e-14 8.193445921733655e-14 + // [11:30:14] Error 129: 1.9539925233402755e-14 8.948397578478762e-14 + // [11:30:14] Error 139: 2.2648549702353193e-14 9.614531393253856e-14 + // [11:30:14] Error 149: 2.3092638912203256e-14 1.0347278589506459e-13 + // [11:30:14] Error 159: 2.531308496145357e-14 1.1013412404281553e-13 + // [11:30:14] Error 169: 2.708944180085382e-14 1.1723955140041653e-13 + // [11:30:14] Error 179: 2.9753977059954195e-14 1.2523315717771766e-13 +} + +let testVector2 = () => +{ + let state = new Vector3(0, 0, 0); + let ori = new Quaternion(); + + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '+'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '-'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '-'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '+'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '+'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + ori = ori.rotate(120, '+'); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + state += ori.headingVector; + log(`${state.x} ${state.y}`); + + // [11:39:17] 1 0 + // [11:39:17] 0.5000000000000002 0.8660254037844388 + // [11:39:17] 1.5000000000000002 0.8660254037844388 + // [11:39:17] 1.0000000000000004 0 + // [11:39:17] 2.0000000000000004 0 + // [11:39:17] 1.5000000000000007 0.8660254037844388 + // [11:39:17] 1.0000000000000009 1.7320508075688776 + // [11:39:17] 0.5000000000000003 0.8660254037844392 + // [11:39:17] -2.220446049250313e-16 6.661338147750939e-16 + + // Renderer results: + // [11:40:39] 1 0 + // [11:40:39] 0.4951538954001423 0.8632093666488738 + // [11:40:39] 1.4951538954001427 0.8632093666488738 + // [11:40:40] 0.9903077908002847 -3.3306690738754696e-16 + // [11:40:40] 1.9903077908002853 -4.440892098500626e-16 + // [11:40:40] 1.4854616862004275 0.8632093666488738 + // [11:40:40] 0.9806155816005697 1.726418733297748 + // [11:40:40] 0.4903547602598699 0.8548429608841593 + // [11:40:40] 0.00009393891917008901 -0.016732811529429403 + + // Rotation half-angle: 120 -> 1.0471975511965976 + // System half-angle: 120 -> 1.05 +} + +let testAngle = () => +{ + let straightNum = 120; + let bigNumConvert = BigNumber.from('120').toNumber(); + let expressionResult = MathExpression.parse('120').evaluate().toNumber(); + log(`${straightNum} vs. ${bigNumConvert} vs. ${expressionResult}`); +} diff --git a/screenshots/35.jpg b/screenshots/35.jpg new file mode 100644 index 0000000..502745d Binary files /dev/null and b/screenshots/35.jpg differ diff --git a/screenshots/36.jpg b/screenshots/36.jpg new file mode 100644 index 0000000..31fa25e Binary files /dev/null and b/screenshots/36.jpg differ diff --git a/screenshots/37.jpg b/screenshots/37.jpg new file mode 100644 index 0000000..3ab66dd Binary files /dev/null and b/screenshots/37.jpg differ diff --git a/screenshots/38.jpg b/screenshots/38.jpg new file mode 100644 index 0000000..64a17eb Binary files /dev/null and b/screenshots/38.jpg differ diff --git a/screenshots/39.jpg b/screenshots/39.jpg new file mode 100644 index 0000000..bdb650f Binary files /dev/null and b/screenshots/39.jpg differ