Commit Graph
207 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 b17a924157 Let a behavior play, pause and seek a video and hear it end (#410)
feuerwerk / gate (push) Successful in 5m58s
feuerwerk / suite (push) Failing after 17m35s
A game should be able to stop a video on a sprite, jump to a spot in
it and start it again, and react when it has run out. Until now a
video on a sprite either ran free on its own or followed a
composition, and no behavior could touch it.

- Five new nodes: Play video, Pause video and Seek video (time in s
  from the clip start), On video finished with a Video field, and the
  data node Video time with time, length and playing.
- Play, Pause and Seek act on the owner when object is not linked.
- On video finished with an empty field hears every video. A chosen
  video also hears its clips.
- A new VideoSystem keeps a clock for every sprite with a video. It
  counts from the moment the sprite appears, as long as it is on. For
  a Driven sprite the clock is its Video time. A paused game holds all
  clocks.
- A free sprite that one of the nodes touches gets the clock
  "steered" at run time. It then has its own player, and other
  sprites with the same clip play on. Pause of the game and sound go
  the existing Driven way.
- On a Driven sprite and in the editor run the nodes do nothing and
  write a line to the log.
- Until its own player has a frame, a steered sprite shows the frame
  of the shared player. Without that it would blink out on the first
  Pause video.
- Play at the end of a clip without loop starts it from the start.
  The issue does not say, this is my choice.

Found, not fixed: with a Driven sprite in the scene the run waits up
to 3 s for its frame at the start. The world does not tick then, but
the free videos already play. Their clocks lag behind the picture by
that wait, so On video finished comes that much late.

Tests:
- Unit: 23 new (video-steer 16, node catalog 7), affected groups
  4820/4820.
- Counter-probes: 16 bends, all red.
- e2e 66 on atx: ALL PASSED, 39 checks. New 66s: Pause at 0.15 s
  held its own player at 0.2 s, the picture stayed red for 1 s. Seek
  landed on 0.5 s exactly, green, held. Play showed green, blue,
  white, and white stayed at the end. On video finished came once.
  Without the clock "steered" all three 66s checks are red.
- Gate on atx: CHECK EXIT 0, 336 files, 5199 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:13:52 +01:00
adminandClaude Opus 5.5 56425fca6a Let a song carry named markers on a strip in the song tab (#338)
feuerwerk / gate (push) Successful in 5m22s
feuerwerk / suite (push) Failing after 19m54s
Stage 1 of #338. Game sync needs named places in a song: an event when
the playhead passes one, and a switch that jumps to one. This stage
adds the markers themselves. Games do nothing with them yet.

- A song has markers, each with id, name and tick. A song without
  markers stores nothing for them. Two markers may share tick and
  name, since nodes will name them by id.
- Markers change neither the sound nor the bake fingerprint.
- The song tab draws a strip under the ruler. A marker has a green
  flag with its name and a faint line through the tracks. Tracks and
  lanes sit 18 px lower. The ruler of the pattern tab is unchanged.
- Double click on an empty place adds a marker on the grid, with Alt
  or Shift on the exact tick. Dragging moves it in one undo step.
- A click on a marker sets the start of playback there. A double click
  renames it in the name dialog of the hierarchy groups.
- The context menu offers Play from here, Rename and Delete marker,
  on an empty place Add marker. A tooltip shows the full name. The
  wheel over the strip zooms time like the ruler.
- New agent commands add_song_marker, change_song_marker and
  remove_song_marker. get_asset lists them under markers.

Tests:
- song-markers in core 6 and editor 4, 1 agent test, with the touched
  song, agent and MCP tests 175/175
- 9 counter-probes, all compile, all red
- e2e 65 on atx: ALL PASSED, 70 checks, 3 new in 65g3. Counter-probe
  where a dragged marker is not taken over: 65g3 red
- gate on atx: CHECK EXIT 0, 332 files, 5093 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:44:58 +02:00
adminandClaude Opus 5.5 24cd9fdeaa Let the agent use stored files, presets and usages (#431 #432 #433)
feuerwerk / gate (push) Successful in 5m16s
feuerwerk / suite (push) Failing after 19m8s
The agent got stuck where the editor has it easy: it could not make a
Sample play without a sound asset, it built instruments node by node
although the Preset menu has them ready, and it could not tell what
names an asset before reworking or deleting it.

- list_files is a new tool. It lists the pictures, fonts, sounds or
  videos the server holds, with size, video state and the asset that
  already wears a file. Bakes and stream copies of songs are left out.
- add_file_asset makes an asset of such a file, as the picker does.
  It reuses the create rules of the import pipeline, so the four
  import kinds are exported now. It refuses a file already in the
  project (naming that asset), a picture without a size, a video not
  yet converted and a file the server does not hold.
- run_commands fetches the file lists its steps need before it runs,
  so the commands themselves stay synchronous.
- apply_preset sets one of the 16 instrument presets or 7 sfxr
  presets, like the Preset menu. Presets take every note, so the
  C4 to B4 range of a new Notes node is no trap there.
- get_asset gains usedBy: every asset naming the asset or one of its
  frames or clips, with the object, node, item or key that does, plus
  start scene, master FX and master routing of the project.
- usesOf in src/core/model/usages.ts walks the project data instead of
  a list of fields, so no new field can be missed. The node every
  asset has in the master routing does not count as a use.
- The relay tells the agent about all three ways.

Found on the way: suite 740 for 2e4c0a0 (#430) was red in e2e 68h
only, a sound clip 61 ms early against 50 allowed. #430 did not touch
sound timing, and e2e 68 on the same tree was green on atx (-4 ms).
Taken as a timing wobble under load until the next suite says more.

Tests:
- agent-files (new, 4 tests) with the other agent and relay tests:
  130/130.
- 8 counter checks, all compiling, all red.
- e2e 71 on atx: 26/26, new 71g9 lists an uploaded sound, makes an
  asset of it and fills an instrument from a preset.
- Gate on atx: CHECK EXIT 0, 326 files, 5021 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:06:15 +02:00
adminandClaude Opus 5.5 2e4c0a09ea Open clips and frames from the asset panel by double click (#430)
feuerwerk / gate (push) Successful in 4m58s
feuerwerk / suite (push) Failing after 19m37s
A double click on a clip row did not open its tab. The first click of
the double click had already selected the clip for the inspector, so
the second click selected the same thing again and the editor saw no
change. Frames of textures and pixel art had no rows at all.

- A double click now asks for the tab with openSelection, even when
  the selection is already right.
- Textures with frames and pixel art with more than one frame get a
  chevron and one row per frame, like sounds and videos. Which rows an
  asset has is decided in one place, src/core/model/assetParts.ts,
  used by the panel and the inspector link.
- A texture frame shows in the inspector on click and opens the
  texture with the frame selected on double click.
- A pixel art frame selects the pixel art on click, so an open tab
  paints on that frame. A double click opens the tab on the frame.
- A frame can be dragged onto a composition track like a video clip.
  The item carries the sprite of the whole image, the frame becomes
  the texture of its object, and it is 3 s long. The agent can do the
  same with add_composition_item.
- A texture label that names a frame now points at the frame row.

Found on the way: the e2e check from #425 only double clicked the
texture label, where the first click is a ping, so the bug never
showed there.

Tests:
- Unit: asset-parts (new, 6), composition-sprites and references
  extended, related tests 303/303, agent tests 21/21
- 5 mutations, all red in the matching test
- e2e 69 (atx): ALL PASSED, 26 checks, 69t new
- Gate (atx): CHECK EXIT 0, 325 files, 5017 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:30:29 +02:00
adminandClaude Opus 5.5 37779a2070 Fold object groups in the timeline, with summary keys (#414)
feuerwerk / gate (push) Successful in 5m21s
feuerwerk / suite (push) Successful in 20m7s
Long animations with many objects fill the timeline with rows. A
group per object can now fold away, and its header row still shows
where the group has keys, so they stay reachable while folded.

- A click on an object header or its triangle folds the group. The
  groups of the objects below it fold along.
- Which groups fold along follows the real parent in the hierarchy,
  not the indent. The root shows at the level of its children, so
  folding it hides only its own rows.
- Every header shows a summary diamond on each frame where a row of
  the group or of a group below has a key. Click selects all those
  keys, drag moves them, Ctrl+drag duplicates, a band catches them
  and Delete removes them, each through the existing commands.
- The fold state is a view of the session per animation. It is no
  undo step and is not saved.
- Selecting a row without keys inside a folded group unfolds it, so
  a fresh row from + Row does not stay hidden.
- The fold rules live in the new timelineFold.ts. keyAt became
  keysAt, which returns every key behind a summary diamond.

Found on the way: the triangle moved the object name to x 22 while
its rows stayed at x 20, left of the name (#408 check in e2e 25).
Rows now sit one step behind the name, as in the hierarchy.

Tests:
- Unit animation + timeline-view 114/114, 6 new
- 6 counter-checks, all red in the matching test
- e2e 25 (atx) ALL PASSED, 41 checks, 4 new (25k)
- Gate (atx) CHECK EXIT 0, 324 files, 5008 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:01:30 +02:00
adminandClaude Opus 5.5 406839baea Drag clips of videos and sounds straight onto a track (#425)
A clip of a video could not be laid on a composition track at all, and
neither could a clip of a sound, although #418 and the wiki said so.
The only place a drag starts is the asset panel, and it listed assets,
not clips. The texture label of an item playing a video clip was dead
too, because the reference lookup did not know video clips.

- The asset panel gives a sound or video with clips a chevron of its
  own. It unfolds one row per clip and starts closed.
- A clip row drags like an asset. One click shows the clip in the
  inspector without leaving the tab (`EditorState.glance`). A double
  click opens its sound or video with the clip picked.
- A video clip dropped on a composition track becomes an item as long
  as the clip. It wears the sprite of its whole video, so a composition
  keeps one sprite per video, and the clip is a texture override on
  the object. Sprite, item and override are one undo step
  (`droppedPicture`).
- A sound clip becomes an audio item as long as the clip. The song tab
  takes sound clips on the same way, through `findSound`.
- `referencedTarget` lets a reference label link to the clip itself.
  One click pings the clip row, a double click opens the asset with the
  clip picked. The Plays row of an audio item reads "Sound / Clip".
- `clipOwner` in `sound.ts` finds the sound or video a clip id belongs
  to. `activateAssetOrClip` uses it for the double clicks.

Found on the way: the Blip of the e2e seed is only 0.25 s long, so a
clip of 0.2 to 0.7 s on it is cut to one frame. 69s takes the Drone
(0.75 s) instead.

Tests:
- tsc, eslint, Prettier clean
- Unit on stein2 (composition, references, sound, video, editor):
  94 files, 1238 tests green
- e2e 69 on melektaus-atx: ALL PASSED, 25 checks, 2 of them new (69s)
- Gate on melektaus-atx with the fix for 68k on top: CHECK EXIT 0,
  313 files, 4841 tests
- Counter checks, all red: no clip rows in e2e 69 (no drag starts),
  no clip override in the unit test, the link aimed at the asset
  instead of the clip
- Not covered by its own e2e check: sound clips on the song tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:01:04 +02:00
adminandClaude Opus 5.5 0701fe3856 Share the parts the sound devices and timelines repeated (#85)
feuerwerk / gate (push) Successful in 5m5s
feuerwerk / suite (push) Failing after 18m39s
Round 12 of the clean-up. Duplicate code had grown from 33 clones
(249 lines) after round 11 to 140 clones (1263 lines), almost all of
it in the panels and faces of the sound FX devices (#379 to #401).
It is down to 57 clones (416 lines) now, and src is 816 lines
shorter.

- deviceParts.ts holds what the device panels built by hand: the
  head, rows of knobs, the screen style and the row layout for
  groups of knobs. Twelve panels use it. panel.knobs stays because
  the debug hooks read it.
- DeviceFace is the base of the twelve faces. It finds the node and
  does wired, preview, commit, set, hide, reload and audition.
  compressorWired lives there as paramWired.
- TextPool replaces ten copies of "make texts as needed, hide the
  rest" in the wave and video tabs, the rulers, the piano roll and
  the song and composition timelines. clipStrip.ts is the clip label
  strip the wave and video tabs share.
- core/util/bytes.ts has the concat that ogg, matroska and avcColour
  each wrote out.
- Dead code is gone: clear on the video players (never called since
  #402), posterFileOf and videoTimeAt (only tests called them) and
  easeOut (the same as glide(t, 3)). Values only used in their own
  file lose their export.
- Comments over 125 columns are wrapped, and comments that told the
  history ("as before", "used to") say what is, in src and in the
  e2e modules.
- Left alone on purpose: AnimationEditor and CompositionEditor share
  only the play button, and the curve panels are built differently.
- Chip now places its boxes through put, which rounds. They were
  whole numbers already. The three row layouts are byte-identical
  before and after.
- README lists the video and composition assets and the Videos and
  Compositions pages.

Tests:
- Gate (melektaus-atx): CHECK EXIT 0, 313 files, 4835 tests.
- e2e 25, 34, 64, 65, 66, 69 (melektaus-atx): ALL PASSED, 330 checks.
- TextPool without keeping its texts and framesToRoom without making
  room both go red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:40:05 +02:00
adminandClaude Opus 5.5 df6dbe97c5 Build only the meshes of the tiles a painted cell changes (#424)
feuerwerk / gate (push) Successful in 4m34s
feuerwerk / suite (push) Failing after 18m22s
Since #348 a painted cell worked out only the cells around it, but the
layer's tiles still came as a new list. groupByTile grouped that list
afresh, so meshFor saw no group again and every mesh on the layer was
built anew. On 120x120 that was the last cost that grew with the map.

- A patched list of tiles remembers the list it came from and which
  entries went and came (tilesPatch). A cell that is drawn the same
  keeps its entry, and a patch that changes nothing drawn hands back
  the very same list.
- groupByTile and layerRows patch the groups of the list before
  (patchedGroups). An untouched group stays the same array, so its
  mesh is not built again. On a layer sorted by Y this holds for each
  row and for the groups in it.
- A tile new to the layer still works the whole layer out again, as
  since #348.
- Stage 2 of the issue (patching quads inside a mesh) is not needed.
  The tilemap geometry no longer shows in the CPU profile.

Measured with the #348 probe, a brush stroke on a copy of Garden of
Sacrifices on stein2:

- Meshes built per frame: 21.9 to 0.94 on 60x60, 40.7 to 1.22 on
  120x120.
- fps while painting: 49.4 to 51.1 on 60x60, 42.0 to 45.8 on 120x120.
- Painting now costs both sizes about 10 fps. The gap left between the
  sizes is drawing the larger map, which shows without painting too.

Tests:
- Gate on melektaus-atx green, 312 files, 4832 tests.
- The random walk from #348 also compares groups and rows with a fresh
  grouping after each of its 400 steps, and counts reused groups.
- A fixed test paints a cell and keeps every other group and row.
- Three counter-checks turn red: entries not removed from a group,
  rows sorted by their first cell, groupByTile ignoring the patch.
- e2e 39 + 40 on melektaus-atx: ALL PASSED, 107 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:41:41 +02:00
adminandClaude Opus 5.5 88e1cf1c7d Work out only the cells around a painted one on a tilemap (#348)
A painted cell worked out the tiles and blocking cells of the whole
map again, so painting got slower the bigger the map. One cell cost
16.8 ms on 60x60 and 54.6 ms on 120x120. Now it costs what is around
it: 3.5 ms and 4.0 ms, against 3.4 ms without painting.

- The brush, the paint command and its undo name the keys they
  changed. Each new version of the cells keeps them for the last 16
  versions (cellsChangedSince).
- The cell index, the tiles of a layer and the blocking cells patch
  only the changed cells and their eight neighbours. Everything else
  stays the same object as before.
- Both lists are in cell order now, so a patch is one walk that keeps
  every other entry.
- The rectangles are merged row by row. A patch joins again only the
  rows around it, one row further than the changed cells, and stacks
  the rest as they were.
- A writer that names no keys costs one walk over the cells. A
  replaced cell object (load, resize, edits at run time) is worked out
  in full as before.
- The tiles a map names come sorted by id. Two trigger tiles on one
  cell now keep that order, whichever way they were painted.
- resolveTilemap walked all tiles every frame, even without painting.
  It now does so only when the list changed.

Found on the way: deleting an outlined autotile can turn the one below
into a full box, which cuts a scaled box two rows further down. Merging
only the patched rows missed that. It is a test of its own now.

A brush stroke on the map doubled to 120x120 runs at 42 fps instead of
28, p95 of a frame 52 ms instead of 92 ms. What still grows with the
map is the mesh of a layer, which is built again on every paint.

Tests:
- tests/core/tilemap-patch.test.ts (new, 7): 400 random steps on two
  layers with every kind of tile, each compared with a fresh full run
- Unit, tilemap and viewport: 1284 green in 57 files
- e2e 39 + 40 (atx): ALL PASSED, 107 checks
- Gate (atx): 312 files, 4831 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:14:39 +02:00
adminandClaude Opus 5.5 4d0a696327 Keep composition sound in step and give compositions a bus of their own
A composition's sound ran on the audio clock while its picture ran on
the frames. A frame counts at most 0.1 s, so after a hitch the sound
stayed ahead of the picture until the next stop. Songs in a trailer
drifted off the cut. The items of a composition also had no bus of
their own, and a video sprite could not pick where its sound goes.

- CompositionSound.follow compares the audio clock with the picture
  each frame. More than 40 ms apart, it starts the sound again where
  the picture is. This holds in the tab and in a run.
- A time scale other than 1 now restarts the sound again and again.
  It stays in time but comes in pieces.
- The sprite of a video item shows "Driven (by composition)" as a
  fixed Video clock row. Video time is hidden there, because the
  composition sets it every frame.
- Every composition has a bus in the mix and a node in the master
  routing. A new one goes to the master unless other compositions are
  linked elsewhere, apart from where the sounds go.
- Audio items pick From asset, To composition, Master or a bus. New
  items start with To composition. Items saved before keep From asset,
  so music already sent through a bus does not bypass it.
- Video sprites have a new row Video bus: From asset by default, the
  master or a bus. In a composition the default is the composition's
  bus, also for video sprites inside a container item.
- The chosen bus is part of the key of the video player, so a sprite
  with another bus gets a player of its own.

Found along the way:

- Six tests with full lists of sprite defaults expected the old rows
  and now list Video bus too.

Tests:
- tsc, eslint, Prettier clean
- Unit: 971 green in the 28 files touched
- npm run check on melektaus-atx: green, 309 files, 4804 tests
- e2e 68 (melektaus-atx): 13 checks green, 68i new. The sound item
  and the video item go into mix:cp68-cut, which goes to the master.
- e2e 69 (melektaus-atx): ALL PASSED, 23 checks, 69q and 69r new.
  After a hitch of 500 ms the sound restarted twice and then lay
  0.5 to 5 ms off the playhead.
- Counter-checks in unit tests: without the comparison the hitch test
  fails, without the bus in the preload the run test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:31:16 +02:00
adminandClaude Opus 5.5 acd12326dc Fade sounds and videos in and out on the items of a composition
A trailer needs its music and its clips to come in softly and leave
softly. Until now an item of a composition could only play at one
level, and a video item could not even be turned down on its own.

- Every item with sound gets a grip at its upper left and upper right
  corner. Dragging one inwards fades the item in or out. Dragging it
  back to the edge removes the fade. Lower down the same edge still
  trims as before.
- The fade is drawn as its curve, with the part above it darkened.
  The inspector shows Fade in and Fade out in seconds, each with the
  curves sound clips already know.
- The model keeps fadeIn and fadeOut in frames plus curveIn and
  curveOut on the item. Old projects load unchanged. An item shorter
  than its fades plays them cut to its length but keeps the setting.
- Sound, pattern and song items ramp the gain of their own bus. The
  scheduler plans the ramps on the audio clock next to the notes.
  Starting in the middle of a fade picks it up from there.
- Video items set the new sprite property Video volume each frame.
  It multiplies with the fade and is only shown for a driven video.
  The player glides to each new level over 15 ms so it does not click
  between frames.

Not covered: a container item that carries a video further down gets
no grips. Only a video on the item's own sprite counts.

Tests:
- tsc, eslint, Prettier clean.
- Unit tests for composition, music, song and sound (12 files): 269
  passed, 7 of them new.
- e2e 69 on melektaus-atx: ALL PASSED, 21 checks. 69p drags a grip to
  a fade in of 10 frames and hears the item bus rise from 0.127 over
  0.229 and 0.339 to 1 within one second.
- Counter-checks as unit tests: without the audio ramps, and without
  the level on the video sprite in the game, both go red.
- Gate on melektaus-atx: 309 files, 4796 tests passed, 8 more than
  at 77ac9d1. The first run failed 14 tests in 6 files that list the
  default sprite properties or the bus levels in full. They now
  expect videoVolume and the new level field of busLevels.
- Not tried in a browser: video fades, pattern and song items with a
  fade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:05:36 +02:00
adminandClaude Opus 5.5 77ac9d1aaf Play sounds, patterns and songs on the tracks of a composition (#418)
feuerwerk / gate (push) Successful in 4m31s
feuerwerk / suite (push) Successful in 18m21s
A trailer needs its music and effects on the same timeline as its
pictures. Until now a composition could only place objects, so sound
had to be started by hand from a behavior and drifted against the
picture.

- Audio items: a sound, a clip of a sound, a pattern or a song dragged
  from the asset panel onto a track becomes an item as long as one
  pass of it. It may share a track with object and video items and
  overlap them.
- Editing works as for other items: move, resize, trim on the left
  (moves the offset), fit to media, delete, copy and paste, box
  selection. Sounds draw their waveform, the colour tells the kind.
- The inspector shows what it plays, start, length, offset, volume
  (-60 to +12 dB) and bus. The bus is "From asset", the master or one
  of the buses, as asked for in the issue.
- An item longer than its material plays in rounds. Sounds and
  patterns start over, a song runs on in its loop. Loop marks on the
  item follow the same rule, a song's first mark sits at its loop end.
- compositionAudio.ts turns the items into sequences, each with its
  own tempo. CompositionSound plays them, in the tab and in a game.
- In the tab the items are heard from the playhead on, straight into
  the master like every tab. A scrub is silent. Playing a song or
  pattern in its own tab stops the composition, and the other way
  round.
- In a game they are planned on the audio clock for the moment the
  composition clock reaches them, with the scheduler's LEAD made up.
  While the clock stands (waiting for a video, paused, placement off)
  they are silent and resume at the right spot.

Found along the way:

- The asset panel never let a song out of its list, so a song could
  not be dragged anywhere. Song and scene tab reject it as they reject
  a pattern.
- The asset panel scrolled to the selected asset on every rebuild of
  its list, so folding a group far below the selection jumped back up.
  Only a new selection scrolls now.
- At the end of the list, folding a group pulled every row down by the
  rows that went away. ScrollContainer has a new opt-in holdOffset,
  set only in the asset panel, that keeps the offset until the user
  scrolls up.

Limits: a time scale other than 1 or a frame longer than 0.1 s moves
the picture and not the sound. A baked song plays its notes here, not
the bake.

Tests:
- tsc, eslint, Prettier clean. Gate on melektaus-atx green,
  309 files, 4788 tests (4772 plus 16 new).
- Unit: composition-audio.test.ts 13 new, scroll-container 3 new.
  Counter-checks: without catching up on load time or without the
  silence on pause the unit tests go red.
- e2e 68h: the sound is planned 1.2 ms after the composition reaches
  0.5 s. Without the LEAD make-up it is 54 ms late and red.
- e2e 04 and 69 ALL PASSED, 86 checks. 69o-ii drags a pattern and a
  song onto a track, 20 frames each.
- e2e 04: folding Sound at the end of the list keeps its header at
  y 234. With the old panel it jumps to 984 and the check is red.
- Not run in a browser: the loop marks of a song item, copy and paste
  of audio items, the bus choice.
- README and wiki count 2072 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:28:36 +02:00
adminandClaude Opus 5.5 126a5a3a62 Let a composition item's clips move along with it
A clip only hung at its track and frame and worked on whatever item it
overlapped. Moving an item left its clips behind, so they started to
animate the next item that came along.

- A clip belongs to the item that lies on its track at its start frame.
  With two there, it belongs to the one in front. A clip that only
  reaches into an item stays with the item before. The model does not
  change, the rule is worked out each time.
- Dragging an item, also onto another track, and typing its Start in
  the inspector take its clips along in the same undo step.
- Where a carried clip would land on a foreign one, the whole move
  holds at the last place where everything fits, as dragging clips
  already does.
- Pressing Alt during the drag moves the item alone. Alt on the press
  already starts the clip band since 0648853.
- Copying an item takes its clips along with new ids. On a collision
  items and clips slide on together.
- Deleting an item deletes its clips. Trimming and dragging its edges
  leave them where they are.

Tests:
- tsc, eslint, Prettier clean
- Unit composition-edit 36 green (6 new), composition-sprites 11
- Without the rule 5 of the 6 new tests are red
- e2e 69 ALL PASSED, 18 checks (69n new)
- Gate on melektaus-atx green: 305 files, 4736 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:03:35 +02:00
adminandClaude Opus 5.5 0648853356 Select composition items with a band and copy clips too (#417)
game-maker / gate (push) Successful in 4m57s
game-maker / suite (push) Successful in 18m29s
The composition tab only took single clicks and Ctrl-clicks, and
animation clips could not be copied at all. A band selection and
Copy/Paste for clips make longer compositions quicker to arrange.

- Dragging from an empty spot draws a band. It selects every item it
  touches, across tracks. Ctrl adds to the selection, because Shift
  plus drag already pans the view.
- With Alt held the band selects clips instead and may start on an
  item, since items fill most of a track.
- Ctrl+C copies the selected clips as their own clipboard kind.
  Ctrl+V places them like items: under the pointer over the tracks,
  otherwise at the playhead on the selected track.
- Clips of a track never overlap. A paste onto taken frames moves the
  whole copy on frame by frame until every clip fits (`pastedClips`).
- The paste is one undo step and selects the new clips.

Tests:
- Gate on melektaus-atx: tsc, eslint, Prettier clean,
  305 files, 4724 tests (2 new).
- e2e 69: ALL PASSED, 17 checks (69m new). Band over three items on
  two tracks, Alt band from an item picks both clips, first paste at
  frames 50 and 60, second paste moves on to 56 and 66.
- Counter-check: without moving on, the paste unit test is red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:56:25 +02:00
adminandClaude Opus 5.5 b16fe8c718 Drop a picture or video onto a composition track (#419)
A track item has always carried an object, and an object needs an
asset. Instead of objects without one, a dropped picture or video gets
a sprite the composition owns, the way a song owns its sounds (#355).
To the rest of the code it is a sprite like any other.

- The asset list starts a drag for textures, vector art, pixel art and
  videos. Dropped on a track, they make the sprite and the item in one
  undo step. A video item is as long as the video and follows it.
- One sprite per picture and composition. A second drop of the same
  video shares it. Once no item wears it, it goes in the same step.
- Deleting a composition takes its sprites along. A duplicate gets
  sprites of its own.
- Pasting into a scene or container turns the sprite into a project
  asset. Pasting into another composition gives that one a copy. The
  clipboard carries the sprite, so cut and paste loses nothing.
- Owned sprites stay out of the asset list and the pickers. The
  inspector shows them with a dot in front.
- "Make project asset" in the item's menu, on the timeline and in the
  hierarchy, turns the sprite into an ordinary one.
- The first track of the Composition tab gets its top line, as the
  Song tab's has.

Found on the way: loading kept an owner only for the asset types a
song can hold. A composition's sprite would have lost its owner and
turned up in the list after a reload.

Tests:
- Unit composition-sprites: 11 new. Without the rule 5 of them are
  red, without the loading fix the loading test is.
- e2e 69 on melektaus-atx: ALL PASSED, 15 checks, 69k new
- Gate on melektaus-atx: 305 files, 4714 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:48:20 +02:00
adminandClaude Opus 5.5 c425eea004 Key what an empty timeline row shows on a double-click (#412)
A double-click into a timeline row without keys set the property's
default, while the field in the row header showed the value the target
actually holds. A sprite standing on x 40 got a key with 0, so the
first key moved it. The double-click now keys the value the row shows.

- trackHeld in src/core/model/animation.ts says what a row without keys
  holds. The field in the row header and SetAnimationKeyCommand both
  ask it, so they cannot drift apart again.
- A relative row still gets 0, a row with keys still takes the value of
  its curve at that frame, and a row whose target is gone still only
  gives a notice and no key.
- README and the wiki page Development name 2047 checks.

Tests:
- Unit animation: 87 green, 1 new. A sprite on x 40 with the override
  scale 2 gets keys with 40 and 2. Red with the old def.default.
- e2e 25 on melektaus-atx: ALL PASSED, 35 checks, 25i-ii new. The arm
  stands on x 40 and a double-click on frame 3 keys { frame: 3,
  value: 40 }.
- Gate on melektaus-atx: green, 304 files, 4703 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:09:02 +02:00
adminandClaude Opus 5.5 6e6b6a4d9a Wait for a composition's videos and copy and paste its items (#417)
A composition lost the first frames of a video item. Its clock ran
while the player was still loading, in the tab and in the game. The
game only waited for videos once, when it started. A composition that
a behavior starts later spawned its items after that wait.

- The view now records which video sprites it left out for lack of a
  first picture, one entry per object.
- The composition tab holds its playhead while the preview misses a
  video, for at most VIDEO_WAIT_MS (3 s) like the game.
- CompositionSystem holds a composition's clock while one of its live
  items misses its video. The rest of the game keeps running.
- The editor play and the published game pass their view to the run
  as RunServices.pictures.
- VIDEO_WAIT_MS moves to core/model/video.ts, because the runtime
  does not import from viewport/.
- Videos loaded ahead for coming items do not hold the clock.

Items can now be copied and pasted inside the composition tab.

- Ctrl+C keeps the timing of the copied items next to the objects.
- Ctrl+V pastes objects and items in one undo step. The earliest item
  lands at the frame under the pointer, on the track there.
- Without the pointer over the tracks it lands at the playhead, on the
  selected track or on the top track of the selected items.
- Several items keep their distances and tracks to each other.
- The same copy pastes into a scene as plain objects. Objects copied
  from a scene do not paste into a composition, they have no timing.

Tests:
- Unit composition-edit, clipboard, composition-play: 3 new for
  pasting, 1 new for the clock waiting in the game
- e2e 69i (play from the end waits for the video), 69j (paste under
  the pointer and at the playhead), 68d (a composition started from
  On start waits for its video)
- each new check is red without its fix
- README: 2036 editor checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:38:19 +02:00
adminandClaude Opus 5.5 c4650e43f0 Let composition items overlap and show where their video lies (#417)
Trying the tab turned up six things to fix. Items of a track could
not overlap, so a drag stalled as soon as the pointer reached another
item. An item's ends gave no hint they could be dragged, a trimmed
video showed nowhere, clip names were hard to read and vanished when
picked, and the track menu did not match the song tab.

- Items of a track may overlap and nothing is cut or pushed aside.
  The later one lies in front, in the picture and on the timeline.
  The overlap is shaded and a line marks the covered end.
- Edges win over bodies when hit, so the covered end of the lower
  item can still be dragged. Hovering an edge lights it up and shows
  the ew-resize cursor, as in the song tab.
- A clip in the overlap plays on both items. Clips themselves still
  do not overlap; a moved clip now runs up to its neighbour instead
  of stopping short.
- Fit to media and a new video no longer cap the length at the next
  item, which only made sense while overlaps were forbidden.
- The left edge can be trimmed past the start of the video. The
  offset goes negative and the sprite shows the first picture until
  the video begins.
- Video marks: a corner triangle where the item cuts into its video,
  a line with a triangle like the song loop where the video starts or
  ends inside the item. Edges snap to the video's ends within 6 px.
- Clip names sit in their own layer above the bars, in text colour.
- The track menu matches the song tab: add above or below, move up or
  down, delete. It opens on the head and on an empty spot of the
  track. The Remove track button left the inspector.

Tests:
- Unit composition and composition-edit: 67 green, several new or
  rewritten. Bending three spots in the rules turned their tests red.
- e2e 69: ALL PASSED, 12 checks. 69b and 69c now drop the second item
  over the first and drag the covered end past it. Without edges
  winning the hit, 69c fails with hover: null.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4672 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:57:26 +02:00
adminandClaude Opus 5.5 4f0272638e Load the next item's video before a composition cuts to it (#417)
Two sprites with videos back to back left a blank frame at the cut.
The video player only came into being once the item began, and a
sprite is not drawn until its video has a first frame. So the second
item was missing until its video had loaded.

- compositionAhead lists the items that begin within the next second.
  Near the end of a looping composition it also lists the items at
  its start.
- An item that is alive already is not loaded ahead. Two times would
  otherwise pull at the same player.
- The frame carries these videos hidden and paused on their first
  picture at the item's offset. Their players get the id the item
  will have, so the cut reuses them instead of opening a second one.
- This works the same in the tab's preview and in the game.
- One second of lead (AHEAD_SECONDS) is an assumption. It can go up
  if large videos still flash.

Tests:
- Unit composition: 27 green, 2 new. Unit composition-play: 14
  green, 1 new for the path in the game.
- e2e 69: ALL PASSED, 12 checks. The new 69h puts two video items
  back to back and checks the second player is open and paused at
  its offset before the cut, and drawn in the same redraw as the
  cut.
- Without loading ahead in the preview 69h fails with cut: null.
  Bending two spots in the model turned their unit tests red.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4665 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:07:46 +02:00
adminandClaude Opus 5.5 e435c54afb Edit a composition's tracks, items and clips in its own tab (#417)
A composition could be placed and played, but not filled. Its tab
puts the picture at the playhead above a timeline of tracks, so a cut
or a cutscene is laid out by dragging assets onto tracks.

- The timeline takes the song tab's geometry with frames for ticks:
  ruler in seconds, scrubbing, play and pause, + Track. Zoom and pan
  glide through viewTween like the other tabs.
- A placeable asset dropped on a track becomes an item, 3 s long or
  as long as its sprite's video. Dropped on an item it lands behind.
- Items move without overlapping. The right edge stops at the next
  item. The left edge trims into a video and not before its start.
- An animation dropped on a track becomes a narrower clip bar above
  the item it plays on. The asset panel now starts a drag for
  animations too.
- The picture is a scene view over compositionHolder. A click picks
  an item or its child, a drag moves the real object. It takes no
  assets, since an object there would have no item.
- The hierarchy lists the items alive at the playhead. Reparenting is
  off there, items stay at the top level.
- The inspector shows a TIMING block on an item (start, length,
  offset for a video, Fit to media) and edits tracks and clips.
- Delete, the context menu and Remove item take item and object in
  one undo step.
- fitMedia: an override that gives an item another video sets its
  length in the same undo step, capped at the next item. Dragging an
  edge or typing a length turns it off.
- A composition inside a composition is refused.

Tests:
- Unit composition-edit (new): 19 tests. Four counter-checks each
  turned their test red. The "back to 3 s" test got a step in
  between after one stayed green.
- e2e 69 (new): 11 checks, 9 own. The hierarchy check reads the rows
  shown, after a counter-check on the model's rows stayed green.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4662 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:47:22 +02:00
adminandClaude Opus 5.5 0334d2508d Place compositions in scenes and play them from behaviors (#417)
A composition can now sit in a scene like a container and play there.
Its items become real objects while they run, so behaviors, colliders
and events work on them as on any other object.

- A composition is placeable with its own symbol, transform and render
  layer. The scene view shows its picture at frame 0. A click on that
  picture selects the placement, not a single item.
- CompositionSystem spawns each item as an object under the placement
  when it begins, with the id <placement>~<item>. On start and the
  other events run. The object is destroyed when the item ends.
- The top track lies in front no matter which item started first.
  World.arrange() orders the entity list and the scene objects.
- Clips and driven videos follow the same rules as compositionAt().
  Loop, restart, stop and the game pause work.
- New nodes: Play composition (with restart), Stop composition and the
  event On composition finished. They were planned for step 4 but come
  now, because without autoplay nothing could play otherwise.
- The editor run ignores Play composition and says so in the log. It
  writes into the saved project, and the items would stay behind.
- contentId() moved to instance.ts. The animation system uses it for
  nested ids too.
- Compositions get no "Sort with parent" for now.

Tests: the gate passed with 301 files and 4643 tests, 18 more than
step 1. 13 of them are in tests/core/composition-play.test.ts. The
first gate run failed in event-order.test.ts, which counts the event
nodes and now finds 19. Five counter-checks each turned exactly their
test red. Two order tests stayed green without the fix at first and
were rebuilt. e2e 68 is new and passed with 5 own checks. Its
counter-check without arrange() failed as expected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:06:33 +02:00
adminandClaude Opus 5.5 73e675835e Add the composition asset with its tracks, items and clips (#417)
A composition is a timeline of scene objects for cuts and cutscenes.
This first step brings the model and the pure evaluation behind it.
The tab and placing it in a scene follow in the next steps.

- CompositionAsset holds objects, tracks, items and clips. Each item
  owns exactly one object with overrides and container edits.
- The composition is a third object holder next to scene and
  container. Delete cascade, undo, usages and references work with it.
- Duplicating gives the copy new object ids and moves the item ids
  along with them.
- compositionAt() tells which items live at a frame. The top track is
  drawn in front.
- A clip acts on the item of its own track that it overlaps. After its
  end it holds the last value until the item ends. The animation's
  play mode is ignored, a clip is one pass.
- Clip targets are found by template path, else by name path, like
  the runtime's animation system.
- The root sprite of an item plays its video driven at offset plus
  time. Children follow unless they are already driven by hand.
- fittedLength() gives the video or clip length minus the offset, or
  3 s without media.
- fps (60) and loop live in their own properties file. Keeping them in
  PropertySchema would close an import cycle.
- The type is listed under Visuals with its own icon. It is not
  placeable yet.

Tests: the gate passed with 300 files and 4625 tests. That is 25 new
tests in tests/core/composition.test.ts and 2 icon checks that run
for every icon. Four counter-checks (track order, overlap rule, child
videos, ids on duplicate) each turned exactly their test red. e2e 04
passed with 67 checks and now counts 33 asset types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:36:30 +02:00
adminandClaude Opus 5.5 8679dfbe65 Group timeline rows by object and edit values in the row (#408)
The rows of the animation timeline stood in the order they were keyed.
With several objects and properties the list was hard to read, and a
value could only be changed by picking a key and using the inspector.
The Play button also kept saying Pause after scrubbing had paused.

- Each object gets a header row with its name, indented by depth. The
  groups follow the scene hierarchy, the root first. Rows whose object
  is missing go to the end.
- Under an object the rows follow the order in which it lists its
  properties: X, Y, Z, then the order of the inspector. Only the view
  is sorted, the animation itself is unchanged and there is no undo
  step. After a move in the hierarchy the order is right by itself.
- The row head shows the same field as the inspector with the value at
  the playhead. Without keys it shows the value of the object, for a
  relative row the offset.
- A change in that field sets or changes the key at the playhead,
  selects it and is one undo step. A running playback pauses first.
- The Play/Pause button follows every change of the playing state, so
  scrubbing that pauses shows Play again.

Tests:
- Unit animation + timeline-view: 100 passed, 3 new.
- e2e 25 on melektaus-atx: ALL PASSED, 34 checks, 3 new (groups,
  order and values at frame 4, a typed 55 becomes a selected key on
  frame 4, Play then a click on the ruler shows Play and pauses).
- Gate on melektaus-atx: exit 0, 297 files, 4561 tests.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:42:10 +02:00
adminandClaude Opus 5.5 774207208c Key a video to its end at 1x and zoom the timeline further out (#407)
To play a clip at its real speed the user had to know its length,
work out the last whole frame by hand and then scroll far to the
right to set the end key. The timeline now does the sum, and it
zooms out far enough to see a long clip in one piece.

- The context menu of a Video time row gets "Video at 1x to its end
  from frame N". It keys the playhead with the row's value there and
  the last whole frame before the clip ends with the matching time.
- The start key turns linear and keys in between are removed. The
  whole run is one undo step (KeyStraightRunCommand).
- The clip is the one the sprite wears at the playhead, also when
  the texture is keyed in the same animation (rowVideoAt).
- The entry is greyed out for relative rows, without a video, while
  the length is unknown, or when the playhead is already at the end.
- The status line names the result, e.g. "frame 572 shows 57.2 s of
  57.27 s".
- The timeline zooms out to 0.02 px per frame instead of 4 px. The
  ruler labels follow a 1-2-5 step without an upper limit.

Tests:
- Unit video-clock + timeline-view: 27 tests green, 8 new
- e2e 66 (atx, alone): ALL PASSED, 23 checks, 66l new
- Gate (atx): CHECK EXIT 0, 297 files, 4553 tests
- Counter-check: the command keeping the keys in between turns
  exactly the command test red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:51:20 +02:00
adminandClaude Opus 5.5 c9e8a3f6ac Let an animation drive a sprite's video picture by picture (#404)
game-maker / gate (push) Successful in 4m16s
game-maker / suite (push) Failing after 16m12s
A video used to run on its own clock as soon as a sprite wore it. An
animation could switch the sprite on and off, but never say which
picture shows at which frame. Cutting a video in an animation needs
the opposite: the animation sets the time and the video follows.

- Sprites wearing a video or a clip get two rows, Video clock and
  Video time. They show only while the texture holds a video.
- With Video clock on Driven the video does not play by itself. It
  shows the picture at Video time, counted from the clip's start.
  Past the end it goes round with Loop and holds the last picture
  without.
- Video time is a number like any other, so keys, step, easing and
  curves give stills, slow motion and ramps.
- A driven sprite gets a player of its own, keyed by the wearer's
  name. The same clip can run twice at different times.
- Following has two ways (`videoFollow.ts`). Running forward at 0.25x
  to 4x the video plays at that rate and is nudged. A still, a scrub
  or a step backwards seeks, and only the newest seek counts.
- Sound is heard only near 1x. The animation tab now plays it too, so
  a cut can follow the sound. Scrubbing is silent.
- Scene, game view, the inspector's PREVIEW and the shader preview
  show the driven picture. In a running game Play animation sets it.

Found on the way:

- After a scrub the video in the animation tab kept running. The
  preview redrew only while the playhead moved, so nobody asked for
  the video once it stood. `AnimationEditor.tick` now redraws it every
  frame while it shows a video.
- Every sprite has two more defaults (`videoClock: 'free'`,
  `videoTime: 0`). Eight tests in five files pinned the old list and
  are updated.

Tests:
- Unit: video-clock and video-follow new, 4 files with 42 tests green.
- e2e 66 (melektaus-atx): ALL PASSED, 22 checks, 3 new. 66i: two
  sprites on one clip stand at 0.2 s red and 0.7 s green. 66j: a
  scrub to frames 2, 7, 12 and 3 shows red, green, blue, red, and
  each colour still stands 0.7 s later. 66k: playing runs 10-30 ms
  behind at rate 1.00-1.03 and is not muted.
- Counterchecks: a seek that keeps the rate turns the seek test red.
  Not seeing Driven turns 66i, 66j and 66k red.
- Gate (melektaus-atx): exit 0, 297 files, 4546 tests.
- Not tested in the browser: Play animation in a running game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:23:07 +02:00
adminandClaude Opus 5.5 d48f174caf Play videos on sprites, converted on the server with clips (#402)
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m49s
A video becomes an asset like a sound: uploaded once, converted to one
format by a worker of its own, cut into clips in its tab, and worn by a
sprite through its Texture row. Only Chrome is a target, without alpha.

- Server: POST ?r=videos queues the upload under the hash of its bytes,
  so the MP4's name is known at once. The worker (VideoWorker over
  VideoFormat) writes H.264/AAC, yuv420p, constant frame rate, a
  keyframe every 15 frames, at most 1920x1080, with a poster and the
  details beside it. A failed job says why and is tried again on the
  next upload.
- An upload may be up to 2 GB. It is streamed from the request to disk
  and hashed there, so php-fpm holds none of it in memory. The other
  routes keep their 256 MB.
- The worker runs in the container game-maker-video on stein2, a
  compose project of its own (tools/video/), and in CI beside the PHP
  server. The CI image has ffmpeg for it.
- Model: asset type video with size, length, frame rate and sound, and
  clips like a sound's. The clip helpers and commands serve both.
- A sprite's Texture row offers videos and clips after the pictures.
  resolvePicture names a playing video by a key, and the texture library
  hands it to a VideoLibrary with one player per key.
- The editor shows a placed video as a quiet preview. Play starts it
  over at its clip's start and lets it be heard through the master bus,
  a pause holds it, Esc makes it quiet again.
- Video tab: the picture, a ruler, the clip strip and a track on the
  Sound tab's time axis. A drag spans a clip, a handle moves an edge,
  Play plays the picked clip.
- The picker shows a video on its way with its progress and adopts an
  upload from it once it is ready. Dropping a video file does the same.

Decided on my own:

- Two sprites wearing the same clip share one player and show the same
  picture. Control per instance belongs to the follow-up with nodes.
- A video plays in real time, the game's time scale does not slow it.
- The container may use 3 of the 4 cores of stein2.

Findings:

- The first real upload (279 MB) got 413: the API's own ceiling of
  256 MB for every request, not nginx or the Brandmauer.
- Chrome has no picture of a video that never played, and a Pixi
  VideoSource drawn before the first frame stays empty for good. The
  texture is made in the first frame callback (openVideo).
- A player nobody asks for in 2 s is let go. The inspector's PREVIEW
  asked only on an edit, so with the sprite not on screen its texture
  was destroyed under it and the batcher threw on every frame. The
  preview now redraws every frame while it shows a video, like a Time
  shader. A let-go texture keeps an empty source instead of none.
- VideoSource.load() reloads the element and cuts off an earlier
  play(). Playing starts after loadeddata.

Tests:

- PHP videos.test.php: 68 checks, with the real ffmpeg
- 300 MB upload through Brandmauer and nginx: 201 in 21 s, php-fpm at
  13.7 MB RSS at most
- Unit videos.test.ts: 20 tests
- e2e 66 (stein2, atx was off): ALL PASSED, 18 checks; e2e 04 with the
  new asset count green
- Gate (atx): 294 files, 4520 tests. serialize.test.ts used video as
  its example of an unknown type and now uses a made-up one.
- Counter-checks: a keyframe gap of 30 fails the PHP check, a key that
  keeps the video's end fails 2 unit tests, a clip without its end
  fails "plays only its stretch" in e2e 66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:12:39 +02:00
adminandClaude Opus 5.5 82e96e680f Decode a clip from a file of its own, not its whole sound (#349)
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Failing after 14m58s
A clip was cut from its sound's file, so a game decoded the whole file
for it and held it for as long as the page was open. A long recording
played through a few short clips cost the recording. Every clip now has
a copy of its cut as Opus, with Stream on or off, and a game decodes
that instead.

- StreamKeeper makes the copy of each clip whether its sound streams
  or not. Stream only decides whether a sound gets a copy of its own.
- clipCopyTable says which cut decodes from which copy. The sound
  library loads the copy for a clip unless the whole file is decoded
  already. Then the clip is cut from that as before.
- projectFiles decodes the whole file ahead only where something may
  play the sound itself: no clips, a clip without a fitting copy, or
  the sound's id anywhere in an asset that can name one. That last
  test walks the data, not the fields, so no place is missed.
- A copy that cannot be had falls back to the whole file. A library
  that bakes keeps to the whole files, as the bake's fingerprint does.

Tests:
- Gate: CHECK EXIT 0, 280 files, 4319 tests (7 new: streams +4,
  sound-library +3).
- e2e 34: ALL PASSED, 108 checks (2 new). With Stream off a fresh
  library decodes the ten-second reversed clip's copy and not the
  five-minute file. It is the cut made from the original to within
  0.0091 a sample and 0.013 dB in any 20 ms. Once a node plays the
  sound itself, the whole file is decoded ahead again.
- e2e 65: all passed with this change.
- Counter-checks red as they should be: the library ignoring copies,
  the whole file always decoded ahead, clip copies only with Stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:53:49 +02:00
adminandClaude Opus 5.5 2764919bb8 Stream long sounds from an Opus copy instead of decoding them (#349)
A sound was decoded whole as soon as the project opened, in the editor
and in a released game: five minutes of stereo held some 115 MB for as
long as the page was open. A sound with Stream on now plays from a copy
as Opus in Ogg, fetched a piece at a time and decoded a few seconds
ahead by WebCodecs, so a game holds a few megabytes for it.

- The Stream switch is a row of the sound, off by default. The Sound
  tab suggests it from a minute on and says how far the copy has got.
- StreamKeeper makes the copies in the browser, a moment after the
  last change, one at a time: the sound's own and one per clip of its
  cut, fades and Reverse baked in. A copy that is no longer wanted is
  taken off and deleted once the project is saved without it.
- streamTable says which played names stream from which copy. A clip
  without a fitting copy, a Sample and a Reverb impulse stay decoded.
  A sound with Stream on is never decoded ahead.
- The page fetches by Range and takes the whole file from a server
  that ignores it, as the CI's PHP server does. It keeps the bytes
  and the first two seconds decoded, the rest goes four seconds ahead.
- A jump decodes 32 packets before the place first. Measured in
  Chrome, what follows is then the same to the bit as decoding from
  the start. With 4 packets it was 0.06 off.
- The stream-player on the FX worklet reads the samples at the rate
  of the context along a Catmull-Rom curve. What comes late is silent
  and skipped once it is there, so a clip keeps its beat.
- The opening stretch goes with the player as it is made. Sent by
  message it came a few milliseconds late, and 557 samples of the
  attack were lost.
- Pitch on a streamed sound is a shifter after the player, as on a
  bus: the length stays and it is heard 42.7 ms late.
- Bakes of patterns and songs (#340) always stream now.
- The song timeline draws a streamed sound from its copy decoded at
  8 kHz, keeping only the peaks.

Tests:
- Gate: CHECK EXIT 0, 280 files, 4312 tests (31 new: ogg +3,
  stream-queue 6, streams 11, streaming 6, sound-library +5).
- e2e 34: ALL PASSED, 106 checks (7 new). Five minutes of mono,
  4.8 MB as WAV, 3.6 MB as its copy: streamed the renderer grows by
  5.8 MB, decoded by 69.6 MB. Offline at 48 kHz, start, loop seam and
  a reversed clip are the copy decoded whole to the sample (0).
- Counter-checks red as they should be: no pre-roll, no catch-up of
  late samples at the end, no opening stretch, a streamed sound
  decoded ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:27:32 +02:00
adminandClaude Opus 5.5 84cd36f902 Bake a pattern or song to Opus, played in its place by a game (#340)
game-maker / gate (push) Successful in 3m49s
game-maker / suite (push) Successful in 15m6s
A pattern or song played by a game runs every note through the
scheduler, the instruments and their FX, live. Bake works that out
once, keeps it as a file on the asset, and Play pattern and Play song
play the file instead, as one voice, for as long as nothing it is made
of has changed. The tabs always play live.

- Bake is an icon in the header of the Pattern and the Song tab, lit
  while the bake holds. A click bakes, cancels a bake that runs, or
  offers Bake again and Remove bake. The toolbar shows the progress.
  The info of the tab says "baked" or "bake outdated".
- The bake holds everything up to the output of the pattern or song.
  The sound FX of the node, fx params, volume, fade in, bus, Master
  routing and stopping with a fade stay live.
- A print of the worked-out plan decides whether it holds: notes and
  clips, tempo, length, loop, tracks, the build of each instrument for
  each note, sound FX, files, cuts and levels. A rename keeps it, an
  undo back to the baked state makes it hold again.
- The file holds the straight run and then one more round of the loop
  as heard the second time, tails included. Looped, it plays to the
  loop end and then round that round, to the sample. Once, it fades
  out over its last 10 ms. Let ring fades it out over 0.3 s.
- It is worked out by a second SoundLibrary on an OfflineAudioContext,
  its scheduler woken by suspends of the offline clock, and what
  reaches the master is taken. A worklet probe tells whether that was
  mono, so a mono bake goes on exactly as the notes did.
- Opus in Ogg, 160 kbit/s stereo or 96 kbit/s mono, encoded with
  WebCodecs, the Ogg pages written by hand. Normalized to full scale,
  the peak kept and played back.
- The file goes up into the sounds of the account as "[bake] name",
  which the sound picker leaves out. An old bake file is deleted once
  the project is saved without it. Only a bake that holds is loaded.
- Ogg files are decoded at 48 kHz: resampled to 44.1 kHz, the loop of
  a bake lost a sample a round.
- Baking and removing a bake are no undo steps, like the EQ's zoom.

Findings:
- Chrome's AudioEncoder refuses opus.format 'ogg' (NotSupportedError)
  but hands out the OpusHead with a pre-skip of 312 for 'opus'.
- The Song tab's header is full at 1280 px: a captioned Bake pushed
  Fit off the tab. Hence the icon.

Tests:
- Unit: tests/core/ogg.test.ts (6), tests/core/bake.test.ts (16),
  four new in sound-library.test.ts, one in song-play.test.ts. One
  test cuts, reverses and fades a clip on a song's track: each change
  outdates the bake, each undo makes it hold again.
- e2e 64 (melektaus-atx): ALL PASSED, 36 checks (6 new). Live and baked
  rendered offline differ by at most 0.06 dB over 114 windows of
  20 ms, two loop ends among them. e2e 65 had no red check with the
  icon in its header.
- Gate (melektaus-atx): CHECK EXIT 0, 277 files, 4281 tests.
- Counter-checks: print without instrument builds, print taking the
  sound's file instead of the clip's cut, round taken from the first
  pass, bake voice without its looping source, Play pattern or Play
  song ignoring the bake: one unit test red each. Pre-skip set to 0:
  the e2e comparison 4.8 dB off, only that check red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:13:11 +02:00
adminandClaude Opus 5.5 25084f8576 Refactoring round 10: split SoundLibrary, fold sound duplicates (#85)
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Successful in 14m52s
Refactoring round 10 covers everything since round 9 (808e1ee): 110
commits with about 47 000 new lines, nearly all of them sound, sound
FX, instruments, patterns and songs. Behaviour is unchanged.

Duplicate code (jscpd over src, 50 tokens, TypeScript only, measured
on 808e1ee and here alike: 47 clones / 426 lines before, 33 / 245
after; round 9 itself ended at 22 / 160 by this count):

- PreviewParams (new, editor/soundfx) is the header's parameter bar
  of the Sound FX and the Instrument tab: values, defaults, fields
  and the voices a moved field reaches. Both tabs had it line for
  line.
- fxWorklet: an Effect base runs the bit crusher, phaser, flanger,
  limiter, LFO and bus shifter, a Source base the pitch player and
  the Chip. Start is the one wait for a voice's first sample, which
  four processors wrote out each.
- nodes/shared has checkPort, detachedPort, fxParamsPort and
  letRingPort. The Play and Stop nodes of sounds, notes, patterns and
  songs spelled each port out, up to five times.
- parameterValues builds both Sound FX parameters and Instrument
  parameters, which differed in name and text only.
- SetLoopCommand sets the ruler loop of a pattern or a song. It
  replaces SetPatternLoopCommand and SetSongLoopCommand.
- addLink in music/routing is the one rule for a link that both the
  song routing and the Master routing keep.
- eqCurves works out each band and their sum for the EQ tab, which
  did that in its drawing code. It replaces eqCurveDb, which only a
  test called.
- Smaller ones: the eight neighbour directions of a tile cell, and
  drawFloatingBox for the shadowed box of Popup and FloatWindow.
- Left on purpose: Play pattern and Play song stay two parallel node
  definitions, and the smaller clones under ten lines stay.

Architecture:

- SoundLibrary.ts goes from 2897 to 2188 lines. What a stage of a
  sound FX becomes as Web Audio nodes, and how the knobs of a playing
  voice move, is now viewport/fxNodes.ts. None of it keeps state of
  the library.

Dead code (TS language service, references per export and member):

- soundRefIds, LFO_SHAPE_INDEX, TREMOLO_ROWS, VIBRATO_ROWS and
  REVERB_ROWS had no reader. The four routing node defs are
  registered without an unused exported name.
- soundFileOf and portTypeOf were called by tests only. The tests
  now ask findSound and portOf, which the editor uses. DRY moves
  from src into the one test that uses it.
- 51 values used only in their own file lose their `export`. Types
  and the node catalogues stay exported.
- Kept on purpose: neighboursOf is the cell by cell reading the
  indexed neighboursIn is checked against, and PitchShifter.latency
  is the test's one view of how late the shifter is.

Comments and docs:

- Seven comments told history ("no longer", "any more", "as
  before", "of before #354"). Nine comment blocks ran far past the
  line width. Proof that this changed no code: the bundle was md5
  identical, and the touched tests and e2e modules were identical
  after esbuild.
- README: sound FX, instrument, pattern, song and bus in the asset
  list, the pages Instruments, Patterns and Songs, 1919 checks.
- Wiki: see its own commit.

Tests:

  Gate (melektaus-atx)    274 files, 4236 tests, CHECK EXIT 0
  e2e 34, 64, 65          ALL PASSED, 180 checks
  Unit, touched areas     16 files, 804 tests

No check was added, the e2e sum stays at 1919. Counter-checks, each
put back by copy and compared with cmp:

- addLink without the loop rule: 2 tests fail, one per routing.
- eqCurves with the sum of the last band only: 1 test fails.
- Start in the worklet never waiting: e2e 34 fails in exactly the
  four checks that hang on a voice's first sample (Flanger, Tremolo,
  Chip, a pitched note planned ahead).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:31:33 +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 42a3759d9d Cut a sound into named clips, as a texture into frames (#357)
A sound could only be cut to one stretch, set on the asset itself. To
play two parts of one file, the file had to be imported twice. Now a
sound is always the whole file, and its clips are the stretches of it,
each with a name and a cut of its own.

- A clip is { id, name, props } on SoundAsset.clips. Its id shares the
  generator with asset ids, so a sound field holds a sound or a clip,
  as a texture field holds a texture or a frame.
- props are the cut: start, end, the fades and their curves, reverse.
  Volume, loop, sound FX and bus stay on the sound for all its clips.
  The cut rows left the sound's schema for CLIP_ROWS.
- findSound resolves either id. Play sound, the Sample of an
  instrument, a Reverb impulse, song items, the scheduler and the test
  sound of the Sound FX tab all go through it.
- Every sound field offers each sound followed by its clips, "Kick",
  "Kick / Clip 1". A clip that is gone reads as "<id>?".
- The Sound tab has a strip under the ruler with the clips as bands.
  A drag there makes a clip, a click picks one, beside the bands the
  sound. Only the picked clip has handles. Reverse, fades and curves
  in the header work on it and are greyed out without one.
- Whole file is gone from the header, as the user asked.
- Picking a clip is a selection of its own, like a frame. The
  inspector shows its name, its cut rows and Delete clip. Delete
  takes it away without asking, as with a frame. Ctrl+Z brings it
  back.
- Copies get fresh clip ids: Duplicate, Embed a copy, a song
  duplicated with its takes, and the copy a split or paste makes of
  an embedded take. An item on a clip plays the same clip of its copy.
- A double-click on a song item playing a clip opens its sound with
  the clip picked. The Sound row of an item on an embedded take
  offers the take and its clips, which no list shows.
- A new take stays the whole sound. Clips are made by hand, as the
  user decided.
- No migration in the code, as the user decided. The one project
  with cut sounds was changed over by hand.

Deviation from the issue: deleting a used clip does not warn. A frame
does not warn either, and the issue had promised "like a frame".

Tests:
- Unit sound-clips (new): 11 green, 7 counter-checks each red
- Unit sounds, embedded: adjusted to clips, green
- e2e 34 (melektaus-atx): ALL PASSED, 90 checks (3 new, 12 rewritten)
- Counter-check in the browser: clips left out of the sound fields,
  the picker check fails
- Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4194 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:18:47 +02:00
adminandClaude Opus 5.5 23cb34f781 Keep a recorded take inside its song (#355)
A take used to land in the Assets panel as "Song – Take N", next to
every other sound of the project. Now a song can keep a sound, a
pattern or a sound FX to itself: an ordinary asset whose new field
`owner` names the song. Everything that finds an asset by its id
finds it, so playing, the tabs and the game need no change. Only
the places that list assets leave it out.

- `core/music/embedded.ts` holds the rules: which types can be
  embedded, `listedAssets`, the title "Song › Take 1", and the
  rows an embedded sound hides (Loop, Bus).
- One user per embedded asset. `SetSongCommand` settles the song
  after every edit: a place naming an asset an earlier place
  claimed, or one of another song, gets a copy, and what nothing
  names any more goes in the same undo step. The outcome is worked
  out once, so a redo brings back the same copies.
- A split, Ctrl+D and a paste get that for free. The clipboard
  keeps the embedded assets of the copied items, so a paste after a
  delete brings the take back.
- Deleting a song deletes what it keeps. Duplicating it copies
  that and points the copy at the copies. A duplicate of an
  embedded asset is a project asset.
- The item menu has Open, Make project asset and Embed a copy.
- An embedded item carries a bullet in front of its name. The
  first pick was a diamond, which the gate's font test refused:
  the UI font has no glyph for it.
- A take is "Take 1", 2, … numbered per song. With the song gone
  while it uploads, it becomes a project sound under a free name.
- Left out of: the Assets panel, every asset picker, the sound FX
  menu of a track, the test sound of the Sound FX tab and the
  naming of new assets. Delete on an embedded asset only says
  where it lives. A ping lights up its song.
- Loading drops an `owner` whose song is gone, the asset stays as
  a project asset. No migration: older projects have no owner.

Tests:
- Unit embedded (new): 13 green, 6 counter-checks each red
- Unit take, song, song-edit, asset-fx, asset-ref, serialize and
  neighbours: 116 green
- e2e 65 (melektaus-atx): ALL PASSED, 47 checks (1 new)
- Counter-check e2e 65 (asset panel unfiltered): exactly that
  check red
- Gate (melektaus-atx): CHECK EXIT 0, 269 files, 4167 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:25 +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 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 101b12504e Play a sound backwards with a Reverse switch on the asset (#321)
A sound asset can now play its stretch backwards. The switch sits on
the asset, so every way the sound is heard plays it reversed: Play
sound, a song item, an instrument's Sample and a Pitch in a sound FX.
There is no Reverse node in the sound FX graph. The user dropped it,
because playing backwards needs the whole sound in advance, which a
bus or a Chip does not have.

- The cut gets a `reverse` flag, stored as the asset property
  `reverse` (default off). The cut name ends in `~reverse` when it is
  on, so a reversed sound over the whole file is a cut of its own.
- The handles keep marking the same part of the file. The reversed
  copy is made once, like any cut, and the fades follow what is heard:
  Fade in ramps at the end handle, Fade out at the start handle.
- The Sound tab has a toggle with a left-pointing play triangle. Its
  playhead runs from right to left, and the wave draws the ramps at
  the edges they now belong to. The inspector has a Reverse row.
- The icon is a tab header icon (`HEADER_ICON_SVG`, next to the snap
  magnet), not a top bar icon. The first gate run had it in the top
  bar set, and the test that fixes that set went red.
- A song item draws the waveform of a reversed sound backwards.
  Lengths stay as they are.
- The header label reads "Fade in" instead of "Fade in (ms)" and the
  fade fields are 8 px narrower. With the new button the readout was
  cut to "0.03 s of 0..." at 1280 px, now it shows in full. The
  inspector keeps the unit.

Tests:
- Unit sounds 22, sound-library 85 (1 new each). Counter-checks:
  without turning the samples round, without moving the fade in to
  the end, and without mirroring the peaks, all red.
- e2e 34: ALL PASSED, 72 checks (2 new). The reversed stretch starts
  with the last samples of the forward one, and the playhead ran
  0.75, 0.60, 0.45, 0.30, 0.15 over the whole file.
- Gate: first run red, 1 of 4031 (the top bar icon list). After
  moving the icon, CHECK EXIT 0, 260 files, 4031 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:08:37 +02:00
adminandClaude Opus 5.5 29b604dc07 Fade a sound's start and end apart, each along its own curve (#345)
A cut sound had one edge fade for both ends, always a straight ramp.
Now its start and its end each have a fade of their own, and each
fade has a curve of its own.

- Fade in (ms) and Fade out (ms) replace Edge fade (ms), 5 ms each by
  default. Like before they apply only to an edge inside the file.
- Fade in curve and Fade out curve each offer Linear (the default),
  Equal power, S-curve, Exponential and Logarithmic. A fade out runs
  its curve backwards.
- The Sound tab's header has a field and a curve dropdown per edge.
  Each dropdown shows its curve as a small picture, rising or falling,
  and names it in its tooltip. With two named dropdowns the header was
  too wide at 1280 px and cut off the readout on the right.
- The inspector shows the same four rows, the curves by name.
- The wave draws each ramp in its curve rather than as a line.
- A cut plays by
  <file>#<start>~<end>~<fadeIn>~<fadeOut>~<curveIn>~<curveOut>.
- No migration, as asked in the issue: the old fade key is no longer
  read, so saved sounds start with 5 ms each and Linear on both.

Tests:
- Unit sounds, sound-library: 96 tests
- Counter-check (fade out along the fade in's curve): 1 red
- e2e 34 (melektaus-atx): ALL PASSED - 69 checks
- Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3978 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:48:59 +02:00
adminandClaude Opus 5.5 6b49a0bea7 Give each account its own folder of uploaded files to delete (#344)
Uploaded images, fonts and sounds belonged to nobody. Every account
saw every file in the pickers, and only an administrator could
delete one, through the API alone. Now every account uploads into a
folder of its own, sees its own files and nobody's, and can delete a
file of its own that no project uses.

- Files are stored as data/<kind>/<folder>/<sha256>.<ext>, and the
  folder is part of asset.file. A name without a folder belongs to
  nobody.
- The folder is a random 12-character name kept in users.json and
  given with the first upload. Renaming an account moves no file, and
  a released game's URLs carry no account name.
- The list shows the caller's own files and nobody's. An
  administrator sees all, each with owner and deletable.
- DELETE takes the name with its folder. It answers 403 unless the
  file is the caller's or the caller is an administrator, and 409
  while a project, snapshot or release of any account names it
  (FileUsage). The 409 names the caller's own projects only.
- The pickers have a Delete button. The first press asks, the second
  deletes, and a refusal shows the server's reason.
- A deleted account's folder stays and only administrators see it.
- tools/migrate-file-owners.php moved every used file into the folder
  of the account using it (a copy each for two accounts) and rewrote
  projects, snapshots and releases. A project's revision goes up, so
  an open editor gets a conflict instead of saving old names back.
- Font families are named after the hash alone, so a folder never
  ends up in a CSS family name.

Run on stein2 on 2026-10-07, backup in
/root/config-backup-20261007/data-before-344: 25 files used,
27 copies, 8 projects, 53 snapshots and 1 release rewritten, 30
unused files stay nobody's. Afterwards all 600 references resolve to
a file in their owner's folder.

Found while building: the Delete button cut its new label to "Del…"
because a button does not measure itself again on a new text. The
picker now lays its button row out again.

Tests:
- PHP files.test.php (new): 46 checks, counter-check 3 red
- Unit core + editor: 220 files, 3403 tests
- e2e 34 (melektaus-atx): ALL PASSED - 67 checks (2 new)
- Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3977 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:25:13 +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 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 8a4e4a6762 Play sounds as notes of an instrument asset #331
game-maker / suite (push) Failing after 2m40s
game-maker / gate (push) Successful in 3m14s
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.

- New asset type instrument, under the heading Music, not placeable.
  A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
  only, the lowest C-1. Notes has From and To, outputs any, note,
  frequency and velocity, and with Single notes a trigger per note,
  each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
  can take any number of edges now (PortDef.many): a trigger, and
  the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
  through the pitch worklet of #317, so it keeps its length. Gate
  fades it out at the end of the note, One-shot plays through. Loop
  runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
  default. Sound FX sends the sound through a sound FX asset, its
  parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
  only for the notes it stands for, a silent source silences what it
  alone feeds, a Mix with a silent side is the other side. velocity
  and the parameters stay open for the voice. Builds are cached per
  note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
  with its own file and volume. release fades the gated ways out,
  one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
  detached, and hands out voice for Set sound FX parameters and
  Stop sound. Velocity is always loudness. Instrument parameters
  picks values for the instrument's parameters like Sound FX
  parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
  128 keys below. Keys without sound are grey. A key plays while it
  is held. Focused, the computer keyboard plays as in a tracker by
  KeyboardEvent.code, the arrows move the octave.

Findings:
- A label of the keys set fill on the shared small text style and
  darkened every small label of the editor. Caught in a screenshot
  before the commit, the keys have a style of their own now.
- Suite task 523 on cf6799a failed two checks of e2e 34 that timed
  audio by the page's clock: a 2.95 s echo tail was gone after 1 s,
  and Quad in was further along than 0.7 s allow. Both time on the
  audio clock now through a new hook __gm.sound.clock().

Tests:
- instrument.test.ts (new, 22): notes, asset, palette, round trip,
  compile per note, drum kit, zones, Mix, velocity and parameters,
  Sound FX node with rename, problems, cache, Play note, the keys
- sound-library +7 (layers, release, duration, early release, loop
  region in buffer and worklet), pitch-player +2 (loop region),
  node-catalog +4, graph adjusted. 582 tests in the touched files.
- e2e 4 and 34 on melektaus-atx: ALL PASSED, 34 with 44 checks (+7)
- Gate on melektaus-atx: CHECK EXIT 0, 243 test files, 3750 tests
- Counter-checks: any always on, release ending one-shots, Sound FX
  inputs ignored, loop region ignored. 3, 2, 1 and 1 tests failed.
- Browser probe on stein2: keys, held key, Single notes names and
  the header looked at in screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:07:40 +02:00
adminandClaude Opus 5.5 e7a360db73 Offer the easings of an animation key on Lerp and sound FX ramps
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Successful in 15m36s
A timed Set sound FX parameters ran only linearly, and a Lerp had four
quadratic eases of its own. An animation key already had the full set.
Now all three offer the same choice: Linear, Step, the ten curves from
Sine to Bounce with In, Out or In-out, and a drawn Curve asset.

- easingParams in src/core/graph/nodes/easing.ts gives a node the
  fields Easing, Direction and Curve from the key's own lists, and
  easingOfParams reads them back as a key interpolation.
- There is no migration. A Lerp saved with the old in, out or inOut
  runs linear until its easing is picked again.
- Set sound FX parameters hands the easing to the player. Linear and
  Step stay ramps of the audio clock, every other easing goes to
  setValueCurveAtTime as 200 samples a second, in semitones and dB.
- Step now reaches its end at t 1. A key never asked for that, it
  reads the next key itself, so animations play as before.
- The curve-in-use count and the warning for a gone curve cover the
  sound node too.

Tests:
- node-catalog: Lerp with cubic in, quad out and in-out, a curve
  without a direction, step, an unknown easing as linear. The sound
  node hands on a curve, step and a gone curve as linear.
- sound-library: an eased change becomes a curve in semitones and
  decibels, a step a jump at the end.
- node-catalog and curve after dropping the old values: 399, green.
- e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a
  2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the
  end, a 1 s Step held 1 and then jumped to 0.5.
- Counter-check: with curves sent as linear ramps, the curve test of
  the sound library fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:05:00 +02:00
adminandClaude Opus 5.5 ed2b58b3c0 Load the sounds of a project ahead of their first play
A sound was fetched and decoded on its first Play sound. That play
started late, its beginning was not heard, and a graph that tuned the
voice in the meantime had moved on by the time it started. Images and
fonts were already loaded when a project opens. Sounds now are too.

- projectFiles lists the sounds of a project, and preloadProjectFiles
  hands them to the new soundLibrary.preload. That covers the editor
  and the published game.
- Preloading opens no audio device. It decodes with an
  OfflineAudioContext at 48 kHz. The real AudioContext is still made
  on the first play, and a source resamples where the device runs at
  another rate.
- A run started right after opening a project can still find a sound
  loading. It then starts late as before.

Tests:
- sound-library: a preload makes only the offline context, and a
  play after it starts at once, opening the device only then.
- sound-library + project-files: 18 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 30 checks.
- Counter-check: decoding with the real AudioContext again fails the
  new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:00 +02:00
adminandClaude Opus 5.5 183f356c99 Let a game hand values to the parameters of a sound FX #311
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 14m15s
A sound FX played the same every time: its numbers were settings of
the nodes. Now the Input declares parameters with a default, every
number of Gain, Pitch and EQ is an input they can feed, and a Play
sound hands in values for them.

- Input declares number parameters in the inspector, each with a
  Default (DeclaredPort.default, SetPortDefaultCommand). They are
  outputs of the Input. Input and Output count as terminals now, one
  of each per graph.
- Gain, Pitch and EQ take their numbers at inputs under the keys the
  settings had, so saved graphs sound as before without a migration.
  Each value is held to its range however it arrives.
- The compiler gives an FxPlan with the parameters still open.
  fxChainOf fills them in when a voice starts. The player still only
  sees an FxChain.
- New behavior node Sound FX parameters: names an FX and picks its
  parameters like a Variables node picks variables. Each picked one is
  a number input that starts at the FX's default. Its output has the
  new port type fxparams, which only fits Play sound's fx params.
- Play sound warns when the parameters name another FX than the one
  it plays. Names the FX does not know are left out.
- Renaming a parameter at the Input renames it in every Sound FX
  parameters node naming that FX, edges and values included.
- The Sound FX tab has a number field per parameter in its header.
  Play uses those values, they are never written into the asset.
- A value counts from the start of a voice. Changing a playing voice
  is #315, a Mix node is #316.

Tests:
- soundfx.test.ts 22 (+11): parameters in the plan, held values, a
  0-by-default stage, EQ band, save with defaults, wrong port types,
  default command with undo, Sound FX parameters into Play sound,
  defaults of picked ones, node problems, mismatch, rename with undo
- node-catalog 368 (+3): Play sound fx params, Sound FX parameters
- e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5,
  a Sound FX parameters node gives the game's voice depth 7)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test
  files, 3636 tests green
- Counter-check: with fxChainOf ignoring the values and the rename
  not followed, 5 of the new tests fail.
- Browser probe: header fields, inspector Default and the picked
  parameter looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:26:20 +02:00
adminandClaude Opus 5.5 de14f0f9e2 Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.

- New asset type soundfx ("Sound FX"), not placeable, with a graph
  and its own tab. The heading in the asset panel is "Sound effects",
  so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
  that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
  EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
  emitter. An input takes one edge, so the result is one line: a
  pitch and a list of gain and filter stages. A node without input
  or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
  sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
  An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
  order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
  through the graph as it stands. Stop there ends only its own voice.

Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
  node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
  2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
  test files green. The red one was sound-library (its fake
  AudioContext had no playbackRate), fixed afterwards, gate not
  re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
  test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:45:42 +02:00
adminandClaude Opus 5.5 7b1796baac Let a container animation reach alike-named objects in another one
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Successful in 15m43s
An animation over a container aims its rows at template ids. Played on
another container, those ids are not there, so every row below the
root was skipped without a word. "Enemy - Hit" built over the Slime
flashed the slime's Sprite but never the skeleton's.

- When the template id is not found below the object, the runtime
  takes the names on the way to the row's target in the root and
  walks the same names below the object, level by level.
- The id still comes first, so the root's own instances bind as
  before.
- A name missing at some level, or taken twice, skips the row, as
  a gone target always did.
- The rule is the pure function targetNamePath in animation.ts. The
  AnimationSystem only walks the children.

Tests:
- animation.test.ts: 84 tests, all green (+2). One plays over a
  second container built alike, renamed and with the name twice.
  The other names a row two containers deep.
- Counter-check: with the name fallback off, the new runtime test
  fails (1 of 84).
- Project cbbf160bb7e8 (revision 14407), Demo scene: "Enemy - Hit"
  and "Enemy - Die" change Flash and scale on the skeleton's Sprite.
  Without the fallback the skeleton stays unchanged.
- The gate did not run before this commit. CI covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:16:42 +02:00
adminandClaude Opus 5.5 3e05cbbed5 Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing
and sorting already read both off the TextureRef every picture hands
out, but the generated pictures always said "no slice, no line". So
the feature moves from the frame to the picture, without a second
copy of any rule.

- PictureCut (border, fill, sortLine) is one type. TextureFrame
  extends it, PixelArtAsset and VectorArtAsset carry it as a whole.
  The texture file format does not change, and no migration is needed.
- The rules in texture.ts take a cut and a size: cutSortLine,
  setCutSlice, rawCutSlice and the rest replace the frame* versions.
  serialize.ts reads all three through one readCut.
- cutTarget.ts resolves a target to cut and size: a texture frame, a
  pixel frame's size or the vector canvas. It lives apart from
  texture.ts because vectorart.ts reaches texture.ts itself.
- SetSliceCommand and SetSortLineCommand take that target instead of
  (assetId, frameId). They refuse an asset that draws no picture.
- generatedRef fills slice and sortLine from the drawing. The whole
  strip of a pixel art asset with several frames gets none, it is no
  picture of that size. With one frame asset and frame are the same.
- Nine-slice scale is one shared schema row, now also on pixel art and
  vector art.
- cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame
  inspector and the drawing inspector both build them.
- SheetCanvas draws a drawing's lines that are set, dashed as in the
  Texture tab. They are not dragged there, the painting tools own the
  canvas (as proposed in the issue).
- drawOrder falls back to the picture's height when a ref has no
  rect, or a sort line on vector art would have been ignored.

Tests:
- Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector
  art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a
  drawing, a cut on the whole strip, no height fallback.
- e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where
  12-pixel borders at scale 2 put them. Without borders both spots
  read green.
- Looked at: the Pixel art tab with the lines, and the sliced sprite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:53:51 +02:00
adminandClaude Opus 5.5 d9c6bbfa4d Add a node that names the render layer of an object (#305)
game-maker / gate (push) Successful in 3m14s
game-maker / suite (push) Failing after 14m18s
The issue asked for Set render layer, Set sees and Render layer of. As
with #211, a probe showed that writing already works. A Properties
node of the object's type writes Render layer during a run, Inherit
included, and one of type Camera writes Sees. The next picture follows
both. As agreed, only Render layer of is built, and writing stays with
Properties.

- Render layer of is a data node with the input object and the outputs
  name and number. It answers the layer that holds, so an object
  without a choice of its own names its parent's.
- NodeContext.renderLayerOf answers the number with the same rule the
  drawing uses (renderLayerAlong). The new field renderLayers on
  GraphLookup brings the names to the node.
- hasRenderLayer in PropertySchema says which types are drawn.
- Decided here: for an object that is not drawn (a camera, a
  behavior, a collider) the name is empty and the number -1, as Layer
  of does without a collider.

Changing the render layer or Sees during a run had no test so far.
Three unit tests now hold the proof from the issue.

Tests:
- Gate: 231 files, 3534 tests (3526 + 8), exit 0
- e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1)
- Unit: the coin moves into the overlay with Set and back with
  Inherit, a HUD container takes its sprite along, the main camera sees
  a second layer once its Sees is set, and Render layer of names the
  inherited layer, and none for a behavior
- Node catalog: four cases for Render layer of
- 63c3 moves a sprite onto UI in a Play run: the main camera no longer
  draws it, and the log reads "UI" and "1"
- Three counter-probes fail 1, 1 and 2 tests: without inheriting in
  the node, without the check for a drawn type, and without
  inheriting in the drawing
- "layer of" in the palette still finds Layer of first (6z-x)
- The e2e sum is 1731 (1730 plus 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:25:47 +02:00