0c5ac32b599232531803f2fea695ad17e7de6820
203
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0c5ac32b59 |
Let structs and lists nest to any depth (#239)
Structs and lists were capped at two fixed levels (list of structs, struct with a list field). Anything deeper was dropped silently on load, in node settings and when written to a variable. The cap came from an early design choice, not from a technical need, so it is lifted completely instead of adding a third fixed level. - One kind list NESTED_KINDS replaces ListItemKind and StructFieldKind. The flat special cases in listItems.ts are gone, every value goes through one recursive path. - A struct inside a struct is declared, like a top-level struct. Values coming in over an edge are brought into that shape. Struct items in lists still take their fields from the value. - New valuePath.ts addresses any nested spot by keys and indices. ListSlot.field is now ListSlot.path. - Field commands (add, rename, remove) take the path to the struct. They change declaration, own value and all overrides in one undo step, as before on the top level. - Inspector: "+ Field" offers Struct and List on every level. A nested struct is a header row with its own "+ Field", its fields are indented one step further. - List tab: a list item shows "n items" and opens one level deeper. The title shows the path, e.g. "Listy · nums · [3]". - Pack in Pack now writes a struct into a struct field instead of text. Objects are kept at any depth and stripped at every exit. - A list edit in a struct field without an existing object override now starts from the shown value instead of an empty struct. - Old projects load unchanged. Save, load and save again gives the same result. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 21 + 22: ALL PASSED, 90 checks (new 21d-iii, 22f-ii, 21d and 22f updated) - 20 new unit tests in nested-values.test.ts and nested-list-slot.test.ts, three tests that pinned the old cap turned around - 6 counter-probes, each fails 1 to 8 tests, originals restored and checked with cmp - Not covered: a nested struct inside a struct item in the list tab has no e2e click Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
483c34bb5c |
Let Destroy keep the children and hand them back as a list
Destroying a container used to take everything below it along. Now Destroy can remove just the object itself and pass its direct children on, so a behavior can keep working with what was inside. - New input keep children (bool, default off). When on, only the object goes. Its direct children move up to the object's parent, or to the top level if there is none. Grandchildren stay where they are. - New output children (object list) with the direct children that were kept. It is empty when keep children is off. - The children keep their place on screen. carriedTransform folds the destroyed object's position, rotation and scale into their own, the same as reparenting in the editor. - Behaviors and the rigidbody of the object are always destroyed with it and are not in the list. Moved up they would steer or simulate the parent. - World remembers which children it reparented, and syncToModel writes their new parent into the scene. A reload of the model keeps it. - Limit, read from the code only: in the editor run the contents of a placed container are not baked. Moved children there last for the run only. Tests: - ./check.sh: 170 files, 2478 tests, CHECK EXIT 0 - e2e 34 (the module with a Destroy node): ALL PASSED, 21 checks - world-sync: world matrix of child and grandchild unchanged under a rotated and scaled parent, parent survives syncFromModel, top level case, already destroyed object gives [] - destroy-keep-children: real graph Destroy(Owner) -> Log(children) logs Lid, Label without the grandchild Nail - 3 counter-probes, all caught: no world transform (1 test fails), no parent in the scene (1), behaviors and rigidbody moved up (2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7979b8ff19 |
Let structs and lists hold objects across behavior runs (#238)
An object could only live in a dedicated object variable, kept in slots
beside the variable bag. Struct fields and list items could not hold
one, so a spawned object packed into a struct or list was lost. This
replaces the slots with one mechanism for all three places.
- "No object" has one resting form everywhere: {"$object": ""}. A
running game can tell an object slot from an empty text that way.
- During play the object handle lives directly in the value, also
nested in structs and lists. objectSlots.ts and the special paths for
Keep and Properties are gone.
- liveObjects.ts strips objects to (null) at every exit: overrides the
editor run writes back to the scene, saved values of persistent
Variables assets, live values in the inspector, and the world
snapshot of the test hooks (objects as names there).
- A destroyed object reads as (null) through isAlive, alone, in a
struct field or as a list item.
- "+ Field" and "+ Item" offer Object. The inspector and the list tab
show a fixed (null) row without input.
- Lists from Raycast all stay objects when written to a variable.
Before they turned into names.
- A struct that only changes by an object writes no override. A list
that grows by an object writes [(null)], its length really changed.
Found on the way: CI at
|
||
|
|
98211a8d54 |
Let a variable hold an object across behavior runs
A spawned object was only reachable in the run that spawned it. The Spawn output could not go into a variable because "object" was a port-only kind, so the next run had no way back to the instance. - "Object" is now a variable kind. The reference lives for the run only. The file stores the declaration and an empty value. - The live reference sits in slots beside the variable bag (graph/objectSlots.ts), not in it. The bag is the behavior's props, and syncToModel would have written an object into the scene. - A destroyed object, or one from a restarted or left scene, reads as nothing. A dead reference is dropped from its slot. - Keep (#229) hands the slots of kept variables on to the next call of a sub-behavior. - Properties of kind Behavior reads and writes another behavior's object variable through the same slots. - The inspector shows "(null)" for it and offers no input. - A persistent Variables asset with an object variable shows a warning under "Persistent". Saved values never keep the object, every run starts it at (null). Objects inside lists and struct fields are left out on purpose. Emptiness is checked with Equals against an empty b. A node "Is valid" does not exist yet. The test helper graphEditor().variables() now also reads the kinds of a Variables asset a node points to. Tests: - ./check.sh: 169 files, 2463 tests, CHECK EXIT 0 - e2e 6 + 23: ALL PASSED, 146 checks (new 23k, 2 checks) - e2e 32: ALL PASSED, 18 checks (new 32f-ii) - 10 unit tests in tests/core/object-variable.test.ts, one of them runs a persistent asset over two runs - 5 counter-probes (value in bag, isAlive always true, no Keep hand-over, Properties without slots, object in saved values) each fail their test - e2e check total 1406, computed only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
923baf2557 |
Let Spawn take an asset over an edge and find a child by name (#237)
An asset could only be picked as a setting on the Spawn node. It could not be wired in, held in a variable or put into a list. Finding an object below the own one needed a scene-wide search by name. - "Asset" is now a kind like Animation or Sound: port, variable, list item and a new "Asset" literal node, with its own cube icon. - Spawn and Is asset have an `asset` input. It uses the key of the old setting, so a saved asset reads as the value of the input and older projects keep spawning without a migration. - New node "Child by name" searches below an object level by level. A child wins over a grandchild of the same name. Nothing found or no object gives null. Tests: - ./check.sh: 168 files, 2453 tests, CHECK EXIT 0. - 9 new unit tests in tests/core/asset-kind.test.ts, catalog cases for Spawn, Is asset, Child by name and the literal. - e2e 6z-ii (2 checks): node rows and a real drag of the literal onto `asset`. e2e 6: 119 checks passed. Spawn checks in 3 and 9 passed. - Counter-checks: depth-first search fails 1, Spawn reading only the old setting fails 3, runtime ignoring the parent fails 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
74b9c60661 |
Stop marking older sprites as changed after loading (#232)
Loading filled every missing asset property with its `default`. For
Sort with parent that is `false`, while the gold mark and Reset compare
against `initial`, which is `true`. So every sprite, text and particle
source saved before #232 counted as changed once loaded, and each save
wrote the `false` into the project file.
- normalizeBag leaves a missing key missing when it has its own
`initial`. It still reads as `false`, so older projects draw as
before, but they no longer count as changed.
- e2e module 4 failed in CI on three checks since
|
||
|
|
1d18862ed6 |
Give unconnected node inputs the default the node shows (#234)
A new node handed null to every unconnected input that had no port default, even a Bool. The inspector showed false, 0, "" or an empty list at the same time, so what the node read was not what the editor said. 97 of 135 inputs of the built-in nodes were affected. - inlineValue() falls back to the default the inspector shows. One place covers every node, also future ones and saved projects. - A default list is handed out as a fresh copy per read, so a node that edits its input in place cannot change the shared default. - The node shows the value it reads: an empty text or list as (empty), an object or struct input without a link as (null). Live values while playing use the same words. - A key input without a key keeps showing a dash, like the key field in the inspector. - Visible changes in old projects: To text without input gives "" instead of "0", Select and First not empty give "" instead of null. Tests: - 8 new unit tests in tests/core/inline-defaults.test.ts, among them one over all built-in nodes that no typed-in input reads null. - The e2e check in module 6 reads the rows of an unlinked Name of node. The check sum stays the same. - ./check.sh: 167 files, 2434 tests, exit 0. e2e 6: 117 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b44f97c35 |
Let a child sort with its parent so its Z stays local (#232)
An animated or set Z of -1 on a weapon put it behind everything on Z 0, not just behind the player holding it. A child can now opt in to being drawn at its parent's place, so its own Z only orders it against the parent and its siblings. - New property "Sort with parent" on sprites, texts and particle emitters, shown in the inspector right under Z. It is a normal property, set on the asset and overridable per object. - With it on, the rest of the scene sees only the parent's Z and, where the layer sorts by Y, the parent's feet. Below 0 the child is drawn behind the parent, from 0 on in front. - It works across levels: a grandchild with the switch hangs on the top object of the chain. - It has no effect under a parent that is not drawn itself (container, behavior, camera, collider) or under a tilemap. - New assets start with it on, saved ones read as off, so old projects draw unchanged. For that a schema row now has "initial" next to "default". The gold "changed" mark and reset follow "initial", so a fresh sprite is not marked as changed. No load migration is needed. - parentGroups() in drawOrder.ts gives a slot only to the top of a chain and inserts the children there. Editor, play mode and picking all use it, so they agree. Tests: - 10 new unit tests in tests/viewport/sort-with-parent.test.ts. - 8 existing tests list the sprite rows or defaults and got the row. - New e2e check 5b-ii clicks the switch and checks order and undo. - Counter-checks, all originals restored by cmp: switch ignored fails 7, children not sorted by Z fails 6, no chain fails 1, "initial" ignored fails 9, row missing in the inspector fails e2e 5. - ./check.sh: 166 files, 2426 tests, CHECK EXIT 0. - e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fae827c087 |
Let an animation key Z so an object can pass behind another
The timeline offered X and Y as position rows but never Z. The list of position rows was simply written without it when animations came in with #97. A swinging weapon could therefore not go behind its character. - Z is the third position row, after X and Y, on every placed root or object. A scene and a behavior still offer no position. - Play and preview already read and write any position row by name, so the one entry in POSITION_KEYS is all the code change. - The draw order is sorted on every render, and play writes z back into the scene each frame. A keyed Z therefore takes effect at once. A blended 4.5 sits above 4 and below 5. Tests: - ./check.sh: 165 files, 2416 tests, CHECK EXIT 0. - 3 new unit tests in tests/core/animation.test.ts: Z is offered after X and Y and not on a behavior or scene, a running game moves the object from behind to in front of another one, and play, scene write-back and preview agree on z. - e2e 25: ALL PASSED, 29 checks. The new check reads X, Y, Z as the first rows of the row dialog. Check sum 1396 -> 1397. - Counter-check: without z in the list all 3 new unit tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
147f8ffbea |
Let animation keys ease with Sine to Bounce in, out or in-out
A key could only ease quadratically, which makes quick moves like a sword swing feel soft. Animators know the Penner curves from CSS, GSAP and Unity, so a key now offers all of them. - Ten curves (Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce), each in, out and in-out, next to Linear and Step. - The inspector shows two fields instead of one long list: Easing, and Direction only when a curve is picked. Switching the curve keeps the direction. Coming from Linear or Step starts at in-out. - Old projects keep working. ease-in/out/in-out now read as Quad and are still saved under that name. New curves save as cubic-out, back-in-out and so on. - Back and Elastic overshoot on purpose. A number property with min or max is clamped in play and preview, so a Back in from scale 0 does not flip the sprite. trackWriteAt now takes the property definition, and animatedKind* is renamed to animatedDef*. - The timeline marker still shows only the direction. - In-out is built for every curve from its in-curve and its mirror. easings.net uses own constants for Back and Elastic in-out, so our Back in-out winds up about 5 % instead of 10 %. Tests: - ./check.sh: 165 files, 2413 tests, CHECK EXIT 0. - 12 new unit tests in tests/core/easing-curves.test.ts: round trip of all 30 values, start/end/midpoint of every curve, reference values, only Back and Elastic leave 0..1, clamping in play and preview, unknown values are rejected. - e2e 25: ALL PASSED, 28 checks. New 25b-ii (3 checks) clicks both fields and undoes the four choices. Check sum 1393 -> 1396. - Counter-checks, each fails its test: no clamping (2), Quad saved as quad-* (2), mirrored in-out wrong (2), curve lookup with `in` instead of Object.hasOwn (1), no Direction field (e2e 25). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
21d7149756 |
Let a nested behavior without a start node run from On start (#230)
A behavior built as a plain behavior, with On start and no Behavior start, did nothing when another behavior called it through Run behavior. The node only showed a warning. Such a behavior should run as a call too, without the author having to rebuild its entry. - New callEntries() in nested.ts returns the Behavior start node or, when there is none, every On start node. callProblem uses it too. - GraphInterpreter.call() follows all entries one after the other, in graph order, in the same frame and with the same variables. - Behavior end still returns what reaches it. A call through On start has no inputs, because only the start node declares them. - It is a fallback only. With a Behavior start present, On start nodes stay out of the call. - The node warning now reads "No start or On start node - does nothing" and only shows when neither exists. - Keep and local state machines behave as before. Tests: - ./check.sh: 163 files, 2396 tests, CHECK EXIT 0 (before 2392). - 4 new unit tests in tests/core/nested-behavior.test.ts: the flow after On start runs, the return value arrives, several On start nodes run in graph order, a start node keeps On start out. - e2e 19: ALL PASSED, 35 checks. Only the expected warning text changed, the check sum stays at 1385. - Counter-checks: without the fallback 3 tests fail. With only the first On start exactly the order test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
454d202109 |
Let each variable of a behavior choose whether calls keep it (#229)
The first version of #229 put a Keep state switch on the Run behavior node. The user wanted it where the variable is declared instead. Whether a value lives on belongs to the variable, not to every call site. - The node switch keepState is gone again. RunBehavior, CallResult and nested.ts are back to what they were before #229. - BehaviorAsset.keptVariables lists the kept names. It lives on the asset and not in PropertyDef, because that is the schema of every inspector row. - A variable added from now on starts with Keep on. Variables in existing projects start with Keep off, so old projects run unchanged. - SetVariableKeptCommand flips the switch as one undo step. Add, remove and rename carry the list along. Loading drops names that are not declared. - The interpreter keeps only the listed variables, per Run behavior node and per object as before. The others start at their defaults. - Local state machines in a called behavior start empty on every call again, as before #229. Global and scene machines are unchanged. - The inspector shows a Keep button in each variable row of a behavior. Its state is part of the asset signature, so a click redraws the row. Without that the button kept its old look until the inspector was reopened. Found: the project Garden of Sacrifices still has keepState = true on the call Player -> Character Movement. It is ignored now. Direction there predates the switch, so Keep is off until set by hand. Tests: - ./check.sh: 163 files, 2392 tests, CHECK EXIT 0 (before 2388) - tests/core/keep-state.test.ts rewritten, 15 tests - e2e 19: ALL PASSED, 35 checks. 19c-ii now clicks the Keep button of a new variable: on when added, off by a click, on again, off after undo. - Counter-checks, each fails its own test: keep list ignored (2), states kept (1), new variable not kept (8), rename drops the list (1), no Keep button (e2e 19c-ii). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7c70245c79 |
Let a nested behavior keep its variables between calls (#229)
A Run behavior call started with fresh variables every time, so a nested behavior could not count, remember or keep a local state machine across calls. A new "Keep state" switch on the node lets the callee resume where its last call left off. It is off by default, so existing projects run unchanged. - RunBehavior gets the boolean parameter keepState and its node description explains it. - GraphInterpreter keeps the state in keptCalls, keyed by the call path, the chain of Run behavior nodes. Two nodes calling the same behavior each keep their own state. - One interpreter exists per behavior instance, so every object gets its own state without further code. - Local state machines of the callee are kept like its variables. - resumedVariables() matches kept values against today's declarations. A new variable starts at its default, a removed one drops out, and one whose kind changed starts over. - The kept state lives as long as the object in the run. It is not written to save games. Limit: a callee input named keepState collides with the switch. The same already holds for the name behavior, so it is not caught. Tests: - ./check.sh: 163 files, 2388 tests, CHECK EXIT 0 (before 2377) - 10 new unit tests in tests/core/keep-state.test.ts and 1 new node catalog case. - e2e 19: ALL PASSED, 35 checks, with the new check 19c-ii for the checkbox, its click and undo. - Counter probes: ignoring keepState fails 5 tests, state per behavior instead of per node fails 1, dropping the outer path fails 1, skipping the kind check fails 1, and without the node parameter the e2e check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
03e7a78c3d |
Keep a tilemap with outlined solids at 60 fps in scene and play
A project with three tilemap layers, two of them with collision on water and hill tiles, ran at 33 to 52 fps. A CPU profile showed that almost all of the time went into the collision geometry of the map. It was rebuilt from scratch every frame although nothing had changed. The animated water makes the views redraw every frame, so the cost was never skipped. - New module tilemapMemo keeps what is derived from the cells: the tiles of a layer, the blocking cells, the rectangles and polygons. It is reused as long as the content stays the same. - A brush stroke paints into the existing cells object. The memo therefore compares the content cell by cell, not the identity. The comparison walks two arrays in insertion order. - Clipped outlines are kept per outline list. The model always replaces that list instead of changing it. - PhysicsSystem no longer rebuilds the shapes of a tilemap each tick while its cells, placement and settings are unchanged. Before, it also built a JSON key from all world shapes every tick. - The overlaid layers after Set tile in a run stay stable. Otherwise the memo would miss on every call after the first edit. The #184 work only helped Move and raycasts, not this path. Measured on stein2 (i5-7500T, HD 630), script time per frame: - Scene, nothing selected: 17.7 ms, 52 fps -> 5.8 ms, 60 fps - Tilemap selected: 26.2 ms, 38 fps -> 8.6 ms, 60 fps - Play: 29.9 ms, 33 fps -> 8.7 ms, 60 fps About 3 ms per frame remain. The content comparison costs roughly 60 us per 700-cell layer and runs about 40 times a frame. The mesh geometries in TilemapField are still rebuilt every frame. Tests: - ./check.sh: 162 files, 2377 tests, CHECK EXIT 0 (before 2370) - e2e 1-5 and 39-43: ALL PASSED, 289 checks - 7 new unit tests in tests/core/tilemap-memo.test.ts - 5 counter checks (content ignored, no memo, outline by kind only, physics always rebuilds, layer settings ignored) each fail their test. All originals are back, checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af6ac12ab5 |
Show sharp tile preview labels and paths with tunnels and crossings
The mask labels under the tile preview sat inside the zoomed board. Pixi rendered them at 10 px and stretched them with the zoom, and each label was shrunk to the width of its tile on its own. They came out blurry and in different sizes. The example map also only showed a ring, so narrow paths and crossings were never previewed. - The labels live in their own layer in screen space and are re-rendered at the fitting font size on every zoom. - All labels share one size, at most the 10 px of the theme. Below 6 px they are hidden. - A label has two lines: the sides on top, the corners dimmed below. - The example map keeps the ring cell for cell and adds a network of 1 tile wide paths below it. It has straight 1xN and Nx1 pieces, curves, two T junctions, a crossing and a dead end. - The map stands next to the grid in a wide panel and below it in a tall one. Fit picks the layout with the larger tiles, and only Fit changes it, so zooming does not make the board jump. - The new hook __gm.tile.captions() reports size, scale and lines. Found on the way: the taller map made Fit zoom out further, and 3 pixel checks on the 16 px terrain tiles failed. The side by side layout fixed that. Tests: - ./check.sh: 160 files, 2364 tests, CHECK EXIT 0 (was 2363). - e2e 39: ALL PASSED, 29 checks (was 28). - New unit test: the example map has straights, a curve, a crossing with four notched corners, a T junction and a dead end. - Counter check: with the labels back in the zoomed board the new caption check fails (scales [0.648, 1]). - The e2e sum in the README is now 1381. It is calculated only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5dbac3a8df |
Add a Select node that picks one of two values by a condition
Choosing between two values meant a Branch with the whole action twice behind it, for example two Play animation nodes that differ only in the animation. Every later change had to be made in both. Select picks the value, so the action stays single. - Select is a data node with the inputs condition, false and true and the output value. It passes on "true" if the condition holds, else "false". An unconnected condition counts as false, and an empty picked input gives null. - The ports are named false and true, not a and b, so nobody has to guess which input is which. The order false, true follows Unreal's Select and Blender's Switch. - false, true and value share one type through `sharedType`, like First not empty. With two Animation literals plugged in, value takes their type and fits Play animation directly. - Both inputs are always evaluated. Data nodes have no side effects, so that does not matter. Tests: - ./check.sh: 160 files, 2363 tests, CHECK EXIT 0 (before 2356). - 5 cases in tests/core/node-catalog.test.ts and 2 in tests/core/shared-port-type.test.ts. - Counter checks: with the inputs swapped all 5 catalog cases fail, without sharedType exactly the type test fails. - No e2e change, the sum stays at 1380. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ffb5bbe039 |
Stop a key getting stuck when Shift changes it while held (#227)
Input remembered keys under KeyboardEvent.key. Pressing a, adding Shift and letting go sent "A" on release, which matched nothing, so "a" stayed down until the next release without Shift. - New rule keyName in src/core/keys.ts lowercases a single character. So a and A are the same key, also under Caps Lock. - Input remembers per physical key (e.code) under which name it went down and releases exactly that name. So 1 released as ! is freed too, which lowercasing alone does not cover. - On key down, up and held, Key is down and Axis input compare with the same rule. A node that already stores A reacts to a. - Recording a key in the inspector stores Shift+A as a. - Limit: holding Shift before pressing 1 still sends !, and a node on 1 does not react. Nothing gets stuck by it. Tests: - 3 new unit tests in tests/core/behavior-events.test.ts: the flow from the issue, 1 released as !, and a node on A hearing a. - Without lowercasing exactly the issue test and the A test fail. - Without the code memory exactly the 1/! test fails. - ./check.sh: 159 files, 2340 tests, CHECK EXIT 0 (before 2337). - No e2e change, the check sum stays at 1377. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4b7e8614a8 |
Keep play start and game view scale in the project (#226)
Both settings lived in the browser's localStorage, so they did not travel with the project and differed per machine. They now belong to the project like the symbol scale. There is no migration, old projects load with the defaults. - New module src/core/model/playSettings.ts reads playFrom and gameScale on load. Unknown or missing values fall back to the start scene and 100%. - src/editor/settings.ts is gone. - Play starts in the project settings dialog is an undo step (SetPlayFromCommand). It is only recorded on a real change. - The game view scale is saved with the project but is no undo step, so Ctrl+Z never undoes a zoom. A scale picked during play is written to the stored project and survives Stop. - Save states of persistent Variables assets (saves.ts) stay in the browser on purpose. They are runtime data of the game, not settings. Tests: - 6 new unit tests in tests/editor/play-settings.test.ts: defaults, save and load, broken JSON, undo of Play starts, scale without undo step, scale picked during play survives Stop. - e2e 03g checks the value in the project, nothing in localStorage and no new undo step. 35f also checks project.playFrom. No new checks, the sum stays at 1375. - Counter checks: no write to the stored project in play, no read on load, and an undo step for the scale each fail exactly their test. - ./check.sh: 158 files, 2332 tests, CHECK EXIT 0 - e2e 1-17,28,30,35,38-40,46: ALL PASSED, 752 checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5afb70a92e |
Let the project set the size of scene view symbols
Pixel art projects draw at very different sizes, so the fixed 18 px icons of behaviors, containers and rigidbodies were too big in some projects and too small in others. The size now lives in the project settings and travels with the project. - New field Symbol scale in the project settings dialog, below Play starts. It takes 0.1 to 4 with two decimals and defaults to 1. - The value is stored as project.symbolScale. Older projects load with 1, and values out of range are clamped on load. - Every change is its own undo step through a project command. - The factor scales the icon size, the spacing of icons that share a spot, the label offset and the selection frame of icon-only symbols. - The footprints in bounds.ts that Fit uses stay as they were. The hit area follows the icon, so clicks match what is drawn. - uiHooks reports the icon size so e2e can measure it. The e2e sum in the README is 1375. That is 1374 plus one new check, and 1374 itself has not been confirmed by CI yet. Tests: - ./check.sh: 157 files, 2326 tests, CHECK EXIT 0 (before 2320). - New tests/core/symbol-scale.test.ts with 5 tests (default, save and load, clamping, undo, icon frame), one more in project-settings.test.ts. - e2e 46f: dialog opens with 1, 0.5 plus Apply gives a 9 px icon, undo gives 18 again. Module 46: ALL PASSED, 13 checks. - Counter checks: without loading the value the save and load test fails. With the icon ignoring the factor exactly 46f fails. Both originals restored per cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2592873d76 |
Pull nearest tilemap uvs a tenth of a texel in, 0.02 still showed lines
At 0.02 texel the lines between tiles of a nearest tilemap were gone at the zooms tried first, but still showed at others. The user raised the inset to 0.1 by hand and checked it in the browser. - A tenth of a texel is still far below half a texel, so the texel picked at every pixel centre stays the same. - The comment names the new value. Tests: - tilemap.test.ts follows the value: the inset test expects 0.1/96, the sliver test allows up to 0.25 texel from the edge, and the quad test checks its uvs to 2 digits instead of 3. - ./check.sh: 156 files, 2314 tests, CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5bf1ee7a4e |
Let Equals match a number against a typed number like 130 (#224)
The float tolerance from the last commit never reached the case it was built for. The inputs of Equals are of type any, and their inline field is a text field. A typed 130 therefore arrives as the string "130", and the tolerance only applied when both sides were real numbers. The node fell back to comparing "129.99999999999997" with "130" as text. - A number against a text that parses as a number now uses the same 1e-9 rule as two numbers. - An empty text does not count as 0. - Two texts are still compared as text, so "1.0" and "1" stay different. - Compare was not affected, its inputs are real number fields. Tests: - 3 new cases in node-catalog.test.ts: 129.99999999999997 vs "130" is equal, 0 vs "" is not, "1.0" vs "1" is not. - Counter-check: with the old rule (two real numbers only) exactly the "130" case fails. Original restored and compared with cmp. - ./check.sh: 156 files, 2314 tests, CHECK EXIT 0 (before 2311). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9d6a0aebd3 |
Let Compare and Equals treat float noise as equal numbers
A graph that checks "time - late == 6" after On timer (#224) failed, because the difference comes out as 5.999999999999972. Both values are sums of measured frame times, and they round along different paths. The only workaround was a Round node in front of every comparison. - New helper nearlyEqual in values.ts. Two numbers count as equal when |a - b| <= 1e-9 * max(1, |a|, |b|). The tolerance grows with the size of the values, so it holds for hours of summed dt. - Compare uses the rule for all six operators. "<" and ">" only hold when the values are not nearly equal, "<=" and ">=" also hold when they are. So "==" and ">" are never true at the same time. - Equals uses the same rule when both inputs are real numbers. Objects still compare by identity, everything else as text. The node description says so. - The tolerance is fixed, there is no parameter on the node. Physics values come from float32 in Rapier and are not covered on purpose. - The log display is unchanged and still prints the raw number. Tests: - ./check.sh: 156 files, 2311 tests, CHECK EXIT 0 (before 2300). - 11 new cases in node-catalog.test.ts, including the value from the log against 6, growth with magnitude, 6.001 != 6 and 1e-6 > 0. - Counter-check exact compare: exactly the 8 noise cases fail. - Counter-check "<"/">" without the exclusion: exactly 2 cases fail. - No e2e change, the check sum stays at 1374. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6fecb2e841 |
Tell On timer how late its frame is, so time minus late is exact (#224)
A timer can only fire on a frame. It fires on the first frame past each due time, so Time.time at that moment is the due time plus up to one frame. On a display that is not exactly 60 Hz this delay creeps up by a millisecond every few seconds and falls back to zero once it reaches a whole frame. The issue saw 82.009 after 82 ticks. The timer itself does not drift, it keeps the remainder when it fires. - On timer gets a number output late. It is what is left of the timer's clock after taking one interval, so time - late is exactly the due time. - When one long frame catches up several ticks, each gets its own late. The first one was due longest ago and is the latest. Tests: - New unit test in behavior-events: late 0.125 after a frame past 1.0 s, 0 on the dot, 1.5 and 0.5 for two ticks caught up in one frame. - Counter-check: late taken before the interval is subtracted fails exactly that test. Original restored with cmp. - ./check.sh: 156 files, 2300 tests, CHECK EXIT 0 (was 2299). - No e2e change. The e2e modules only wire the exec of On timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aef1eba917 |
Stop thin lines between tiles of a nearest tilemap at odd zooms
At zoom levels that do not land quarter edges on whole screen pixels, a tilemap whose texture uses Nearest showed one-pixel lines in the wrong colour. The float uv of a pixel centre right at a quad edge could round onto the first texel of the neighbouring quarter in the sheet, which is usually a different tile. Terrain sheets showed it most, because every tile is four quads there. - Nearest now pulls the uvs 0.02 texel inside each quarter. That is far below half a texel, so every picked texel stays the same and pixel art keeps its size. - Linear keeps its half-texel inset. - The first draft used 0.01. The user raised it to 0.02 after checking the result in the browser. Tests: - ./check.sh: 156 files, 2299 tests, CHECK EXIT 0 (before 2298). - New unit test: every nearest uv sits strictly inside its quarter, by less than 0.05 texel. Two older tests now expect the inset. - Counter-check: with no inset for nearest the new test and the inset test fail. - No e2e check. The user checked the result in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c1a702bc39 |
Add a terrain autotile kind for 6x4 sheets like TD Plains
Sheets such as "TD - Tileset - Plains" draw an autotile as a 6 x 4 block: a 3 x 3 nine-slice, a vertical and a horizontal strip and a single tile on the left, the inner corners on the right. The floor kind only knew the RPG Maker 2 x 3 layout, so these sheets could not be used as autotiles at all. - New TileKind "terrain" with 16 px tiles in a 6 x 4 block. All 47 neighbour combinations are resolved per quarter, so the renderer, the collision and the palette stay untouched. - Whole tiles are used where the sheet draws them: the 16 side cases, the four single notches and the two diagonal pairs. Hand-drawn strips and single blocks therefore look as drawn. - Only the missing cases are composed, for example an edge with a notch or two notches side by side. They take the side tile and put the notch quarters in. - A notch also takes the quarter above or below from its notch tile when fill would stand there. Tall cliff notches keep their wall. On ground and stones fill meets fill, so nothing changes there. - The label rule for floor and terrain now runs through one notch function. Floor still passes the MZ table test. Not touched: the tile board only refits on open and on Fit. Switching an open tile to a kind with another tile size leaves it small until Fit is pressed. That was the case before. Tests: - ./check.sh: 156 files, 2290 tests, CHECK EXIT 0 (before 2283). - e2e 39,40: ALL PASSED, 90 checks (before 86). 39j checks 47 tiles, pixels of single, notch, pair and composed tiles, and undo to floor. - Counter-checks: without whole notch and pair tiles one unit test and one e2e check fail. Without the taken quarter one of each fails. Originals restored by cmp and rebuilt. - Check sum: 1363 measured by CI plus 4 is 1367, computed only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d257298bc7 |
Keep a dragged rigidbody on the mouse while the game plays
Dragging a sprite with a dynamic rigidbody in the scene view during play barely moved it. Only a quick drag and release got through. A dragged object should follow the mouse as if it had no rigidbody and start from rest when it is let go. - The scene view reports the dragged objects as held through EditorState. The game tick no longer writes its own position over a drag position that has not been read in yet. This affected every object in play, not only rigidbodies. - A held dynamic body turns kinematic and is set straight to the drag target. Gravity and velocity no longer pull it away between two mouse moves, and the "moved directly" warning stays quiet. - A body counts as held when the object itself, an object above it or its rigidbody is dragged. - On release the body turns dynamic again with linear and angular velocity set to 0. Not changed: a press on an unselected crate still lands on its same-sized collider (rule from #203). Dragging while play is paused and in the editor run is untouched. Tests: - ./check.sh: 155 files, 2273 tests, CHECK EXIT 0 (before 2271). - e2e 41 alone: 21 checks. e2e 1-16,39-43: 641 checks, all passed. - 2 unit tests: a held body stays where the drag puts it and starts from rest, also for a grip shorter than one physics step. The tick does not overwrite a drag position that is not read in yet. - 1 e2e check in 41b: the crate is lifted 150 units with the real mouse, holds exactly at -3490 for a second, then falls from rest (1.3 px, then 5.1 px) with an empty log. - Counter-checks: without holding in physics the crate falls back to the ground and the old warning shows. With the tick writing back the unit test fails. Without zeroing, a short grip keeps 490 px/s and the test fails. - Check total 1356 + 1 = 1357, calculated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d444b27fae |
Let behaviors read and set tilemap cells while the game runs (#135)
A behavior could move sprites around but had no way to look at or
change the tilemap it was standing on. Doors, breakable blocks and
"is there ground ahead?" checks all need that.
- New nodes Tile at, Set tile, World to cell and Cell to world. The
conversion honours position, rotation, scale and mirroring of the
placed map. A small Tile literal node lets a graph compare what it
read against a fixed tile.
- The layer is picked by name from all tilemap layers in the project
through a new field kind. "First layer" takes the map's first one.
Because layer is an input, a text can also be wired in.
- Each placement keeps only its edited cells in liveTilemap.ts,
layered over the asset's cells. A second placement of the same map
stays untouched, and the asset itself is never written.
- A read sees a Set tile from earlier in the same flow. Drawing picks
up the edit at once and neighbouring autotile edges adapt.
- Solids see the edits straight away, so a Move in the same frame
already hits the new cell. Rapier rebuilds its shapes on the next
step.
- After Stop the map is back to what was painted. The editor run shows
the edit in the scene view while it lasts and does not touch the
project.
- An unwired tilemap input means no map, like Get position. Cells
outside the grid are refused and done reports false.
Tests:
- ./check.sh: 155 files, 2271 tests, CHECK EXIT 0 (before 2245).
- tests/core/tile-access.test.ts (14), one physics test for a floor
set at runtime, catalog entries for the five nodes.
- e2e 39,40: all passed, 86 checks (before 81), five new in 40s.
- Counter-checks: collision ignoring edits fails 3 unit tests, the
solids cache missing same-tick edits fails only the "at once" test,
drawing ignoring edits fails the 2 drawing checks, the editor run
not reporting a new tile fails only its check.
- 40s uses a still tile. The animated fixture tile redraws every frame
and hid the last counter-check at first.
- Check total 1351 + 5 = 1356, computed. CI confirmed 1351 at
|
||
|
|
abf3b1f618 |
Pick an existing collider or rigidbody from the object menu (#222)
"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.
- "Add collider…" and "Add rigidbody…" open a second menu at the same
spot: a search field, "Create new …", a separator, then every asset
of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
the collider commands. An object without a resolvable asset is
greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
collider can now sit on several sprites. A value equal to the asset
value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
90 degrees and gives a circle the radius that fits (min/2). It is
greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
"Add Component" in Unity. That costs one click more than before.
Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
centring each fail at their own check. Breaking the override rule
fails 2 unit tests.
- CI confirmed 1345 checks at
|
||
|
|
1aae97b335 |
Open a sprite's texture or shader from the menus and inspector (#221)
A sprite's texture and shader were only reachable by hunting for them
in the asset panel. Every place that shows such a reference now leads
straight to it.
- Object menu (hierarchy and scene view): "Open texture" and "Open
shader" sit right below "Open asset" when the sprite has one. The
object's own values count, so an overridden shader opens the
override.
- Asset context menu: the same entries at the top.
- Inspector: every asset field with a value shows its label as a blue,
underlined link. One click selects the asset, a double click opens
it. An override stays gold and gets the underline too.
- The first click rebuilds the inspector, so the second one lands on a
different row. The inspector catches that second click before any
row sees it.
- New pure rule `references.ts`: the asset behind a reference field. A
frame leads to its sheet. Kinds without a tab (palette, font, sound)
get no menu entry.
- `followsPress` in `clicks.ts` keeps the double-click window and
tolerance in one place.
- `activateAsset` in `actions.ts`: opening means the same everywhere
as a double click in the asset panel, so a scene also becomes the
active one. The asset panel uses it too.
Tests:
- ./check.sh: 150 files, 2226 tests, CHECK EXIT 0 (before 148/2214).
- 12 new unit tests: 6 for the reference rule, 4 for the link in
createFieldRow, 2 for followsPress.
- 5 new e2e checks in 28l-ii: both menus, the link's look, click
selects, double click opens.
- e2e 1-16,26,28,35,39,40 on the final build: ALL PASSED, 732 checks.
- Counter-probes: no "Open ..." entries, no underline and a second
click never recognised each fail exactly at their checks.
- Check total 1341 (CI counted 1336 at
|
||
|
|
ceaa44e5b8 |
Add roles and a user management tab for admins (#180, #179)
Until now every account could do the same, and managing accounts meant running make-user.php on the server. Accounts now carry a role, and admins get a tab in the editor to create, rename, re-role, reset and delete accounts. Both issues share the same server files, so they land in one commit. #180 Roles: - Every account is admin or user. A missing or unknown role counts as user, on the server and in the client. - auth/session and auth/login return {user, role}. - Auth::requireAdmin() answers everyone else with 403. - make-user.php takes the role as a third argument. Without it an existing account keeps its role, so a password reset from the CLI never demotes an admin. - Admins see and save other users' projects like their own. Saving keeps the original owner. #179 User management: - The account menu gets "User management" (admins only) and "Plugins" (everyone). Both open the same closable tab, accounts on top and plugins below. The fixed Plugins tab is gone. - Per account: Rename, Role, Password (no old password needed) and Delete with a confirmation. "New user..." creates one. - Each account lists its projects with an Open button. - Delete is refused while the account still owns projects. - Renaming moves the projects along and ends every session of the account. transfer takes the same lock as save. - On their own row an admin can only set the password. The last admin cannot be demoted or deleted. The server enforces both. - The account rules live in AccountRules.php and accountRules.ts as pure functions, so they are testable without a server. Findings: - At 1280 px the Delete button reached past the column edge and was not clickable. The name column is flexible now, and module 49 checks the layout. - The server lets an admin delete other users' projects, but the editor has no button for it yet. Left open in the issue. Tests: - ./check.sh: 148 files, 2214 tests, exit 0 (was 147 / 2204). - New tests/php/accounts.test.php, 51 checks, plain PHP. CI runs it as its own step in the gate job, the host has no PHP. - New e2e module 49 with 18 checks. CI creates the e2e-admin account in serve.sh. Modules 14, 38, 46 and the scaffold follow the new tab. - e2e 1-16,37,38,45,46,49: all passed, 581 checks. - Counter-checks: owner on save, admin view, default role and self delete in PHP fail 7, 5, 5 and 1 checks. Unchecked requireAdmin, a wider column and the caller as owner each fail exactly their e2e check. - Check sum 1314 -> 1333 (+1 in 38, +18 in 49), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f9d5e8bca5 |
Move the one-way rule and collider geometry out of physics (#215)
PhysicsSystem had 940 lines. The step logic shares its state with the engine handles of parts and bodies, so a separate class would need almost the whole system as its interface. That part stays. Two pieces without own state move out instead. Behaviour is unchanged. - passesOneWay in oneWay.ts is the one-way rule as a pure function. It can now be tested without Rapier. - bodyGeometry.ts holds the collider shapes and the pose maths. - PhysicsSystem is down to 820 lines. Tests: - ./check.sh: 146 files, 2199 tests, CHECK EXIT 0 - 5 new unit tests for the one-way rule without an engine. - New tests/core/body-geometry.test.ts: a capsule turned by 90 degrees went unnoticed before. Both tests fail against that bend. - Counter-checks on landing hold and teleport detection fail in 3 to 6 tests each. - e2e 1-28,31,36,39-43 on the final state: ALL PASSED, 1060 checks - Each of the four #215 commits was checked green on its own in a scratch worktree: 2190, 2192, 2192, 2199 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
487b3aa5b4 |
Painting over identical tiles no longer adds an empty undo step (#212)
A rectangle drawn over cells that already hold exactly that tile
pushed a command that changed nothing. Undo then seemed to do nothing
and the project was marked as modified. PaintTilesCommand.changes()
was meant to prevent that, but nothing called it since #128.
- New paintTiles() in tilemapCommands.ts returns the command for a
finished stroke or fill, or null when no cell changes. It is the
first caller of PaintTilesCommand.changes().
- All three paint paths in the Scene View go through it: brush and
eraser, rectangle (both in endStroke) and flood fill (fillFrom).
- Flood fill was not affected, because floodFill already returns []
for the same tile. The guard is there anyway, as the issue asked.
- A rectangle that changes even one cell stays a normal undo step.
Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (before 2187).
- 3 new unit tests in tests/core/tilemap.test.ts: same rectangle
twice gives no command, a wider rectangle still does, a fill that
finds nothing gives no command.
- 1 new e2e check in 40d: the same rectangle again, then one undo
must take 9 cells to 0. A redo restores them for later blocks.
- GM_STEPS=39,40 on the final build: ALL PASSED, 81 checks.
- Counter-probe with changes(...) || true: exactly the two unit
tests fail, and in the browser exactly the new check fails with
{"afterUndo":9}. Original restored via cmp (IDENTICAL), rebuilt.
- Editor suite check total: CI measured 1312 at
|
||
|
|
0e8585665a |
Load the 500 weight and draw missing glyphs as SVG icons (#214)
The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.
- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
DOMParser.
- Tests find icons through the SVG id. The new hook
__gm.locateIcons(id) and the harness helpers findIcon/clickIcon
replace locate() on the old characters.
Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.
Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
character in src/ exists in every shipped face. The cmap reader
matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
back, the struct icon removed (one unit test each). In the browser,
500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at
|
||
|
|
056a622524 |
Refactoring round 6: shared sheet canvas, fewer copies (#85)
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).
- SheetCanvas is the new base of the texture, pixel art and vector art
canvases. It holds background, mask, hint, panning, wheel zoom, fit,
view save and restore and the screen mapping. Pixel art keeps its
whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
GeneratedPictures.has and SnapToggle.snapping. Tests now check the
same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
wiki pages.
Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
has not been called since #128. The rectangle tool therefore records
a no-op undo step when it paints the same tile over itself. Left as
is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
could become 10 without a red test. tests/core/raycast.test.ts now
covers it.
- CI measured 1308 editor checks at
|
||
|
|
d7c0b7cfdb |
Variables and state machines can reset with every scene (#164)
A shared variable or machine used to live either per object or for the
whole run. Levels need a third lifetime: shared like global, but built
fresh from the declarations whenever a scene is loaded.
- New scope `scene` for Variables assets and state machines. A missing
field keeps the old meaning: `global` for assets, `local` for
machines, so existing projects do not change.
- Returning to a scene that was visited before is fresh as well. There
is no memory per scene.
- A `scene` machine announces its start state again after a scene swap,
so `On state entered` in the new scene hears it. A global machine
stays quiet because it never left its state.
- The rule lives in two pure functions in `sharedState.ts`
(`carriedForward`, `restartedAssets`). BehaviorSystem only calls them.
- The inspector shows a Scope dropdown on Variables assets and a third
step on the machine dropdown, with a note on what `scene` means.
Decisions:
- `scene` and `persistent` exclude each other (variant A). The choice
dialog was cancelled, so this is an assumption to revisit. A
scene-scoped asset hides the Persistent switch, and switching to
`scene` drops the flag (undo restores it). The guard sits in
`isPersistent` itself, so no path can save values the scope throws
away anyway.
Findings:
- `clickText('Scene')` hits the Scene tab, not the menu entry. One e2e
check was red because of that, and its partner was falsely green
because it compared `undefined` with `undefined`. Both use `clickLast`
now.
Tests:
- ./check.sh: 144 files, 2142 tests, CHECK EXIT 0 (before 143 / 2133)
- 9 new unit tests in tests/core/scene-scope.test.ts
- e2e 32 fresh: ALL PASSED, 17 checks
- e2e 1-3,17,18,26,32,35 fresh: ALL PASSED, 213 checks
- 3 new e2e checks (35: scene-scoped asset resets on swap, 32h: dropdown,
note, hidden Persistent switch)
- Counter-checks bite: ignoring `restarted` fails 2 unit tests and the
new check in 35, the old announcement rule fails 1, dropping the scope
guard in `isPersistent` fails 1. Originals restored, cmp IDENTICAL.
- Check sum 1307 (1304 + 3) is computed. Last CI measurement: 1293.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6e3e8d23c5 |
Combining vector shapes keeps the curves it did not cut (#208)
Until now every boolean operation on vector art threw the curves away. The shapes were flattened into line segments, the operation ran on those segments, and the result came back as a polygon. A circle that nothing even touched came out of an Exclude with 40 corners instead of its four points and handles. Now the result carries the original curves again, and only the edges the operation really drew stay straight. - While flattening, every sample point remembers which curve it came from and where on that curve it sits. The operation runs on the same segments as before, so nothing about the boolean result changes. - When a run of result points walks along one of those curves, that piece is cut out of the original curve again with de Casteljau. This is an exact cut, not a fit through the points. - An edge whose two ends sit on the same curve but whose middle does not follow it stays straight. Without that rule a cut-off cap would bulge back into the shape. - A closed contour keeps at least three points, so a lens shape does not collapse into two. - The rule lives in its own module, src/core/model/vectorCurves.ts. vectorBoolean.ts only hands it the table of sampled points. Findings: - The two intersection points themselves sit up to 0.09 canvas pixels off the original curve. Clipper cuts the flattened segments, not the curves, so this was the case before as well and stays inside the 0.2 px sampling tolerance. - Refining those points with Newton steps on the curve was measured and gave nothing back (0.02952 px instead of 0.02978 px), because the error is in the intersection itself and not in the estimate. The code was removed again. - Theme.ts rowHeight 22 -> 24 is in this commit. The change is not mine, it was already in the working tree; gate and module 47 are green with it. Tests: - ./check.sh: 143 files, 2133 tests, exit 0 (was 2127) - e2e module 47 on a fresh seed: all passed, 59 checks (was 57) - 6 new unit tests: untouched circle, untouched side plus a straight cut edge, a cut in the middle of a curve, a chord crossing one curve twice, two overlapping circles, the three point minimum - 2 new e2e checks in block 47p: point count and handles from the model, plus a pixel pair on the round edge - Four counter-proofs, all bite: no reconstruction -> 5 unit tests and the e2e check fail with 35 points and nothing curved; no chord test -> the cap bulges back to 28.28; no three point rule -> the lens test fails; handles halved -> the pixel check reads background Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d7a2521ca7 |
A port can take its type from what is plugged into it (#207)
Until now every port of "First not empty" was grey and typeless, so the
editor could not tell you what actually travels through the node. The
output is now typed as soon as both inputs agree, and every any-input is
painted in the colour of its own edge. Nothing changes at runtime.
- A NodeDef can declare which of its ports carry the same value
(`sharedType: { inputs, outputs }`). The rule itself lives as a pure
function `agreedType` in `src/core/graph/portTypes.ts`.
- Only the output inherits. It becomes the shared type when every input
of the group is connected and they all carry the same type, otherwise
it stays `any`.
- The inputs stay `any` on purpose. `canConnect` checks against the port
type and a busy input is replaced by the new edge, so a typed `a`
could never be re-plugged with a text without disconnecting first.
- Chains do not propagate. When looking up what feeds an input, a node
of the same kind reports the `any` it is registered with, so two of
them in a row stay untyped and a data cycle cannot ask itself in a
circle.
- An edge that the new type makes invalid stays where it is and turns
red. `EdgeLayer` already paints a mismatching edge with `danger`,
nothing is disconnected on its own.
- New `PortDef.paintType`: colour only, never truth. It is set in
`drawnDef` and read in `NodeView.redraw`, so `canConnect`, the
interpreter and the compiler keep seeing `any`. Every connected
any-input is painted, not just members of a `sharedType` group; ghost
ports keep their hollow red look.
- The pin now matches the edge that ends in it, because both take the
colour from the resolved output type of the neighbour. Mixed sources
therefore show two colours, which explains why the output stays grey.
- New hooks `__gm.graph.portTypes(id)`, `__gm.graph.edgeColor(id)` and
`__gm.themeColor(name)`. `portTypes` reports type and paint colour
separately so the type checks do not silently become colour checks.
Findings:
- `resolveDef` only appends derived ports to the registered ones, so a
fixed port cannot be retyped that way -- it would appear twice. The
shared type is therefore resolved before the ports are built.
- Answer 5 of the issue (inline field follows the type) has no case left
under variant A: an unconnected input stays `any` and keeps its text
field.
- `Item at`, `Join` and `Equals` do not profit: `Item at` has no
any-input, the other two have fixed outputs. The next candidates are
the shader arithmetic nodes, which were left alone.
Tests:
- ./check.sh -> 143 files, 2127 tests, exit 0 (was 142 / 2113)
- GM_STEPS=1-6 fresh -> ALL PASSED, 245 checks (was 242), 9 of them new
- 14 new unit tests in tests/core/shared-port-type.test.ts
- 9 new e2e checks in block 6z, one of them a pixel check straight on
the pin via pixelAt + themeColor
- Counter-probes all bite: filtering unconnected inputs out of
`agreedType` fails 3 unit tests, ignoring `inherit` fails 1, killing
the derivation in the bundle fails exactly the 2 expected e2e checks,
and never setting `paintType` fails 4 unit tests plus the 3 pin
checks (#bdbdbd instead of #e6a8ff / #6fcf97)
- Screenshot read: `a` pink, `b` green, output `value` grey, edges in
the same colours
- Check total 1302 (1299 + 3), 1293 was measured by CI at
|
||
|
|
1a3babf9c5 |
First not empty gets a setting for what counts as empty (#206)
The node treated 0, false and an empty text alike, which is right for a missing behavior parameter but wrong the moment a real 0 travels through the graph. A setting now says how much counts as empty, so the same node works for both cases. - The new parameter "Empty means" has three steps: "Any empty value" (the default, the behaviour so far), "Nothing there (0 and false count)" and "Only a missing value". - The default keeps every existing graph as it is, including the common setup where an unset animation parameter arrives as an empty text. - The rule lives in isEmptyValue(value, mode) in values.ts; the node only passes the step through. An unknown setting behaves like the default, the same way Compare does. - The node description is shorter because the old one was cut off in the inspector. Findings: - In the "Nothing there" step a struct is empty only when its fields are empty under the same step, so a position (0, 0) stays a real value. Tests: - ./check.sh: 142 files, 2113 tests, exit 0 (before 2104). - 9 new cases in the node catalog table: the edge cases per step (0, false, empty text, empty list, position (0, 0), unconnected a) plus an unknown setting. - Two counterproofs hold: letting "Nothing there" act like "Any" drops the 3 cases with 0, false and the position; removing the "Only a missing value" step drops the 2 cases with the empty text and list. - Seen in the browser: the node shows "Empty means: Any empty value" and the inspector dropdown lists all three steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d98c2898b1 |
A new node takes the first of two values that is not empty (#205)
A graph that reads an optional value needed a chain of Equals and
Branch just to fall back on a default. The new node does that in one
step: it passes a through unless a is empty, in which case b comes
out. Both ports are any, so it works for numbers, text, lists,
structs and objects alike.
- `First not empty` sits in the data category with two any inputs
`a` and `b` and one any output `value`. Are both empty, b comes
out, so the second input doubles as the default.
- `isEmptyValue` in values.ts holds the rule as a pure function:
nothing connected, 0, an empty text, false, an empty list, and a
struct whose fields are all empty.
- The struct case is what covers a position: behavior graphs have no
vector type of their own, so (0, 0) arrives as a struct. `{x: 0,
y: 2}` therefore counts as set, `{x: 0, y: 0}` as empty.
Findings:
- A real 0 or false on a is passed over. That follows from counting
0 as empty and is written down in the issue.
- The output stays any. Ports that inherit their type from an input
do not exist in the graph; `Item at` has the same shape.
Tests:
- ./check.sh: 142 files, 2104 tests, exit 0 (was 2094).
- 10 new catalog cases: a set, a unconnected, 0, "", false, an empty
list, a position (0, 0), a struct with one field set, an object,
and both empty.
- Counter-check: with structs never empty and 0 a real value exactly
those two cases fail, the rest stays green. Originals restored and
compared with cmp, then rebuilt.
- No e2e check: single data nodes have no e2e coverage here, the
palette fills itself from BUILT_IN_NODES.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
41772e53f7 |
Variables assets can keep their values from one run to the next (#166)
A game needs a high score or an unlocked level to survive the end of
a run. As agreed in the issue there are no save or load nodes. A
Variables asset gets a switch instead, and the existing Variables
nodes read and write the values as before.
- A "Persistent" checkbox in the Variables inspector. Toggling it is
one undo step, and absent means not persistent.
- The behavior system loads the saved values at start and writes them
after every frame that changed one, so a closed tab loses nothing.
- Values live in localStorage per project, not in the project file.
Another browser or person starts fresh.
- Play and "Run in editor" save apart, so trying a behavior in the
editor leaves the Play save alone.
- Saved values are read through the current declaration. A removed
variable drops out, a new one starts from its default, and a changed
kind is converted.
- "Clear saved values" in the inspector clears this asset in both
areas. The main menu gets "Clear saved data" for the whole project.
Clearing takes effect from the next run on.
Decisions:
- The switch covers the whole asset, not single variables. Mixing
both means two assets.
- A changed default in the inspector shows only after the save is
cleared. That is what persistent means.
Findings:
- The first e2e run died at "debug hook present" right after a Vulkan
error from Chrome. The second run passed, so it was a GPU flake at
start-up.
Tests:
- ./check.sh: 140 files, 2070 tests, exit 0 (before 138 / 2055).
- 15 new unit tests in tests/core/saved-values.test.ts and
tests/editor/saves.test.ts.
- GM_STEPS=32,46 fresh: ALL PASSED, 24 checks. Six new checks in
module 32 go through the UI. Module 46 knows the new menu entry.
- Counter-checks both fail as expected. Loading switched off: 2 unit
tests and 2 e2e checks fail. Editor run writing into the Play area:
1 e2e check fails.
- Check total 1278 (1272 measured by CI at
|
||
|
|
bbbf34109b |
Lists duplicate, rename and edit values right in their rows
Name, opacity and Z each sat in a separate field below or beside
the list. You had to select a row first and then look elsewhere to
change it. Duplicating was a separate button or not possible at all.
Now the row itself carries everything.
- SortableList gets three optional callbacks. onDuplicate shows a
copy button between the eye and the delete button. onRename opens
a text field on double click, Enter or a click outside commits,
Esc cancels, an empty name is ignored. value puts a NumberField
into the row.
- Dragging on the number changes the value and never moves the row.
A whole drag is one undo step, the canvas follows live.
- Copy and delete only show on the hovered and the selected row.
Their space stays reserved, so nothing jumps on hover.
- Pixel layers: copy right above with the same pixels on all
frames, rename, opacity in percent. The Name and Opacity fields
below the list are gone.
- Pixel frames: copy right after, replaces the Duplicate button.
- Tilemap layers: copy with cells, Z and visibility, rename, Z in
the row. The Name and Z fields in the panel are gone.
- Vector shapes: copy at the same place and select it, rename.
- New in core: DuplicateVectorShapeCommand,
DuplicateTilemapLayerCommand, withLayerDuplicated and copyName
("X copy", "X copy 2"). copyName lives in src/core/util/names.ts
because the model never imports from commands/.
Findings:
- Double clicking a shape that is not selected yet only selects it.
The first click moves the list up, so the second click misses the
name. Module 47 selects the row first.
Tests:
- ./check.sh: 138 files, 2055 tests, CHECK EXIT 0 (before 2052).
- GM_STEPS=47,48 (fresh, twice): ALL PASSED, 91 checks (before 86).
- GM_STEPS=39,40,47,48: module 40 green, 47 died on the rename
check, which was a test fault and is fixed.
- Counter probes: a constant copyName fails the shape and pixel
layer tests. A dead copy hit fails the copy check in 47.
- Check total 1272 in README and wiki, computed (1266 + 6).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
66d6aafbb4 |
Pixel art can be drawn in its own tab with layers and frames (#130)
Textures are uploaded files named by their content, so they cannot be edited in place. Pixel art is therefore its own asset type, like vector art. Its pixels live in the project and the tab rasters them into a texture that sprites, tiles and animations can use. - New asset types Pixel art and Palette. The first pixel art of a project gets a palette with the 16 PICO-8 colours in the same undo step. Every pixel is a palette index, 0 is transparent. - Each layer on each frame is its own file data/pixels/<sha256>.px. The project only holds the names, so undo swaps names. The browser computes the hash and uploads new cels before the project is saved. - New route POST ?r=pixels in api/lib/PixelStore.php. It answers 201 and 422 for broken data. data/pixels/ is git-ignored. - Tools: pencil, eraser, fill (contiguous or all pixels of a colour), eyedropper, line, rectangle and ellipse (outline or filled), brush size 1 to 8. - Layers can be added, deleted, renamed, reordered, hidden, faded and merged down. Frames can be added, duplicated, deleted and reordered, with onion skin and a small playing preview. - Zoom 1x to 32x in whole steps, grid from 8x, a 3x3 tile preview and Resize... with a 3x3 anchor. - All frames sit side by side in one image. Each frame is a region like a cut texture frame, so texture rows offer "Asset / Frame 2" and the context menu has Create animation from frames. Decisions: - Merging down drops the opacity of the upper layer. Indexed colours know no blends. - Only the last palette colour can be removed. Otherwise the indices of every image using the palette would shift. - Undo stores the whole layer per stroke instead of pixel diffs. - Alt+click picks a colour. It is a mouse gesture, not a shortcut. Follow-up issues: #197 import from texture, #198 selection and flipping, #199 PNG export, #200 more tools. Tests: - ./check.sh: 136 files, 2049 tests (before 135 / 2011). 34 new in tests/core/pixel-art.test.ts, 4 from the icon tables. - e2e module 48 fresh: ALL PASSED, 29 checks, console empty. - Counter-probes bite: five on the unit tests, upload order (2 tests fail) and cels not uploaded in the browser (1 check fails). - Check total 1259 in README is computed (1232 measured in CI + 27). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
592ea942d9 |
Vector shapes can fill with gradients and textures (#186)
A vector shape could only be filled and outlined with one flat colour. Shading, glows and patterned surfaces had to be faked with stacks of shapes. Every shape now has a paint type for its fill and its outline. - Fill type: solid colour, linear gradient, radial gradient or texture. Line type offers the same without texture. - Colour stops are edited on a bar in the inspector, as agreed in the issue. A click on the bar adds a stop in the colour the gradient already has there, so adding never changes the picture. At least two stops remain. - The new Paint tool in the tab shows two handles for the selected shape: axis start and end, centre and radius, or the corners of one texture tile. "Handles: Fill / Outline" picks which paint is edited. - Moving a whole shape moves its gradients and tile with it. Every gesture is one undo step. - A texture fill always tiles, because Pixi turns a non-repeating pattern into a repeating one anyway. When switched on, one tile covers the whole shape. - Only uploaded textures and frames can be a fill, never a drawing. A drawing filled with itself would rasterise itself mid-raster. - The fingerprint of a drawing includes the texture and frame a fill points to, so the picture redraws when the image is swapped. Findings: - graphics.destroy(true) also destroyed the textures of the fills. With a texture fill it deleted the tile slice and the next redraw crashed on reading 'scaleMode' of null. It is now destroy(). - The gradient cache destroyed old gradients itself, which gave 20 Pixi warnings "destroyed while still bound". They are now handed to Pixi's texture GC with autoGarbageCollect switched on. The console of module 47 is empty again, the last CI log had 0 of them. Tests: - ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44), 0 console warnings - 25 new unit tests, 10 new e2e checks in block 47i - Four counter-checks bite, originals restored and verified with cmp - Check sum 1232 in README and wiki, calculated (1222 + 10) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1116818011 |
Vector shapes can be combined by union, subtract and more (#187)
Building a shape out of simpler ones meant drawing the result point by point. The Vector Art tab now selects several shapes at once and combines them with a boolean operation into one ordinary shape. - Ctrl/Cmd+click in the tab and Ctrl or Shift+click in the shape list add a shape to the selection. Shift stays pan in the canvas. - A COMBINE block in the inspector offers Union, Subtract, Intersect and Exclude. Subtract removes every upper shape from the lowest one. - The result replaces the lowest selected shape and keeps its name, colours, stroke and place in the order. It is one undo step. - Curves are flattened to 0.2 canvas pixels, a circle gets about 48 points. Open lines and single shapes cannot be combined, the buttons are off and a hint gives the reason. An empty result is rejected. - Delete now removes every selected shape, and the selection drops shapes that no longer exist. - The geometry comes from clipper2-ts 2.0.1, pinned. It has no dependencies and speaks even-odd directly. Only the engine module is imported, the bundle grows by 55 kB instead of 110 kB. Findings: - clipper2-js was tried and dropped. It pulls in Angular 15 with a high npm audit finding and no fix. - Pixi finds holes by containment, not by even-odd depth. An island inside a hole would be drawn as a hole again. Result rings are emitted outer ring before its islands, and a test mirrors Pixi's rule. - A depth sort turned out to be dead code in a counter-check and was removed. A pixel check in 47h stayed green without the subtract and now checks both colours. Tests: - ./check.sh: 134 files, 1986 tests, exit 0 (before 133 / 1968) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 44 checks (before 39) - 18 new unit tests, 5 new e2e checks in block 47h - Five counter-checks bite, originals restored and verified with cmp - Check sum 1222 in README and wiki, calculated (1217 + 5) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3427f84a55 |
A game can pause itself and set its own time scale (#167)
Until now only the editor toolbar could pause a running game. A pause menu inside the game had no way to stop the world while its own graph kept running, and there was no slow motion. - Five new nodes: Pause game, Resume game, Is game paused, Set time scale and Time scale. Pause and resume take effect on the next frame. - A game pause stops world time, physics, animations, particles and shader time. Only behaviors with the new "Run while paused" switch keep running. - Set time scale 0 freezes the world but graphs keep running, as agreed in the issue. - Play and Stop reset the time scale to 1, Load scene keeps it. The pause belongs to the scene, so Load scene and Restart unpause. - The toolbar pause stays separate and also freezes the menu. When the game pauses itself the status line reads "The game paused itself". - While paused, collision events are not reported, sounds keep playing, and the editor run ignores both nodes with a log line. Findings: - The first status line was longer than any before and got cut with an ellipsis. It is shorter now. - The e2e harness did not know the key "p". It does now. Tests: - ./check.sh: 133 files, 1968 tests, exit 0 (before 132 / 1952) - npm run build: exit 0 - GM_STEPS=18 fresh: ALL PASSED, 33 checks (before 26) - 16 new unit tests: 8 catalog cases, game-pause.test.ts, editor run - 7 new e2e checks in block 18i, driven by real keys in the game - Six counter-checks all bite: behavior filter, collision lock, world time, editor run, clock reset and the game clock in the browser - Check total 1217 in README is computed (1210 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d79230b55 |
A frame's collision outline can be generated from its pixels (#161)
Drawing an outline by hand for every frame of a sheet is slow, and the outline mostly follows the opaque part of the image anyway. The texture tab now traces it: one click, a corner budget, and every Frame collision collider and tile using that frame blocks with the result. - "Generate collision..." sits in the texture tab header and is off while the texture has no frames. - The dialog has Max points (default 8, 3 to 64), Solid from ... % alpha (default 50) and All frames of the texture. A live line says what Generate will write, and Generate stays off when nothing is solid enough. - traceCollision walks each island of solid pixels along the pixel edges, then drops the least important corners until at most N are left and no stair step remains. A slope becomes one straight edge. - An outline never crosses itself. Where the image would force that, the island's convex hull is taken instead. - Crumbs under 1 % of the frame area are dropped unless they are the only island. Holes are not reproduced. - Existing outlines are replaced. One frame or all frames is a single undo step. - TextureLibrary hands out the sheet's RGBA data, and a pure function cuts a frame's region from it. Findings: - The wedge in the e2e fixture gets 4 corners, not 3. Its tip is one pixel wide in the image, and that real edge stays while corners are still free. Tests: - ./check.sh: 131 files, 1941 tests, CHECK EXIT 0 (was 130 / 1931). - 10 new unit tests in tests/core/trace-collision.test.ts, including 150 random shapes that must all be valid and within the budget. - GM_STEPS=42, freshly seeded: ALL PASSED, 20 checks (was 16). - Three counter-probes bite: no hull fallback, no stair threshold, dialog ignoring the alpha threshold. - Check sum 1202 in README is computed (1198 + 4), CI measures it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4ec23d3d3 |
Colliders take a frame's outline and tiles have their own surface (#193)
Issue #193 asked where friction, bounciness and density of a tile live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a collider could not use the outline drawn on a texture frame (#141). Both gaps are closed here. - A collider has a fifth shape "Frame collision" with a Frame row. The frame is picked like a sprite's texture and stored as the value 'frame'. - The shape is linked, not copied. Redrawing the outline in the texture tab changes every collider using that frame, in the editor and on the next play. - The frame centre sits on the collider origin, one image pixel is one unit. The shape turns, scales and mirrors like any other. - Every outline ring blocks. Each ring is split into convex parts the same way a polygon is. - Without a frame, with a whole texture or with a frame that has no outline, the collider is drawn red as a box and blocks nothing. - Move, Rapier, raycast, detection, hit test, drawing and footprint all resolve the frame. At runtime an entity looks the frame up live in the current project. - A tile has Friction and Bounciness next to Solid and One-way. The rows only show when Solid is on. Defaults and mixing rules match the collider. Density is left out because a tilemap never moves. - Tilemap cells are grouped by surface as well as by one-way angle. Ice and stone never merge into one rectangle, and every cell brings its surface to Move and Rapier. Findings: - A tile with a collision scale below 1 is no longer flush with a neighbour of another surface. It is inset there like at an open side, the same as next to one-way tiles today. - A bounce test first measured 0.84. The ball had landed on the hero at the origin, not on the tile. On the tile it measures 0.993. Tests: - ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917). - 14 new unit tests in tests/core/surface.test.ts. The Move tests run against the editor run and the physics system. - 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider has frame size, a walker climbs the slope in the frame). - GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks. - Three counter-probes bite: Move ignores the tile surface (2 fail), Rapier ignores it (1 fails), frame shape falls back to a box (5 fail). - Check sum 1198 is computed (1195 + 3), the full run belongs to CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3ff7e91dbf |
Vector art points have four types and the pen draws whole curves (#195)
The pen only pulled out the outgoing handle of a new point, because the opposite handle was mirrored only if it already existed. The first segment of every dragged point was therefore half a curve. Beyond that bug, a point had no way to say how its handles relate. - A vector point has an optional mode: straight, corner, smooth or symmetric. Missing means corner. The mode is not part of the fingerprint, because it changes no pixel, only the next drag. - Dragging a handle follows the mode. Corner leaves the other handle alone, smooth turns it along and keeps its length, symmetric mirrors it exactly. - The pen pulls out both handles mirrored and marks the point symmetric. A plain click still sets a point without handles. - The tab header has a "Point: ..." menu for the last clicked or set point. That point is drawn filled. Changing its type is one undo step. Text instead of four icons, because the UI kit has no tooltips. - Switching a point without handles to a curved type first grows handles along its neighbours. Smooth and symmetric line the handles up and keep the rough direction of the curve. - A point inserted on a curve is smooth. Symmetric neighbours become smooth, because the split shortens one of their handles. - Alt-click still toggles between straight and symmetric. Found on the way: the old Alt rounding took the point at the other end of an open line as a neighbour, so its handles came out skewed. Fixed and held by a unit test. Existing points with handles now count as corner. Before, every drag mirrored. No pixel changes, only the next drag behaves differently. Tests: - ./check.sh: 129 files, 1917 tests, exit 0 (before 1903) - GM_STEPS=47, freshly seeded: ALL PASSED, 39 checks (before 32) - Check sum 1195 in README and wiki is computed (1188 + 7), CI measures it - Counter-checks: pen without 'symmetric' fails 3 checks, no smooth branch fails 1 unit test and 1 e2e check Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6371576d37 |
A vector art asset is a picture now, which is stage 2 of #129 and the first stage that shows something in the browser: the asset sits in the picture picker, a sprite carries it in its Texture row and what the sprite shows has been rasterized from the shapes, the generator of #185 being registered for the type vectorart - installVectorArtPictures(app.renderer) hooks the source (core) and the generator (viewport) in once per page from App.ts, by a call and not by a module side effect, so a test may install it or leave it out; the rule lives in core and the painting in viewport - the new core/model/vectorPath.ts turns a shape into plain drawing commands (move/line/curve/close), closed rings being filled while all rings are stroked, a segment staying a line as long as neither of its ends carries a handle - a single handle is enough for a curve, the missing control point sitting on its own end, so a corner may stand next to a round side - and the way back to the first point is only drawn when it is curved, close doing it otherwise; holes go through one door only, Pixi recognising them from a GraphicsPath with checkForHoles, which tests containment rather than even-odd depth, which is enough for a ring inside a ring while without the flag the inner ring simply paints over the outer one (measured, see the counter-checks); the oversampling is the 2x from the plan - RenderTexture.create({resolution: 2}) puts 512 px on the GPU for a 256 canvas while texture.width stays the canvas size, which is the measure the sprite hangs on, and the render texture is the clip itself, so what juts out over the canvas is not in it and needs no mask; drawn pictures get the same sampler state as uploaded ones, setPictureFactory wrapping the factory for it, mirroring keep(); two measuring traps cost me runs - __gm.objectRect(id) gives the centre of the object and not its corner, so the first pass read four pixels next to the sprite and reported background while the rasterizer was correct, and the grid lines of the scene cross exactly in the centre point, so the hole check read #363940 and failed for the right reason, measuring beside it now; renderer.render() runs inside the picture request, which is safe here because every caller asks during its own widget redraw and the frame is drawn on the ticker afterwards, a nested render inside a running render pass would not be - that warning stands in the module head and in the notes; new module 47 with 8 own checks in its own lane (the picture on the GPU at double resolution measuring the canvas, the sprite showing the fill, the inner ring being a hole with the scene shining through, the later shape on top, the stroke running along the ring, a changed drawing getting a new key with the old one freed, and the sprite showing the new colour), the shapes being put into the model by mutate because the tab that draws them is stage 3, and 21 new unit tests in tests/core/vector-path.test.ts; three counter-checks hold: oversampling back to 1 fails the GPU check, checkForHoles off fails the hole check, and not drawing the curved way back fails a unit test; the full suite ran locally instead of only in CI because the change sits in TextureLibrary and touches every picture: ALL PASSED - 1165 checks, measured and carried into README and the wiki (#129)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bdb3f066cf |
The asset type "vector art" exists as a model, which is stage 1 of #129 and shows nothing in the browser yet on purpose: a vectorart asset is not placeable, sits in the + menu and in the asset panel with its own icon (a curve between two points) and has no tab, so it behaves like a font - selectable, its rows in the inspector, a double click leading nowhere - because the tab is stage 3 and half an asset would only be in the way; the data model in Asset.ts is a shape built from rings (even-odd, so a shape may have holes, answer 6 in the issue), a ring being points with optional bezier handles in/out stored as an offset from the point and a closed flag (an open path is stroke only, answer 4), plus fill colour, opacity and an optional stroke (answer 5) - the two handles are kept apart instead of mirrored, because what "smooth" means is the editor's business and not the model's; canvas width, height and the filter are properties, so they get an inspector row and undo like any other, while the shapes are intrinsic data like the frames of a texture; the revision that #185 hands to the picture path is a fingerprint of what is drawn, not a counter, which is exactly why that key took a string token - no command can forget to bump it - and the name of a shape is deliberately left out of it, renaming changing no pixel; six undo commands in the new vectorArtCommands.ts (add a shape numbered in order, remove one which comes back to its old place - unlike a tilemap layer the last one may go too -, rename, move, write the rings as one step per gesture with the list copied, and set the style where stroke: null takes it away) and the pure function shapeOrderTarget for the four buttons of the planned inspector list (one forward, to front, one back, to back), which also serves dragging in that list; loading from a project file is strict - a point without numbers falls away, a broken handle leaves a corner behind, duplicate shape ids fall away like duplicate layer ids, a stroke of width 0 is no stroke - while a ring with a single point, which is what someone is drawing right now, stays; three findings on the way: JSON.stringify as the fingerprint was wrong because the same drawing came back from a project file as {points, closed} and was built as {closed, points}, so a reload would have rasterized the same picture under a new key (the fingerprint writes its fields in a fixed order now and a test measures that case), toNumber in serialize.ts turns any nonsense into 0 so the Number.isFinite checks ran into nothing and {x: 'far'} became the left edge instead of vanishing (shapes read numbers strictly via asFinite, 0 being a place on the canvas here), and "Vector art" would have been both a menu entry and the panel heading, which an existing test catches because a click by text would hit the heading - the group is called "Vector drawings" now while the menu entry stays "Vector art"; 20 new unit tests in tests/core/vectorart.test.ts and three counter-checks hold: the fingerprint back on JSON.stringify fails 2, reading the numbers loosely again fails 4, and not copying the ring list fails 1; no e2e check and the check sum stays 1157, only module 04 counting 19 asset types instead of 18 (#129)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |