021a6dcb5649992f4248bdad317258ca17240ca9
92
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
021a6dcb56 |
Zoom the tile tab in the same steps as the scene view
The tile tab zoomed freely, so the pixels of a tile could end up
unevenly wide at odd factors. Scene view, container tab and animation
preview already snap to FREE_ZOOM_STEPS. The tile tab now does the
same, so every pixel editor zooms alike.
- The wheel steps through FREE_ZOOM_STEPS, counted in device pixels.
The board position is rounded to whole device pixels.
- New wheelSteps() in src/viewport/view.ts turns wheel events into
steps: one notch is one step, a trackpad gathers 50 px per step.
FreeViewport uses it too, so the rule exists only once.
- Fit takes the largest step at which the whole board fits. Refitting
on a new tab or board size stays as before.
- panAndZoom takes an optional zoom rule instead of limits. Graph and
node board keep zooming freely.
- A saved view from before the steps snaps to the nearest step.
- The cap parameter of frameBoxes from
|
||
|
|
50bddf68d4 |
Keep scene view names readable on plates and in rows of their own
Symbol names in the scene view were plain grey text. On a background of a similar colour they could not be read, and names of symbols close together were drawn over each other. - Each name stands on a rounded, nearly black plate at 70 % opacity, 3 px padding left and right, 1 px top and bottom. - The text is light (viewportLabel) instead of muted grey. - The plate of a selected object gets an accent border. - New rule stackLabels in src/viewport/labelRows.ts. A name that overlaps another name or an icon moves one row down, then one up, then two down and so on, until a row is free. - Names are placed top to bottom, so the upper one keeps its row. A name only moves vertically and stays on the side of its icon. - Names outside the view still claim their row. Otherwise the visible names would jump while panning. - New hook __gm.sceneLabels() for the e2e check. The frame names in the texture editor already had a plate, so they are unchanged. Not covered by tests: the accent border on selection. Readability on a light background was only judged from a screenshot on a dark one. Tests: - 5 unit tests in tests/viewport/label-rows.test.ts: free names stay, the lower of two colliding names moves, the stacking order, names avoid icons, 30 crowded names never overlap. - e2e 5c-ii: 8 symbols on one spot give 8 plates, no overlap with each other or with icons, each name on the side of its icon. - Counter-checks: without the overlap test in the rule 4 of 5 unit tests fail. With the scene view ignoring the rows exactly 5c-ii fails. - ./check.sh: 161 files, 2369 tests, CHECK EXIT 0. - e2e 1-5: ALL PASSED, 140 checks. The e2e sum is now 1382 (computed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
89048413f1 |
Let a texture frame set where a sprite stands for Y sorting (#228)
A sprite sorted by Y stood on the bottom of its box. With a shadow or empty rows under the feet that is too low, so a figure slipped in front of a tree it should stand behind. The line belongs to the frame, not the texture, because one texture can hold several sprite sheets. - New field `sortLine` on a frame: whole image pixels up from the frame's bottom edge, 0 to the frame height. 0 is stored as nothing, so old projects stay the same. Shrinking a frame keeps the line inside it. - `feetOf` in drawOrder.ts lifts a sprite by the same share of its box. Scene view, game view, play and picking all use it. In an animation the line of the frame shown right now counts. - The texture tab draws the selected frame's line dashed in yellow with a knob left of the frame. At 0 the line shares the bottom edge with the nine-slice border and a resize handle, so the knob is tested first. A line inside the frame can be dragged directly, tested after the nine-slice lines. - The inspector has a new "Sorting" section with a "Sort line" field. Dragging or typing is one undo step each. Limit: frames of one walk cycle need the same value, set by hand. Otherwise the figure changes its place in the order while walking. Tests: - ./check.sh: 160 files, 2356 tests, CHECK EXIT 0 (before 2350). - 6 new unit tests in tests/viewport/sort-by-y.test.ts. - e2e 37 and 39-43: ALL PASSED, 162 checks. New check 37b-ii drags the knob up 6 px and undoes it, plus the inspector row. - Counter checks: if feetOf ignores the line, exactly the 3 order tests fail. If the knob cannot be grabbed, 37b-ii fails. - The e2e sum in the README is 1380, counted, not yet seen in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
50854389bb |
Let a tilemap layer sort its rows by Y with the objects (#228)
A tilemap layer was one piece in the draw order, so a figure stood either in front of a whole layer of trees or behind all of it. For a top-down game the trees in front of the figure's feet have to cover it and the ones behind must not. - A new "Sort by Y" box under the layer list sets `sortByY` on the painted layer. Every change is its own undo step. - Such a layer comes as one draw slot per row. On its `z`, the rows and all objects on that `z` are sorted by where they stand: the bottom of an object's box, the bottom of a row. Ties keep the hierarchy order, and other `z` values are untouched. - A layer drawn as one piece and the map's own node stay behind the sorted ones on the same `z`, since neither has a single place to stand. Ground belongs on a lower `z` or stays unsorted. - A new tile property "Sort offset" (whole rows, -16 to 16) lets a crown sort with the trunk below it. - `TilemapField` draws a container per row for such a layer, and picking takes the row into account, so a click picks what is drawn in front. Scene view, game view and play share the same rule. Limits: on a rotated map "down" is world Y. A layer sorted by Y costs about one draw call per row and tile instead of one per tile. Tests: - ./check.sh: 160 files, 2350 tests, CHECK EXIT 0 (before 2340). - 10 new unit tests in tests/viewport/sort-by-y.test.ts. - e2e 39-43: ALL PASSED, 148 checks. New check 40p-iii ticks the box and finds the slot of row 0, undo brings back the whole layer. - Counter checks: without the Y sort 5 unit tests fail, with the offset ignored 5, with no row in picking 1. If TilemapField draws no rows, exactly 40p-iii fails in e2e. - The e2e sum in the README is 1378, counted, not yet seen in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f4cc681df4 |
Fill texture and tilemap collision only where it is being edited
The faint fill on every frame outline and on every tilemap made the scene and the texture editor look green all over. Like the collider before, the fill now shows only on what is selected, and a bit stronger, so it marks the thing being worked on. - Texture collision: all frame outlines stay visible. Only the outlines of the selected frame are filled, at alpha 0.3 instead of 0.15 on every frame. - Tilemap: the outline still shows where the whole map blocks. The fill appears only while the tilemap alone is selected, and only for the part the layer chosen in the panel adds. Solid is 0.3 and one-way 0.15, before 0.12 and 0.06 on every map. - New query layerSolids(layerId) in items.ts. It builds the same solids for one layer and runs only for the selected map. - The small landing wedge on one-way edges stays filled. It belongs to the edge marker, not to the area. Not covered by tests: the texture canvas and the scene view wiring that picks the map and the layer. Neither was looked at in a browser. Tests: - ./check.sh: 159 files, 2337 tests, CHECK EXIT 0 (before 2334) - e2e 1-17,39-43: ALL PASSED, 668 checks - 3 new unit tests in tests/viewport/tilemap-fill.test.ts - Counter-check: with layerSolids ignoring the layer, exactly the layer test fails. Original restored, checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
495d87847d |
Tint a collider's area only while the collider itself is selected
A collider filled its area with a faint green in the scene view all the time. It lay over every sprite a collider sits on and made the art look washed out. The outline alone shows where a collider is. The area only matters while one works on that collider. - drawItem takes a selected flag. Only a selected collider is filled, every other one is drawn as its outline. - The fill is stronger than before, alpha 0.3 instead of 0.12, so the selected area is easy to see. The value is COLLIDER_FILL_ALPHA in src/viewport/symbols.ts. - The scene view says what is selected through isSelected. The game view keeps the default and never fills. - Only the collider object itself counts. Selecting its body leaves the area unfilled. Tests: - 2 new unit tests in tests/viewport/collider-color.test.ts read the Pixi instructions: outline only when not selected, fill at 0.3 and outline when selected. - Counter check: with the fill drawn regardless of selection the outline-only test fails. The original is back per cmp. - ./check.sh: 158 files, 2334 tests, CHECK EXIT 0. - e2e 1-17,28-30: no FAIL. e2e 39-43: ALL PASSED, 145 checks. A first run left out 39, which 40 builds on, and failed 8 checks in 40 for the missing tile only. 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> |
||
|
|
5399ef17e5 |
Snap the scene view zoom to pixel exact steps
The wheel zoomed smoothly and fit computed any factor, so the scene view almost never showed pixel art with even texels. Under Windows scaling even zoom 1 was off. The zoom now only takes steps that give every texel the same number of screen pixels. - The zoom counts device pixels per world pixel. freeView divides by the renderer resolution, so zoom 1 is one screen pixel per texel. - FREE_ZOOM_STEPS: 1/8, 1/4, 1/2, then 1 to 6, 8, 12, 16 and 24. - The wheel moves one step per notch. A trackpad collects its travel and steps every 50 px. - Fit rounds down to the next step. A saved zoom from before snaps to the nearest step on a log scale. - Pan, drag, grid and resize use CSS pixels per world unit, so they still follow the pointer under Windows scaling. - Always on, no toggle, as decided in the session. Tests: - 4 unit tests: zoom 1 at 125%, whole steps, wheel steps and ends, fit rounding and snapping of a saved zoom. - Counter-check: without the ratio in freeView the 125% test fails. - check.sh: 156 files, 2320 tests, CHECK EXIT 0. - e2e 1-17,28,30,38-40: 726 checks passed on the steps before the final values (up to 8). Not rerun on the final steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
22a89ed0f0 |
Keep game view steps pixel exact under Windows display scaling
At 125% Windows scaling the game view drew 100% as 1.25 screen pixels per camera pixel, so pixel art showed texels of uneven width even at the round steps. The steps now count device pixels instead of CSS pixels. - gameScaleFactor, cameraFactor and cameraView take the device pixel ratio. 100% is one screen pixel per camera pixel, 200% two. - Auto picks the largest step that fits in device pixels. Fit reports its factor in device pixels too, e.g. 156% at 125% scaling. - Fixed steps start the frame on a whole device pixel. - The game view reads the ratio from the renderer resolution. Limits: main.ts caps the resolution at 2, so scaling above 200% is stretched by the browser again. A changed ratio at runtime only counts after a reload, as before. Tests: - 2 unit tests at 125%: 100% is one device pixel on a whole pixel, auto and fit count device pixels. - Counter-check: without the conversion the 100% test fails. - check.sh: 156 files, CHECK EXIT 0 on the combined working tree. - No e2e: the harness runs at devicePixelRatio 1, same math as before. 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> |
||
|
|
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> |
||
|
|
9ef360ba1b |
Let the game view show the picture at a chosen scale (#225)
The game view always stretched the camera picture to fit the pane.
Pixel art came out blurred at odd factors, and there was no way to
see the game at its real size. The view now has a header with a
scale drop-down, and 100 % is the default.
- The header of the new GamePane offers Auto, Fit, 25 %, 50 %,
100 %, 200 % and 400 %. Auto and Fit show the factor they land
on, e.g. "Auto · 100%", and follow the pane when it resizes.
- 100 % means one screen pixel per pixel of the camera resolution
(width x height). The camera zoom scales the world on top of
that, so a zoomed camera still fills the same frame.
- Auto takes the largest fixed step that fits the whole picture
and falls back to 25 % when nothing fits. Fit keeps the old
behaviour.
- A picture larger than the pane is cropped around the camera
centre. Fixed steps start on a whole pixel so pixel art stays
sharp at 100 %.
- The choice is kept in the browser like "Play starts". Nothing
is written into the project.
Found on the way: the first draft applied the step to the world
instead of the camera resolution. The camera zoom then had no
effect at 100 %, which the existing check 12c caught (zoom 1.37
measured as scale 1).
The CI smokes were not run locally, because that would reseed
projects the user works in. All three smoke cameras are 640x360
and fit the 670x383 harness view at 100 %.
Tests:
- ./check.sh: 156 files, 2298 tests, CHECK EXIT 0 (before 2290).
- 8 unit tests in tests/viewport/view.test.ts for whole-pixel
steps, cropping, Auto and Fit with fallback, and zoomed cameras.
- 7 e2e checks in 03g. e2e 1-17,28,30,38-40: ALL PASSED, 727
checks.
- Counter-checks: no cropping, no rounding, no factor reported to
the header, and no zoom share each fail their own tests.
- Check total: 1367 (CI at
|
||
|
|
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
|
||
|
|
cb32b4bc23 |
Draw scene symbols as white panel icons that make room for each other
The scene view drew its symbols from its own SVGs, each in a type colour and some on a plate. They looked heavier than the panel icons and were hard to read on busy ground. Symbols of different objects also sat on top of each other whenever their positions met. - Every symbol is now the panel icon of its kind, one file for both places. The scene-only SVGs in src/viewport/icons are gone. - Symbols are white in a thin dark outline with no plate behind them. haloSvg widens each shape of the file by its own stroke and draws that silhouette dark under the icon, so no second file is needed. - A butt stroke end would leave its end without outline. The halo caps such strokes square, and the rigidbody arrow shaft is now a filled rect so its top edge is outlined exactly. - Symbols are 18 world units across, stay upright and are clickable. A click selects the object, a drag moves it. - symbolLayout places every symbol on its object's position. Symbols that overlap stand on a ring round the middle of their positions, evenly spaced one icon apart. A ring that reaches into another symbol takes it in until nothing overlaps. Ownership plays no part. - Names sit beside their symbol, on the outside of a ring. Selection frames and parent links follow the symbol. - A collider icon no longer shows trigger or invalid state. The outline colour of the shape still does. - A lone collider sits on its sprite's middle again. A press there on an unselected sprite drags the collider, so e2e 02 and 16 now grab the sprite beside its middle. Hook __gm.container.objectRect helps 16 find that spot. Tests: - check.sh: 2245 tests, CHECK EXIT 0 (+8 for layout and halo) - e2e 1-16,31,39-43: 670 PASS, 2 FAIL. Both were sampling too strictly for the thinner, smaller icons. 5c looked for the exact outline colour, which the thin outline only reaches blended at this zoom. 31n started the ring scan one pixel past the smaller ring. Both are fixed, and e2e 5,31 then passed with 48 checks. - 5c checks white with an outline and none of the old colours, 41b checks the ring positions and that a click on a symbol selects its object, 31n checks the now white particle ring. - Counter-checks: without the halo only 5c fails, also with the looser outline match. Without the layout the ring and click checks in 41b fail. Originals restored by cmp. - The e2e check total stays 1351. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e3d47ca9c1 |
Show a clickable symbol for colliders in the scene view
Colliders were the only placed part without a symbol in the scene view. A collider that sits exactly on its sprite was hard to find and could only be picked by its outline. - The symbol is the box with four corner handles from the asset panel. It stays upright and has a fixed size in world units. - colliderColor() holds the colour rule of the outline (green, trigger colour, red for a shape that collides with nothing). The outline and the symbol share it. - The symbol sits 28 units left of the collider centre. The sprite centre stays free for grabbing the sprite, and a rigidbody symbol on the same spot is not covered. - The symbol has its own 26x26 hit area and the outline's handler. A click selects the collider and a drag moves it. sceneStack counts it as a hit under the pointer. - The hook __gm.sceneIcon(id) also returns x, y, color, screen and clickable. Findings: - With the clickable symbol on the sprite centre, four checks in 01 and 09 dragged the collider instead of the crate. That is why the symbol moved beside the centre. Tests: - ./check.sh: 153 files, 2237 tests, CHECK EXIT 0 (was 152 / 2234). - New unit test for colliderColor. svg-icons.test.ts lists the collider symbol. - e2e 41b: 3 new checks (beside the centre without and with a rigidbody, click selects the collider). - e2e 1-16,39-43: ALL PASSED, 635 checks. e2e 41 alone: 20 checks. - Counter-probes: no symbol fails both placement checks. A symbol without hit area fails only the click check. - Check total 1348 + 3 = 1351, computed, not yet measured by CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
8c5846f956 |
Right-click in the scene view opens the object menu (#204)
The object menu lived only on the hierarchy rows. Now a right-click in the scene view opens the same menu for the object under the cursor. - The menu moves out of HierarchyPanel into src/editor/objectMenu.ts. Hierarchy and scene view call the same function, so their entries cannot drift apart. Only the hierarchy adds "Expand all" and "Collapse all". - Which object the menu is for follows the press rule from #203. A selected object under the cursor, even a hidden one, keeps the selection and the menu acts on all of it. Otherwise the topmost object is selected first. - The rule is the pure function menuTargetOf in src/viewport/pick.ts, next to pressOn. - Empty space, including an empty tilemap cell, opens no menu. - The container tab gets the same, because ContainerCanvas extends the scene view. Tests: - ./check.sh: 142 files, 2094 tests, exit 0 (was 2090) - e2e 39,40 fresh: ALL PASSED, 80 checks - e2e 1-16,39,40 fresh: ALL PASSED, 565 checks - New block 40r (3 checks): menu on a hidden selected crate, topmost object without a selection, no menu on an empty cell. - Counter-check: a right-click that always takes the topmost object fails 2 unit tests and the hidden-crate e2e check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ad2a2d7921 |
Clicks in the scene view pick what is drawn under the cursor (#203)
Pixi's own hit test decided what a click in the scene view selected. A tilemap caught clicks over its whole area, even on empty cells and over sprites sorted above one of its layers. A press always grabbed the topmost object, so a hidden selected object could not be dragged, and there was no way to reach an object underneath another one. - New pure rule in src/viewport/pick.ts. It lists the objects under the cursor front to back, in the order they are drawn. - A tilemap counts only where it has a tile. Its depth there is the Z of the highest layer with a tile at that spot. An empty cell is empty space, so a click clears the selection or starts a marquee. - Pressing on any selected object, hidden ones included, drags the selection. Otherwise the press selects the topmost object and drags it, as before. - A click without drag on a selected object selects the next one underneath. After the lowest it starts again at the top. - New debug hook __gm.sceneStack(x, y) returns that list. Decisions: - With a paint tool the selected tilemap paints anywhere on its grid, even under a sprite. With empty cells as empty space an empty cell could not be painted otherwise. - Icons such as colliders and cameras stay in front. A second click now reaches the sprite under a collider. Tests: - ./check.sh: 142 files, 2090 tests, exit 0 (was 141 / 2080) - 10 new unit tests in tests/viewport/pick.test.ts - GM_STEPS=39,40: ALL PASSED, 77 checks - GM_STEPS=1-16,29,39-43: ALL PASSED, 626 checks - 4 new checks in block 40q: sprite over a lower layer, empty cell, cycling crate, map, crate, dragging the hidden selected crate. - Counter test: whole-area tilemap hits and always-topmost presses fail 4 unit tests and all 4 new checks. - Check total 1290 (1286 + 4), computed, not yet counted by CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a2b06dd4b |
Texture fills show in the vector tab as soon as they load (#196)
After signing out and back in, a texture fill stayed in its base colour until something else redrew the vector tab. The texture library is a singleton that outlives the editor. Four widgets subscribed to it and never unsubscribed. After a new sign-in the first loaded texture called a destroyed canvas, which threw and stopped the loop before the vector tab heard about the image. - TextureCanvas, TileCanvas, ShaderPreview and WorldViewport now unsubscribe in destroy(). This is the pattern FontPreview and VectorArtCanvas already use. WorldViewport leaves both the texture and the font library. - FileLibrary.notifyChange catches errors per listener. A broken listener no longer starves the others. The error is still logged as "[textures] a listener failed". - e2e 47i no longer forces a redraw. It waits up to 3 s for the tab to show the texture on its own. Findings: - The trigger was e2e module 45 (sign out, sign in), not the texture fill. My note in #196 that the browser showed no exception was wrong. The counter-check shows it clearly. Tests: - ./check.sh: 137 files, 2051 tests, exit 0 (before 136 / 2049) - 2 new unit tests in tests/viewport/file-library.test.ts - GM_STEPS=45,47 after a fresh seed: ALL PASSED, 71 checks - Counter-check with the old source and the new check: 1 FAIL (shape stays #0000ff) plus the TypeError. Old FileLibrary.ts fails the first unit test. - No full e2e run locally. The check count is unchanged. 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> |
||
|
|
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> |
||
|
|
3d7ad65f34 |
Vector art has a tab to draw in, which is stage 3 of #129
Stages 1 and 2 gave the asset a model and a picture, but the only way to put a shape into it was a test calling mutate. The tab closes that gap: what you draw there is what the sprite in the scene carries, with no extra step, because the fingerprint changes and the picture registry of #185 redraws. - The tab has a header with Select, Pen, Grid and Fit, a chequerboard behind the canvas and the canvas itself as a frame. It paints with the same commands as the rasterizer, paintDrawing being exported from viewport/vectorArtPicture.ts for it, so the tab cannot drift away from the picture. - Pen sets points on a click and pulls the handles out on a drag, both of them mirrored; a click on the first point closes the ring and Esc ends an open line. - Select drags a point, a handle or a whole shape, a click on an edge puts a point there, a double click takes one away, and Alt turns a corner round and back. Delete removes the selected shape. - The rules grew inside polygonEdit.ts instead of a second module, as the plan says, so a collider polygon and a frame outline get the same behaviour where it applies. - A point on a curved edge splits the curve by de Casteljau instead of cutting it: both halves together draw exactly what was there before. - A curved edge is walked instead of measured over its chord, or a click on the bow of a curve would grab nothing; the same walk carries the test for the inside of a ring. - One gesture is one undo step. The preview writes the model directly and the old state is put back on release before the command runs, which is the pattern the frame outlines of #141 already use. - state.ts gained a selection kind "shape". The inspector shows the rows of the asset for it, the shape list with colour, opacity and stroke being stage 4. One counter-check was no counter-check: disabling the pen's closing tolerance by multiplying it with 0 left the run green, and rightly so, because the closing click lands on exactly the screen coordinate point 0 came from, so the distance is 0 and <= 0 is true. Only an unsatisfiable condition really switched it off, and then 2 checks fail. A second trap on the way: a false && in front of the guard breaks the TS narrowing, npm run build bails out, and two runs went against the old bundle. Tests: check.sh 127 files, 1878 tests, EXIT 0 (was 126 / 1860). Full e2e suite ALL PASSED - 1172 checks, module 47 alone 17 (was 10). 18 new unit tests in tests/editor/ring-edit.test.ts and 7 new e2e checks in blocks 47c and 47d. Two counter-checks hold: no de Casteljau fails 1 unit test, measuring a curve over its chord fails another. Originals checked back with cmp and rebuilt. |
||
|
|
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> |
||
|
|
cb1804dcf2 |
A picture in the editor may now be drawn instead of uploaded, which is the piece #129 (Vector art) and #130 (Pixel editor) both need and neither could build alone: TextureRef.file has been a file name on the server, isTextureFile() checking a sha256 hash plus an extension and TextureLibrary fetching it over HTTP, so an asset that paints its own image had nowhere to put itself - the key now also takes the form gen:<assetId>@<revision>, which an uploaded file can never look like, and everything that only passes a ref along (sprite, tile, tilemap mesh, particle source, shaders, aspect ratio) is untouched because it still sees a Texture; the revision is a string, not a counter, which is the one deviation from the plan in the issue and costs nothing - gen:a1@7 and gen:a1@c0ffee go through the same door, so a generator may fingerprint its own content instead of relying on every command to remember to bump a number; two registries keyed by asset type as a plain string, so a plugin may bring its own type - registerPictureSource(type, read) in the new core/model/generated.ts tells the model canvas size and revision and resolvePicture() builds the ref from it (no rect, no slice, smooth from props.filter like a texture), and registerPictureGenerator(type, draw) in the new viewport/generatedPictures.ts hands out the finished texture, projectPictureFactory(() => state.project) tying the two together per session in App.ts; invalidation is a rule instead of guesswork at twenty call sites: GeneratedPictures<T> holds one picture per asset keyed on the revision and frees the old one as soon as the new is drawn, TextureLibrary destroying it and dropping the crops cut from it, while a generator that cannot draw yet is not remembered but asked again next time - the class imports Pixi as a type only, so the whole bookkeeping is measurable without a GPU; carried along are the picture picker (a drawn asset sits among the textures by name, without frames), the delete warning (counts tiles and animations for it too) and the asset context menu, where "Create sprite from picture" now covers it while "Create animation from texture" stays texture-only because it needs frames; one finding while probing - crop() could not have cut a generated texture at all, a RenderTexture having no source.resource to draw back from and drawImage running onto null, so it is handed out whole, a drawn picture having no sheet whose neighbouring row could bleed in; 18 new unit tests, 10 in tests/core/generated-picture.test.ts (building and splitting the key, an uploaded file is not one, no registry means no picture, resolving to canvas size, a new revision is a new key, a canvas without a size, the filter, the sprite size, picker order) and 8 in tests/viewport/generated-pictures.test.ts (drawing once, freeing the old revision and only for its own asset, null is not remembered, no key means nothing drawn, a session change gives everything back, peek), and three counter-checks hold: dropping generated assets from resolvePicture fails 5 of the 10 core tests, not freeing the old revision fails 1 viewport test, and narrowing the picker back to textures fails 3; no e2e check and the check sum stays 1157, because without a generator the groundwork shows nothing in the browser - that evidence comes with #129 (#185)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ae3aa7d3bd |
Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away (#183), DEFAULT_FILTER (#152) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182, and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before (#85)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3ebf44593 |
Asset panel grouped by kind with icons: assets are listed under a header per kind (Sprites, Behaviors, ...) with the count on the right, empty groups are not shown, every group starts folded when a project is opened, a click on the header folds and unfolds it, selecting an asset elsewhere unfolds its group and folding the group of the selected asset clears the selection; every asset kind has an SVG icon in src/editor/panels/icons/ drawn before the name, the pure rule for grouping and sorting lives in assetGroups.ts; the same icons replace the kind word in document tabs and the overflow menu (a list icon for the List tab, which also fixes the close x lying over the end of the active tab's text), stand before every entry of the + menu (Button got an icon option) and on the right of scene object rows instead of the type name; the colour swatch next to sprite names is gone from both panels; the SVG loader moved to src/ui/svg.ts for both icon sets; test hook assetRow unfolds the group and scrolls to the row, assetRowShown only reads, new hook menuIcons; e2e 4g and 4h, 14 and 44a check the icons (#176)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1c212b1ccc |
Scene view symbols come from SVG files: the behavior, container, rigidbody, camera and particles icons live in src/viewport/icons/ and are changed by editing the file and running npm run build; they are bundled, parsed once and drawn into a Graphics by the new module symbolIcons.ts, currentColor becomes the theme color of the symbol, the origin sits in the middle with a centered viewBox and the same units as before, so the symbols are pixel for pixel unchanged; what depends on the object (camera frame, collider outline, one-way cone) stays in code; unit tests keep the files to what Pixi's SVG parser and a browser draw alike (no transform, no polygon, fill always given), e2e 5c reads the five symbols in the theme color (#175)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47eea2f6e4 |
Each tilemap layer has its own Z: an offset added to the Z of the placed tilemap, like a child's Z to its parent, default 0 keeps the previous order (layers right after their map, in list order), a negative Z sorts a layer below the map, on a tie layers keep their list order and follow the hierarchy against other objects; editor scene and game use the same order through the new pure function drawSlots, a tilemap draws into one container per layer, and the tilemap panel gains a Z field for the selected layer with one undo step per change (#174)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5385d9e152 |
The one-way tolerance angle is a field: a one-way collider and a one-way tile gain One-way angle (0 to 90 degrees, default 45), a contact blocks when its normal is within that angle of the blocking side, without a value the limit is bit-identical to before, and the rule holds for Move, Raycast and dynamic bodies in Rapier; tiles with different angles never merge into one rectangle, the scene view draws the angle as a cone in the middle of the thick side, and the inspector shows Blocks from and One-way angle only while One-way is on, through the new schema option shownWith (#159)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a31f5e0ad |
One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
32057a96fc |
Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78899b1ace |
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f53279d065 |
The active tilemap layer can be renamed from the painting section, and a Show setting fades or hides the other layers in the scene view while the tilemap is selected (#138)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c411c1ff3 |
Tilemaps can block movement: a tile gains a Solid switch, the solid cells of all layers are merged into rectangles and the placed tilemap takes part in the collision system as one body, reported as other in collision events (#132)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
732f6518ae |
The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
33a7b1b63e |
Tilemaps are a new placeable asset type that is painted with autotiles in the scene view, with layers, brush, eraser and rectangle, and is drawn at runtime and in the export (#128)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
193378e6a9 |
An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes. - AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one property of the root or of an object inside it, keys sit on whole frames. - The last key decides the length (its frame counts as a frame of its own). The playhead runs between frames, so numbers and colors blend every tick even at 10 fps; everything else holds. - Playback gains `once-reset`, which puts back what was there before the start. A row can be relative, adding to where its target stood. - Play / Pause / Stop animation act on an object; each instance plays on its own, the later start wins on a shared property, and a behavior writing the same property still writes last. - A sprite no longer wears an animation through `texture`. "Create animation from texture" builds a sprite plus a texture row keyed frame by frame instead. - The Animation tab shows a placeholder until the timeline editor (part b) lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d3563897d8 |
Two compilers that walk a graph the same way walk it through one emitter (#85)
Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection, duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own contract. The three Create*Command share ImportFileCommand, the three renames share RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in portSection.ts replace in-file clones, and three comments that told history now state the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
94a0a578ed |
A border drawn in pixels is as wide as the texture says a pixel is, so a small sheet can frame a large sprite (#96)
A sliced frame drew its borders one source pixel to one sprite unit. Pixel-art sheets are small: a 16px corner on a 400-unit button is a thin line, and the only way around it was a larger image. The texture now carries `Nine-slice scale` (default 1), next to `Filter`. It sets how many sprite units one border pixel takes, for every sliced frame of that texture. The UVs stay as they are - a corner shows the same pixels, only larger or smaller. When the sprite is too small for both borders they are still scaled down together, now starting from the scaled size. With `Repeat` the tile size scales too, otherwise a doubled border would sit beside a pattern of the old size. The value is a schema row, so inspector field, saving and undo come without their own code. Anything that is not a size above 0 reads as 1, and documents without it draw as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cc6db5f350 |
Three stores, three importers and three pickers that differ only in their type are one of each (#85)
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts, and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list, check count 958, the two-job CI, the Scenes wiki page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
be8f2331de |
A frame that keeps its corners can be stretched to any size (#41)
game-maker / suite (push) Successful in 6m21s
TextureFrame carries optional `border` (left/right/top/bottom) and `fill` (stretch | repeat); absent keys mean "not sliced", so no format bump and undo restores the plain frame instead of four zeroes. The sliced sprite builds its own geometry instead of the shared UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the frame cell's real UVs -- tiling in the geometry, not in the sampler, so a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per axis it stretches, to bound the buffer. A sprite narrower than left+right scales both borders down together until the middle is exactly 0, so the corners meet at a seam and never overlap. The texture editor gets four draggable border lines plus the same four numbers in the inspector. Hit order is resize handles, then the lines, then the frame body -- handleAt reports 'move' anywhere inside a frame, so lines placed after it were unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e9d21cd5d |
A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays. |
||
|
|
5671a38044 |
A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter. |
||
|
|
d5e67cd80c |
A shader costs a source nothing, because a field was already one draw call (#88)
game-maker / suite (push) Successful in 10m9s
A particle source can wear a shader asset the way a sprite does, and it is the
cheapest place in the editor to wear one: a shaded sprite gives up its batch and
costs a draw call of its own, while `ParticleContainerPipe` breaks the batch for
every field regardless. Five hundred shaded particles are the one call five
hundred plain ones were.
The fragment the compiler emits needed no change, only a second vertex stage:
Pixi's particle vertex is GLSL ES 1.00 and ours is ES 3.00, so it is translated
rather than invented. What it sees is a particle's picture, not its life — the
buffer carries five fixed attributes and takes no sixth, so age, index and seed
stay on the graph, which is what the graph is for.
Two things the issue did not ask about:
`Shader.resources` is an accessor over the names its constructor declared, so a
slot that is not declared becomes a loose JS property and is never bound. The
adaptor writes `uTranslationMatrix` into whichever shader the container names —
without an empty block for it the vertex gets no matrix and the particles land
elsewhere.
`schemaOf` in serialize.ts hung on `schema !== schemaFor('sprite')`, an identity
check a source never satisfies, so its shader values would have been dropped on
save.
A source is the one asset drawing rows from two graphs, so a name can arrive
twice. Its own row wins and the uniform keeps its default; nothing is rejected
on the shader's side, because the collision exists only at this one wearer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0e0a21dee1 |
One clock under everything that moves by itself (#90)
game-maker / suite (push) Successful in 10m4s
Particle age and shader time each read performance.now() off their own library, and the world got the frame's delta straight from the ticker. Three clocks, so a global time scale would have been three multiplications and a pause three separate freezes. core/time.ts holds one clock instead: it accumulates dt x scale per frame rather than reading the wall clock, and Editor.tick hands the very step it took to the play session, the editor run and the animation preview. Delta time, particle age, shader time and that preview are the same quantity now, so the scale is a single multiplication and a pause is that scale at 0 — nothing underneath needs rebasing, because the clock itself stands still. The Math.min(dt, 0.1) caps in PlaySession and EditorRun are gone: the cap (MAX_FRAME) now sits on the raw frame before scaling, and capping twice would make the world run slower than the particles at scale 2. A pause gates the scale rather than overwriting it — effectiveScale() answers 0 while paused, and for anything that is not a positive finite number, while gameClock.scale keeps its value. A slider later survives a pause that way; the issue asks for none yet. The clock goes back to zero at both ends of a run, so a run is reproducible from second 0 and the scene view looks freshly loaded after Stop. Resetting only on Stop would not do it: the editor clock runs on afterwards, and the next Play would start at "seconds since the last Stop". The trap in that: ParticleField keeps its origin as an absolute second of this clock, so after a reset it lies in the future and every particle would age backwards. One line in sync() starts such a field over, which covers the scene, the game and the tab preview at once — and fixes an older bug of the same shape, where releasing a particleTime() freeze left the preview empty for good. Found while measuring that: ParticlePreview stood at second 0.5 by computing the clock reading that would mean 0.5 since the field started. Over 200000 random origins the round trip misses the last bit in 18.8% of them and shows one particle too few in 9.4%, because floor(0.5 * 20) comes out 9. The check had been flaky since it was written. The panel hands the field the age itself now (ParticleField.hold), and the faked clock is gone. 833 checks (824 + 9), 931 unit tests (921 + 10). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78baf0c719 |
A source wears an icon without becoming decoration (#89)
game-maker / suite (push) Successful in 9m49s
Behavior and Container draw an icon because isSymbolType() is true for them, but that predicate does not mean "draws an icon" — it means "is editor decoration". Four places read it that way: collectWorldItems, GameViewport. collectFrame and syncDrawOrder all drop symbols out of the running world and the game picture, and placementOf() gives them an upright unscaled box. Listing 'particles' there would have handed the source an icon and taken away its particles, in the scene view as well as in the game. So the two questions are two functions now. hasSceneIcon() says a type draws an icon; isSymbolType() keeps meaning decoration. The icon itself is a SceneViewport decoration, which is editor-only for free because GameViewport never syncs decorations. A ring (r 10, #c26a1f) with four sparks, upright and unscaled like the other symbols, its middle left open — unlike Behavior and Container a source draws something of its own, and the field is densest exactly there. The name sits beside it and a dashed line runs to the parent, same colour. Found while writing the tests: the source's grab box scaled with its transform while the icon did not, so the two came apart. placementOf() returns only the translation for 'particles', making the box the upright 24 x 24 that covers its icon. The source's own transform still feeds every particle untouched. 824 checks (821 + 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8ae20d044f |
Time is an argument, so speed scales it and a prewarm offsets it (#87)
game-maker / suite (push) Successful in 9m48s
Speed and Prewarm are not state and not a second loop: both are arithmetic on the clock argument the field already passes to every particle formula. fieldTime = elapsed * timeScale + prewarmOffset, a pure rule next to liveRange, called at the one place ParticleField.sync forms its time, so preview and world inherit it together. Prewarm offsets by one lifetime because that is the smallest offset which fills a continuous emitter; a burst is dead at time >= lifetime, so the same offset would skip it entirely and it is suppressed there. The tab's slider counts wall-clock seconds, so its span divides by the scale. A missing row falls back to 1 / off, so old project files need no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fbb2d157f0 |
A held preview is rebased, not paused, and it wears the asset's own rows (#86)
game-maker / suite (push) Successful in 9m42s
Three reports plus one found on the way.
Hold only froze the *picture*: the preview computed a fake second while drawing,
the library clock kept counting underneath, so letting go jumped to "how much
would have passed by now". That is the flip side of the formula model — a thing
that is a function of the clock is not stopped, its origin is moved.
ParticleField.rebase() does that; hold(null) calls it with the second stood at.
The Hold label now mirrors the preview's state instead of its own click history,
so dragging the slider (which holds too) reads `Run`, and Restart puts a held
preview back to 0.00 s rather than leaving it on the second it stopped at.
previewProps only read defaultsFor('particles'), so the panel showed the type's
defaults while the inspector showed the asset's rows. AssetBase.props is a full
bag on a graph asset as well — asset.props now lie over the defaults, the
asset's texture is resolved instead of always drawing a white square, and the
slider's span follows its lifetime. The span is re-read in tick(), because the
rows are edited in the inspector and the tab hears nothing about it.
The preview moves from under the graph to beside it, past a draggable splitter:
a graph is read across, so width is what it can spare and height is not.
AssetTabEditor grows a body() hook for that — the base class still stacks header,
strip and canvas, a subclass may put something next to the canvas.
Found by the full run, not reported: the graph canvas's empty-state hint ran
38 px past its frame now that the preview takes 280 px away. It is cut with
truncate() like any other label.
816 checks (809 + 7).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|