085e6c8cceb4942daa03b98ed26fc2e08caa8b9a
29
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
05f7a2745e |
Frames: cut each frame into its own GPU texture, no more fill matrix (#47)
game-maker / suite (push) Successful in 8m18s
A frame drawn straight from the sheet showed a hairline of the neighbouring cell along any edge that fell inside a screen pixel: with antialias on, a partly covered pixel is still evaluated at its centre, past the edge. On top of that Pixi rewrites a Graphics fill texture from clamp-to-edge to repeat at draw time (generateTextureFillMatrix.mjs), so the top row of a sheet read its bottom row. TextureLibrary.get(ref) now cuts the rectangle out of the loaded sheet with an OffscreenCanvas on first use and uploads the cut (key file|x,y,w,h, kept for the session). Address mode is mirror-repeat: Pixi only rewrites clamp-to-edge, and within a texel of the border mirror behaves like clamp, for linear as well as nearest. Loaded sheets get it too, since whole pictures wrapped at their own borders the same way. viewport/textureFill.ts and its test are gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c74037b6e1 |
Show the camera's picture in the game tab without playing
The game tab held a "Press Play" hint where the picture belongs. It now previews the scene through the active camera, and says "No active camera" when none is switched on. Camera choice and its world position moved into activeCamera.ts so both are testable without Pixi. Entity.x/.y are parent-relative; the viewport used them as the camera's spot, which framed a parented camera at the wrong place. |
||
|
|
4d84231b9f |
Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners and the notes that carry a reason, a measured fact or an external contract. ButtonOptions.width claimed to be a fixed width but only set minWidth, so the Graph and Texture pickers grew with the name they showed. It is now what it promises, with a separate minWidth for the buttons that have two forms. Narrow buttons shrink their padding, otherwise a 24px icon button had 8px left for its glyph and truncate dropped it entirely. __gm.labelsDropped() guards that class: the overflow guard only saw text that was too wide, never a label truncated to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
35a1a23af8 |
Add a Texture tab for defining frames
A new Texture tab next to Scene/Game/Graph shows a texture sheet and lets frames be drawn on it: drag to create, move, resize via eight handles, edit name and rect in the inspector, Delete to remove. Every change is a command, so one gesture is one undo step. "Slice..." cuts a grid from columns and rows with optional offset and spacing, previewing the resulting frame count and cell size live. Sprites pick a frame instead of the whole texture through the same `texture` property: it now holds either a texture id or a frame id. Frame ids come from the same generator and are document-wide unique, so one picker, one property and one set of instance overrides cover both cases, and every sprite written before this still means the same thing. A Graphics fill ignores a Pixi sub-texture's frame and draws the whole image, because it derives its uvs from the shape bounds and the inverted style matrix. The cutout is therefore a matrix (`viewport/textureFill.ts`), which keeps a sheet at a single GPU upload no matter how many of its frames are on screen at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d54a620ebd |
Add ESLint and Prettier, and close the node test gaps
Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f140c2fd77 |
Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |