Commit Graph
17 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 b2b4cbad28 Keep a video's loading out of recordings and first frames (#417)
game-maker / gate (push) Successful in 4m21s
game-maker / suite (push) Failing after 17m16s
A Start recording right after Play composition still began with
empty frames. The composition's clock waited for its video, but the
recording went on and took every frame while the video loaded. A
clip that starts later in the video also slipped through, because
its player already had one picture while it still sought to the
clip's start.

- The view now counts every video sprite it leaves out as missing,
  not only one without any first picture.
- GameRecorder takes no frame while the game view misses a video,
  for at most VIDEO_WAIT_MS (3 s) at a time. The recorded time
  stands and the sound of that time is dropped, so it stays in sync.
- RecordingClock.start forgets a hold from before the first frame.
  Without that, the first real frames all got the time 0.
- A followed player starts at 1x until the first speed reading.
  Before, it paused and sought every frame for about 100 ms, so the
  first pictures after a wait stuck and then jumped. A step faster
  than 4x and a time running backwards do not count as a start.
- Test hooks: __gm.gameVideo(id) reads the game view's own player,
  TakeInfo.lastFrameAt shows when the last frame was taken.

Tests:
- Unit recording (clock after an early hold), video-follow (2 new,
  1 adapted): 449 green in tests/core/recording, tests/viewport and
  tests/core/composition*
- e2e 68e (a clip from 0.8 s shows that picture first and then
  plays), 68f (a recording that already runs takes no frame while
  the composition's video loads)
- 68e is red without the start at 1x, 68f without the hold
- README: 2038 editor checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:38:37 +02:00
adminandClaude Opus 5.5 4c7861b5b8 Keep the frame rate up while painting a big tilemap (#347)
Painting on the 60x60 map of a project with fifty objects dropped the
editor to 20 to 39 fps. A profile of a brush stroke showed four causes,
none of them the brush itself.

- Each frame of a stroke previews the model. The asset and hierarchy
  panels then built every row anew, so every label became a new text
  and a new upload to the GPU. One run spent 46 % of the main thread
  in those uploads. ListView.setItems now keeps a row whose item draws
  the same (sameListItem) and builds only the rows that changed.
- The brush drew the whole scene on every pointer event, on top of the
  preview. Now the preview draws what the stroke changed, once a frame.
  The rectangle tool asks for one drawing a frame for its box.
- An animated tile made the view draw in the tick and the preview drew
  it again in the same frame. animate now asks just before the frame
  is drawn and stays out when the view was drawn since the tick began.
- layoutSymbols ran on every drawing and costs up to the cube of the
  symbols. SymbolLayoutMemo keeps the spots while no anchor moves.
- A painted cell made the map read every "x,y" key again, and once
  more for each of the eight neighbours of every cell. layerCells reads
  a layer once per content into an index by place. layerTiles and the
  blocking cells take their neighbours from there (neighboursIn).
  parseCellKey reads a key without split, with the same results.

The test hook sceneRedraws counts how often the scene view was drawn.

Not done: a painted cell still works out the tiles and blocking cells
of the whole map again, so that part grows with the map.

Tests:
- Brush stroke on a copy of the project (stein2, Chrome): 20 to 39 fps
  before, 47.5 after, 55 to 57 without painting. Labels drawn again
  during the stroke 2012 before, none after. Scene drawings per frame
  1.32 before, 1 after.
- One painted cell, headless: 28 to 16 ms on 60x60, 109 to 49 ms on
  the same map doubled to 120x120.
- frame-preview: 2 new, red with the old pointer redraw.
- tilemap: 3 new (key reading, index, refresh), list-ping 1 new,
  symbol-layout 1 new. 5 single reversals turned each red.
- e2e 39,40 (melektaus-atx): ALL PASSED, 107 checks (1 new, a fast
  stroke: 6 drawings in 6 frames, no label drawn again). With the row
  reuse and the pointer redraw reverted it fails: 24 drawings in
  3 frames, and the panels draw their labels again.
- Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4038 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:54:59 +02:00
adminandClaude Opus 5.5 1665576fb9 Let a pixel perfect camera draw on the game view's pixel grid
game-maker / gate (push) Successful in 3m6s
game-maker / suite (push) Successful in 16m1s
A camera that follows smoothly stands between pixels most of the time,
and pixel art then wanders between them. The new camera switch draws
what the camera shows on the game view's own grid, while positions in
the model keep their fractions.

- `Pixel perfect` is a camera row, off by default. It works at every
  fixed game scale and at Auto, but not at Fit and not in the Scene
  tab, where one works with the fractions.
- The grid has one step per device pixel of a world unit. At 100% that
  is whole units, at 500% fifths. The view carries it (`View.grid`),
  so no view draws with the grid of another.
- The view looks from a point that puts the world's origin on a whole
  device pixel (`lookFrom`). This also covers odd camera sizes and
  views that cut the picture to an odd size.
- Sprites, texts and tilemaps snap their top left corner when they are
  unturned, so an odd size lands on whole pixels too. A turned object
  snaps its pivot (`snapPlacement` in `pixelGrid.ts`).
- Under a camera shader the picture is taken the same way, and the
  objects in it use the picture's grid. With Camera pixels that is one
  step per camera pixel.
- An overlay camera has its own switch and snaps only while the main
  picture is at a fixed scale.
- Particles are drawn where they are.
- New test hook `__gm.gameDrawnRect(id)` reads where a sprite's picture
  is drawn, which `gameRect` (the model's) cannot tell.

Found: the e2e fixture at 300% drew the crate at y 205.5 device pixels
without moving the camera, because the view cut to an odd height puts
its centre on half a pixel. With the switch on it is at 206.

Tests:
- Gate: 237 files, 3587 tests (+10 in pixel-grid.test.ts)
- e2e 2,3: ALL PASSED, 75 checks (+1, 3j)
- Counter-checks: without the origin snap 3 unit tests fail, with the
  centre snapped instead of the corner 2 fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:30:31 +02:00
adminandClaude Opus 5.5 cf6043a066 Show a wheel step or Fit as a short zoom transition (#309)
The wheel and the Fit button used to jump to the new view. Now the
view takes a short way there, 200 ms by default, everywhere the editor
zooms. The zoom steps stay the same, only the change is shown.

- viewTween: tweenView is the way between two views as a pure
  function. The scale moves by equal ratios, and the world point that
  sits at the same place on screen in both views stays there. After a
  wheel step that is the point under the pointer.
- ViewTween drives it on the ticker. A surface hands it reading,
  writing and drawing its own view and wraps its wheel and Fit code in
  change(). A step during a transition starts from where that one was
  heading, so quick steps add up to exactly as many steps.
- Anything else that moves the view ends the transition. A pan jumps
  to the target first, and saving a view saves the target.
- Wired into FreeViewport (Scene, Container, animation preview),
  SheetCanvas (Texture, Curve, Pixel art, Vector art), GraphCanvas,
  panAndZoom (Tile board, plugin node board) and the timeline.
- Menu > General settings... sets the time in ms, 0 jumps at once. It
  is kept in this browser like the panel sizes, not in the project,
  and it is not an undo step.
- A document that opens still comes up framed at once. The animation
  preview frames a new subject itself instead of calling fit().
- The pixel art readout names the step the view is heading for.

Measured in the scene view at 200 ms on melektaus-atx, where every
frame of the transition is a full redraw:

- 500 sprites: redraw 6.7 ms, 16 frames, mean 18 ms
- 1000 sprites: redraw 13.0 ms, 9 frames, mean 27 ms
- 2000 sprites: redraw 32.2 ms, 5 frames, mean 58 ms

Tests:
- The harness sets 0 ms on every load, so the existing modules read
  the step and not the way.
- e2e 46i samples one wheel step at 1000 ms a frame later: zoom 1.033
  between 1 and 2, the point under the pointer kept at (134, -40). It
  lands on the view a 0 ms jump reaches.
- e2e 46 and 49 expect the new menu entry.
- Gate (atx): 234 files, 3563 tests, CHECK EXIT 0 (+15)
- e2e 46,49 (atx): ALL PASSED, 43 checks (+3, sum 1735)
- Three counter-checks fail view-tween.test.ts as expected. e2e 46i
  fails when the scene view does not carry its centre along.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:01:10 +02:00
adminandClaude Opus 5.5 b2326085ed Show each camera only its layers and lay overlay cameras on top (#302)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m26s
A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.

- The project has a list of up to 16 render layers, edited in Project
  settings next to the collision layers. It is a list of its own, read
  by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
  `Render layer` under their switches. Its default `Inherit` takes the
  parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
  camera is the first one switched on without Overlay, so two plain
  cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
  one see-through view per overlay camera over itself. That view is a
  `WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
  distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
  other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
  it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
  takes the list it sets.

Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).

Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
  red main sprite at z 1000, in the preview and in the running game.
  A `UI` sprite in front of the main camera and a `Default` one in
  front of the overlay stay away. Without Overlay the second camera
  draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
  test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:32:52 +02:00
adminandClaude Opus 5.5 7dd3ae1131 Tie two objects with a hinge, a spring, a rope or for good (#154)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Failing after 11m41s
Two bodies could not be connected: no door on a hinge, no lamp on
a rope, no wheel on an axle. This is the first step of the issue,
the four joints without a motor. The slider and the motors follow
in a second step under the same issue.

- Joint is a new asset type, placed as a child of the object it
  holds, like a collider or a rigidbody. "Add joint…" in the
  object menu makes one or places one the project has. An object
  takes as many as it needs.
- Where the joint sits is the point it holds its object at. A
  hinge and a fixed joint have that one point only. A spring and a
  rope end at a connected anchor on the other object, counted in
  that object's own units, or in the scene's without one.
- The rows follow the type: length, stiffness and damping for a
  spring, length for a rope, an angle limit for a hinge. Length 0
  takes the distance the two ends were placed at. The limit counts
  from how the two were placed.
- The connected object is an object reference in the schema of the
  type. Pasting, containers and removing treat it as they treat
  every reference since #258.
- shownFor is a new schema option: a row is shown while an enum of
  the same bag holds one of the named values.
- The object needs a rigidbody, its own or one further up. Without
  one the joint holds nothing and the console says so once.
- A connected object without a rigidbody is a moving anchor: the
  joint has a kinematic body of its own that follows it.
- A connected object destroyed while playing makes the joint let go
  with a hint. Removed in the editor the reference empties, so the
  joint holds on to the scene. The issue asked for "inactive" there.
- "Add joint" ties two objects while the game runs, from a joint
  asset as the template. Destroy on the joint lets go, there is no
  Break joint node. Destroy with keep children takes the joints
  along with the object.
- The scene view draws a line from each joint to what it holds on
  to and rings the held point. A selected spring or rope has a
  round handle on its far end, and dragging it is one undo step.
- Joints hold in Play only. The editor run simulates nothing
  (#148), and the inspector of a placed joint says so.
- Rapier counts a revolute joint's limits absolutely, not from the
  start, so the placed angle is added. Measured with a probe.
- The + menu has 25 types now and no longer fits under its button
  on a canvas 800 high. It scrolls there, and e2e 04 scrolls to
  the last entry instead of expecting it in view.

Tests:
- Gate on melektaus-atx for this state: 218 files, 3390 tests
  (3336 + 54), exit 0.
- e2e 1-16,24,39-46,51,52,57,58 on melektaus-atx: 831 checks, all
  pass. Module 41 alone: 25 checks (21 + 4).
- 54 new unit tests: 40 in joints.test.ts (rope, spring, pendulum,
  limit, fixed, the other end, the node, containers, paste), 6 for
  the line the scene view draws, 4 catalog cases of the node. The
  remaining 4 come from tests that make a case per asset type.
- Four new e2e checks in module 41: Add joint from the menu, the
  rope to the hook with its line and handle, dragging the handle
  with one undo, the bob hanging in Play next to one that falls.
- e2e 04, 40 and 46 count the new type in their lists.
- 21 of 22 unit counter-probes fail their tests. The other one is
  equivalent: Rapier ignores removing a joint that left with its
  body, the guard stays.
- Three browser counter-probes fail their checks (2, 1 and 2).
- One screenshot looked at: icon, line, handle and the rope rows.
- The e2e sum of 1691 is counted (1687 + 4, 1687 measured by CI
  on e066613). The full suite and the smokes did not run.
- Not seen in the browser: spring, hinge with limit and fixed
  joint, the node Add joint, a connected object that moves, joints
  inside a container, the released game. Unit tests cover them
  but the last. Limits under a mirrored parent are not handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:43:12 +02:00
adminandClaude Opus 5.5 77f4c2c1a3 Show entities and time per area while playing in the Game View (#275)
game-maker / gate (push) Successful in 2m35s
game-maker / suite (push) Successful in 11m44s
A game that stutters gave no hint where the frame goes. The Game tab
now has a Performance switch. It shows a small overlay in the top left
corner of the Game View while a game runs: fps, the number of entities
and the mean milliseconds per frame for each area, averaged over half
a second like the fps counter.

- src/editor/frameMeter.ts holds the pure rule. FrameMeter sums the
  areas per frame, closes a window every 0.5 s and formats the lines.
- World.timeSystem times every system the world ticks. PlaySession
  maps the systems to Behaviors, Physics and Animation and sets the
  hook again after Load scene.
- Animation has its own line. Counting the AnimationSystem under
  Behaviors would have skewed that number.
- Drawing is the build of the game picture plus app.render(). It is
  main thread time only. The GPU work after it is not in the number,
  and app.render() draws the whole canvas including the panels.
- Editor is the rest of the editor tick after the systems and the game
  picture: mirroring into the model, panels and console.
- The overlay lets clicks through to the game. It shows dashes until
  the first window closes. The switch is off by default and is not
  saved. The released game (#266) runs without a meter.

Tests:
- Unit frame-meter: 9 passed.
- Gate on melektaus-atx: 197 files, 2853 tests, CHECK EXIT 0.
- e2e 59 on melektaus-atx: ALL PASSED, 18 checks (13 before, +5 in
  59e with a real mouse on the switch). Run twice.
- Three mutations were caught: the editor share not subtracting the
  systems (2 unit tests), systems running in pause not reported
  (1 unit test), the overlay getting no times (59e).
- The e2e total of 1601 is computed (1596 + 5), not measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:44:53 +02:00
adminandClaude Opus 5.5 ded71a92c9 Pick the target of an object variable in the Scene View (#276)
game-maker / gate (push) Successful in 2m32s
game-maker / suite (push) Failing after 10m45s
Choosing the target of an object variable meant finding its name in
a dropdown. With many objects, or several with the same sprite, the
name alone does not say which one it is. The eyedropper lets you
click the object where you see it instead.

- Every object dropdown gets an eyedropper button next to it. This
  covers variables, struct fields and list entries, in the Inspector
  and in the Variables tab.
- The button opens the Scene View or the container tab the object
  lives in. A strip above the view names the field and the current
  candidate and offers Take and Cancel. The cursor is a crosshair.
- A click picks a candidate and writes nothing yet. The candidate
  gets its own cyan frame, the selection stays on the object with
  the variable.
- Clicking follows the selection rule. `pickOn` in
  `src/viewport/pick.ts` uses `pressOn` and only counts objects the
  dropdown offers. A click on empty space keeps the candidate.
- Enter or Take writes through the same handlers as the dropdown.
  That is one undo step, or none if nothing changes.
- Esc, Cancel, a right click in the view or a second click on the
  button cancel without writing. So do a tab switch, another
  selection, undo, redo, play and loading a project.
- After Take or Cancel the editor goes back to the tab the pick came
  from.
- While the eyedropper runs, the view does not drag, marquee, paint,
  show handles or open its context menu. Panning and zooming stay.

Found on the way:

- Started from the Variables tab, the click missed the object. The
  Scene View is hidden while that tab is open, and the layout skips
  hidden children. The strip appeared then, the view moved down by
  32 px but kept its old height. `Editor.display()` now lays the row
  out again after showing it.

Tests (on melektaus-atx):

- unit pick and object-pick: 27 passed, 13 new
- gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from
  svg-icons, which checks every icon twice.
- e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g)
- Counter-checks work. Without the layout fix 57g fails. Picking
  objects the dropdown does not offer fails 2 tests. Writing on
  cancel fails 2 more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:31:41 +02:00
adminandClaude Opus 5.5 cab1c0177f Show only the game view in fullscreen, in the editor and the link (#267)
game-maker / gate (push) Successful in 4m10s
game-maker / suite (push) Failing after 11m53s
A game is easier to judge and to play without the editor around it.
The Game tab and the game link now both have a button that puts the
game view itself into fullscreen, not just the browser window.

- The Game tab header has a fullscreen button on the right. It hides
  toolbar, panels, tab headers and console and keeps the zoom from the
  picker. A running game keeps running.
- A corner button at the top right, or Esc, leaves fullscreen and puts
  the view back into the tab where it was.
- The game link shows the same corner button. Over a running game it
  hides after 2 s without mouse movement and returns on the next move.
- A click on the corner button never reaches the game.
- Stop ends fullscreen. Stop switches back to the Scene tab, and any
  switch away from the Game tab ends fullscreen, so no empty screen is
  left behind.
- src/viewport/fullscreen.ts wraps the Fullscreen API and holds the
  rule for when the button shows. FullscreenCorner.ts is the button
  used by both pages. Two SVG icons are new.
- Editor.showGameFullscreen moves the GameViewport into the root while
  fullscreen is on.

Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests).
- e2e 60: 2 new checks for the corner button on the game link.
- e2e 61 (new): 4 checks for fullscreen in the editor.
- Full e2e run (local): ALL PASSED, 1580 checks.
- Counter-checks fail as expected: rule without "no game running",
  no stopPropagation (60h), no end on tab switch (61c), no reparenting
  of the view (61a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:44:14 +02:00
adminandClaude Opus 5.5 30de4d35a8 Give the scene tab a header with Fit and the eye (#245)
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Failing after 9m15s
The scene view had no header bar of its own, and its eye sat in the
top bar. Switching between Scene and Game also showed two different
pictures, since nothing tied the free view to the camera.

- Theme.headerPadding (4) replaces Theme.padding in the top bar and
  in TabHeader, for the padding and for minHeight. Both bars shrink
  from 40 to 32 px. Nothing else uses the new value.
- The new ScenePane wraps the scene view in a header bar with Fit
  and the visibility eye. The eye is gone from the top bar, which
  keeps the menu, undo and redo.
- Fit with an active camera shows the world exactly as the Game tab
  does at its chosen step: the same zoom, and the camera centre on
  the same screen pixel. The rule is the pure freeViewOfCamera in
  viewport/view.ts, which also mirrors the letterbox snapping.
- Fit without a camera frames all content on a whole step, as
  before.
- New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there.
- The doc comment above Theme.treeIndent went away in the working
  tree along with this. It is committed as found.

Not checked: the two views only match while they have the same size.
With the tilemap panel open the scene view is narrower, and Auto may
pick a smaller step there.

Tests:
- ./check.sh: 178 files, 2575 tests, CHECK EXIT 0
- 2 new unit tests in view.test.ts: three cameras times four steps
  land on the same screen point as in the Game tab, Auto stays whole
- e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate,
  camera frame and view box match in both tabs
- GM_STEPS=1-3,46: ALL PASSED, 94 checks
- Counter-probes: without the centre offset the unit test fails,
  with Fit ignoring the camera both 3h checks fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:57:49 +02:00
adminandClaude Opus 5.5 6015869297 Let the scene view hide symbols, names and shapes per asset kind
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Failing after 9m33s
The scene view gets crowded once a scene holds many behaviors,
colliders and tilemaps. A new eye button in the topbar opens a list of
all placeable asset kinds, and each row has toggles for what the scene
view draws for that kind.

- The popup lists all 9 placeable kinds from Behavior to Tilemap, with
  the columns Symbol, Name and Shape. It stays open while switching.
- Sprite, Text and Tilemap draw no symbol, so their symbol toggle is
  greyed out. Their name toggle shows a new centred name tag, which is
  off by default.
- Shape exists for Collider and Tilemap. A hidden collider outline is
  neither drawn nor clickable. A hidden tilemap shape removes the
  collision outlines and the paint tint, the tiles stay visible.
- A hidden symbol takes no slot in the ring and draws no dashed link to
  its parent. A name that stays on moves to the centre of the object.
- The setting lives in the project as `sceneVisibility`, not in
  localStorage. Older projects load with the defaults, which match the
  view as it was.
- Switching has no undo step, like the game scale. It only changes the
  view. A change made during Play survives Stop.
- The rules sit in `src/core/model/sceneVisibility.ts`, the popup in
  `src/editor/panels/SceneVisibilityPopup.ts`. Four new SVG icons.

Not covered by a check: symbol off with name on for a kind with a
symbol, Text and Tilemap with names, and the paint tint with shape off.

Tests:
- ./check.sh: 173 files, 2515 tests, CHECK EXIT 0
- e2e 46: ALL PASSED, 17 checks. 46g clicks the popup, lays a small
  tilemap over the crate and removes it again.
- e2e 8 alone on a fresh seed: ALL PASSED, 23 checks. A red check in
  the partial run 2,8,46 came from the module selection.
- 8 new unit tests in tests/editor/scene-visibility.test.ts.
- 4 counter-probes, each fails its test: load ignores the saved value,
  Play skips the saved model, scene view ignores collider shape, scene
  view ignores tilemap shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:32:58 +02:00
adminandClaude Opus 5.5 50bddf68d4 Keep scene view names readable on plates and in rows of their own
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 8m37s
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>
2026-09-25 08:05:39 +02:00
adminandClaude Opus 5.5 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>
2026-09-24 19:32:37 +02:00
adminandClaude Opus 5.5 9ef360ba1b Let the game view show the picture at a chosen scale (#225)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 9m28s
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 c1a702b) + 7 = 1374, computed only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 07:02:48 +02:00
adminandClaude Opus 5.5 32cb5b4ba5 Size, turn and scale a selected sprite or collider in the scene (#223)
game-maker / gate (push) Successful in 2m54s
game-maker / suite (push) Successful in 9m14s
A sprite or collider could only be sized, turned and scaled through
the inspector. The scene view now draws a size gizmo on the selection,
so the common edits happen where the object is.

- Edges and corners set Width and Height. The opposite side stays
  where it was, so the position moves by half the change. Sizes land
  on whole units, at least 1.
- A circle collider has edge handles only and stays round. A polygon
  or frame collider keeps its own corner editing and gets no size
  handles.
- One knob on a stem above the top edge turns or scales. The first
  5 px of the drag decide which: out from the centre scales, sideways
  turns. The choice holds until the release, so one drag never does
  both. Turning and scaling share one handle by the user's choice.
- Shift snaps a turn to 15 degrees. The turn adds up the swept angle,
  so it goes on past a half circle instead of jumping back.
- The values are overrides of this placement, like "Fit to sprite".
  A value equal to the asset's own clears the override. One drag is
  one undo step.
- A handle reaches 7 px outwards but only half that into the object.
  Otherwise a press near the edge of a small sprite sizes it instead
  of moving it.
- The rules live in src/editor/sizeGizmo.ts without Pixi. The handles
  are drawn and driven by src/editor/viewport/sizeGizmoEditor.ts. The
  test hook __gm.sizeGizmo() reports them in screen pixels.

Found on the way:
- The first e2e run failed the play-mode drag check in 41b. It grabs
  the 40 px crate 6 px inside its bottom edge, and the full grip zone
  took that as a resize. The half zone inside fixes it.
- A first gizmo check failed because the sprite's right edge lay
  outside the scene view (x 940, view ends at 925). The test was
  wrong, the sprite now sits further left.

Tests:
- ./check.sh: 156 files, 2283 tests, CHECK EXIT 0 (before 2273).
- 10 unit tests in tests/editor/size-gizmo.test.ts.
- 6 new e2e checks in 29j: handles present, edge resize with the
  opposite edge fixed, undo, knob turn, knob scale that ignores a
  later sideways pull, circle radius.
- e2e 1-16,29,39-43 on the final state: ALL PASSED, 663 checks.
- Counter-checks: deciding the knob mode on every move fails the
  "does not turn it" check. Not moving the centre on an edge drag
  fails both edge checks. A full grip zone inside fails the new unit
  test. Originals restored.
- Check total 1357 + 6 = 1363, computed. CI measured 1357 at d257298.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:37:17 +02:00
adminandClaude Opus 5.5 cb32b4bc23 Draw scene symbols as white panel icons that make room for each other
game-maker / gate (push) Successful in 2m54s
game-maker / suite (push) Successful in 9m5s
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>
2026-09-23 20:07:47 +02:00
adminandClaude Opus 5.5 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>
2026-09-23 10:20:06 +02:00