2cf72022eb2be7dc601dd948e7c66b98ad01aa4d
306
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2cf72022eb |
Ping an asset from its inspector link instead of selecting it (#221)
A single click on an asset link used to select the asset. That swapped
the inspector over to the asset, so the object being edited was gone
and the second click of a double-click landed on a different row. A
click now pings the asset in the asset panel, like Unity does in its
Project window, and a double-click opens it.
- state.pingAsset(id) emits a new `ping` event. It runs no command and
leaves the selection alone, like openAsset.
- ListView.ping(id) draws an accent overlay on the row. It stays full
for 200 ms and is gone after 1 s, driven by onFrame. destroy cancels
the animation.
- The asset panel expands the group, scrolls to the row and pings it.
- The capture listener and followedLink in the inspector are gone.
The inspector is no longer rebuilt, so pressCount on the label is
enough. followsPress and pressOf are no longer exported from ui/.
- The "Asset" and "Part of" rows of a scene object were a ghost button
that selected the asset. They are now a link label with the same
ping and open behaviour, next to a read-only field with the name.
- The link logic lives in linkLabel() in fieldFactory.ts. createFieldRow
and the asset row share it.
- TextField gets a readOnly option: no focus, no caret, no hover.
- New hook __gm.assetPing(). inspectorLink also reports the field
next to the label (value, readOnly).
Findings:
- Renaming an asset while its object was selected left the old name
in the inspector. This was already the case with the old button.
The refresh now updates the name.
Tests:
- ./check.sh: 151 files, 2228 tests, CHECK EXIT 0 (before 150 / 2226).
New: tests/ui/list-ping.test.ts for pingAlpha and pingAsset.
- e2e 1-16,28 on the final build: ALL PASSED, 575 checks.
28l-ii has 9 checks instead of 5.
- Counter-checks, each failing exactly at its check: ping also selects,
no follow-up frame (alpha stays 0.55), inspector never opens, field
takes focus, name not refreshed, label not a link.
- Check total 1345, calculated. CI last counted 1336 at
|
||
|
|
1aae97b335 |
Open a sprite's texture or shader from the menus and inspector (#221)
A sprite's texture and shader were only reachable by hunting for them
in the asset panel. Every place that shows such a reference now leads
straight to it.
- Object menu (hierarchy and scene view): "Open texture" and "Open
shader" sit right below "Open asset" when the sprite has one. The
object's own values count, so an overridden shader opens the
override.
- Asset context menu: the same entries at the top.
- Inspector: every asset field with a value shows its label as a blue,
underlined link. One click selects the asset, a double click opens
it. An override stays gold and gets the underline too.
- The first click rebuilds the inspector, so the second one lands on a
different row. The inspector catches that second click before any
row sees it.
- New pure rule `references.ts`: the asset behind a reference field. A
frame leads to its sheet. Kinds without a tab (palette, font, sound)
get no menu entry.
- `followsPress` in `clicks.ts` keeps the double-click window and
tolerance in one place.
- `activateAsset` in `actions.ts`: opening means the same everywhere
as a double click in the asset panel, so a scene also becomes the
active one. The asset panel uses it too.
Tests:
- ./check.sh: 150 files, 2226 tests, CHECK EXIT 0 (before 148/2214).
- 12 new unit tests: 6 for the reference rule, 4 for the link in
createFieldRow, 2 for followsPress.
- 5 new e2e checks in 28l-ii: both menus, the link's look, click
selects, double click opens.
- e2e 1-16,26,28,35,39,40 on the final build: ALL PASSED, 732 checks.
- Counter-probes: no "Open ..." entries, no underline and a second
click never recognised each fail exactly at their checks.
- Check total 1341 (CI counted 1336 at
|
||
|
|
9b62d572b4 |
Stop the bead spacing check failing on one slow frame
CI task 261 at
|
||
|
|
fd6af84474 |
Keep the texture tab smooth with many collision outlines (#220)
With a large sheet the texture tab stalled on every mouse move. A
profile over a pan with 1728 frames showed two causes. Half of the time
went into convexPieces, which split every outline into convex pieces
only to pick its colour. And every pointer move during a pan rebuilt
and re-tessellated the whole overlay geometry.
- TextureCanvas draws on two layers. `sheet` holds every frame except
the selected one and is rebuilt only when the zoom, the selection or
one of those frames changes. `overlay` holds the selected frame, its
handles and the rubber band of a new frame.
- Both layers are drawn without the pan. A pan only moves the
container and places the name labels again.
- SheetCanvas has a new hook `panned()`. By default it calls `redraw()`
as before, so Pixel Art and Vector Art are unchanged.
- The validity of an outline is remembered per outline and comes from
polygonProblem, with the same result. This works because the model
replaces an outline list on every change and never edits it in
place, the same as frameOutline.ts already relies on.
- The name plates stay below the outlines and the texts on top. Checked
on a screenshot: the corner handle next to a label sits on top.
- New debug hook `__gm.texture.drawInfo()` with the rebuild count, the
outline count and the screen positions of outlines and names.
Measured with a throwaway probe, 1728 frames, 120 moves, main thread
time per move, before and after in the same browser:
- Middle-click pan: 76.9 ms -> 4.7 ms (4.4 ms without frames)
- Dragging a frame: 51.7 ms -> 10.1 ms
- Wheel zoom: 52.2 ms -> 30.9 ms
- Selecting another frame: 67.6 ms -> 41.0 ms
Findings:
- Wheel zoom and selection still rebuild `sheet` once on purpose. That
stays a single hitch per notch or click on very large sheets.
Tests:
- ./check.sh: 148 files, 2214 tests, CHECK EXIT 0.
- e2e 1-16,37,39-43 on the final build: ALL PASSED, 641 checks.
- Three new checks in 42c-ii. A corner drag does not rebuild the other
frames. An undo on an unselected frame reaches the picture (1 -> 2
outlines). A pan moves outlines and names by exactly 40/25 px without
a rebuild.
- Counter-checks: a missing outline in the cache key and a pan that
does not move each fail at their check. The selected frame in the
key fails alone with 9 rebuilds during an 8-step drag.
- Check total 1333 -> 1336 (confirmed by CI at
|
||
|
|
ceaa44e5b8 |
Add roles and a user management tab for admins (#180, #179)
Until now every account could do the same, and managing accounts meant running make-user.php on the server. Accounts now carry a role, and admins get a tab in the editor to create, rename, re-role, reset and delete accounts. Both issues share the same server files, so they land in one commit. #180 Roles: - Every account is admin or user. A missing or unknown role counts as user, on the server and in the client. - auth/session and auth/login return {user, role}. - Auth::requireAdmin() answers everyone else with 403. - make-user.php takes the role as a third argument. Without it an existing account keeps its role, so a password reset from the CLI never demotes an admin. - Admins see and save other users' projects like their own. Saving keeps the original owner. #179 User management: - The account menu gets "User management" (admins only) and "Plugins" (everyone). Both open the same closable tab, accounts on top and plugins below. The fixed Plugins tab is gone. - Per account: Rename, Role, Password (no old password needed) and Delete with a confirmation. "New user..." creates one. - Each account lists its projects with an Open button. - Delete is refused while the account still owns projects. - Renaming moves the projects along and ends every session of the account. transfer takes the same lock as save. - On their own row an admin can only set the password. The last admin cannot be demoted or deleted. The server enforces both. - The account rules live in AccountRules.php and accountRules.ts as pure functions, so they are testable without a server. Findings: - At 1280 px the Delete button reached past the column edge and was not clickable. The name column is flexible now, and module 49 checks the layout. - The server lets an admin delete other users' projects, but the editor has no button for it yet. Left open in the issue. Tests: - ./check.sh: 148 files, 2214 tests, exit 0 (was 147 / 2204). - New tests/php/accounts.test.php, 51 checks, plain PHP. CI runs it as its own step in the gate job, the host has no PHP. - New e2e module 49 with 18 checks. CI creates the e2e-admin account in serve.sh. Modules 14, 38, 46 and the scaffold follow the new tab. - e2e 1-16,37,38,45,46,49: all passed, 581 checks. - Counter-checks: owner on save, admin view, default role and self delete in PHP fail 7, 5, 5 and 1 checks. Unchecked requireAdmin, a wider column and the caller as owner each fail exactly their e2e check. - Check sum 1314 -> 1333 (+1 in 38, +18 in 49), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
30fb3a2316 |
Dragging previews once per frame instead of on every move (#213)
Scene view, texture tab and graph called state.preview() on every pointer move. Each call rebuilds panels, the editor run and generated images, so a drag in a busy project cost up to 34 ms per move. The model is still written on every move. Only the preview now waits for the next frame. - New FramePreview in src/editor/framePreview.ts. request() goes into the move handler, cancel() into the release. The rule lives in one place instead of six copies. - Scene view: object drag, polygon corners and tilemap strokes use it. - Texture tab: frames, slice edges and collision corners use it. - Graph: nodes, groups and comments use it through GraphDrags. That covers the state machine, shader and particle tabs as well. - Vector art is unchanged. Its frame already writes the model since #129. Findings: - The texture tab paid twice per move (232 project events for 120 moves). Pixi sends a second pointer event 4 to 8 ms after each real move as a hover update. It now lands in the same frame for free. - Inspector scrubbing and gradient stop dragging still preview on every input. Both lie outside the three canvases of the issue. Measured with a throwaway probe, 120 moves at 8 ms, 120 extra objects: scene 26.2 -> 18.5 ms/move, texture 34.2 -> 17.9, graph 28.8 -> 17.0. Project events dropped from 120/232/119 to 48/48/41. Tests: - ./check.sh: 147 files, 2204 tests, exit 0 (before 146 / 2199). - 5 new unit tests in tests/editor/frame-preview.test.ts, including the real GraphDrags. Counter-checks (direct preview in the graph, no cancel on release, one key per name) each fail the expected test. - e2e 1-16,29,31,36,37,39-43: 708 checks passed. - e2e 1-16,26,28: 634 checks passed. - No new e2e check, the check sum stays at 1314. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f9d5e8bca5 |
Move the one-way rule and collider geometry out of physics (#215)
PhysicsSystem had 940 lines. The step logic shares its state with the engine handles of parts and bodies, so a separate class would need almost the whole system as its interface. That part stays. Two pieces without own state move out instead. Behaviour is unchanged. - passesOneWay in oneWay.ts is the one-way rule as a pure function. It can now be tested without Rapier. - bodyGeometry.ts holds the collider shapes and the pose maths. - PhysicsSystem is down to 820 lines. Tests: - ./check.sh: 146 files, 2199 tests, CHECK EXIT 0 - 5 new unit tests for the one-way rule without an engine. - New tests/core/body-geometry.test.ts: a capsule turned by 90 degrees went unnoticed before. Both tests fail against that bend. - Counter-checks on landing hold and teleport detection fail in 3 to 6 tests each. - e2e 1-28,31,36,39-43 on the final state: ALL PASSED, 1060 checks - Each of the four #215 commits was checked green on its own in a scratch worktree: 2190, 2192, 2192, 2199 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ed29b2f3dc |
Split the debug hook into one file per topic (#215)
debugHook.ts had grown to 1097 lines of unrelated test hooks. They now live in seven files under src/editor/debug/, grouped by topic. debugHook.ts only assembles them and is down to 86 lines. - The shape of window.__gm is unchanged for tools/. Measured in the browser before and after: 120 top-level keys and 313 leaves with their arity, byte-identical JSON. - The Text import is explicit in the helpers. Without it the name fell back to the DOM type and instanceof would never have matched. Findings: - The comment for vectorArt sat above pixelArt. It is back in place. - An orphaned duplicate comment on assetRow is gone. Tests: - ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (unchanged) - e2e 1-16,37,39-43,47,48: ALL PASSED, 734 checks - The shape probe turns red against a deliberately misnested hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b830f9c747 |
Split wiring, drags and the frame layer out of GraphCanvas (#215)
GraphCanvas had grown to 1406 lines. Wiring, dragging and the frame
views each carry their own state, so they now live in their own
classes behind a narrow host interface. Behaviour is unchanged.
- GraphCanvasHost is what the new classes see of the canvas.
- Wiring owns dragging a pin, the palette on a drop into empty space
and "Make transition".
- GraphDrags owns moving nodes and frames, including snapping.
- AnnotationLayer holds the frame views. The test hooks stay on the
canvas and read the views through the layer.
- BeadClock moves into beads.ts and can now be tested without a
browser.
- The canvas builds host, wiring and drags in its constructor because
state is a parameter property and not set during field init.
- GraphCanvas is down to 1019 lines.
Tests:
- ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (+2 for BeadClock)
- e2e 1-28,31,36: ALL PASSED, 932 checks
- New e2e check in 36k: a frame caught by the rubber band is also
drawn as selected. No check covered that before. Against a bent
build it fails with selected: false.
- Counter-checks for drop target, snapping and beads fail exactly
the expected checks.
- Editor check sum is now 1314 (CI measured 1313 at
|
||
|
|
e31db3df1d |
Split tile painting and polygon corners out of SceneViewport (#215)
SceneViewport had grown to 1216 lines. Three blocks carried their own state and only needed a few things from the view, so they now live in their own classes. Behaviour is unchanged. - SceneViewHost is the narrow interface the new classes see of the view: state, coordinate mapping, redraw and the preview trigger. - TilemapPainting owns the paint stroke, the rectangle preview and the flood fill, with press/move/release called in the old order. - PolygonCorners owns hovering, dragging, inserting and deleting the corners of a polygon collider. - TilemapSolids draws the solid outlines of tilemap layers. - SceneViewport is down to 668 lines. Findings: - PaintStroke.holderId was written but never read. It is gone. Tests: - ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (unchanged) - e2e 1-16,29,31,39-43: ALL PASSED, 679 checks - Counter-checks in the browser: bending the brush, the corner drag and the solid outline each fails exactly the expected checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
487b3aa5b4 |
Painting over identical tiles no longer adds an empty undo step (#212)
A rectangle drawn over cells that already hold exactly that tile
pushed a command that changed nothing. Undo then seemed to do nothing
and the project was marked as modified. PaintTilesCommand.changes()
was meant to prevent that, but nothing called it since #128.
- New paintTiles() in tilemapCommands.ts returns the command for a
finished stroke or fill, or null when no cell changes. It is the
first caller of PaintTilesCommand.changes().
- All three paint paths in the Scene View go through it: brush and
eraser, rectangle (both in endStroke) and flood fill (fillFrom).
- Flood fill was not affected, because floodFill already returns []
for the same tile. The guard is there anyway, as the issue asked.
- A rectangle that changes even one cell stays a normal undo step.
Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (before 2187).
- 3 new unit tests in tests/core/tilemap.test.ts: same rectangle
twice gives no command, a wider rectangle still does, a fill that
finds nothing gives no command.
- 1 new e2e check in 40d: the same rectangle again, then one undo
must take 9 cells to 0. A redo restores them for later blocks.
- GM_STEPS=39,40 on the final build: ALL PASSED, 81 checks.
- Counter-probe with changes(...) || true: exactly the two unit
tests fail, and in the browser exactly the new check fails with
{"afterUndo":9}. Original restored via cmp (IDENTICAL), rebuilt.
- Editor suite check total: CI measured 1312 at
|
||
|
|
0e8585665a |
Load the 500 weight and draw missing glyphs as SVG icons (#214)
The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.
- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
DOMParser.
- Tests find icons through the SVG id. The new hook
__gm.locateIcons(id) and the harness helpers findIcon/clickIcon
replace locate() on the old characters.
Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.
Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
character in src/ exists in every shipped face. The cmap reader
matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
back, the struct icon removed (one unit test each). In the browser,
500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at
|
||
|
|
056a622524 |
Refactoring round 6: shared sheet canvas, fewer copies (#85)
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).
- SheetCanvas is the new base of the texture, pixel art and vector art
canvases. It holds background, mask, hint, panning, wheel zoom, fit,
view save and restore and the screen mapping. Pixel art keeps its
whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
GeneratedPictures.has and SnapToggle.snapping. Tests now check the
same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
wiki pages.
Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
has not been called since #128. The rectangle tool therefore records
a no-op undo step when it paints the same tile over itself. Left as
is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
could become 10 without a red test. tests/core/raycast.test.ts now
covers it.
- CI measured 1308 editor checks at
|
||
|
|
d7c0b7cfdb |
Variables and state machines can reset with every scene (#164)
A shared variable or machine used to live either per object or for the
whole run. Levels need a third lifetime: shared like global, but built
fresh from the declarations whenever a scene is loaded.
- New scope `scene` for Variables assets and state machines. A missing
field keeps the old meaning: `global` for assets, `local` for
machines, so existing projects do not change.
- Returning to a scene that was visited before is fresh as well. There
is no memory per scene.
- A `scene` machine announces its start state again after a scene swap,
so `On state entered` in the new scene hears it. A global machine
stays quiet because it never left its state.
- The rule lives in two pure functions in `sharedState.ts`
(`carriedForward`, `restartedAssets`). BehaviorSystem only calls them.
- The inspector shows a Scope dropdown on Variables assets and a third
step on the machine dropdown, with a note on what `scene` means.
Decisions:
- `scene` and `persistent` exclude each other (variant A). The choice
dialog was cancelled, so this is an assumption to revisit. A
scene-scoped asset hides the Persistent switch, and switching to
`scene` drops the flag (undo restores it). The guard sits in
`isPersistent` itself, so no path can save values the scope throws
away anyway.
Findings:
- `clickText('Scene')` hits the Scene tab, not the menu entry. One e2e
check was red because of that, and its partner was falsely green
because it compared `undefined` with `undefined`. Both use `clickLast`
now.
Tests:
- ./check.sh: 144 files, 2142 tests, CHECK EXIT 0 (before 143 / 2133)
- 9 new unit tests in tests/core/scene-scope.test.ts
- e2e 32 fresh: ALL PASSED, 17 checks
- e2e 1-3,17,18,26,32,35 fresh: ALL PASSED, 213 checks
- 3 new e2e checks (35: scene-scoped asset resets on swap, 32h: dropdown,
note, hidden Persistent switch)
- Counter-checks bite: ignoring `restarted` fails 2 unit tests and the
new check in 35, the old announcement rule fails 1, dropping the scope
guard in `isPersistent` fails 1. Originals restored, cmp IDENTICAL.
- Check sum 1307 (1304 + 3) is computed. Last CI measurement: 1293.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6e3e8d23c5 |
Combining vector shapes keeps the curves it did not cut (#208)
Until now every boolean operation on vector art threw the curves away. The shapes were flattened into line segments, the operation ran on those segments, and the result came back as a polygon. A circle that nothing even touched came out of an Exclude with 40 corners instead of its four points and handles. Now the result carries the original curves again, and only the edges the operation really drew stay straight. - While flattening, every sample point remembers which curve it came from and where on that curve it sits. The operation runs on the same segments as before, so nothing about the boolean result changes. - When a run of result points walks along one of those curves, that piece is cut out of the original curve again with de Casteljau. This is an exact cut, not a fit through the points. - An edge whose two ends sit on the same curve but whose middle does not follow it stays straight. Without that rule a cut-off cap would bulge back into the shape. - A closed contour keeps at least three points, so a lens shape does not collapse into two. - The rule lives in its own module, src/core/model/vectorCurves.ts. vectorBoolean.ts only hands it the table of sampled points. Findings: - The two intersection points themselves sit up to 0.09 canvas pixels off the original curve. Clipper cuts the flattened segments, not the curves, so this was the case before as well and stays inside the 0.2 px sampling tolerance. - Refining those points with Newton steps on the curve was measured and gave nothing back (0.02952 px instead of 0.02978 px), because the error is in the intersection itself and not in the estimate. The code was removed again. - Theme.ts rowHeight 22 -> 24 is in this commit. The change is not mine, it was already in the working tree; gate and module 47 are green with it. Tests: - ./check.sh: 143 files, 2133 tests, exit 0 (was 2127) - e2e module 47 on a fresh seed: all passed, 59 checks (was 57) - 6 new unit tests: untouched circle, untouched side plus a straight cut edge, a cut in the middle of a curve, a chord crossing one curve twice, two overlapping circles, the three point minimum - 2 new e2e checks in block 47p: point count and handles from the model, plus a pixel pair on the round edge - Four counter-proofs, all bite: no reconstruction -> 5 unit tests and the e2e check fail with 35 points and nothing curved; no chord test -> the cap bulges back to 28.28; no three point rule -> the lens test fails; handles halved -> the pixel check reads background Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d7a2521ca7 |
A port can take its type from what is plugged into it (#207)
Until now every port of "First not empty" was grey and typeless, so the
editor could not tell you what actually travels through the node. The
output is now typed as soon as both inputs agree, and every any-input is
painted in the colour of its own edge. Nothing changes at runtime.
- A NodeDef can declare which of its ports carry the same value
(`sharedType: { inputs, outputs }`). The rule itself lives as a pure
function `agreedType` in `src/core/graph/portTypes.ts`.
- Only the output inherits. It becomes the shared type when every input
of the group is connected and they all carry the same type, otherwise
it stays `any`.
- The inputs stay `any` on purpose. `canConnect` checks against the port
type and a busy input is replaced by the new edge, so a typed `a`
could never be re-plugged with a text without disconnecting first.
- Chains do not propagate. When looking up what feeds an input, a node
of the same kind reports the `any` it is registered with, so two of
them in a row stay untyped and a data cycle cannot ask itself in a
circle.
- An edge that the new type makes invalid stays where it is and turns
red. `EdgeLayer` already paints a mismatching edge with `danger`,
nothing is disconnected on its own.
- New `PortDef.paintType`: colour only, never truth. It is set in
`drawnDef` and read in `NodeView.redraw`, so `canConnect`, the
interpreter and the compiler keep seeing `any`. Every connected
any-input is painted, not just members of a `sharedType` group; ghost
ports keep their hollow red look.
- The pin now matches the edge that ends in it, because both take the
colour from the resolved output type of the neighbour. Mixed sources
therefore show two colours, which explains why the output stays grey.
- New hooks `__gm.graph.portTypes(id)`, `__gm.graph.edgeColor(id)` and
`__gm.themeColor(name)`. `portTypes` reports type and paint colour
separately so the type checks do not silently become colour checks.
Findings:
- `resolveDef` only appends derived ports to the registered ones, so a
fixed port cannot be retyped that way -- it would appear twice. The
shared type is therefore resolved before the ports are built.
- Answer 5 of the issue (inline field follows the type) has no case left
under variant A: an unconnected input stays `any` and keeps its text
field.
- `Item at`, `Join` and `Equals` do not profit: `Item at` has no
any-input, the other two have fixed outputs. The next candidates are
the shader arithmetic nodes, which were left alone.
Tests:
- ./check.sh -> 143 files, 2127 tests, exit 0 (was 142 / 2113)
- GM_STEPS=1-6 fresh -> ALL PASSED, 245 checks (was 242), 9 of them new
- 14 new unit tests in tests/core/shared-port-type.test.ts
- 9 new e2e checks in block 6z, one of them a pixel check straight on
the pin via pixelAt + themeColor
- Counter-probes all bite: filtering unconnected inputs out of
`agreedType` fails 3 unit tests, ignoring `inherit` fails 1, killing
the derivation in the bundle fails exactly the 2 expected e2e checks,
and never setting `paintType` fails 4 unit tests plus the 3 pin
checks (#bdbdbd instead of #e6a8ff / #6fcf97)
- Screenshot read: `a` pink, `b` green, output `value` grey, edges in
the same colours
- Check total 1302 (1299 + 3), 1293 was measured by CI at
|
||
|
|
1a3babf9c5 |
First not empty gets a setting for what counts as empty (#206)
The node treated 0, false and an empty text alike, which is right for a missing behavior parameter but wrong the moment a real 0 travels through the graph. A setting now says how much counts as empty, so the same node works for both cases. - The new parameter "Empty means" has three steps: "Any empty value" (the default, the behaviour so far), "Nothing there (0 and false count)" and "Only a missing value". - The default keeps every existing graph as it is, including the common setup where an unset animation parameter arrives as an empty text. - The rule lives in isEmptyValue(value, mode) in values.ts; the node only passes the step through. An unknown setting behaves like the default, the same way Compare does. - The node description is shorter because the old one was cut off in the inspector. Findings: - In the "Nothing there" step a struct is empty only when its fields are empty under the same step, so a position (0, 0) stays a real value. Tests: - ./check.sh: 142 files, 2113 tests, exit 0 (before 2104). - 9 new cases in the node catalog table: the edge cases per step (0, false, empty text, empty list, position (0, 0), unconnected a) plus an unknown setting. - Two counterproofs hold: letting "Nothing there" act like "Any" drops the 3 cases with 0, false and the position; removing the "Only a missing value" step drops the 2 cases with the empty text and list. - Seen in the browser: the node shows "Empty means: Any empty value" and the inspector dropdown lists all three steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d98c2898b1 |
A new node takes the first of two values that is not empty (#205)
A graph that reads an optional value needed a chain of Equals and
Branch just to fall back on a default. The new node does that in one
step: it passes a through unless a is empty, in which case b comes
out. Both ports are any, so it works for numbers, text, lists,
structs and objects alike.
- `First not empty` sits in the data category with two any inputs
`a` and `b` and one any output `value`. Are both empty, b comes
out, so the second input doubles as the default.
- `isEmptyValue` in values.ts holds the rule as a pure function:
nothing connected, 0, an empty text, false, an empty list, and a
struct whose fields are all empty.
- The struct case is what covers a position: behavior graphs have no
vector type of their own, so (0, 0) arrives as a struct. `{x: 0,
y: 2}` therefore counts as set, `{x: 0, y: 0}` as empty.
Findings:
- A real 0 or false on a is passed over. That follows from counting
0 as empty and is written down in the issue.
- The output stays any. Ports that inherit their type from an input
do not exist in the graph; `Item at` has the same shape.
Tests:
- ./check.sh: 142 files, 2104 tests, exit 0 (was 2094).
- 10 new catalog cases: a set, a unconnected, 0, "", false, an empty
list, a position (0, 0), a struct with one field set, an object,
and both empty.
- Counter-check: with structs never empty and 0 a real value exactly
those two cases fail, the rest stays green. Originals restored and
compared with cmp, then rebuilt.
- No e2e check: single data nodes have no e2e coverage here, the
palette fills itself from BUILT_IN_NODES.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
d9c71c2ce7 |
e2e checks follow the new theme instead of fixed sizes (#202)
The suite job for
|
||
|
|
0d207769b9 |
Focused text fields show their outline on every edge (#202)
The focus outline of a text field was only visible at its corners. The mask that clips the text covered the whole field, border included, so the 1 px stroke was cut away along every straight edge. Only the rounded corners bent inwards far enough to survive. With the larger radius the new theme tried, this became obvious. - TextField puts text, selection and caret into their own container. Only that container is masked. Background and outline are not. - NumberField and the rename field in lists inherit the fix. - The theme radius goes back to 2 px. - New debug hook __gm.focusedRect() returns where the focused widget sits, so a check can read its border. Tests: - ./check.sh: 141 files, 2080 tests, exit 0 - GM_STEPS=2: ALL PASSED, 24 checks (one new) - The new check in 2c reads the middle of all four edges of the focused name field. All four must be the focus blue. - Counter-check with the old full-field mask: the check fails, all four edges read #24272e. - Editor check total is now 1286 (1285 + 1), computed, not measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
940a78e8ee |
The editor ships its own font, Source Sans 3 and Code Pro (#202)
Theme named Inter and JetBrains Mono, but neither was ever shipped, so every machine drew the editor in whatever it had installed. The e2e runs measured text in a different face than the user saw. - Five Latin woff2 faces live in src/ui/fonts/ next to their OFL licences: Source Sans 3 400/500/600/700 and Source Code Pro 400. - fonts.ts loads them through FontFace, and main.ts waits for them before the first widget exists, so Pixi measures the real face. - A face that fails to load is reported in the console and the system fallback in Theme takes over. - Theme is retuned by hand: 12 px body, 22 px rows, smaller gaps and padding, radius 8, and weight 500 for bold and headings. - The scene background default follows Theme.colors.graphBackground. This makes core import from ui for the first time. Findings: - The 500 file is in the folder but fonts.ts does not register it yet, so weight 500 currently falls back to the 400 face. Tests: - ./check.sh: 141 files, 2080 tests, exit 0. - e2e not run. Checks that measure widths or row positions are expected to need updating for the new metrics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
28b1afa7af |
Panning the scene view no longer redraws the whole scene
A middle-mouse pan used to run the full redraw on every frame. It collected every object from the project, cleared and redrew every shape, re-sorted all children and made Pixi re-upload every buffer, although only the camera had moved. The user saw the frame rate drop while panning. - WorldViewport gets moveCamera(). When zoom and view size are unchanged it only moves the world and redraws the overlays drawn in screen pixels. collectFrame, the shape redraw and the re-sort drop out of a pan frame. - Every full redraw remembers the box each object is culled by. A pan that moves an object across the edge of the view still takes the full redraw, so objects appear and vanish as before. - Grid, selection outlines, rays and the container origin mark move into a shared drawOnScreen hook. It runs after every full redraw and after every pan. The parent links stay in afterItems because they are drawn in world units. - Wheel zoom keeps the full redraw, because line widths follow the zoom. A cache for collectFrame was left out: it would save about 1 ms and risk stale data. - debugInfo() reports redraws and cameraMoves, and __gm gets a hook that forces a full redraw, so a check can compare against it. Findings: - Chrome already coalesces mouse moves to one per frame (500 moves gave 183 redraws in 189 frames). A throttle was tried and removed again, because its counter-check did not fail. - A check in module 03 compared the whole gameInfo() object and broke on the new counters. It now compares camera, objects and rendering. - The stall without ?vsync most likely comes from the desynchronized context in src/main.ts. It cannot be reproduced headless. Tests: - Measured on Jump and Run, main-thread time per frame, 3 runs of 240 moves: pan 5.7-7.4 ms before, 3.8-4.8 ms after. Plain mouse moves stay at 2.1-2.4 ms. 208 of 240 moves took the fast path. - ./check.sh: 141 files, 2080 tests, exit 0. - GM_STEPS=1-16,29,42,43: 535 checks, 1 failed (the module 03 test above). After the fix GM_STEPS=1-5: all passed, 130 checks. - 3 new e2e checks in 5d: 12 pan steps give 12 camera moves and 0 full redraws, pixel rows after the pan match a fresh redraw, a box panned out of view and back is drawn again. - Counter-checks: overlays not redrawn on pan gives "3 of 3 rows differ". Edge test disabled keeps the box visible with 0 redraws. - Check total 1285 (1282 confirmed by CI task 229, plus 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d2d63e1681 |
e2e runs always test the build that was just made
The test browser keeps Chrome's default profile, disk cache included, and nginx sends no Cache-Control header. Right after `npm run build` a run could load the index.html of the previous build and test old code. It showed up while measuring the scene view pan: a new debug hook returned NaN although server and bundle already had it. - signIn() turns the network cache off via Network.setCacheDisabled before the first page load. Tests: - GM_STEPS=5 (fresh seed): ALL PASSED, 10 checks. - ./check.sh: CHECK EXIT 0, 2080 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fe894c630d |
Pixel art tools show only useful options and remove any colour (#201)
The pixel art side bar showed every tool option all the time, the palette used only part of the column width and "Remove" could only drop the last colour. Now each tool shows what it needs, the palette fills the column and any colour can be removed. - Options per tool live in toolOptions.ts. Brush shows for pencil, eraser, line and unfilled shapes. Filled shapes shows for rectangle and ellipse. Contiguous shows for fill. Picker has none. - All options share one row with a fixed height, so nothing below jumps when the tool changes. - The palette picks its column count from the width and stretches the swatches until the row ends flush. At 1280 px that is 11 per row instead of 8. - The colour field takes the space the two buttons leave, so "Remove" is no longer cut off. - "Remove" deletes the selected colour. Pixels of that colour turn transparent, later indices move down by one in every image that uses the palette, and palette plus images change in one undo step. Transparent itself cannot be removed. Decisions: - Pixels of a removed colour become transparent, like erasing (chosen by the user before the build). Findings: - The Filled checkbox slid away under the pointer. The brush button sat to its left and vanished when Filled turned on, so the second click missed. The brush button now sits to the right, and a check clicks the same spot twice. - One start-up FAIL "debug hook present" came back never in three later runs. It is the known start flake. Tests: - ./check.sh: 141 files, 2080 tests, exit 0 (before 140 / 2070) - 10 new unit tests in tests/editor/pixel-palette.test.ts - GM_STEPS=48 (fresh seed): ALL PASSED, 40 checks, 4 of them new - Counter-check: indices not shifting and brush always visible make 4 unit tests and exactly the 2 matching e2e checks fail - Check total 1282 (1278 + 4) in README and wiki, computed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
41772e53f7 |
Variables assets can keep their values from one run to the next (#166)
A game needs a high score or an unlocked level to survive the end of
a run. As agreed in the issue there are no save or load nodes. A
Variables asset gets a switch instead, and the existing Variables
nodes read and write the values as before.
- A "Persistent" checkbox in the Variables inspector. Toggling it is
one undo step, and absent means not persistent.
- The behavior system loads the saved values at start and writes them
after every frame that changed one, so a closed tab loses nothing.
- Values live in localStorage per project, not in the project file.
Another browser or person starts fresh.
- Play and "Run in editor" save apart, so trying a behavior in the
editor leaves the Play save alone.
- Saved values are read through the current declaration. A removed
variable drops out, a new one starts from its default, and a changed
kind is converted.
- "Clear saved values" in the inspector clears this asset in both
areas. The main menu gets "Clear saved data" for the whole project.
Clearing takes effect from the next run on.
Decisions:
- The switch covers the whole asset, not single variables. Mixing
both means two assets.
- A changed default in the inspector shows only after the save is
cleared. That is what persistent means.
Findings:
- The first e2e run died at "debug hook present" right after a Vulkan
error from Chrome. The second run passed, so it was a GPU flake at
start-up.
Tests:
- ./check.sh: 140 files, 2070 tests, exit 0 (before 138 / 2055).
- 15 new unit tests in tests/core/saved-values.test.ts and
tests/editor/saves.test.ts.
- GM_STEPS=32,46 fresh: ALL PASSED, 24 checks. Six new checks in
module 32 go through the UI. Module 46 knows the new menu entry.
- Counter-checks both fail as expected. Loading switched off: 2 unit
tests and 2 e2e checks fail. Editor run writing into the Play area:
1 e2e check fails.
- Check total 1278 (1272 measured by CI at
|
||
|
|
bbbf34109b |
Lists duplicate, rename and edit values right in their rows
Name, opacity and Z each sat in a separate field below or beside
the list. You had to select a row first and then look elsewhere to
change it. Duplicating was a separate button or not possible at all.
Now the row itself carries everything.
- SortableList gets three optional callbacks. onDuplicate shows a
copy button between the eye and the delete button. onRename opens
a text field on double click, Enter or a click outside commits,
Esc cancels, an empty name is ignored. value puts a NumberField
into the row.
- Dragging on the number changes the value and never moves the row.
A whole drag is one undo step, the canvas follows live.
- Copy and delete only show on the hovered and the selected row.
Their space stays reserved, so nothing jumps on hover.
- Pixel layers: copy right above with the same pixels on all
frames, rename, opacity in percent. The Name and Opacity fields
below the list are gone.
- Pixel frames: copy right after, replaces the Duplicate button.
- Tilemap layers: copy with cells, Z and visibility, rename, Z in
the row. The Name and Z fields in the panel are gone.
- Vector shapes: copy at the same place and select it, rename.
- New in core: DuplicateVectorShapeCommand,
DuplicateTilemapLayerCommand, withLayerDuplicated and copyName
("X copy", "X copy 2"). copyName lives in src/core/util/names.ts
because the model never imports from commands/.
Findings:
- Double clicking a shape that is not selected yet only selects it.
The first click moves the list up, so the second click misses the
name. Module 47 selects the row first.
Tests:
- ./check.sh: 138 files, 2055 tests, CHECK EXIT 0 (before 2052).
- GM_STEPS=47,48 (fresh, twice): ALL PASSED, 91 checks (before 86).
- GM_STEPS=39,40,47,48: module 40 green, 47 died on the rename
check, which was a test fault and is fixed.
- Counter probes: a constant copyName fails the shape and pixel
layer tests. A dead copy hit fails the copy check in 47.
- Check total 1272 in README and wiki, computed (1266 + 6).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ae0247d86f |
Lists add and delete items right in the list header and rows
The layer, frame and shape lists still had their add and delete buttons scattered below the list, each in its own layout. They now live in the shared SortableList, so every list works the same way. - SortableList gets an optional header with the title on the left and a + on the right, plus an optional x per row next to the eye. - The keepOne option hides the x on the last remaining row, so a pixel art image and a tilemap always keep one layer. - Pixel art layers and frames and tilemap layers use header, + and x. Only Merge down and Duplicate stay below the lists. - Vector art shapes get the x but no +. Add shape stays below the list and Delete shapes stays for a multi-selection. - The section headings in the pixel art column use the same style as the list titles and the inspector. Findings: - The wait loop added to check 47i for #196 only waited for the right half of the shape. If the texture landed between the two reads, the left half was still blue and the check failed. It now waits for both halves. Tests: - ./check.sh: 138 files, 2052 tests, exit 0. - GM_STEPS=39,40,47,48 (fresh): ALL PASSED, 156 checks (was 154). - GM_STEPS=47,48 (fresh, final): ALL PASSED, 86 checks. - Two new e2e checks: x on a shape (47) and x on a frame (48). The tilemap check in 40 now uses the x instead of Delete layer. - Counter-check: with the x not reporting to its owner, exactly the two new checks fail. - Check total 1266 in README and wiki is computed (1264 + 2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5b9b57fd19 |
Layer, frame and shape lists all reorder by drag and drop
Reordering shapes by dragging worked well in the vector tab. Pixel art layers and frames and tilemap layers still used up/down buttons. All three lists now share one widget built from the shape list. - SortableList in the UI toolkit, with the drag rules next to it in src/ui/sortable.ts. A row has an optional thumbnail, a name, a hint on the right and an eye toggle. Every drop is one undo step. - The shape list is a thin wrapper around it. shapeRows.ts is gone, its tests moved to the new rules. - Pixel art layers show the layer alone on the current frame, frames show the whole frame. Up/Down, Hide, Earlier and Later are gone. - Tilemap layers get the eye instead of the checkbox. The per-row up, down and delete buttons are replaced by Delete layer next to Add layer. - A row is selected on release instead of press. Selecting rebuilt the inspector or tilemap panel and cancelled a drag that started on a row not yet selected. Tests: - ./check.sh: 138 files, 2052 tests, exit 0 (before 137 / 2051). - GM_STEPS=39,40,47,48 (fresh seed): ALL PASSED, 154 checks. - 5 new e2e checks. Check total 1264 in the README is computed (1259 + 5), CI never measured 1259. - Counter-checks bite: dropMove ignoring reversed fails a unit test, a tilemap drop not applied fails the drag check, a layer thumbnail showing all layers fails the thumbnail check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8adef83f23 |
Pixel art e2e checks run through in a full CI run (#130)
CI has been red since
|
||
|
|
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> |
||
|
|
1c8a20fcac |
Texture fill e2e checks pick the right image in a full run (#186)
CI run 217 for
|
||
|
|
592ea942d9 |
Vector shapes can fill with gradients and textures (#186)
A vector shape could only be filled and outlined with one flat colour. Shading, glows and patterned surfaces had to be faked with stacks of shapes. Every shape now has a paint type for its fill and its outline. - Fill type: solid colour, linear gradient, radial gradient or texture. Line type offers the same without texture. - Colour stops are edited on a bar in the inspector, as agreed in the issue. A click on the bar adds a stop in the colour the gradient already has there, so adding never changes the picture. At least two stops remain. - The new Paint tool in the tab shows two handles for the selected shape: axis start and end, centre and radius, or the corners of one texture tile. "Handles: Fill / Outline" picks which paint is edited. - Moving a whole shape moves its gradients and tile with it. Every gesture is one undo step. - A texture fill always tiles, because Pixi turns a non-repeating pattern into a repeating one anyway. When switched on, one tile covers the whole shape. - Only uploaded textures and frames can be a fill, never a drawing. A drawing filled with itself would rasterise itself mid-raster. - The fingerprint of a drawing includes the texture and frame a fill points to, so the picture redraws when the image is swapped. Findings: - graphics.destroy(true) also destroyed the textures of the fills. With a texture fill it deleted the tile slice and the next redraw crashed on reading 'scaleMode' of null. It is now destroy(). - The gradient cache destroyed old gradients itself, which gave 20 Pixi warnings "destroyed while still bound". They are now handed to Pixi's texture GC with autoGarbageCollect switched on. The console of module 47 is empty again, the last CI log had 0 of them. Tests: - ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44), 0 console warnings - 25 new unit tests, 10 new e2e checks in block 47i - Four counter-checks bite, originals restored and verified with cmp - Check sum 1232 in README and wiki, calculated (1222 + 10) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1116818011 |
Vector shapes can be combined by union, subtract and more (#187)
Building a shape out of simpler ones meant drawing the result point by point. The Vector Art tab now selects several shapes at once and combines them with a boolean operation into one ordinary shape. - Ctrl/Cmd+click in the tab and Ctrl or Shift+click in the shape list add a shape to the selection. Shift stays pan in the canvas. - A COMBINE block in the inspector offers Union, Subtract, Intersect and Exclude. Subtract removes every upper shape from the lowest one. - The result replaces the lowest selected shape and keeps its name, colours, stroke and place in the order. It is one undo step. - Curves are flattened to 0.2 canvas pixels, a circle gets about 48 points. Open lines and single shapes cannot be combined, the buttons are off and a hint gives the reason. An empty result is rejected. - Delete now removes every selected shape, and the selection drops shapes that no longer exist. - The geometry comes from clipper2-ts 2.0.1, pinned. It has no dependencies and speaks even-odd directly. Only the engine module is imported, the bundle grows by 55 kB instead of 110 kB. Findings: - clipper2-js was tried and dropped. It pulls in Angular 15 with a high npm audit finding and no fix. - Pixi finds holes by containment, not by even-odd depth. An island inside a hole would be drawn as a hole again. Result rings are emitted outer ring before its islands, and a test mirrors Pixi's rule. - A depth sort turned out to be dead code in a counter-check and was removed. A pixel check in 47h stayed green without the subtract and now checks both colours. Tests: - ./check.sh: 134 files, 1986 tests, exit 0 (before 133 / 1968) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 44 checks (before 39) - 18 new unit tests, 5 new e2e checks in block 47h - Five counter-checks bite, originals restored and verified with cmp - Check sum 1222 in README and wiki, calculated (1217 + 5) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3427f84a55 |
A game can pause itself and set its own time scale (#167)
Until now only the editor toolbar could pause a running game. A pause menu inside the game had no way to stop the world while its own graph kept running, and there was no slow motion. - Five new nodes: Pause game, Resume game, Is game paused, Set time scale and Time scale. Pause and resume take effect on the next frame. - A game pause stops world time, physics, animations, particles and shader time. Only behaviors with the new "Run while paused" switch keep running. - Set time scale 0 freezes the world but graphs keep running, as agreed in the issue. - Play and Stop reset the time scale to 1, Load scene keeps it. The pause belongs to the scene, so Load scene and Restart unpause. - The toolbar pause stays separate and also freezes the menu. When the game pauses itself the status line reads "The game paused itself". - While paused, collision events are not reported, sounds keep playing, and the editor run ignores both nodes with a log line. Findings: - The first status line was longer than any before and got cut with an ellipsis. It is shorter now. - The e2e harness did not know the key "p". It does now. Tests: - ./check.sh: 133 files, 1968 tests, exit 0 (before 132 / 1952) - npm run build: exit 0 - GM_STEPS=18 fresh: ALL PASSED, 33 checks (before 26) - 16 new unit tests: 8 catalog cases, game-pause.test.ts, editor run - 7 new e2e checks in block 18i, driven by real keys in the game - Six counter-checks all bite: behavior filter, collision lock, world time, editor run, clock reset and the game clock in the browser - Check total 1217 in README is computed (1210 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7063c8d074 |
Shader preview sliders show again after a property edit (#168)
Changing min, max or default of a property rebuilt the slider rows of the shader preview, but the rows stayed at 0 x 0. Only the labels were visible until the preview was resized by hand. - Widget.resize only lays out when the size changes. The row count stays the same on such an edit, so the row stack kept its height and never laid out its new children. - syncParams now relayouts the row stack itself after the column around it. Tests: - e2e 28n: a new max rebuilds the slider at 136 x 24, a click at its right end sets level 2. Red against the old build (w 0, h 0). - GM_STEPS=28,31 fresh: ALL PASSED, 106 checks (was 105) - ./check.sh: 132 files, 1952 tests, exit 0 - Check sum 1210 in README, computed (1209 + 1), CI measures it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
73a71473f1 |
The shader tab previews the effect on the last picked image (#168)
A shader could only be judged by placing it on a sprite and running the scene. The shader tab now shows its effect next to the graph, on the image the user picked last, without a picker to maintain. - The preview follows the last selection that has an image: a placed sprite with its overrides, size and tint, a sprite asset, a texture or a frame. Clicking the shader, a node, a behavior or a scene keeps the picture. - Without such a selection, or when the picked image is deleted, a built-in 64 px test image is shown. Its colour runs with u and v, a checkerboard shows where a UV lands and the border fades for alpha. - Each property gets a slider. The sliders belong to the preview and never write to the project. They start at the sprite's values when it carries this shader, otherwise at the default. - The preview has its own clock. Hold and Restart are only enabled when a Time node is in the graph. - The graph | splitter | panel layout of the particles tab moves into PreviewSplit, and both tabs use it. - A nine-sliced frame is previewed unsliced. This is documented. Findings: - The slider row stayed squeezed to a few pixels when the properties changed while the tab was hidden. The layout was triggered one level too high. The slider check in block 28n holds this. - The graph canvas is narrower now, 385 px at a 1280 px window, the same as in the particles tab. Block 28b clicked at a fixed x+420 and now uses the measured width. Tests: - ./check.sh: 132 files, 1952 tests (was 131 / 1941), exit 0 - npm run build: exit 0 - GM_STEPS=28,31 freshly seeded: ALL PASSED, 105 checks - 11 new unit tests in tests/editor/shader-preview.test.ts - 7 new e2e checks in block 28n - Counter-probes: start values ignoring the sprite fail 1 unit test, a preview ignoring the selection fails 2 e2e checks - Check total 1209 in README is computed (1202 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d79230b55 |
A frame's collision outline can be generated from its pixels (#161)
Drawing an outline by hand for every frame of a sheet is slow, and the outline mostly follows the opaque part of the image anyway. The texture tab now traces it: one click, a corner budget, and every Frame collision collider and tile using that frame blocks with the result. - "Generate collision..." sits in the texture tab header and is off while the texture has no frames. - The dialog has Max points (default 8, 3 to 64), Solid from ... % alpha (default 50) and All frames of the texture. A live line says what Generate will write, and Generate stays off when nothing is solid enough. - traceCollision walks each island of solid pixels along the pixel edges, then drops the least important corners until at most N are left and no stair step remains. A slope becomes one straight edge. - An outline never crosses itself. Where the image would force that, the island's convex hull is taken instead. - Crumbs under 1 % of the frame area are dropped unless they are the only island. Holes are not reproduced. - Existing outlines are replaced. One frame or all frames is a single undo step. - TextureLibrary hands out the sheet's RGBA data, and a pure function cuts a frame's region from it. Findings: - The wedge in the e2e fixture gets 4 corners, not 3. Its tip is one pixel wide in the image, and that real edge stays while corners are still free. Tests: - ./check.sh: 131 files, 1941 tests, CHECK EXIT 0 (was 130 / 1931). - 10 new unit tests in tests/core/trace-collision.test.ts, including 150 random shapes that must all be valid and within the budget. - GM_STEPS=42, freshly seeded: ALL PASSED, 20 checks (was 16). - Three counter-probes bite: no hull fallback, no stair threshold, dialog ignoring the alpha threshold. - Check sum 1202 in README is computed (1198 + 4), CI measures it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4ec23d3d3 |
Colliders take a frame's outline and tiles have their own surface (#193)
Issue #193 asked where friction, bounciness and density of a tile live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a collider could not use the outline drawn on a texture frame (#141). Both gaps are closed here. - A collider has a fifth shape "Frame collision" with a Frame row. The frame is picked like a sprite's texture and stored as the value 'frame'. - The shape is linked, not copied. Redrawing the outline in the texture tab changes every collider using that frame, in the editor and on the next play. - The frame centre sits on the collider origin, one image pixel is one unit. The shape turns, scales and mirrors like any other. - Every outline ring blocks. Each ring is split into convex parts the same way a polygon is. - Without a frame, with a whole texture or with a frame that has no outline, the collider is drawn red as a box and blocks nothing. - Move, Rapier, raycast, detection, hit test, drawing and footprint all resolve the frame. At runtime an entity looks the frame up live in the current project. - A tile has Friction and Bounciness next to Solid and One-way. The rows only show when Solid is on. Defaults and mixing rules match the collider. Density is left out because a tilemap never moves. - Tilemap cells are grouped by surface as well as by one-way angle. Ice and stone never merge into one rectangle, and every cell brings its surface to Move and Rapier. Findings: - A tile with a collision scale below 1 is no longer flush with a neighbour of another surface. It is inset there like at an open side, the same as next to one-way tiles today. - A bounce test first measured 0.84. The ball had landed on the hero at the origin, not on the tile. On the tile it measures 0.993. Tests: - ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917). - 14 new unit tests in tests/core/surface.test.ts. The Move tests run against the editor run and the physics system. - 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider has frame size, a walker climbs the slope in the frame). - GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks. - Three counter-probes bite: Move ignores the tile surface (2 fail), Rapier ignores it (1 fails), frame shape falls back to a box (5 fail). - Check sum 1198 is computed (1195 + 3), the full run belongs to CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3ff7e91dbf |
Vector art points have four types and the pen draws whole curves (#195)
The pen only pulled out the outgoing handle of a new point, because the opposite handle was mirrored only if it already existed. The first segment of every dragged point was therefore half a curve. Beyond that bug, a point had no way to say how its handles relate. - A vector point has an optional mode: straight, corner, smooth or symmetric. Missing means corner. The mode is not part of the fingerprint, because it changes no pixel, only the next drag. - Dragging a handle follows the mode. Corner leaves the other handle alone, smooth turns it along and keeps its length, symmetric mirrors it exactly. - The pen pulls out both handles mirrored and marks the point symmetric. A plain click still sets a point without handles. - The tab header has a "Point: ..." menu for the last clicked or set point. That point is drawn filled. Changing its type is one undo step. Text instead of four icons, because the UI kit has no tooltips. - Switching a point without handles to a curved type first grows handles along its neighbours. Smooth and symmetric line the handles up and keep the rough direction of the curve. - A point inserted on a curve is smooth. Symmetric neighbours become smooth, because the split shortens one of their handles. - Alt-click still toggles between straight and symmetric. Found on the way: the old Alt rounding took the point at the other end of an open line as a neighbour, so its handles came out skewed. Fixed and held by a unit test. Existing points with handles now count as corner. Before, every drag mirrored. No pixel changes, only the next drag behaves differently. Tests: - ./check.sh: 129 files, 1917 tests, exit 0 (before 1903) - GM_STEPS=47, freshly seeded: ALL PASSED, 39 checks (before 32) - Check sum 1195 in README and wiki is computed (1188 + 7), CI measures it - Counter-checks: pen without 'symmetric' fails 3 checks, no smooth branch fails 1 unit test and 1 e2e check Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
90627ccf47 |
Snap is a magnet button in every graph and vector art tab (#194)
The snap toggle was a checkbox with the word "Snap" next to Fit, and the vector art tab had its own "Grid" button for the same idea. Both now share one square icon button, so snapping looks and works the same wherever it exists. - SnapToggle is a square toggle button with a magnet icon and no text. It lights up blue when snapping is on, like the other toggle buttons. - The magnet is drawn with cubic curves instead of arcs, because the Pixi SVG parser draws arcs as jagged edges. - The vector art tab drops its Grid button and uses the same SnapToggle. It still snaps to 8 px, the graph tabs to 24 px. - SnapToggle accepts anything with setSnapping(on). The vector art canvas method is renamed from setSnap to setSnapping for that. - Every tab exposes the button as __gm.<tab>.snapButton(). Modules 06, 26 and 36 click through that hook instead of the label "Snap" plus 30 px. Tests: - ./check.sh: 129 files, 1903 tests (was 128 / 1897) - 3 new tests in tests/editor/snap-toggle.test.ts, 3 in the icon tests - GM_STEPS=1-6,26,36,47 fresh: ALL PASSED, 346 checks - GM_STEPS=47: ALL PASSED, 32 checks (was 29) - Counter-checks: toggle not switching its target fails 2 unit tests, the canvas ignoring the switch fails module 47 - Check total 1188 in README and wiki is computed (1184 + 4), the full run is left to CI Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ad62a0f525 |
A vector art asset can be made and used without touching the model (#129)
Stage 5 closes the loop that the earlier stages left open: every check so far put the shapes into the project with mutate, because the tab that draws them came later. Block 47f now walks the path a user walks - the + menu, the asset panel, the tab, the picture picker - so a regression in any of those four doors is caught by the suite instead of by you. - The + menu offers "Vector art" among the types that only live in the library, and the entry makes an asset that is selected and stands in the panel under its own heading. - A double-click on the row opens the Vector art tab on that asset, which is what stage 3 built and no check measured until now. - The Texture row of a sprite offers the drawing by name, sorted among the other pictures, and picking it there makes the sprite wear it - measured at the pixel in the scene, not at the row. One finding: the picker listed three entries where I expected more, and the reason is sound - a fresh fixture holds one texture and the drawing, "None" being the third. The check names all three instead of counting, so a lost entry says which one it lost. Tests: ./check.sh 128 files, 1897 tests, exit 0 node tools/e2e.mjs (all modules) ALL PASSED - 1184 checks (was 1179) GM_STEPS=47 (fresh seed) ALL PASSED - 29 checks (was 24) Counter-checks: both hold - no generator registered fails the picker check, and opening the tab without the asset fails the double-click check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78595ec80f |
A selected vector shape has its own inspector rows (#129)
Stage 4 of #129. Until now the inspector showed the rows of the asset when a shape was selected, so there was no way to give a shape a colour, an outline or a place in the drawing order without editing the project file. The rows for a shape now sit where every other selection puts its rows. - SHAPES lists every shape of the asset, topmost row drawn last, the same way a layer list reads. Selecting the asset itself shows the same list below its own rows, with no shape selected. - Each row paints its shape with the commands the rasterizer uses (shapeDrawing plus paintDrawing) into a 22 px swatch, the whole canvas scaled in, so the row also says where on the canvas the shape sits. - The four order buttons from the issue (forward, to front, backward, to back) sit in two rows of two, and a row can be dragged to another place with a marker line. They are disabled when there is nothing to move. - VECTOR SHAPE carries name, fill, opacity and an outline checkbox, the checkbox adding and removing the stroke instead of hiding it; line colour and width appear with it. A dragged colour slider is one undo step, the style rows going through previewingHandlers like every other row. - A line width of 0 is no stroke, which the command rejects, so the row clamps at 0.1. Findings: - locate('Outline') aims beside the control, because it gives the centre of the label text and a wider label pushes a fixed offset out of the field. __gm.inspectorField(label) knows the centre of the control. - A click into a text field sets the cursor and selects nothing, so a typed hex landed appended to the old one and parseHex returned null without anything failing - typeText only checks that the text is contained. A Ctrl+A goes first now. - Four buttons side by side cut "Backward" to "Backw...", which only the screenshot showed. Two rows of two now, as in the tilemap panel. Tests: ./check.sh 128 files, 1897 tests (was 127/1883). Full e2e suite locally instead of only CI because the change sits in the inspector and touches every selection: ALL PASSED - 1179 checks (was 1172), module 47 at 24 checks (was 17). Three counter-checks hold: not reversing the list fails 1 unit test and 2 e2e checks, dropping the gap correction when dropping a row fails 2 unit tests and the drag check, and a constant signature kills module 47 because the outline rows never appear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3a68067079 |
Dragging a point in the vector art tab keeps up with the mouse (#129)
Dragging a point only redrew once the hand stopped moving, which the user reported after stage 3 and which measures as a real backlog, not a feeling: every pointer move wrote the model and fired state.preview(), and a project event rebuilds the panels, the run in the editor and the picture the drawing is rasterised into. A mouse reports about 125 times a second while the screen takes 60, so the queue grew instead of the drawing moving. - Measured with a throwaway e2e probe sending 120 moves at the 8 ms of a real mouse, against a growing scene. Per move event: 10 -> 7 ms with an empty scene, 19 -> 4 ms at 40 objects, 36 -> 3 ms at 120. The 120 moves took 4173 ms before and 960 ms after, which is the time they were supposed to take. - New onFrame(key, fn) in src/ui/render.ts collects such work per key and runs it in the ticker before app.render(), because afterwards it would be one frame late in the picture. The rule lives in ui/ rather than in the canvas so a second caller gets it for free. - VectorArtCanvas only queues in pointermove now, and cancels its slot on pointer up, where the command writes the final state anyway. During the gesture the drag's own copy of the rings is the truth and the model is never read back, so nothing is lost by the delay. The same cost sits in the scene viewport, the texture tab and the graph, which all call preview() from their move handler. They are untouched: that is outside the reported bug. Tests: check.sh 127 files, 1883 tests, exit 0 (was 1878). Full e2e suite locally instead of CI only because the change sits in the render loop: ALL PASSED - 1172 checks, the sum unchanged - a timing check in the browser would flutter under CI load. Five new unit tests in tests/ui/render.test.ts. Two counter-checks hold: running the work after the frame fails one unit test, and taking the coalescing out of the canvas puts the measurement back at 36 ms and 4153 ms. 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> |
||
|
|
bdb3f066cf |
The asset type "vector art" exists as a model, which is stage 1 of #129 and shows nothing in the browser yet on purpose: a vectorart asset is not placeable, sits in the + menu and in the asset panel with its own icon (a curve between two points) and has no tab, so it behaves like a font - selectable, its rows in the inspector, a double click leading nowhere - because the tab is stage 3 and half an asset would only be in the way; the data model in Asset.ts is a shape built from rings (even-odd, so a shape may have holes, answer 6 in the issue), a ring being points with optional bezier handles in/out stored as an offset from the point and a closed flag (an open path is stroke only, answer 4), plus fill colour, opacity and an optional stroke (answer 5) - the two handles are kept apart instead of mirrored, because what "smooth" means is the editor's business and not the model's; canvas width, height and the filter are properties, so they get an inspector row and undo like any other, while the shapes are intrinsic data like the frames of a texture; the revision that #185 hands to the picture path is a fingerprint of what is drawn, not a counter, which is exactly why that key took a string token - no command can forget to bump it - and the name of a shape is deliberately left out of it, renaming changing no pixel; six undo commands in the new vectorArtCommands.ts (add a shape numbered in order, remove one which comes back to its old place - unlike a tilemap layer the last one may go too -, rename, move, write the rings as one step per gesture with the list copied, and set the style where stroke: null takes it away) and the pure function shapeOrderTarget for the four buttons of the planned inspector list (one forward, to front, one back, to back), which also serves dragging in that list; loading from a project file is strict - a point without numbers falls away, a broken handle leaves a corner behind, duplicate shape ids fall away like duplicate layer ids, a stroke of width 0 is no stroke - while a ring with a single point, which is what someone is drawing right now, stays; three findings on the way: JSON.stringify as the fingerprint was wrong because the same drawing came back from a project file as {points, closed} and was built as {closed, points}, so a reload would have rasterized the same picture under a new key (the fingerprint writes its fields in a fixed order now and a test measures that case), toNumber in serialize.ts turns any nonsense into 0 so the Number.isFinite checks ran into nothing and {x: 'far'} became the left edge instead of vanishing (shapes read numbers strictly via asFinite, 0 being a place on the canvas here), and "Vector art" would have been both a menu entry and the panel heading, which an existing test catches because a click by text would hit the heading - the group is called "Vector drawings" now while the menu entry stays "Vector art"; 20 new unit tests in tests/core/vectorart.test.ts and three counter-checks hold: the fingerprint back on JSON.stringify fails 2, reading the numbers loosely again fails 4, and not copying the ring list fails 1; no e2e check and the check sum stays 1157, only module 04 counting 19 asset types instead of 18 (#129)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |