Commit Graph
148 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 17f47f6956 Drop the Gain's engraved name and centre its knob (#384)
The plate was taller than its one knob needs, and the engraved
"GAIN" only repeated the node's title. Without it the plate is
square, and the knob sits in its middle.

- The plate is 160x160 instead of 160x220, without the "GAIN"
  engraving.
- The knob is centred on what it shows: the "DB" caption and the
  dial. Since #382 its number hangs inside the dial's open bottom,
  so it adds no height.
- CAPTION_AIR (4) takes out the empty space above the caption's
  capitals. Without it the knob sat a little too low.

Measured on a picture at zoom 0.8: 19 px of air above "DB" and
19 px below the number. The user's screenshot showed about 14
above and 35 below.

Tests:
- Unit hardware-view: 12 tests green. The layout test now asks
  for the same air above and below.
- e2e 34 (melektaus-atx): ALL PASSED, 126 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4359 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:29:28 +02:00
adminandClaude Opus 5.5 01ee61868b Show a Gain node as an orange device with one knob (#384)
A Gain node now draws a small device in the graph, as the Compressor
does since #380: the brushed plate in orange, 160x220 graph units, with
one chrome knob between its inputs and its output.

- GainPanel and GainFace join the node faces. The knob drags, resets
  on a double-click and keeps the wheel for the graph (#383). Each turn
  is one undo step, a linked Gain (dB) greys it.
- The knob turns with a taper of 0.26, so 0 dB stands straight up and
  the range below -24 dB takes a fifth of the turn.
- drawPlate, engrave and the brushed look move out of CompressorPanel
  so both devices share them. The Knob takes an arc colour, because
  the orange arc vanished on the orange plate.
- No meter and no tap. Gain sits in every voice of an instrument, and
  a tap would add two audio nodes to each note for a display nobody
  asked for.

Found on the way:
- Knob.dialGlobal added half the dial unscaled. In a zoomed graph it
  pointed beside the dial. The Compressor's e2e only hit by luck.

Tests:
- Unit: 2 new for the Gain plate and its taper.
- e2e 34 on atx: ALL PASSED, 126 checks (1 new).
- Gate on atx: CHECK EXIT 0, 283 files, 4359 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:07:44 +02:00
adminandClaude Opus 5.5 79a04e6764 Edit a Parametric EQ in its node, let the wheel only zoom graphs (#381)
The small curve below the EQ node's rows (#375) only showed the bands.
Like the Compressor since #380, the node now shows the editor of its
tab between its inputs and its output, and is worked right there. A
double-click on the node still opens the same editor in a tab.

Two wishes for the nodes with a panel came on top and share the same
files, so they are in this commit as well: the numbers below the
Compressor's knobs read as plain labels (#382), and the wheel over a
node zooms the graph instead of turning a value (#383).

#381, the EQ in its node:
- EqPanel holds the curve, the band's numbers and the help line, taken
  out of EqEditor. The tab puts its one panel under a header, each EQ
  node has a panel of its own at 720 x 360 (EqFace).
- A press on the curve neither picks nor moves the node. A
  double-click on the curve adds a band, one on the title or the edge
  opens the tab.
- In the node a press on the Type or Slope button stays with the
  button, so the node is not picked or moved.
- The spectrum runs in the node while it is in the picture. The tab
  lets go of the tap once, like the Compressor's, and Editor.tick lets
  every unseen tab and graph go before any seen one listens.
- The count of bands moved from the tab's header into the band row,
  so the node shows it as well.
- The node picture of #375 is gone with its only user: NodePicture,
  EQ_PICTURE and NodeView's drawing of it.

#382, the numbers below the Compressor's knobs in its node:
- A Knob with typed: false shows its number as a centred Label without
  a box, in the panel's ink. The ratio reads 4:1 (ratioText).
- The label is as high as its text and sits up in the dial's open
  bottom, right under the ends of the scale.
- Label takes a colour of its own.

#383, the wheel in a graph:
- GraphCanvas takes the wheel on the way down and stops it there. It
  zooms over a node too, and no knob or curve in a node sees it.
- The tabs keep their wheel: a knob's step, a band's Q and the zoom
  of the EQ's dB.

Found on the way:
- A Menu opened below a button in the zoomed graph would have been
  placed by the button's own height. openBelow now takes the bottom
  edge as drawn.
- The band row did not lay out its labels after a text change when
  its own size stayed. In the tab a later resize hid it, in the node
  "Band 1" and the count were missing. The row now lays out itself.
- Handle and point positions for the checks were unscaled. They now
  go through toGlobal and hold at the graph's zoom.

Tests:
- Unit parametric-eq: 16 tests. The #375 tests now check
  eqCurvePoints and eqRangeOf directly. compressor-view: ratioText.
- e2e 34 (atx): ALL PASSED, 125 checks (3 new: the EQ panel and its
  ports, editing in the node, the spectrum in the node with the tab
  open). The Compressor node's check reads its numbers as labels and
  8:1. Both node checks now expect the wheel to zoom the graph and
  leave the ratio, the Q and the dB zoom alone.
- The unseen-tab check now leaves for the Scene tab, since the node
  in the graph listens to the same tap.
- Counter-check: the tab letting go of the tap every unseen frame
  stalls the node's spectrum with the tab open (1 frame against 22,
  probe on stein2).
- Gate (atx): CHECK EXIT 0, 283 files, 4357 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:40:11 +02:00
adminandClaude Opus 5.5 1f2b64fb4c Show a Compressor's panel in its node, its tab on a double-click (#380)
game-maker / gate (push) Successful in 4m2s
game-maker / suite (push) Successful in 16m14s
The panel of #379 opened in a window of its own and replaced the tab.
The user wanted the device in the graph instead, and the tab of #378
back on a double-click. The switch Hardware look goes again.

- A node can show a face instead of its rows (`NodeFace`). Its inputs
  sit on the left of it and its outputs on the right, in the middle of
  the height. Each side is as wide as its longest port name.
- A Compressor node shows the panel at full size, 820x280 graph units.
  Its settings and numbers are not listed as rows, the panel has them.
- The knobs and the lever work in the node. A drag, a notch of the
  wheel and a double-click are one undo step each. A press on them
  neither picks nor moves the node, and the wheel does not zoom the
  graph. The number below a knob is only shown, typing stays in the
  tab and the inspector.
- The meter and the LEDs run while the node is in the picture of a sound
  FX or instrument graph that is seen. Out of the picture, folded or
  hidden, the node lets go of its taps.
- The window of one size, `fixedSize`, `refit`, `keptSize` and the rule
  that kept a Compressor out of the tab row are gone again.
- A knob's drag as one undo step lives in `nodeParamDrag.ts` now, for
  the tab and the node. The numbers of a node and what auto-gain says
  moved to `compressorView.ts`.

Found and fixed on the way:
- The tab and the node listen to the same taps. The tab let go of them
  on every frame it was not seen, so the node's meter stood still while
  the tab was open. Now the tab lets go once, and in a frame everything
  unseen lets go before anything seen listens.

Tests:
- Unit: compressor-view (2 new), documents (the test of `keptSize` is
  gone with it)
- e2e 34 (stein2, melektaus-atx was off): ALL PASSED, 122 checks. The
  four checks of #379 are now four of the node: ports beside the panel,
  knobs and lever in the node, the needle when played (21.7 dB with the
  tab open), and the double-click opening the tab.
- Gate (stein2): CHECK EXIT 0, 283 files, 4356 tests
- Counter-checks: an old auto-gain number read as off turns exactly its
  unit test red. A tab that lets go on every frame leaves the node's
  needle at 0 dB and no LED lit (probe on stein2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:41:28 +02:00
adminandClaude Opus 5.5 9917253751 Open a Compressor as a device in a window of one size (#379)
The tab from #378 shows a Compressor as an editor. The user wanted it
to look like a device as well, after a brushed aluminium plug-in, with
a meter of the gain reduction instead of the pictures. The tab stays
and is switched to per node.

- Hardware look is a switch under Settings, on by default for new and
  existing Compressors. Only the editor reads it.
- The panel is 820x280 px and opens straight in a window of that size.
  The window has no sizing corner and no Dock. A pane says so through
  `fixedSize`, and `keptSize` keeps the window's middle when it
  changes to that size.
- A Compressor in this look never sits in the tab row. Opening one
  floats it, and switching the look on moves its tab into the window.
- The panel has chrome knobs (threshold and makeup large), a meter
  with a needle that moves like a VU's, an LCD with the name and the
  reading, a lever with a lamp for auto-gain, and LEDs for the level
  going out. Everything is drawn in code.
- The Knob takes a size and a chrome look, and leaves out its caption
  when it has no name, so the panel engraves its own.
- `autoGainOn` is now `switchOn`, because the new switch reads the
  same way.
- Where parts sit, the meter's scale, the needle's speed, the LEDs and
  the plate's grain are in `hardwareView.ts`, apart from the drawing.

Decided myself:
- The meter's scale runs by the square root, so the first dB is about
  as wide as the last nine. It is red from 10 dB on.
- The LEDs light one every 3 dB from -39 to 0 dB and fall by 20 dB a
  second.
- Switching the look changes the graph's fingerprint, like any other
  setting of a node. The sound FX is built again once.

Findings, fixed on the way:
- Pixi's `arc` goes on from the last point of the path. The meter's
  scale had a line from the corner of the face until each arc got a
  `moveTo` to its own start.
- A text put into a Graphics makes Pixi warn that only Containers take
  children. The LCD is a Container now.
- The first browser check read the needle at one moment and missed the
  short sound. The panel keeps the most its needle and LEDs showed.

Tests:
- Unit: hardware-view (10 new), documents (1 new)
- e2e 34 (melektaus-atx): ALL PASSED, 122 checks (4 new). Played,
  the needle reached 20.9 dB and 5 LEDs lit.
- Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4355 tests (11 new)
- Counter-checks: a window that keeps its corner instead of its
  middle, and a linear meter scale, each turn exactly their test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:06:56 +02:00
adminandClaude Opus 5.5 2d4909d248 Fit the Compressor's tab to any size, make auto-gain a switch (#378)
Two things came up on the new tab. Floated and sized down, it spilled
its knobs out of its window, and sized up it left the window empty.
Auto-gain lifted far too much: it brought 0 dB back to 0 dB, which at
-40 dB, 4:1 and knee 6 is +28.1 dB, so a sound at -24 dB came out at
-6 dB. And as a knob from 0 to 1 it blended between makeup and
auto-gain, so at 1 the makeup knob did nothing.

Layout:

- The knobs stay on the bottom line, centred, with the help line
  above them.
- The levels fill the height left above. The curve is a square at
  their right, centred up and down. It grows with the tab from 180 to
  360 px and takes no more than 40 % of its width (picturesLayout).
- A floated window cannot be sized below what its pane needs. A pane
  says so in its minWidth and minHeight, and the Compressor's is every
  knob and the smallest pictures. Other panes keep 320 x 200.
- A pane put into an open window that is too small grows the window.

Auto-gain, as decided in #378:

- A Compressor's auto-gain gives back what it takes from a sound at
  -18 dB, so that one comes out as loud as it went in. At the numbers
  above that is +14.6 dB.
- Auto-gain is a switch on the Compressor, the Limiter and the
  Saturation alike, a setting and no longer an input. The makeup or
  output is added on top of it instead of being blended with it.
- A project from before reads a number as on above 0. Nodes never
  set keep their defaults: off on the Compressor, on on the other two.
- The tab shows the switch at the right of the knobs, with the dB it
  gives below it. With it on, the curve has a line at -18 dB from the
  curve up to the diagonal.
- Every level reading keeps the gain of its time. Before, the whole
  picture was read by the current makeup, so after a change the
  tinting of the last seconds was wrong or missing.

Findings:

- My first e2e run of the layout clicked Dock where it was before the
  window was sized up, so the window stayed open and the next check
  went red. The check reads the button again.

Tests:

- Gate (melektaus-atx): 282 files, 4344 tests (6 new)
- e2e 34 (melektaus-atx): ALL PASSED, 118 checks (2 new)
- Offline in Chrome: a sine at -18 dB through -24 dB and 4:1 came out
  at -17.92 dB with auto-gain on and -22.42 dB with it off.
- Counter-checks: the curve set at the top instead of the middle, and
  every reading read at a gain of 0, each fail exactly their test.
- Looked at the window in screenshots: at its least every knob and
  the switch show. At 1280 x 797 the levels fill the height, the
  curve is centred at the right and the knobs are at the bottom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:57:26 +02:00
adminandClaude Opus 5.5 1df38bd430 Edit a Compressor in a tab of its own, with knobs and levels (#378)
A Compressor had only its number rows in the node. Its numbers are
easier to set by feel on a dial, and what it does is easier to judge
when the level going in and the level going out are seen side by side.

- Knob is a new widget in the toolkit: a slider bent into a circle,
  its name above and a number field below. A drag up or down turns
  it, Shift a tenth as far, a notch of the wheel a fiftieth of the
  turn, a double-click sets the default, the number takes a typed
  value. A taper gives the low end more of the turn.
- The Compressor's tab opens on a double-click, one per node, like
  the Parametric EQ's. It has a knob for each of the seven numbers,
  greyed while its input is linked.
- Its curve shows level in to level out from -60 to 0 dB. The point
  where it bends sets the threshold, the end of the curve sets the
  ratio that ends there.
- Its levels show the last 4 s on both sides of the node. Where it
  takes more than 0.5 dB away, the stretch below the level coming in
  is tinted. The makeup is left out of that.
- The Compressor is tapped like the EQ. SpectrumTaps listens to more
  than one node at once and reads the loudest sample of the last
  25 ms on each side.
- The node tabs share one map in the editor (nodeTabs), so a third
  kind needs no new plumbing.

Findings:

- CI task 614 for 23922ee was red on the memory check of the stream:
  the tab's renderer grew by 94 MB with nothing decoded. Play mode
  made garbage of its own and was read before a collection. Both
  readings in play now come after one, and the check compares the
  growth streamed with the growth decoded (3.7 MB against 59.8 MB).
- The ratio handle sat on the right edge of the picture, where a
  widget's hit area ends. The curve is inset by the grab radius.

Tests:

- Gate (melektaus-atx): 282 files, 4338 tests (17 new)
- e2e 34 (melektaus-atx): ALL PASSED, 116 checks (6 new)
- Counter-checks: an inverted ratio search, stop() letting go of
  every node and a drag without its least step each fail exactly
  their test.
- Looked at the tab in a screenshot: a blip at -4.4 dB came out at
  -16.7 dB, the gap tinted, the threshold line at -24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:12 +02:00
adminandClaude Opus 5.5 23922ee4a2 Show the zoom of the music tabs and the EQ as a transition (#377)
game-maker / gate (push) Successful in 3m58s
game-maker / suite (push) Failing after 14m51s
The zoom transition of #309 left out the Pattern, Song and Sound tabs
and the Parametric EQ's curve. There a wheel step or Fit still jumped,
whatever "Zoom (ms)" in the general settings said.

- axisTween wraps ViewTween for a single axis. The music tabs zoom
  their time across and their rows, tracks or height down apart, so
  each keeps one tween per axis and either goes its own way.
- Fit by its button is shown as a transition too, as in the other
  tabs. The fit on opening a document still jumps.
- A middle-button pan first lands a transition under way, and saving
  a document's view lands it on the step it was heading for.
- The EQ's range is saved on the node at once. While the zoom is on
  its way the curve draws a range of its own and writes nothing.
  Its dB lines are those of the new range, sliding to their place,
  so no line shows a crooked number.

Found on the way, not part of #377:
- CI task 611 for 82e96e6 failed the stream check of e2e 34. The
  renderer had grown by 60.1 MB with nothing decoded. The check
  summed the RSS of every Chrome renderer, and the test Chrome runs
  four of them. It now measures the largest one, the tab's own, and
  writes all of them into the check's detail.

Tests:
- view-tween.test.ts: 18 tests, 2 new. They fail when axisTween
  writes the wrong offset.
- e2e 34, 64, 65 on melektaus-atx: ALL PASSED, 202 checks, 4 new.
  At 1000 ms each zoom is between its two steps a frame after the
  wheel and lands on the step a jump reaches.
- Stream check: the tab's renderer grew by 8.6 MB streamed and by
  66.9 MB decoded.
- Gate on melektaus-atx: CHECK EXIT 0, 280 files, 4321 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:28:50 +02:00
adminandClaude Opus 5.5 76f5097a38 Float a document's tab in a window of its own, and dock it (#371)
game-maker / gate (push) Successful in 3m54s
game-maker / suite (push) Successful in 15m17s
The Parametric EQ has a tab of its own since #370, but Play is in the
sound FX's tab. A tab that can float keeps both in view. There is one
editor per kind, so the kind's pane is lent to the window and given
back on Dock, the way fullscreen moves the game view.

- A right-click on a document's tab opens a menu with Float and Close.
  `DocumentTab` passes the press to `TabStrip`'s new `onMenu`.
- `FloatWindow` sits on the overlay. Its title bar moves it, its lower
  right corner sizes it, Dock and the x close it. A press brings it to
  the front.
- `floating.ts` is the pure part: where a window opens, held whole on
  the screen, sized down to 320 x 200. Nothing of it is remembered.
- One window per kind. Another document of that kind opens there and
  closes the one it had. Its tab, if it had one, leaves the row.
- When the row showed the kind, it shows the nearest document whose
  kind is not in a window, else the scene (`neighbourOf`).
- Delete, Copy and Paste go to the window pressed last, and back to
  the tab showing after a press in the editor (`keyTab`).
- Previews tick and redraw for a kind in a window as for the tab
  showing. Fullscreen hides the windows. Loading a project or signing
  out closes them.
- Only documents float, Scene and Game stay in the row.
- Hooks: `floatWindows`, `floatDocument`, `keyTarget`.

Tests:
- Unit documents: 16 green, 5 new. Counter-check: `neighbourOf`
  without the window check turns its test red.
- e2e 44 (melektaus-atx, after steps 1-12 and 37): ALL PASSED,
  505 checks in the chain, 6 new for float, move, size, another
  texture, keys, Dock and x.
- Gate (melektaus-atx): CHECK EXIT 0, 274 files, 4236 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:27:12 +02:00
adminandClaude Opus 5.5 0e73fd85b9 Route sounds, songs and buses in one Master routing graph (#351)
game-maker / gate (push) Successful in 4m20s
game-maker / suite (push) Failing after 15m17s
A project bus could only go into one other bus, and a sound, an
instrument, a pattern or a song into exactly one bus. A shared reverb
needs a sound to go there in part and on dry at the same time. Instead
of a list of sends on every asset, the project gets one routing graph
like the routing of a song (#350), one level up.

- Menu -> Master routing opens a tab with one graph per project. It has
  a node per sound, instrument, pattern, song and bus, named after its
  asset, and one Master. Embedded assets have none, their song routes
  them.
- The nodes follow the assets. Every edit settles the routing in the
  same undo step (KeepRoutingCommand), so an undone delete brings the
  links of its asset back. A new asset gets a node and a link to the
  master. What went only into a deleted bus goes to the master.
- New nodes start in three columns: what plays, the buses, and the
  master a row above, so no link into it runs behind a bus.
- Links are sends: -60 to +12 dB, and a Mode on links out of a bus or a
  song. Out of a sound, an instrument or a pattern a link always takes
  the sound after its volume and FX. Loops are refused.
- A node without a link is silent and says "Goes nowhere".
- The rows Bus and Into are gone. Play nodes with a picked bus still
  overrule the routing.
- A click on a node shows its asset in the inspector, the Master shows
  the master volume and sound FX. A double click opens the asset's tab.
- The graph lives in project.routing and is held to the assets when a
  project is read, but nothing is added there. The editor adds the
  nodes when it opens a project. An asset without a node plays on the
  master, so old projects sound as before without a migration.
- In the mix an asset with one link at 0 dB goes straight into that bus.
  A sound or instrument with more gets a bus of its own with the sends.
  A pattern or song sends from the master of its playing, which is
  where its Mode taps. Buses of the mix can have sends now too.

Found:

- The check "the scheduler plans a looped bar" in e2e 34 wobbles. It
  failed in 2 of 5 runs here and in CI task 580 at 490fcba, before
  this change. HEAD passed it 2 of 2. Not fixed here.

Tests:

- Unit master-routing (new, 10), sound-library (2 new), buses
  rewritten onto the routing, 5 lists without the Bus row. 5 counter
  checks red.
- e2e 34 (melektaus-atx): ALL PASSED, 93 checks (3 new, 2 rewritten).
  Counter check with the Sound tab ignoring the routing: the routing
  check red.
- Gate (melektaus-atx): CHECK EXIT 0, 273 files, 4225 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:18:46 +02:00
adminandClaude Opus 5.5 d0b76ec355 Fit a song both ways and zoom its time in further (#362)
Fit in the Song tab only set the time, from the very left edge, and
kept the height the tracks had. The time also stopped zooming at the
bound of the Pattern tab, too soon to lay a take down closely.

- fitSong in songView.ts frames the song with 9 % of the lanes free on
  every side. The tracks share out the height in between, held to
  28 to 320 px. A song opened without a kept view fits the same way.
- The time zooms in to 8 px a tick, eight times the bound of the
  Pattern tab. zoomTime takes the bound as a parameter, so the Pattern
  tab keeps its own.

Tests:
- Unit song-edit: 15 green (1 new), the counter-check without the
  margin is red.
- e2e 65: ALL PASSED, 56 checks (1 new). The new check presses Fit
  after the tracks are added and finds room around bar and tracks.
  Adding a track presses Fit first now: a new song opens with its one
  track 320 px tall, so the track added below it lay off the edge.
- Gate: 272 files, 4213 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:33:47 +02:00
adminandClaude Opus 5.5 5cb7ef7bed Show empty asset groups on request, and add by the heading (#361)
game-maker / gate (push) Successful in 3m47s
game-maker / suite (push) Successful in 15m1s
The asset panel only listed the categories and types that hold assets.
To make the first asset of a kind you had to know its category in the
`+` menu. A switch now lists all of them, and a right-click on a
heading offers just what sits under it.

- General settings has "Show empty asset categories and types", off by
  default and kept in this browser. Applying it refreshes the panel at
  once through a new `onGeneralSettings` listener.
- `groupAssets(assets, empty)` gives every category and type when
  asked. The empty ones stand faint with a count of 0, and an empty
  type has no chevron, since there is nothing to unfold.
- `ListItem.faint` draws a row's label, hint and icons at half opacity,
  like a disabled widget.
- A right-click on a category heading offers the types of that
  category without a submenu, on a type heading only that type. The
  empty space below the rows still opens the whole `+` menu.
  `ListView.onListMenu` passes the heading's id for this.
- Every menu of new assets says "Create asset" above its entries, so a
  click on a type reads as making one. `MenuOptions.title` draws that
  muted line. It is not an entry and matches no search.

Tests:
- Unit asset-groups and view-tween: 23 green, 2 new. Counter-test
  with the empty flag ignored: the new groups test fails.
- e2e 04 (melektaus-atx): ALL PASSED, 67 checks, 4 new and 1
  rewritten. Counter-test with the heading id dropped: the 3 menu
  checks fail. The title is checked on the `+` button and on a type
  heading (2 checks extended), the rerun passed with 67 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 272 files, 4212 tests. It ran
  before the menu title. Afterwards only the menu and asset-groups
  units, typecheck and lint ran, all clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 06:51:59 +02:00
adminandClaude Opus 5.5 cf96442835 Keep the Sound tab smooth while it pans (#360)
game-maker / gate (push) Successful in 3m51s
game-maker / suite (push) Successful in 15m55s
Panning the Sound tab drew the whole wave anew on every move, one
filled rectangle per pixel column, and gave the ruler's labels new
texts whenever a tick left the area. At 2560 px across that took the
tab down to 16 fps.

- The wave is drawn half the area's width past either side of it. A
  pan within that moves the drawing. Zoom, height, size or a pan
  past the margin draw it anew.
- The bars of the wave share one fill instead of one fill each.
- A ruler label keeps the second it already shows. Before, a pan
  shifted every label by one and each had to be rendered anew.
- A clip's name is given its style only when the style changes.
- The rule for what a drawing covers is pure in `waveView.ts`
  (`shownSeconds`, `drawnSeconds`, `coversShown`).

Measured with a copy of the user's project, "Take 1" with 4 clips,
240 pan moves with the middle button, headless on stein2:

- 2560x1440: main thread 712 -> 373 ms/s, texts rendered 694 -> 65
- 1280x800: main thread 429 -> 225 ms/s

What is left in the profile is Pixi's render pass and the pointer
events, which hovering pays as well.

Tests:
- Unit wave-view: 12 green (1 new), counter-probe red
- e2e 34 (stein2, melektaus-atx was down): ALL PASSED, 92 checks
  (1 new). Counter-probe with the labels given out by position: red,
  7 labels set anew instead of 1.
- Gate (stein2): 272 files, 4210 tests green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 06:19:05 +02:00
adminandClaude Opus 5.5 490fcba88f Meter each track and the song, and limit the output at -1 dBFS (#359)
game-maker / gate (push) Successful in 3m43s
game-maker / suite (push) Failing after 25m11s
A song could clip at the output without anyone noticing: a track and a
sound item at +12 dB each add up to +24 dB. The Song tab now shows how
loud each track and the whole song are, and a fixed limiter behind the
master keeps every sample under -1 dBFS in the editor and in the game.

- Each track head has a stereo meter up its right edge. The head is
  190 px wide instead of 176.
- Out sits in the corner above the heads, with the loudest peak since
  Play in dB. In the tab header it made the row too wide for a 1280 px
  window, and the inspector lost its fields.
- The meters read AnalyserNodes beside the buses, one per side after a
  channel splitter, so a peak panned hard is not halved. They are tapped
  only while the song plays and its tab is shown.
- A clip lamp lights past 0 dBFS and stays through Stop. The next Play
  or a click on the meter puts it out.
- The meter of a track that mute or solo keeps from being heard is grey.
- Music always plays through a bus of its own, with or without a sound
  FX. That bus is what Out measures.
- The limiter is the one from #326 and needs the FX worklet. Until that
  is loaded the master goes straight out.
- It is 5 ms late. latency() counts that in, so the playhead and the
  alignment of a recorded take follow it.
- Unit tests build SoundLibrary without the limiter, as their wiring
  checks follow every node of a voice.
- Out shows "-inf" while nothing was loud. The UI font has no infinity
  sign, which the font test found.

Tests:
- Unit meter-view 8 new, sound-library 112 (2 new), 3 counter-tests
  each red
- e2e 65 (melektaus-atx): ALL PASSED, 55 checks (3 new). Out wrote
  +16.6 dB, behind the limiter the loudest sample was 0.8913
- Gate (melektaus-atx): 272 files, 4209 tests, one red in the font
  test. Fixed after it, then the font and meter tests ran green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:08:39 +02:00
adminandClaude Opus 5.5 e721bba120 Let the places of a song share what it keeps to itself (#357)
game-maker / gate (push) Successful in 3m54s
game-maker / suite (push) Successful in 15m58s
With clips, one take serves many items: four items on the four clips
of one take. The rule from #355 gave every further place its own copy
of an embedded asset, so a cut made in the Sound tab reached one item
only. Now the song, its tracks and its items share a sound, a pattern
or a sound FX the song keeps.

- settleEmbedded no longer copies within the song. An embedded asset
  stays as long as one place names it, and goes with the last one.
- A paste from another song still gets a copy, but one copy for all
  the places naming the same asset, with its clip ids mapped.
- The picks in a song offer what the song keeps, under a line after
  the project's assets: the Sound row of an item the song's sounds
  and their clips, the Pattern row its patterns, every sound FX pick
  its sound FX.
- Make project asset and Embed a copy are unchanged.

The project cbbf160bb7e8 was merged by hand afterwards: the items of
Song 2 on three copies of Take 1 now play the same clips of the first
one, and the copies are gone.

Tests:
- Unit embedded, sound-clips, own-fx and the song tests: 66 green,
  3 counter-checks red as expected
- e2e 65 on melektaus-atx: ALL PASSED, 52 checks
- Gate on melektaus-atx: CHECK EXIT 0, 271 files, 4198 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:13:22 +02:00
adminandClaude Opus 5.5 852dce5142 Shrink a clip with the wave and drop it with a click beside (#357)
Zoomed out, the wave left room above and below it, but the bright file,
the picked clip and its handles still took the whole height. And a click
in the wave beside the picked clip kept it picked, so only the strip
could take it back to the sound.

- fileReach in waveView.ts is how far up and down the file reaches:
  full scale and its inset, never past the area. At level 1 it is the
  whole height, zoomed in too.
- The bright file, the shade outside the clip and the two handles are
  drawn within that reach.
- A click over the wave beside the picked clip picks the sound. Inside
  the clip or on a handle nothing changes, and a drag still spans the
  clip anew.

Tests:
- Unit wave-view: 11 green (1 new), a counter-check red
- e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 new)
- Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4195 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:13:09 +02:00
adminandClaude Opus 5.5 c7fa1d15b0 Zoom the height of a sound's wave over its level scale (#357)
The Sound tab always drew the wave at the full height of the tab. Quiet
stretches were hard to see, and there was no room above a loud wave.

- A level scale in dB stands left of the wave, a mark every 6 dB down
  from 0, as long as a mark lies inside the area and at least 14 px
  from the middle.
- The wheel over the scale zooms the height, from 0.25x to 16x. Over
  the wave it zooms the time as before.
- What is louder than the area stops at its edge. The fade ramps grow
  with the wave.
- Fit puts the height back to 1x. The tab keeps the height with its
  view when another tab is shown.
- The rules live in waveView.ts (zoomLevel, levelMarks), the hooks
  give the height, the scale and the top of a full-scale wave.

Tests:
- Unit wave-view: 10 green (2 new), 2 counter-checks each red
- e2e 34 (melektaus-atx): ALL PASSED, 87 checks (1 new)
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4183 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:21:02 +02:00
adminandClaude Opus 5.5 668682c4ff Give a song, a track or an item a sound FX of its own (#355)
A sound FX for one place in a song had to be a project asset, which
then stood in every picker. Now every pick of a sound FX in a song
can make one the song keeps for that place alone, on top of the
embedded assets of the first two parts.

- Create sound FX sits at the end of the Sound FX row of the song,
  a track and an item, and of the menu of a track's FX button. It
  makes "Sound FX 1", 2, … per song with the Input wired to the
  Output, picks it and opens it. That is one undo step.
- Embed a copy, while the place names a project FX, gives the place
  a copy under the same name.
- While the place names its own, the menu shows it with the mark and
  offers Edit sound FX and Make project asset.
- Picking anything else lets it go in the same undo step. For a
  track or an item `settleEmbedded` did that already. For the song's
  own row `AssetPropertyCommand` now settles the song after `fx`.
- The mark `•` moved to the core. `assetRefLabel` puts it in front of
  an embedded asset, so the inspector rows show it like the timeline.
- `FieldHandlers.moreChoices` lets a caller end a reference menu
  with entries of its own. `embeddedPatternName` is `embeddedName`
  with a base now.

Tests:
- Unit own-fx (new): 6 green, 6 counter-checks each red
- Unit embedded and 80 neighbouring files: 1434 green
- e2e 65 (melektaus-atx): ALL PASSED, 51 checks (2 new)
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4178 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:32:41 +02:00
adminandClaude Opus 5.5 daeffe0f9d Route the tracks of a song through a graph (#350)
game-maker / gate (push) Successful in 3m47s
game-maker / suite (push) Failing after 14m20s
Every track of a song went straight into its output. Now the Song tab
has a second view, Routing: a graph with a node per track and one
Output, the links between them sends. A track can go into other
tracks, into several at once, and a link has its own volume and the
point of the track it taps.

- The song asset has a graph like an instrument (SongTrack, SongOutput).
  heldSongGraph keeps it in step with the tracks on load and in
  SetSongCommand: a node per track named after it, one output, a new
  track and every track of an old song linked to the output. Links of
  gone tracks, doubled links and links closing a loop are dropped.
- A link carries an optional send (volume in dB, tap). The inspector
  shows Volume and Mode for a picked link of a song: Post volume (Post
  pan), Pre volume (Post FX), Pre volume (Pre FX). SetSendCommand.
- NodeDef has two new flags. `fixed` nodes are never added or deleted
  by hand. Links between `acyclic` nodes may not close a loop, checked
  in canConnect, dropTargetPort and ConnectCommand.
- songBuses gives each track its sends instead of one `into`, and a
  bus `song:<song>:<track>:#items` for its own items. That bus is what
  solo closes on a track that only passes a soloed one on.
- Mute and solo now act on the sends (trackGates). A muted track passes
  nothing on, on any tap. A soloed track is heard with what goes into
  it, through the tracks it goes through, their own items silent.
  Only sends on a way through a soloed track stay open, so a track
  going into it and into the output is heard through it only.
- SoundLibrary: a bus has either `into` or sends. Each send is tap,
  delay, level, target. busLatency takes the latest way (arrivalOf),
  and each send is held back so all ways of a track arrive together
  (sendDelays). Taps are wired again whenever a chain comes or goes.
- The Song tab has Timeline and Routing in its header. The routing
  uses GraphCanvas with an empty palette. A track linked nowhere shows
  "Goes nowhere". Delete takes links, never a track node.
- Game playings route the same way, the output being the master of
  the playing.

Decided without asking: a send ranges -60 to +12 dB like a track,
default 0 dB Post. Mute closes the sends, the track's own level stays.

Found on the way: the inspector built the Mode row with the send as it
was when the rows were made. Setting Volume and then Mode wrote the old
volume back. Both rows now read the current send when they change.

Tests:
- Unit song-routing (new): 13 green. Sound library: 4 new, 94 green.
  11 counter-probes (loop on load, canConnect, solo, solo past the
  soloed track, mute, preFx latency, graph sync, delete guard, delays,
  taps, glide), each red.
- Existing tests adjusted to the new bus names and send shape: song,
  bus-fx, song-play, graph registry, edge description.
- e2e 65 on melektaus-atx: ALL PASSED, 43 checks (5 new). The first
  run caught the inspector bug. Counter-probe with the old closure:
  2 FAILED.
- Gate on melektaus-atx: typecheck, lint, format clean, 2 tests red
  (registry rule, edge shape), fixed and rerun locally: 56 green.
- Not rerun after the per-send solo fix: e2e 65 and the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:18:09 +02:00
adminandClaude Opus 5.5 3a52f2290f Mix sounds through buses and a master (#325)
game-maker / gate (push) Successful in 3m36s
game-maker / suite (push) Failing after 14m30s
Until now every sound went straight to the speakers. There was no
point where all of them came together, so nothing could turn the
music down against the effects, and a compressor or a shared echo
over everything had nowhere to sit. The bus technique of #346 was
there already, only the end of the chain was missing.

- Bus asset (Audio): Volume, Sound FX and Into. Into is empty for
  the master and never offers the bus itself nor a bus that already
  goes into it. The list is worked out when the menu opens, so a
  change made meanwhile counts. A loop that is in a project anyway
  sends the buses on it into the master.
- The master is no asset. Its volume and sound FX are two new rows
  under Master in Project settings, one undo step each. A project
  written before has 1 and none, which is how it sounded.
- Sound, instrument, pattern and song have a row Bus, empty for the
  master. Play sound, Play note, Play pattern and Play song have an
  input bus: From the asset, Master or a bus. Inside a song or a
  pattern only the bus of the song or pattern counts.
- Set bus volume (bus, volume, time, easing) and Bus volume. An
  empty bus is the master. The volume set holds for the whole run,
  across scenes, over that of the asset. GameRun resets it at start
  and end. Set sound FX parameters without a voice now reaches the
  sound FX of the bus picked in its new bus input.
- SoundLibrary: the master is the bus "mix", each bus "mix:<id>".
  Every bus, the playing masters of #346 included, ends there, and
  only the master is heard. setMix sets them up once a context
  exists, so an editor nobody listened to opens no audio device. The
  same mix again changes nothing, which lets the editor hand it over
  on every change of the project.
- The tabs play as the game does: Sound, Sound FX, Instrument,
  Pattern and Song tab through the bus of their asset and the master.
- Pause game does nothing to the buses (the user's answer 4b). A
  game that wants quiet in its pause menu sets a bus volume itself.
- Streaming long music files is #349, split off as asked.

Finding on the way: the Into menu first worked from a list made when
the inspector row was built. After another bus was sent into the
open one, e2e 34 saw it still offered. It is now asked on open.

The SoundLibrary tests expected voices to end at the destination.
They now end in the master, which adds its input, level and pan to
every wiring and two gains to every list. Those expectations are
updated, nothing else in them changed.

Tests:
- Unit buses (new): 15 green. Ten counter-checks, each red on its
  own: loop check, bus picked, reset at run start, bus of Set sound
  FX parameters, master volume in the settings, and in SoundLibrary
  default into the master, voices into the master, run volume over
  the mix, no context before a play, a playing on its bus.
- Unit sound-library: 90 green (5 new). Node catalog: 416 green
  (9 new cases).
- e2e 34 (melektaus-atx): ALL PASSED, 75 checks (3 new). The voice
  of the Sound tab was heard on mix:<bus>, that bus into the other,
  that one at 0.5 into the master. Master volume 0.6 via the dialog,
  undo back to 1.
- Gate (melektaus-atx): CHECK EXIT 0, 262 files, 4072 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:13:32 +02:00
adminandClaude Opus 5.5 a848cf8469 Pick a new asset from category submenus in the + menu
game-maker / gate (push) Successful in 3m48s
game-maker / suite (push) Successful in 15m0s
The + menu was one long list in two groups, placeable types and
library ones. That split is no longer wanted. The menu now lists the
categories of the asset panel, and each opens its types beside it, so
a new asset is filed where the panel shows it.

- Menu learns submenus: an entry with submenu instead of onSelect
  opens a second Menu right of the first, or left of it where the
  canvas ends. Its first entry lines up with the row that opened it.
- A submenu opens on hover or click. Resting on another entry swaps
  or closes it after 150 ms, so a pointer cutting across the rows on
  its way into a submenu does not lose it.
- The row of an open submenu stays lit, and a triangle pointing right
  marks every such row.
- A submenu has no backdrop and no Escape of its own. Popup takes a
  backdrop option for that. The first menu's backdrop and Escape
  close the whole stack, and picking an entry does the same.
- assetMenuCategories replaces assetMenuGroups and takes categories
  and their order from ASSET_GROUPS. The types are sorted by label.
- New test hooks __gm.menuSubmenus() and __gm.addMenuCategory().
  pickAssetType opens the category first, so the modules that pick a
  type through it need no change of their own. Modules 04, 10, 47,
  48, 53 and 54 open the category themselves where they read the menu.

Tests:
- Unit asset-menu: 4 green
- e2e 04: ALL PASSED, 63 checks. New: categories in panel order, each
  type once and sorted, submenu placement, hover swaps the submenu,
  picking closes menu and submenu
- Gate: CHECK EXIT 0, 261 files, 4040 tests
- The other modules that use the + menu did not run here

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:17:21 +02:00
adminandClaude Opus 5.5 5e09ba9a50 Group the asset panel by category, then by type
The asset panel listed one heading per type, 30 of them side by side.
Now the types sit under five categories, and each type heading reads
like its entry in the + menu.

- ASSET_GROUPS replaces ASSET_GROUP_LABELS and gives every type a name
  and a category: Basics, Visuals, Logics, Physics or Audio.
- The categories keep that fixed order. The groups in a category and
  the assets in a group stay sorted by name.
- A category heading folds like a group heading. A project opens with
  every category open and every group folded, as before.
- Selecting an asset unfolds its category and its group. Folding the
  category of the selected asset lets go of the selection.
- The headings are singular now (Scene, Sprite, Sound FX). The old
  plural names only kept them apart from the + menu entries.
- locate looks in the popup layer first. A click by text meant for a
  menu entry hit the heading of the same name, since the panel is drawn
  first. Below an open menu a click only closes the menu anyway.
- New test hook __gm.assetCategories().

Tests:
- Unit asset-groups: 6 green, counter-check (empty categories kept)
  1 red
- e2e 04: ALL PASSED, 63 checks (1 new). Counter-check with locate
  taking the first hit again: 2 red, the click on Behavior created
  no asset
- Gate: CHECK EXIT 0, 261 files, 4040 tests
- The category names got their final plural form after these runs.
  The unit test ran again on them (6 green), e2e 04 did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:59:08 +02:00
adminandClaude Opus 5.5 8154569405 Fix the two checks the full run 304 found red
game-maker / gate (push) Successful in 3m42s
game-maker / suite (push) Successful in 14m23s
The full CI run on 4c7861b had two red checks. Neither showed when
only the touched modules ran, because each needs a module before it.

- e2e 40c-iii (#347): a stroke on a tilemap drew the captions
  "Comment" and "Held" again. They are a note and a group that e2e 36
  leaves in a graph. AnnotationView gave its caption a new TextStyle
  on every redraw, and Pixi draws a text again for a new style. The
  graph redraws on every edit of the project. The caption now keeps
  its style while colour and width stay the same.
- e2e 49a: a plain user deleting a shared file got 404 instead of
  403. Since #344 the DELETE route asked whether the file exists
  before whose it is. It now refuses a stranger first, so another
  folder tells them nothing. The wiki's API page says so.

Tests:
- Unit annotation-view (new): 2 green, counter-check 1 red
- e2e 36,39,40,49 (melektaus-atx): both checks green, 49 then hit a
  harness timeout (CDP Network.enable) once
- e2e 49 alone (melektaus-atx): ALL PASSED - 22 checks
- Gate (melektaus-atx): CHECK EXIT 0, 261 files, 4040 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:26:35 +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 d505f6ed27 Show what the song plays around a pattern as ghost notes (#339)
A bass line is written against the chords it goes with. Until now the
piano roll showed only the pattern itself, so writing a part to fit the
rest of a song meant switching between the Pattern and the Song tab.

- A double-click on a pattern item remembers song, item and the round
  clicked into for that pattern. The place lives in the session, not in
  the project, and goes by the item's id, so moving the item moves the
  ghost notes along.
- Opened from the Assets panel, the pattern shows the item of the last
  double-click. Without one, a pattern that lies in exactly one item of
  all the songs shows that item's first round.
- Ghost notes are the pattern items of the other tracks all along the
  song, and on the own track the items before and after and the other
  rounds of the item. The round edited itself does not count. Sound
  items have no notes.
- A ghost note sits on the row it sounds with: its own transpose, less
  the transpose of the item the pattern was opened from.
- The roll draws them grey, without an edge, under the notes. Resting
  on one names its track and its pattern.
- The header has a Ghosts switch, on by default, and names where they
  come from, e.g. "Tune · Bass · bar 5".
- The time axis pans back to the start of the song, with bar and grid
  lines before the pattern. Fit still frames only the pattern.
- A click or a paste outside the pattern no longer lands. Before, only
  the time left of bar 1 was closed, notes could still be drawn past
  the end, where they are kept but never played.
- The rounds of a pattern item are now worked out in one place,
  itemNotes() in song.ts, which songSequence() and the ghost notes
  share.

Tests:
- Unit ghosts (new): 5 green. Counter-check with the round edited
  not left out and the own transpose not taken off: 2 red.
- Unit piano-roll: 1 new (pan back to the song, lines before bar 1).
  Counter-check without the song's start: red.
- e2e 65 (melektaus-atx): ALL PASSED - 38 checks (3 new). The first
  run was red on the tooltip check: the ghost note it rested on lay a
  fifth up, outside the rows Fit frames. The test now uses a tone.
- e2e 64 (melektaus-atx): ALL PASSED - 30 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4027 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:06:32 +02:00
adminandClaude Opus 5.5 c33d6d98e5 Keep a song item's left edge from snapping on to its own end
Dragging the left edge of a song item to the right snapped to the grid
and to the points where the item's content starts again. One of those
points lies exactly on the item's end. When the edge caught it, the
item shrank to a single tick and showed as a thin white line. The
right edge already refused to snap back on to its own start, the left
edge had no such rule.

- trimDelta: a snapped start on or past the item's end now takes the
  last grid line before the end instead.
- If that grid line lies before the current start, the start stays
  where it is.
- Free trimming with Alt or Shift is unchanged and may still shrink
  the item to one tick.
- Dragging the left edge far past the end now leaves one grid step,
  as the right edge already did, instead of one tick.

Tests:
- song-edit unit tests: 14 green (2 new expectations, 1 changed)
- Counter-check with the rule removed: 2 red (1399 instead of 960,
  1919 instead of 1800)
- Prettier, ESLint, tsc clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:00:58 +02:00
adminandClaude Opus 5.5 b519cd192f Send a track, a pattern item or a whole song through a sound FX (#346)
A sound FX could only be put on a single sound. Now a track, a pattern
item, a Play pattern and a Play song send everything they play through
one chain of it, as an insert in a DAW. A Play note gets one of its own.

- Track: an FX box in the head next to M and S opens a menu of the
  project's sound FX, and the track's inspector has the row too.
  Order: items, track FX, volume, pan.
- Pattern item: Sound FX in its inspector. Its notes play on a bus of
  their own into the track's bus.
- Play pattern and Play song take fx and fx params. The chain sits on
  the master of the playing, after the tracks and before the level and
  the fades. Set sound FX parameters reaches it by the playback.
- Play note takes fx and fx params next to the instrument's params.
  The FX parameters go by "fx:" so the two sets stay apart. Set sound
  FX parameters on a note changes a name both have in both.
- A Pitch on a bus is a new PitchShifter: the phase vocoder of the
  PitchPlayer, fed while it plays. It cannot look ahead and is exactly
  one frame late at every pitch, 2048 samples.
- That latency is made up. The other way of a Mix is held back by a
  delay of the same length. Every note and clip starts as much earlier
  as the buses it passes are late. The scheduler plans that much
  further ahead, and the first note waits that much after Play.
- The Song tab waits for the chains before it plays and lets its buses
  go a moment after Stop. A playing in a game lets them go after its
  echoes and what the shifters still hold.
- songBuses moved to its own file. Kept in song.ts it pulled the FX
  compiler into the node registry through an import cycle.

Decisions of the user: one chain for the whole (1a), FX everywhere
asked for (2a), Set sound FX parameters on playbacks (3a), and a
pitch shifter of its own with exact latency compensation for a Pitch
on a bus.

Tests:
- Unit pitch-shifter (new): 11 green. At 0 semitones the output is
  the input 2048 samples later, off by at most 1.2e-7.
- Unit sound-library: 84 green, 8 new. Counter-checks (no early
  start, no Mix delay, chain made every time, tune misses the master)
  each turned their test red.
- Unit bus-fx (new): 6 green, 4 counter-checks red. Scheduler ahead: 1
  new, its counter-check red.
- e2e 65 (melektaus-atx): ALL PASSED, 35 checks, 4 new, first run.
  Chrome at 44.1 kHz: the bus was 0.046440 s late, the note started
  exactly that much early. Offline the shifter moved 1 kHz to 2000 Hz
  and passed a click 2048 samples later.
- Gate (melektaus-atx): CHECK EXIT 0, 259 files, 4021 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:15:52 +02:00
adminandClaude Opus 5.5 af9b202100 Keep the game's music when Play ends a run in the editor
game-maker / gate (push) Successful in 3m39s
game-maker / suite (push) Failing after 19m26s
A behavior with "Run in editor" kept its world until the frame after
Play. By then the game's On start had already started its pattern or
song, and ending the editor run called stopAll() on the sound library.
The game came up silent. This is what turned the game blocks of e2e
64 and 65 red in suites 542 and 550.

- EditorRun.modeChanged() ends the run the moment the mode leaves
  edit. enterPlay() announces the mode before the game is built, so
  the editor run's sounds stop first and the game's start after.
- The Editor wires the mode event to it instead of invalidate().

Tests:
- e2e 64 starts its game with an idle "Run in editor" behavior in
  the scene. Without the fix it fails like CI did, "game": [] and
  2 FAILED. With it: ALL PASSED, 30 checks (stein2).
- Unit editor-run: 21 green, 1 new.
- Typecheck, ESLint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:52:24 +02:00
adminandClaude Opus 5.5 f1f4a57589 Cut a sound to a stretch of its file in its own Sound tab (#343)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 18m20s
A sound asset now opens in a tab that shows its file as a waveform.
Two handles set where the sound starts and ends in the file, and that
stretch is the sample: Play sound, a song item and an instrument's
Sample all play it and nothing else. The file itself is never changed.

- The asset has three new rows: Start (s), End (s) and Edge fade (ms).
  End 0 is the end of the file, the fade is 10 ms by default. Older
  assets have no values and play the whole file as before.
- Every way of playing asks playedSoundOf() for a name instead of the
  file: the file, or <file>#<start>~<end>~<fade> for a cut.
- SoundLibrary makes the cut from the decoded file the first time that
  name plays and ramps each edge inside the file over the fade. Length
  and peaks of a cut are read from the file without a copy.
- Cut buffers can be made again at any time, so the library lets the
  oldest go past 64 Mi samples. A voice holds its buffers itself.
- The tab draws min/max per pixel from 256-sample block peaks, and the
  samples as a line when zoomed in far. Wheel zooms, the middle button
  pans, as in the music tabs. Wave, cut and ruler sit under a mask,
  so a handle panned out of view does not draw over the panels beside.
- Dragging a handle moves that edge, dragging elsewhere spans a new
  stretch. One drag is one undo step for both edges. Whole file puts
  the handles back, Play or Space plays the stretch with a playhead.
- The inspector preview and the Sound FX preview play the cut too, and
  the song lanes draw and repeat it.

Tests:
- Unit: sounds 20, wave-view 8 (new), sound-library 68, plus 258 in
  the touched areas (instrument, song, pattern, inspector, tabs)
- e2e 34 on melektaus-atx: ALL PASSED, 57 checks (7 new)
- Gate on melektaus-atx: CHECK EXIT 0, 253 files, 3936 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:42:21 +02:00
adminandClaude Opus 5.5 5b96d0b329 Paste under the pointer and pull song items out left in music tabs
game-maker / gate (push) Successful in 3m25s
game-maker / suite (push) Successful in 16m33s
A round of fixes to the Pattern and Song tabs: where a paste lands,
what Shift does on a drag, how far the rows zoom, and how an item of
a song is sized at its ends.

- Shift does what Alt does on a drag: off the grid, on the tick. A
  Shift click on a note or an item still adds or unpicks it. A Shift
  press only decides on letting go: no drag toggles, a drag moves.
- Ctrl+V lands under the pointer. The earliest copy goes on the grid
  line before it, the highest note on its row or the topmost item on
  its track, held to the keyboard or the tracks there are. Off the
  grid or left of bar 1 it lands at the start of playing as before.
- The rows of the piano roll zoom to 64 px instead of 28, the tracks
  of a song to 320 px instead of 160.
- Fit in the Pattern tab zooms the rows as well, so the notes fill
  the height, no longer held to the default of 14 px.
- An item of a song can be pulled out by its left end past the start
  of what it plays, which then repeats. The offset goes round to the
  end of a round. A sound not loaded yet still stops at its start.
- Both ends of an item snap to the start of a round of what it plays
  as well as to the grid, whichever is nearer. The end of a round
  between two ticks is cut to the tick before. A start put within
  half a tick of a round goes right on it, so it plays from the very
  beginning.
- An offset past a whole round is not folded back to 0, so a pattern
  that grows later still plays from where the item said.
- A middle-drag pans past the first and the last row, as it pans
  left of bar 1: the highest note or the first track up to half the
  height down, the other end up to half the height up. The room past
  them is dark in the piano roll and empty in the song.

Tests:
- Unit: song-edit 14, piano-roll 19, song 14 tests. New cases for
  the paste spot, pulling out left, the round snap and Fit.
- e2e 64+65 on melektaus-atx: ALL PASSED - 54 checks (+4). New:
  Shift drag and Shift click, Ctrl+V under the pointer in both tabs,
  Fit zooming the rows, pulling a pattern out left and snapping a
  sound to the end of its second round. e2e 64 zooms the rows out
  again after Fit, so the keys it plays later are on screen.
- Gate on melektaus-atx: 251 files, 3912 tests, CHECK EXIT 0.
- Then the pan down the rows: e2e 64 on melektaus-atx ALL PASSED -
  30 checks. Its pan check now also pans the top row down, half the
  grid's height, and a click above it draws nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:14:28 +02:00
adminandClaude Opus 5.5 a3101c590c Size notes by either end and pan before bar one in music tabs
A round of small fixes to the Pattern and Song tabs: what the ruler
and a pan do in both, and a few things the tabs carried that were in
the way.

- A middle-drag can take bar 1 up to half the grid or lanes in. The
  time before it is drawn dark on the ruler, the grid and the lanes.
  Nothing lands there: a click draws no note, a drop shows no frame.
- A press or drag on the ruler puts the start on the nearest grid
  line, no longer on the one before the pointer.
- A new song has one track instead of four. The "+ Track" row under
  the heads is gone, a head's menu adds tracks above or below. A song
  still keeps at least one, so that menu is always there.
- A note can be sized by its left end too: the start goes to the
  nearest grid line and the end stays put, never before tick 0.
- An end of a note under the pointer lights up and shows a resize
  cursor, as on an item of the Song tab.

Tests:
- Unit: piano-roll, song-edit, song, song-play 52 tests. Two new
  cases for the left end and the pan, the song fixtures add their
  tracks themselves now.
- e2e 64+65 on melektaus-atx: ALL PASSED - 50 checks (+2).
  e2e 65 adds its tracks through the head's menu and clicks the
  ruler three quarters into a bar, which only the nearest line
  passes.
- Gate on melektaus-atx: 251 files, 3909 tests, CHECK EXIT 0.
- A probe shot of the piano roll panned in: the time before bar 1
  dark, the left end of a note lit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:40:13 +02:00
adminandClaude Opus 5.5 aa097bd076 Zoom with the wheel and set the loop on its own strip in music tabs
The ruler of the Pattern and Song tabs did two things with one drag,
and the wheel did three things with Alt and Shift. Both now do one
thing each, and the Song tab gets a few edits it was missing.

- The ruler has a loop strip along its top. A drag there sets the
  loop, a press or drag below it moves only where playing starts.
- The wheel only zooms: the time over the grid, lanes and ruler, the
  rows over the keys, the tracks over their heads. Alt and Shift no
  longer change it. A middle-drag still pans.
- Tracks can be zoomed from 28 to 160 px. Below 46 px a head shows
  only its name, M and S. An old saved view gets the old height.
- A click on nothing in the lanes picks the song itself.
- A right-click on an item opens a menu with Delete instead of
  deleting it. The inspector has no delete buttons any more.
- A sound item goes up to +12 dB, a track up to +12 dB. A pattern
  item stays at 0 dB, since a note is held to unity.
- An end of an item under the pointer lights up and shows a resize
  cursor. A long item marks every point where it starts over.
- The Pattern tab has an Audition switch in its header, off at first.
  Only with it on does a note sound when drawn, grabbed or moved.
  The keys down the left sound either way.
- A looped sound item draws every round alike. Each round is drawn
  from its own start, and each column shows the loudest part of the
  sound it covers instead of one sampled bucket.
- A song is at least one bar long instead of four. An empty song
  fits that bar across, and Play song ends after the last item.
- The ghost notes held back in #334 got their own issue, #339.

Finding: suite 542 for f97961c failed 5 game checks in e2e 64 and 65
while my runs on the same Windows machine overlapped it. Modules
60-65 alone without load passed, so this push's suite has to show it.

Tests:
- Unit: song-edit 11, piano-roll 17, song 14, sound-library 67.
  Four of them are new, one in song changed.
- e2e 64+65 on melektaus-atx: ALL PASSED - 46 checks (+3).
- e2e 60-65 on melektaus-atx: ALL PASSED - 86 checks.
- Gate on melektaus-atx: 251 files, 3907 tests, CHECK EXIT 0.
- Audition, after the gate: e2e 64 alone ALL PASSED - 25 checks
  (+1), transitions and piano-roll 30 tests, typecheck and ESLint
  clean.
- Waveform and one bar, after the gate: song, song-edit, song-play
  and the node catalog 438 tests. e2e 65 alone ALL PASSED - 25
  checks (+1). A probe shot of a looped pop sound: all six rounds
  pixel for pixel alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:25:49 +02:00
adminandClaude Opus 5.5 8f9c250eb6 Lay patterns and sounds out on tracks and play them as a song #334
game-maker / gate (push) Successful in 3m29s
game-maker / suite (push) Successful in 16m29s
Patterns could only be heard one at a time, in their own tab or from
a behavior. A song lays patterns and sounds out on tracks along one
time axis and plays them through the scheduler of #332, the third
piece of the music tools of #330.

- New asset type song under the heading Songs, with four empty tracks,
  120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows
  in the inspector. Tracks, items and the loop are the asset's data,
  held to their rules on load and in every command.
- A track has a name, volume, pan, mute and solo. Each is heard on a
  bus of its own, so a change reaches what is sounding at once.
- A pattern item repeats its pattern for as long as it is, with a
  transpose and another instrument if wanted. A sound item plays its
  sound as a clip at its own speed, round and round, through a sound
  FX if wanted. Both have a volume on top of their track's.
- Cut back at the left end or split, an item starts as far into what
  it plays as the cut is: ticks into a pattern, seconds into a sound.
- The tab shows the heads with M, S, volume and pan, the ruler of the
  Pattern tab and the items, a pattern with its notes, a sound with
  its waveform, a missing asset red. Patterns and sounds are dragged
  in from the Assets panel. Body, right and left end move, lengthen
  and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits
  where playing starts, Delete, arrows. A double-click opens a
  pattern item in its tab.
- What is picked is a new selection of its own, so the inspector
  shows the item or the track, with a button to delete it.
- The scheduler plans clips beside notes. Where the playhead starts
  or jumps back inside a clip, it is taken up that far into its
  sound. A clip fades out at the end of its item or of the loop.
- The editor plays one piece at a time: a pattern started stops the
  song and the other way round.
- The rules are pure functions in core/music/song, editor/song/
  songView and songEdit.

Findings:
- The suites of 4ea752f and 2a3b90b were red at e2e 54: the Pattern
  pushed Struct below the fold of the + menu. The module now scrolls
  to it, as e2e 47 does for Vector art. It also left the menu open,
  which made e2e 55 fail behind it.
- The Assets panel only dragged assets a scene can place. Patterns
  and sounds are dragged now as well, and a scene says why it does
  not take one instead of calling every asset a scene.

Tests:
- song.test.ts (new, 14) and song-edit.test.ts (new, 9): model,
  round trip, sequence of patterns and clips, mute and solo, cuts,
  clips on the scheduler, moving, sizing, trimming, splitting,
  duplicating, hits, head parts. sound-library.test.ts +5: clip
  offset, end fade, pitched clip, track bus, peaks.
- e2e 65 (new) on melektaus-atx: ALL PASSED, 18 checks. e2e 54 and
  55 with the fix: ALL PASSED, 33 checks. e2e 4 with 30 asset
  types: ALL PASSED, 62 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 250 test files, 3884 tests.
- Counter-checks: no taking up of a clip, no pattern offset, no clip
  end each fail 3, 1 and 1 tests.
- Probe on stein2: the tab with four tracks, before and while
  playing, looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:28:25 +02:00
adminandClaude Opus 5.5 4ea752feef Draw notes for an instrument in a piano roll and loop them #333
game-maker / gate (push) Successful in 3m22s
game-maker / suite (push) Failing after 13m18s
Notes could only be played one at a time, by a key or a behavior. A
pattern is a stretch of notes for one instrument, drawn in a piano
roll and played in time through the scheduler of #332. Songs (#334)
will lay patterns out on tracks.

- New asset type pattern under the heading Patterns. Instruments are
  listed under Instruments now, since a heading is one per type.
- Notes are a start and a length in ticks, a note number and a
  velocity, kept to their rules on load and in every command. The
  instrument, the length in bars, the grid, the meter and the tempo
  are rows in the inspector. The loop is the asset's own data, the
  whole pattern while none is set.
- Grids from 1/1 to 1/32 and the triplets 1/4T to 1/32T, all whole
  ticks. The meter only moves the bar lines, the notes stay put.
- The tab shows the keys down the left, a ruler with bars, beats,
  loop and start along the top, the notes between them and a
  velocity lane below. Keys the instrument plays nothing for are
  grey, the names of a drum kit stand on its keys.
- A click draws a note on the grid line before the pointer, as long
  as the last one drawn or lengthened and as hard as the last
  velocity set. The body moves a note, the right end lengthens it,
  Alt leaves the grid. A drag on empty grid picks with a band, Shift
  adds, a right-click deletes.
- Keys: Space plays, Delete, Ctrl+C/V/D/A, the arrows by a semitone
  or a grid step and with Shift by an octave or a bar. A paste lands
  where playing starts. Every gesture is one undo step.
- A click on the ruler sets where playing starts, a drag the loop or
  one of its ends, its menu loops a bar or the whole pattern.
- The wheel scrolls, Shift across, on the ruler or with Alt it zooms.
  Ctrl+wheel stays with the browser, Pixi listens passively.
- Play runs the loop through the scheduler. An edit goes to it on
  the next window without a restart, a new loop or tempo counts from
  what is planned. Another pattern shown or a project opened stops it.
- The time axis and the ruler are their own pieces in editor/music
  for the song tab. The rules of the roll are pure functions in
  rollView and rollEdit.

Findings:
- Music.play could start after a stop that came while it was still
  loading its files. A run counter makes the late start give up.
  Music.edit now loads what a new note needs as well.
- A click on an end of the loop would have set the loop again. A
  press there only moves the end once it is dragged.

Tests:
- pattern.test.ts (new, 9) and piano-roll.test.ts (new, 16): model,
  grids, meter, held notes, sequence, commands, loop, round trip,
  drum names, time lines, rows, hits, bands, lane, edit rules.
- e2e 64 (new) on melektaus-atx: ALL PASSED, 20 checks. e2e 4 with
  29 asset types: ALL PASSED, 62 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 247 test files, 3839 tests.
- Counter-checks: the move without its clamp, the sequence without
  the end of the pattern, the duplicate offset without whole grid
  steps each fail 1 test. Without passing edits to the scheduler the
  e2e check for a note drawn while playing fails, the rest passes.
- Probe on stein2: the tab with a drum kit, before and while playing,
  looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:06:51 +02:00
adminandClaude Opus 5.5 3f12a77aea Preview a sprite at the end of its inspector, placed or as an asset
A sprite's inspector listed its rows but never showed what they add up
to. The picture, its frame, nine-slice, tint and shader could only be
judged in the scene, often far from the object being edited.

- SpritePreview draws the sprite through syncSpriteMesh, the function
  the scene draws it with. Slices, padding, tint and shader come out
  the same without a second path. syncSpriteMesh now takes only the
  three fields of a DrawItem it reads.
- The subject is resolved by previewPicture from the Shader tab, so a
  placed sprite shows its own overrides.
- previewFit is the rule for size and place: whole device pixels per
  unit from 1 on, at most 8, and the corner on a whole device pixel.
  Pixel art stays sharp. A sprite larger than the group shrinks freely.
- A shader with a Time node keeps the preview redrawing once a frame
  while it is on screen. A folded group or a hidden panel stops it.
- New test hook __gm.inspectorSpritePreview().

Finding: the preview redrew on a model change in a microtask at first.
A drawn picture whose revision moved on is retried once, in a
microtask queued by the scene's synchronous redraw. That retry ran
before the preview had let go, and the old pixel art sheet stayed
waiting (e2e 48l-iii read waiting [1,1,1]). The preview now redraws
at once on a model change, as the scene does.

Tests:
- unit: 238 files, 3592 tests (+5 in preview-fit.test.ts), and the
  group list of a placed sprite in sections.test.ts ends in PREVIEW
- e2e 48: ALL PASSED, 47 checks (+2: 48l-i asset preview with
  the frame, 48l-iii placed preview with its shader values)
- counter-checks: without whole-pixel zoom 2 unit tests fail; with
  the microtask redraw 48l-iii fails with waiting [1,1,1]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:27:35 +02:00
adminandClaude Opus 5.5 2c19c89878 Let variables, fields, ports and list items be dragged into order (#310)
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 19m34s
Variables of a behavior, the fields of a struct, the ports of a start
or end node and the items of a list could only be added and removed.
Changing their order meant removing and adding them again. Now each of
them is dragged by the mark in front of its row.

- sortBlocks (ui/blockDrag.ts) makes the rows of one column sortable.
  A block is every row it covers, so a struct moves with its fields and
  a list item with its nested list. The block is dimmed while it is
  carried, an accent line shows where it lands, and a scrolled panel
  follows the pointer at its edge. Less than 4 px stays a click.
- The arithmetic is pure and lives in ui/sortable.ts: blockGap,
  gapLineY, dropSlot and edgeScroll. dropIndex of the tile strip moved
  there too, so the reorder maths sits in one file.
- MoveVariableCommand, MoveStructFieldCommand and MovePortCommand
  build on the existing MoveCommand. Undo moves back.
- Lists need no command. InlineList replaces the list as a whole, as
  it does for every other edit of its items.
- The grip is the kind mark, for a list item its number. Moving only
  happens among siblings: a field stays in its struct.
- On a Struct asset the move runs through WithStructTypes. Typed
  variables, Pack/Expand ports and split outputs follow the new order.
- Values and edges hang on names, so only the order changes. Nodes
  running a behavior read their ports from the start node, the new
  order reaches them without a copy.
- Not sortable by design: the variables of a placed object, the fields
  of a typed struct (moved on the Struct asset) and the picked ports
  of Variables/Properties nodes.
- Test hook __gm.inspectorMark(label) finds the mark of a row.

Tests:
- Gate on melektaus-atx: 235 files, 3572 tests, CHECK EXIT 0 (+9).
- tests/core/reorder.test.ts: a variable, a field two levels deep, a
  Struct asset reaching a typed variable and Pack, and start ports
  reaching the calling node with edges and result kept.
- e2e 22: ALL PASSED, 39 checks (+4, sum 1739). 22f-iii drags a
  variable past a struct of several rows, then a field inside it.
- Counter-probes: dropSlot by top edge instead of middle and a field
  move that ignores its path fail 3 unit tests. With sortBlocks
  switched off, the three order checks of 22f-iii fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:45:04 +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 70f3608733 Let Mouse position and Is hovered see through any camera (#304)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 16m23s
An overlay camera from #302 can stand elsewhere and zoom differently.
A HUD button only it shows lay somewhere else in the world than the
mouse of the main camera, so Is hovered never found it. Both nodes now
take a camera and see the mouse through that camera's picture.

- New input `camera` on Mouse position and Is hovered. Left open, both
  read the main camera exactly as before.
- `pointerThrough` in core/properties/camera is the rule as a pure
  function. It fits the camera's picture into the main one and centres
  it, the same way overlayView draws it.
- The game view reports where the mouse lies on the main picture, as a
  part of its width and height. The world turns that into a camera's
  view, so a main camera on the move does not drag the mouse across
  the HUD while the mouse stands still.
- `in view` is true only on the camera's own picture. A camera that is
  not drawn (switched off, a second plain one, not a camera) never has
  the mouse on it.
- On click and Mouse button stay with the main camera, as agreed in
  the issue.

Also fixes a unit test that 1d0fa7b broke. play-settings expected 8
to be no game view scale, but the steps go up to 20 since then. It now
checks 20 as the top and 21 as no choice.

Tests:
- Unit: the rule lands on the same world point as overlayView plus
  screenToWorld at four screen points for two cameras. One mouse gives
  (100, 50) through the main camera and (1050, 25) through an overlay
  at (1000, 0) with zoom 2.
- e2e 63c2: the real mouse 30 units right of the picture centre hovers
  the overlay's square through it, through the main camera not.
- Counter-checks: Math.max instead of Math.min in the rule, and no
  check for "drawn", each fail one unit test. The view reporting the
  centre of the picture always fails 63c2 with x 2000 instead of 2015.
- Gate: 231 files, 3526 tests (+6). e2e 63: 12 checks (+1), sum 1730.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:09:53 +02:00
adminandClaude Opus 5.5 2aa635f453 Keep an output's name readable beside a long inline value (#306)
An input and an output in the same port row were cut to fit each on
its own. The input text could reach x = 123 and the output label could
start at x = 83, so a long inline value ran under the output's name.
At Animation playing a long animation name covered "progress".

- The output label is placed first and keeps its room. The input text
  is cut with an ellipsis 6 px before it, as decided in the issue.
- The rule is a pure function in src/editor/graph/portRow.ts. A row
  without an output keeps the input's old limit of 108 px.
- The value a run shows beside an output uses the same rule. Its left
  edge can no longer lie right of its right edge.
- New test hook __gm.graph.rowSpans(nodeId) gives each row's input
  and output text as cut and placed, in node units.
- NODE_WIDTH stays as it is, as decided in the issue.

Tests:
- Gate: 230 files, 3520 tests (6 new in port-row.test.ts).
- e2e 6: all passed, 149 checks (148 + 1).
- Counter-check: with the rule switched off, the input text reaches
  x = 121.4 under "progress" at x = 114.1, and the new check fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:33:06 +02:00
adminandClaude Opus 5.5 808e1eefc2 Clean up duplicates and dead code, and fix the + menu check (#85)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 15m29s
Refactoring round 9 covers everything since round 8 (14972d0): 62
commits with about 29 000 new lines in src, api, tools and tests.
Behaviour is unchanged. The one fix is in a check: e2e 04 counted 25
asset types, and the shader function from #303 makes 26, so the run
of a526e1b failed there.

Duplicate code (jscpd over src, 50 tokens, without the two licence
texts: 35 clones / 289 lines before, 29 / 232 after):

- ShaderPicture (new, viewport/shaderPicture.ts) is the picture a
  camera's shader and a particle field's shader are laid over. It
  keeps the render texture while its size holds and frees the
  replaced one once the shader holds the new one. CameraShade and
  ParticleField.shadeField drew it the same way, line for line.
- CameraShade builds its mesh on the first draw. Before, every redraw
  of a view without a camera shader built a mesh and threw it away.
- emptyHost in inspector/host.ts builds the empty host the Inspector,
  the Variables tab and the Tilemap panel start from.
- paddedQuad(null) is the plain unit quad. symbols.ts and
  shaderPasses.ts each wrote it out by hand.
- In graph/nested.ts, calls of shader functions and On behavior
  nodes are followed by one walk, `reaches`, over ids `namedBy` a
  node type.

Dead code:

- ShaderEditor.colorFieldOf and the getter CurveCanvas.snapping had
  no reader.
- 22 values used only in their own file lose their `export`. Types
  and the node catalogues of shaders and particles stay exported.
- Kept on purpose: PhysicsSystem.holdsJoint is read by tests only,
  but it is their one view of whether a joint holds.
- A pass with stricter lint rules over every file changed since
  round 8 found no bug. PixelArtEditor.started is now readonly.

Comments and docs:

- Ten comments told history ("since #190", "used to", "any more").
  They now describe what is. The bundle is md5-identical before and
  after, and the four e2e files minify to the same bytes.
- README: joint and shader function in the asset list, joints on the
  Physics row, what the Shaders page covers, two CI runners.
- e2e 04 keeps the number of asset types once, as ASSET_TYPE_COUNT.
- screenshot.png in the README is a new picture of the editor
  (3840x2160 instead of 1825x1140), taken by the user.

Tests:
- Gate on melektaus-atx: 229 files, 3514 tests, CHECK EXIT 0
  (+3 for emptyHost, +1 for the unit quad).
- e2e 1-4, 31, 63 on melektaus-atx: ALL PASSED, 195 checks. That is
  the + menu of 04, the field shader of 31 and the camera shader of
  63, both through ShaderPicture.
- No new e2e check, so the sum stays 1728.
- Counter-checks, each put back and compared with cmp: reaches that
  never goes on fails 1 test, listensTo reading the wrong parameter
  fails 1, emptyHost dropping what a panel hands in fails 1, folding
  without a relayout fails 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:38:39 +02:00
adminandClaude Opus 5.5 2e55071fa5 Lay a shader over a camera's whole picture (#299)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m59s
A shader so far worked only on its own wearer. Effects over the whole
game picture, such as a vignette, grey, scanlines or a red flash on a
hit, had no place to live. A camera now wears one, drawn the way a
field shader of a particle source is (#170).

- The camera gets a Shader row and, while one is picked, a Shader
  resolution row. The shader's properties follow as camera rows. They
  load, animate and appear in a Properties node under Type Camera.
- Every view keeps masks and objects in one container. CameraShade
  renders it into the camera's picture, cleared to the background,
  and draws the shader over the frame. Game tab and play.html take
  the container off the stage while the picture stands in for it.
- Screen pixels gives the picture as many pixels as it covers on
  screen. Camera pixels uses View width x View height and scales up
  without smoothing.
- A pixel counts as a camera pixel in both (the user's choice). The
  uniform uPixelScale makes Texture size read View width x View
  height, so a Blur keeps its reach at any game scale.
- Each camera wears its own shader, overlay cameras included. A red
  flash on the game camera leaves the HUD of an overlay camera alone.
- The scene view's eye has a Shader switch on the Camera row, off by
  default. With it on, the main camera's shader lies over its frame,
  below symbols and gizmos, and the world around it stays drawn.
- New tool tools/perf-camera-shader.mjs measures the cost.

Measured on stein2 (HD 630, Game tab at Fit 1310x679): a camera
shader that hands the picture through stays inside the scene's own
spread of 3 to 5 fps between runs. A Blur of 4 cost 4 to 8 fps.

Tests:
- Gate on melektaus-atx: 227 files, 3497 tests (3487 + 10).
- e2e 63 alone: 11 checks (6 + 5), e2e sum 1725 counted.
- 63e-63i: pass-through is byte-equal and upright at 100%, flat red
  reaches the corners while the overlay keeps its green, Texture
  size reads 640 x 360 in both resolutions at Fit, no shader draws
  as before, and the scene view shows it only with the switch on.
- Counter-checks: a camera left out of the shader wearers fails 1
  unit test. Counting screen pixels fails 63g with #ab6000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:03:09 +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 ca5b2db449 Keep where each divider was dragged to across a reload
game-maker / gate (push) Canceled after 2m35s
game-maker / suite (push) Canceled after 2m40s
A panel dragged wider was back at its first size after every reload.
The sizes are now kept in the browser, next to the folded groups.

- editor/panelSizes.ts holds one entry in localStorage,
  game-maker.panel-sizes: a name per divider and its width, height
  or share.
- A panel reads its size once when it is built, held to the bounds
  its divider keeps today. Every drag writes the new size.
- Kept are the columns left and right of the centre, the console,
  the split of the left column, the tilemap panel, the preview of
  the Shader, Particles and Pixel art tabs, each under its own name,
  and the split of the user management.
- The sizes belong to the browser, not to the project. They hold for
  every project opened there.
- A browser that refuses the storage, or holds rubbish under the
  entry, starts with the sizes the editor comes with.

Tests:
- Gate on melektaus-atx: 219 files, 3400 tests, CHECK EXIT 0.
- Seven new unit tests in tests/editor/panel-sizes.test.ts. All
  five counter-probes bite.
- Smokes on melektaus-atx: 24, 19 and 21, all pass.
- One new e2e check in module 48: the left column dragged 40 px
  wider is as wide after a reload, and dragged back after.
- Full suite on melektaus-atx: 1692 checks, all pass. It ran before
  the fix to the animation players of the commit before last.
- Browser counter-probe: without the write to the browser exactly
  that check fails.
- Only the left column is measured across a reload. The other
  dividers go the same way and have no check of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:24:21 +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 b45bbcc976 Hold in tests that a graph edit reaches a running behavior (#216)
The issue said a behavior that runs in the editor keeps the graph
it started with, until it is switched off and on. Measured before
building anything, that is no longer so. An edit runs from the next
frame on. Since when, was not found out.

- No code changes. A command raises the project event, EditorRun
  refreshes, and syncFromModel of the behavior system empties the
  graph cache.
- Decided in the issue: the instance carries on and is not started
  over. Its variables and timers stand, and On start does not fire
  again. A restart would write what On start writes or spawns into
  the saved scene with every edit.
- Starting over stays what it was: Run in editor off and on, or
  Play.
- Module 43 of the e2e no longer switches the behavior off and on
  before it measures the changed layers of a ray cast.

Tests:
- Gate on melektaus-atx for this change alone: 212 files, 3264
  tests (3260 + 4), exit 0. The gate right before this commit ran
  on the tree of this and the next commit together: 213 files,
  3278 tests, exit 0.
- e2e 18,20,43 on melektaus-atx: 57 checks, all pass. The check in
  43 is the old one, it now holds without the workaround.
- Four unit tests in editor-run.test.ts: a changed node setting, a
  node wired in and one cut off, a flow hung on On start that does
  not fire until the switch goes off and on, and an undo.
- One counter-probe: without the emptied cache the wiring test
  fails. The setting test does not, settings change in place.
- No full run and no smokes. The sum of e2e checks stays 1676.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:59:19 +02:00
adminandClaude Opus 5.5 c88f57cf0a Let a project choose how its game link is zoomed (#279)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 14m31s
The page of a released game always drew the camera at Auto, the
largest whole step that fits. A camera of 300x180 in a window of
1280x800 stands at step 4 and leaves a border left and right. The
issue asks for Fit next to it. Answered there: a setting of its own
rather than the scale of the Game tab, and no switch for the player.

- The project has a field gameLinkZoom, 'auto' or 'fit'. A document
  without it reads as 'auto', so nothing changes for a project that
  is there already. No format bump.
- Project settings has a row "Link zoom" with a menu of Auto and
  Fit. It is an undo step of its own, SetGameLinkZoomCommand.
- GameScreen hands the field to cameraView where it passed a fixed
  'auto'.
- gameScale, the scale of the Game tab, is not touched and does not
  reach the link.
- The released copy is frozen, so the change reaches the link only
  with "Update release".

Decided here:

- The row is called "Link zoom", not "Game link zoom" as proposed.
  The longer label is wider than the label column of the dialog and
  pushed its button 12 px out of line with the others.
- The undo step keeps the name "Game link zoom", which is what the
  tooltip of the undo button shows.
- A note under the row says "The released game at the largest whole
  step that fits, or filling the window."

Tests:
- Gate on melektaus-atx: 211 files, 3227 tests (3219 + 8), exit 0
- e2e 60 on melektaus-atx: 19 checks, all pass
- e2e 35,46,59,60: 74 checks, all pass
- No full run and no smokes. The sum of 1671 (1668 + 3) is reckoned,
  1668 is what CI counted for 9e869df.
- Eight unit tests: the default, saving and loading, values that
  are not a choice, undo, gameScale left alone, the factor of Fit
  for a window larger and smaller than the camera, and the rule
  behind Apply.
- Three e2e checks in block 60i of module 60, with the real mouse:
  Auto to Fit in the dialog, update the release, open the link
  without a session and measure scale, frame and pixels. Fit reads
  4.267 where Auto reads 4.
- Nine unit counter-probes and three browser counter-probes all fail
  their tests.
- Not checked in the browser: Fit in a window smaller than the
  camera, which has a unit test, and Fit under a display scale above
  100 %. Nothing was looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:53:10 +02:00
adminandClaude Opus 5.5 9e869df063 Free a redrawn picture only once no shader names it (#293)
game-maker / gate (push) Successful in 2m48s
game-maker / suite (push) Successful in 13m44s
Editing a vector drawing made Pixi warn "destroyed while still bound
to a shader", twice with every change, once it had begun. It began
where an object wears the drawing through a shader asset. Each
change draws the picture anew and destroyed the one before it, but
the mesh of that object keeps its picture bound until it syncs. The
issue also asks for the button "Add shape" to go: a shape without
points cannot be given any, the Pen draws a new one.

- GeneratedPictures takes two more callbacks. inUse says that a
  picture cannot be freed yet, soon runs a retry once whoever asked
  is done.
- A replaced picture that is still in use waits in a list of its
  own. It is freed right after the redraw that asked, as a rule. On
  a later ask where its wearer was not part of that redraw.
- TextureLibrary tells a bound picture by the listeners for change
  on its source. Under WebGL the bind groups are the only ones that
  listen there. The retry is a microtask.
- The rule is asked for every drawn picture alike, whoever wears it.
- The inspector of a drawing and of a shape has no "Add shape" any
  more. "Delete shapes" for several selected shapes stays.

Decided here:

- Under the list of shapes a second note says "The Pen draws a new
  shape." while only the asset is selected.

Found on the way:

- Reproduced on a throwaway copy of the project the report came
  from: 6 warnings for 3 changes with the shader override on the
  object, none without it. After the change none, and no replaced
  picture is left waiting.
- The fixture project of the tests never showed the warning. No
  object there wore a drawing through a shader.

Tests:
- Gate on melektaus-atx: 211 files, 3219 tests (3209 + 10), exit 0
- e2e 47,48 on melektaus-atx: 113 checks, all pass
- e2e 28,30,31,39,40,47,48: 350 checks, all pass, run before the
  wording of the new note changed
- The first gate was red: an existing test read the wording of the
  note that had changed. Only three test files had been run after
  the change.
- No full run and no smokes. The sum of 1668 (1667 + 1) is reckoned,
  1667 is what CI counted for ec2fd90.
- Ten unit tests, five of them against the real bind group of Pixi
  in tests/viewport/bound-pictures.test.ts
- One e2e check in block 47b-ii of module 47: a drawing worn through
  a shader, three changes, no warning. The check on "Add shape" now
  reads that there is no such button.
- Nine unit counter-probes and two browser counter-probes all fail
  their tests. Without the change the new check counts 6 warnings.
- Not checked: a tilemap or a particle emitter holding a picture
  bound the same way. Nothing was looked at after the build, the
  new note included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:03:37 +02:00
adminandClaude Opus 5.5 345289982b Scale, turn and mirror the shapes of a vector drawing (#291)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 14m25s
A shape could only be moved and reshaped point by point. Whoever
wanted it larger or turned pulled every point alone, and for several
shapes or a group there was no way at all. The answers in the issue
settle the rest: the change is baked into the points, nothing of it
is remembered, and the line width stays.

- With Select the selection stands in a frame of eight squares and a
  knob, drawn like the one of the scene view. It holds one shape,
  several shapes or a group, the same selection that is dragged as
  one.
- A corner square scales from the opposite corner, a square on an
  edge in its own direction. Shift keeps the proportions, Alt scales
  about the middle.
- The knob turns round the middle of the frame, with Shift in steps
  of 15 degrees. After the pull the frame stands upright again.
- The inspector has a band TRANSFORM with X, Y, Width, Height and
  Rotate by, and the buttons Flip horizontal and Flip vertical.
- Points, handles, gradients and texture tiles are taken through the
  same matrix. The rule is a pure function in vectorTransform.ts,
  transformShapesCommand makes one undo step of any number of
  shapes.
- A press goes to a point or a handle first, then to the frame, then
  to a line. shapeFrame.ts holds where the handles are and what a
  pull of each means.

Decided here:

- The frame stands 15 pixels off the shapes. The points of a shape
  lie on its bounds, so a square on the bounds would cover them.
- The knob only turns. In the scene view it scales as well.
- A side cannot be pulled through the opposite one, the frame keeps
  at least 1 pixel.
- Coordinates are rounded to a thousandth of a pixel, so a quarter
  turn is exact.
- A texture tile stays upright under a turn and grows to the bounds
  of the turned tile. The bands of a gradient stay square to its
  axis under an uneven scale.
- The switch Scale stroke of the proposal is not built.

Found on the way:

- The inspector of a shape is now taller than the window. Two checks
  of module 47 no longer found their row or button on the screen and
  use the new hook inspectorButton, which scrolls there first.
- Not solved: with a shape at the top edge of the view the knob lies
  outside the tab. The wiki names it under the limits.

Taken over from #217: the e2e check for two selected shapes without
a group, and unit tests for the snap of the common offset and for
the gradients of the shapes dragged along.

Tests:
- Gate on melektaus-atx: 209 files, 3176 tests (3121 + 55), exit 0
- e2e 47,48 on melektaus-atx: 112 checks, all pass
- No full run and no smokes. The sum of 1664 (1660 + 4) is reckoned,
  1660 is what CI counted for 6d79cf5.
- 55 unit tests: 25 in tests/core/vector-transform.test.ts and 30 in
  tests/editor/shape-frame.test.ts
- Four e2e checks in block 47k of module 47: pull a corner, turn
  with Shift, type width and rotation, drag two shapes without a
  group
- 47 unit counter-probes and four browser counter-probes all fail
  their tests. Four of the unit ones did not at first: two showed
  dead code, which is gone, two got tests.
- Not checked in the browser: the edge squares, Alt, the cursors,
  the flip buttons and the rows X, Y and Height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:20:55 +02:00
adminandClaude Opus 5.5 6d79cf59a3 Move a large graph instead of drawing it again (#292)
game-maker / gate (push) Successful in 2m44s
game-maker / suite (push) Successful in 13m55s
In a graph of many nodes the frame rate dropped while panning, and
more so while one node was dragged. Every pointer move rebuilt every
box, every text and every curve, and a drag also announced a changed
project, so the panels rebuilt along with it.

- Panning and zooming move the picture. GraphCanvas.applyView sets
  position and scale and draws anew only the grid and the rubber
  band. It used to call redraw().
- A node draws its box and makes its texts only when what they show
  has changed. NodeView keeps a signature of what redraw reads and
  one of what rebuildTexts reads, and returns early while they hold.
- A drag does not announce a changed project. GraphDrags writes the
  model on every pointer move and asks the canvas once a frame to
  follow (GraphCanvasHost.moved). state.preview() waits for the
  command of the release.
- Nodes outside the picture are not drawn. inView in graphView.ts is
  the rule, cull() applies it after every redraw and every change of
  the view.
- The graph is a render group, and box and pins go into the batch.
  The mask sits on a container above the group, on the group itself
  it would be panned and zoomed along.
- The rubber band and the drop mark of a dragged behavior are drawn
  on their own (drawBand), without a redraw of the graph.

Measured in the CI image with 388 nodes and 540 links, fitted to the
canvas, 240 pointer moves 16 ms apart:

- Pan, script per move: 25.2 ms before, 2.0 ms after
- Pan: 4.4 frames per second before, 18.7 after
- Drag a node: 2.7 frames per second before, 22.4 after
- The still picture: 9 frames per second before, 21 after

Not solved: the still picture of such a graph stays at 21 frames per
second in the test browser. What is left is the drawing itself, and
the frame rates are those of that browser, not of a desktop GPU.

Tests:
- Gate on melektaus-atx: 207 files, 3121 tests (3114 + 7), exit 0
- e2e 4,6,18,19,20,23,24,26,28,31,36,43,51,53 on melektaus-atx: 542
  pass, 1 fail. The fail is module 51 after module 43, an order the
  suite does not have, and it fails the same way without this
  change. Module 51 alone: 12 checks, all pass.
- No full run and no smokes. The sum of 1660 (1656 + 4) is reckoned,
  and the CI workflow is switched off.
- Seven unit tests: six for inView in tests/editor/graph-view.test.ts
  and one in frame-preview.test.ts
- Four e2e checks in block 6z-xii of module 06: pan, drag, zoom in
  and fit, a changed value
- Seven browser counter-probes fail 2 to 3 checks each

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:18:20 +02:00
adminandClaude Opus 5.5 32a0af6e2c Put the text and boxes of the interface on device pixels
On a screen scaled to 125 % or 150 % the editor looked soft beside
a page the browser draws itself. The layout counts in whole CSS
pixels, and most of those fall between two device pixels there. The
GPU averages both: text loses its edges, and a 1 px line becomes a
pixel and a half of mixed colour. Now text and boxes are rounded to
device pixels, and a line is one device pixel wide.

- ui/pixelGrid.ts holds the rule. crispText() makes a text with
  Pixi's roundPixels, crisp() does the same for a Graphics of
  upright boxes, and hairline() is the width of one device pixel.
- Every text of the interface is made with crispText() instead of
  new Text().
- crisp() only rounds where the scale is not a whole number. At
  100 % and 200 % nothing changes.
- drawControlBox, drawHeaderBar and drawSectionBand draw their
  lines a hairline wide and flush with the edge. The widgets that
  draw lines of their own do the same.
- Two bands share exactly the line they draw, and a column still
  ends on a whole pixel.
- The stage is nudged by 1/32 px where the scale is not whole. At
  150 % every odd pixel is an exact half, and without the nudge one
  band came out a row shorter than its neighbours.
- Curves, icons and everything of the game stay unrounded. Rounding
  their points would bend them.
- The harness takes deviceScaleFactor as an option, and the hooks
  deviceColumn, edgeContrast and pixelGrid read the frame as the
  screen gets it.

Not in here: the subpixel smoothing a browser gives its own text.
Pixi draws a text into a transparent picture first, and there only
greyscale smoothing is possible.

Measured at 150 %, edge contrast before and after: top bar 31.5 to
47.8, the lists on the left 68.6 to 86.4, inspector 27.6 to 38.4.
At 100 % it is 46.5, 90.1 and 37.9, and the picture there is the
same as before pixel for pixel, apart from the fps display.

Tests:
- Gate on melektaus-atx: 206 files, 3114 tests (3104 + 10), exit 0
- Full run on melektaus-atx: 1655 of 1656 checks passed, smokes
  24/19/21. The one that failed was a condition of the new step
  62 itself: it scrolled an inspector that had nothing to scroll.
  The step now makes the window lower for that reading.
- e2e 61,62 after that fix: 14 checks. The full run was not
  repeated, and CI is switched off for now.
- Ten unit tests, nine in tests/ui/pixel-grid.test.ts
- Eight e2e checks in the new step 62, which runs at 150 %
- Eleven unit counter-probes fail tests
- Four browser counter-probes fail their checks. They ran before
  the fix to step 62 and were not repeated for it.
- Screenshots looked at for 125 % and 150 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:42:26 +02:00