166
Architecture
MelekTaus edited this page 2026-10-18 11:09:00 +02:00

Stack

  • PixiJS v8 — rendering and pointer events. No game engine; runtime systems live in src/runtime/. main.ts creates the WebGL context itself with desynchronized, which Pixi has no option for: frames skip the compositor queue, so the UI keeps up with the pointer (click to picture measured 72-77 ms before, 16-18 ms after). The price is possible tearing while things move fast; ?vsync falls back to Pixi's own context. A browser without the mode simply ignores it. ?w=1920&h=1080 lays the canvas out at that size in CSS pixels instead of the window's (ui/pageSize.ts). A smaller window shows it shrunk to fit, the editor and a released game alike.
  • Rapier 2D (@dimforge/rapier2d-compat, WebAssembly) — the physics engine. Pixi draws only, so bodies, forces and contacts come from here (#144). It is a chunk of its own (about 790 KB gzip) that runtime/physics/engine.ts loads once in the background after the editor has opened its project, so the first picture never waits for it; a failed load is reported in the console and not retried. Outside the engine everything stays in pixels, inside 100 px are one metre (toMetres/toPixels).
  • Clipper2 (clipper2-ts, Boost licence, no dependencies) — polygon clipping for the boolean operations on vector art (#187). Only its engine is imported (dist/Engine.js, dist/Core.js), not the package's Clipper namespace, which would bring offsetting, triangulation and Minkowski sums along: about 50 KB of the bundle instead of 101 KB, minified.
  • Own canvas widget toolkit (src/ui/) — panels, buttons, lists, text/number/color fields, popups. The only lasting DOM element is a hidden <input> used as a keyboard bridge (IME, mobile keyboard, clipboard) while a canvas text field is focused; Upload… adds a short-lived file input. Columns of rows are grouped by bands (#290, ui/sections.ts): a SectionStack lays a SectionHeader across its padding and the rows after it inside, and hides the rows of a folded header down to the next one (hiddenBySections, sectionOffsets are the rules, as pure functions). A SortableList with a title opens a group the same way and draws its band past its own edges. The toolkit only folds; which groups are folded is kept by the editor (editor/sectionFolds.ts, one entry in localStorage), and so is where each divider was dragged to (editor/panelSizes.ts, a second entry: a name per divider and its width, height or share, read once when the panel is built and held to the bounds the divider keeps). Every title of the inspector goes through addTitle or sectionHeader in inspector/host.ts, so a new section gets the band by itself. Text and boxes sit on device pixels (ui/pixelGrid.ts). The layout counts in whole CSS pixels, and on a screen scaled to 125 % or 150 % most of those fall between two device pixels: the GPU averages both, text goes soft and a 1 px line becomes a pixel and a half of mixed colour. So every text of the interface is made with crispText() instead of new Text() — it sets Pixi's roundPixels, which rounds the corners of what is drawn to device pixels — and a Graphics that holds nothing but upright boxes is passed through crisp(), which does the same where the scale is not a whole number and nothing at 100 % or 200 %. A curve, an icon or anything of the game must not go through it: rounding its points bends it. A line is drawn hairline() wide, one device pixel as a browser draws 1px, and flush with the edge it belongs to (h - hairline(), not h - 1); drawControlBox, drawHeaderBar and drawSectionBand do both already. At 150 % every odd pixel is an exact half, and which way the GPU rounds a tie is decided by the last bit of its arithmetic — one band came out a row shorter than its neighbours — so the stage is nudged by 1/32 px (tieBreakFor), which sends every tie the same way. What this does not give is the subpixel smoothing a browser applies to its own text: Pixi draws a text into a transparent picture first, and there only greyscale smoothing is possible. Tooltips (#209). A Button takes a tooltip — a text, or a function asked each time — and shows it on a small sign once the pointer has rested on it for half a second. A button without one says its own label where that had to be cut short, which is how a tab with a long name shows the whole of it. There is one sign for the whole page (tooltips, a TooltipLayer in ui/widgets/Tooltip.ts), on the overlay above the menus: a control reports each pointer move over itself (point) and the pointer leaving (leave), the layer does the waiting. A press, a key or the wheel takes the sign away (initUI listens on the window), and the control stays quiet until the pointer has left it. placeTooltip in ui/tooltip.ts is the rule for the place, a pure function: under the control and flush with its left edge, flush with its right edge where the sign would leave the screen on the right, above the control at the bottom. A widget that draws its own marks reports them the same way with a key per mark — the eye, the duplicate mark, the cross and the plus of a SortableList. What a sign says is decided where the button is made: toolbarTip for the top bar ("Undo Paint tiles (Ctrl+Z)", from CommandStack.undoLabel), rowActionTip for the row actions of the inspector (the one thing a click would do, or the name of the group once it opens a menu). A button that is greyed out takes no pointer events and so shows no sign. __gm.tooltip() reports the sign on screen with its box and the box of its control; the text hooks (locate, has) do not see it. Double-clicks are counted in clicks.ts, not read off the event: Pixi only puts a click count on the click it synthesises and leaves detail at 0 on pointerdown, so every press handler that trusted e.detail never saw a second click. Presses are counted per key rather than per widget, because a list rebuilds its rows when the first click changes the selection. Menus read as lists: entries are left-aligned and scroll once they are taller than there is room for, instead of running off the screen. The room is the canvas, not a constant (#43): Popup.roomFor() takes the window height, less a tenth of it at each end, less the chrome around the scrolling part — and it measures that chrome (a search box, a header, a row of buttons) rather than repeating it as a number, so no popup can forget one. A fixed cap would waste a tall screen or overflow a short one, and a constant gap that frames a popup on a laptop is a hairline on a large screen. It is what lets the image, font and sound pickers show six rows of what the server has instead of two. A submenu (MenuSubmenu, an entry with submenu instead of onSelect) is a second Menu built with under set, opened right of its menu (left of it where the canvas ends), and without a backdrop or Escape of its own: a backdrop would cover the menu below it, and the first menu's backdrop and Escape close the whole stack. Picking an entry closes the stack from its first menu. A popup that does not fit below its anchor is still moved up rather than squeezed, and clampPopupY() enforces the tenth while moving it — otherwise the gap ends up entirely above it and the popup sits flush on the bottom edge, which is exactly what it used to do. Upwards that tenth is a budget the height cap spends, not a distance to keep: a menu hanging off a button near the top of the screen belongs under its button, so nothing pushes it down to reach the margin. Below is the rule, above is what is left over. A menu that grows with the project — the parent picker lists every object of the scene — is opened with { searchable: true }, which puts a filter box above the entries; that search ranks by ui/search.ts and Enter takes the best match, the same rule the node palette uses. Entries can be grouped with { separator: true } — a line, dropped when a search flattens the list or when it would open, close or double up the menu. A field that opens a menu is a Dropdown: value flush left, chevron drawn flush right rather than written into the label. A Dialog (a question with buttons) takes Escape as its plain button, the one neither primary nor danger: Escape and Cancel lead back to the same place. Every label that can be wider than its control is cut with an ellipsis (truncate in Label.ts, shared by Label, ListRow and Button). Prose is the exception: a Label with wrap breaks into lines at the width it gets, so a node's description and every inspector note (addNote) show in full. Its height depends on that width, which measure() cannot know. Widget.heightFor(width) answers it instead: VStack and SectionStack lay a column out with it, and ScrollContainer asks once more at the narrower width when its scrollbar shows. Every other widget answers with measure().h. A Button keeps the full text and measures from it — only what is drawn is shortened — and a subclass that puts something else inside the control takes its room off labelSlot(), so a long value is cut rather than run underneath the chevron. Given a width, a button is exactly that wide and truncates instead: the Animation root and Tile kind pickers keep their size whatever they show. The tab row has no floor at all: it is label-wide at half the theme padding, a document's name stops at 160 px (DocumentTab), and TabStrip drops what does not fit into an overflow menu rather than letting it spill (the rule is tabRowLayout in editor/tabRow.ts, so it is checkable without a browser; __gm.tabRow() reports what is in the row, what went away and the whole order). A button that can draw its label bold — on an accent fill: primary or danger — is measured bold, or a dialog's Delete comes out as Del…; the style it draws and the one it measures come from labelToken() and measureToken(), which a subclass overrides (TabButton draws and measures plain). The box behind the label is drawBackground(), likewise overridable: TabButton paints an index card there, rounded above and square below (#176). The padding is an inset rather than a claim, so a 24 px icon button spends it on the icon instead of truncating it away; a row action that switches between its plain and its menu form (the reset picture alone, or with a chevron) asks for minWidth and grows by the chevron's room (trailingRoom(), which Dropdown uses for its chevron too). __gm.textOverflows() guards the whole class of bug: it reports any visible text drawn wider than the widget owning it, __gm.labelsDropped() guards the other direction — a button too narrow to draw any of its label (e2e step 14).
  • Vite + TypeScript, Vitest for core, toolkit and editor logic.
  • PHP 8 JSON API (api/) storing one JSON file per project in data/projects/, with atomic writes and an integer revision that rejects stale saves (HTTP 409).

Layout

web/index.html         Vite source page (dev)            index.html   built page (generated, served)
web/play.html          a released game (#266)             play.html    built page (generated, served)
src/main.ts, play.ts   the two entries; page.ts is the canvas both are (the WebGL context, root, resize)
src/core/model/        Project (object holders), Asset + AssetTypes, SceneObject (and what a list calls one:
                       `objectName`, `groupName`), Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines, and the nine-slice and sort line of any picture, #308), cutTarget (which picture a nine-slice or sort line is set on: a texture frame, or pixel art and vector art as a whole, #308), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), video (#402: a video or a clip of it by id, the stretch a clip plays inside its video, and the key `video:<file>#<start>~<end>~<loop>~<volume>` a playing video goes by where a picture is named by `TextureRef.file`), composition (#417: objects along a time axis in frames of the composition's own fps; `compositionAt` is the one rule of what runs at a frame, for the tab and the game alike, `compositionHolder` writes it out as objects for a view) + compositionEdit (what the Composition tab does to items and clips, as plain functions on lists: items of a track may overlap, clips may not, and where a paste lands), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by), renderLayers (#302: which cameras see an object — the project's named render layers, an object's own choice or its parent's, a camera's `Sees` and `Overlay`)
src/core/properties/   PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), joint (what it reads off a joint: its settings, where its far end is, its length and a hinge's limits, #154), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), videos (#402/#404: a video's rows and a sprite's Video clock, time and volume), compositions (#417: a composition's rows, apart from its model), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/util/         list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips; a cubic Bezier at `t` and cut in two at `t` — shared by vector art, its Boolean operations and the curve grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance), bytes (`concatBytes`, shared by the Ogg and Matroska writers)
src/core/commands/     undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), MoveCommand base (undone by moving back: tile picture, tilemap layer, vector shape), vectorArtCommands (#129: the shapes of a drawing, one gesture one command), compositionCommands (#417: tracks, items and clips written whole once the pointer lets go, an item together with its object) + compositionSprites (#419: the sprites a composition keeps to itself, held to that after every edit), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/graph/        graph model, NodeRegistry/NodeContext/values (a travelling list may hold live objects, #157;
                       liveObjects' `withoutObjects` empties them on every way out of the run, #238), interpreter, dropTarget, nested (behaviors as nodes),
                       emitGraph (the walk both graph compilers share: each node once, in dependency order; cycles and property names reported),
                       sharedState (what a run shares: a bag per Variables asset, the state of a global machine; what a scene change carries forward and what it starts afresh, #164),
                       eventOrder (#264: the Order setting of event nodes and how the pulses of one event kind are sorted),
                       portTypes (#207: ports of one node that carry the same value, and the type their inputs agree on),
                       savedValues (#166: the SaveArea a run reads persistent Variables assets from and writes them to, and restoring a save through today's declaration),
                       stateMachine (the State node, a machine's states, the ports they become), variablePorts, propertyPorts,
                       typedValues (#271: what fits a struct type, and a written variable held to its declaration),
                       lookup (what a node may look up: behaviors, state machines, struct types, what a variable declares), keyRepeat; nodes/{events,flow,data,action,variable}/<Name>.ts,
                       nodes/shared (the ports several nodes share: `checkPort`, the sound pickers, `detachedPort`,
                       `fxParamsPort`, `playedBusPort`, `letRingPort`)
src/core/particles/    nodes (the catalogue), compile (a graph → one function per output),
                       emission (which particles are alive at a second — a pure rule, no state)
src/core/store/        serialize (untrusted JSON → Project), projectPlugins (which plugins a project switches on, read off the raw
                       file before it is deserialized, #119), tilemapCells (the compact form of a layer's cells in the file),
                       http (one API call, `jsonBody`), ProjectStore/HttpProjectStore (load takes a `prepare` that sees the raw JSON first; a 409 becomes `StaleRevisionError` through `staleRevision`, and restoring a snapshot answers the same way),
                       SnapshotStore/HttpSnapshotStore (the snapshots of a project, #231),
                       ReleaseStore/HttpReleaseStore (a project's released copy and the link to it, #266),
                       DemoStore/HttpDemoStore (the demo copies a guest picks from, and an administrator's half: making one, #428),
                       MemoryProjectStore (a guest's project: the demo copy, held in the tab and saved nowhere else, #428),
                       AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182),
                       FileStore (what the server holds of one kind: upload and list, no delete) + HttpFileStore (one
                       generic client over the four file collections) — TextureStore, FontStore, SoundStore and
                       VideoStore (#402: an upload with its status on the way to an MP4) are its type
                       parameters, nothing else
src/core/time.ts       the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
src/core/music/        time (ticks, 480 to the quarter, tempo and meter, a position as bar, beat and tick), sequence
                       (the notes of a window of playing time, across the seam of a loop — a pure rule), Scheduler
                       (#332: plans the notes ahead on the audio clock through a host, so it is measured with a clock
                       of the test's own; a song's sound items as clips, taken up mid-way where the playhead
                       lands, #334), pattern (#333: a pattern's notes, grid, meter, tempo and loop, held to
                       their rules, and the sequence it plays), song (#334: tracks and items held to their rules,
                       the sequence a song plays, the level of a track's bus, an item cut at a tick),
                       routing + routingNodes (#350: a song's routing graph, a node per track and the output;
                       `addLink` is the one rule for a link both routings keep), projectRouting (#351: the
                       Master routing, a node per sound, instrument, pattern, song, video with sound and bus
                       and the master; a new asset takes the links most others of its type have), songBuses
                       (the buses a song plays on, mute and solo as gates), mixer (the master bus and
                       `mix:<id>`, `videoBusesOf` the bus of each video file), needs (a
                       sound FX with its parameters, or none), playbacks (what a sequence loads before it
                       plays), embedded (#355: what a song keeps to itself), ghosts (#339: the notes around a
                       pattern item), take (#342: a recorded take as 16-bit WAV, cut to where the song started),
                       bake (#340: what a bake is made from as a print, where the parts of its file lie, how they
                       are put together from the playing looped and the one played once, and played back) +
                       ogg (#340: Opus in Ogg, the pages around an encoder's packets and back, also read as
                       the bytes come in, #349), streams (#349: which names stream from which copy, which
                       clips decode from their own, what copies are missing, how a played sound walks its copy) + streamQueue (#349: the
                       samples a streamed sound plays from on the worklet, read at the rate of the context),
                       compositionAudio (#418: the audio items of a composition as sequences on the audio
                       clock, each on a bus of its own, its fades ramps of that bus' level)
src/core/recording/    recording (#405: what Start recording asks for and the rules a recording is made by —
                       size, name, H.264 profile, when a frame is taken, the steady pace of one not in real
                       time, #423), matroska (the `.mkv` writer: the encoders' frames and Opus packets in
                       clusters as they come, the head at the end) + avcColour (the colour written into
                       the stream's SPS, where a player that reads only the stream finds it),
                       editorRecording (main menu → Start recording…: what the dialog offers and the size
                       and sharpness a choice comes out at), cursors + cursors/ (the mouse pointer an editor
                       recording draws, one SVG per shape with its hot spot)
src/core/site.ts       the path the site is served under (the folder of the open page: `/feuerwerk/` or `/`) and the data paths built from it
src/core/soundfx/      the sound FX graph (#311): nodes (every effect and what it adds to a plan), compile (the
                       graph walked back from the Output to a plan; a parameter stays open until a voice
                       starts; a tapped node gets a `tap` in front and behind, #374), parametricEq (#367:
                       the bands, the curve Web Audio plays of them, and its axes); the
                       effects the FX worklet runs, plain classes tested in Node: pitchPlayer (#317/#346, a
                       phase vocoder), chip (#329), crusher (#322), phaser (#341: Phaser and Flanger),
                       dynamics (#326), lfo (#320: Tremolo and Vibrato), acid (#421: the
                       Acid's saw or square through a resonant low-pass, sample by sample); reverb (#328) and saturation (#323)
                       for the Web Audio nodes that do theirs
src/core/instrument/   the instrument graph (#331): nodes (Notes, Sample, Parameters, Sound FX, Envelope,
                       Chip, Acid),
                       compile (one note walked back from the Output, its triggers and sources), notes (MIDI
                       numbers and names), envelope (#335), chipPresets (#329: Preset, Randomize and Mutate the
                       way sfxr makes its sounds)
src/runtime/           GameRun (one run: start, tick, `Load scene` — shared by Play and a released game, #266),
                       World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection),
                       solids (what `Move` collides with and how it slides — one-way solids let it through unless
                       it lands on them —, the first solid a ray meets, which body a collider belongs to, the
                       CollisionSource both collision systems give the behaviors, the contact of a staying pair,
                       and the broad phase of #184 — a move, a ray and the detection ask a body only for the
                       shapes inside the rectangle they can reach), AnimationSystem, BehaviorSystem (the instances, event
                       order, state changes and saves) + behaviorPulses (the pulses one frame's events send into an
                       instance: keys, timers, collisions, finished animations) + behaviorContext (the `NodeContext`
                       a node is handed: world, collisions and rays, physics, sound, the clock),
                       Audio (the AudioSink a run plays into — the counterpart of Input on the way out),
                       audioSwitch (#423: the run's sound goes to the live sink until a recording not in real
                       time diverts it into its own), CompositionSystem (#417: the compositions playing, the
                       objects of their items and the videos they drive) + compositionSound (#418: the sound
                       of one composition, started again where the picture is after a hitch)
src/runtime/physics/   engine (loads Rapier once, reports its state, converts between pixels and metres),
                       PhysicsSystem (Play's collisions: one engine body per rigidbody, free collider and tilemap,
                       the fixed step, the poses handed back, the contact pairs sorted into events and their
                       normal, impulse and point; whether a dynamic body lands on or passes a one-way collider,
                       decided before each step for the contact hook; the push a `Move` gives a dynamic body, and
                       what the physics nodes reach through `NodeContext.physics`), oneWay (the rule itself:
                       whether a body lands on, is held by or passes a one-way collider — pure, so it is measured
                       without the engine), bodyGeometry (a world shape as the engine colliders of a body, and the
                       poses handed between the two), joints (one engine joint per joint entity whose object has
                       a rigidbody, and a body of the joint's own for an end that has none, #154)
src/ui/                widget toolkit (generic, knows nothing about assets); fonts (the faces the editor ships:
                       Source Sans 3 in 400, 500 and 600, Source Code Pro 400, Latin only, #202/#214/#273), icons +
                       Icon (the menu's tick boxes and current-entry dot as SVG files, and a widget that shows one file
                       on its own: a symbol the Latin subset lacks is never written as a character, #214); SortableList + sortable (the layer list of every editor that has one: rows dragged into order, eye, duplicate, cross, rename in place; the index arithmetic of a list shown upside down, of a drop in the tile strip and of blocks of rows of any height, kept apart so it is measured without a canvas); blockDrag (`sortBlocks`: the inspector's own rows dragged into order by a `Grip` (the row's mark, wrapped: dots once it drags, lit under the pointer while the block is washed), a block being every row it covers, #310); TextPool (the texts of a ruler or a row of names: made as they are wanted, a text that
                       already shows a string keeps it, #360), download; Knob + knob (#378: a slider bent into a circle, its name above and a number field below, or a chrome dial of any size, #379; where a value sits on the turn and what a drag or the wheel makes of it, kept apart so it is measured without a canvas)
src/viewport/          WorldViewport (the drawing of a world or a holder, the base of every view; it draws from
                       a `ViewSource` — the project and word of a change — so a released game draws without the
                       editor, #266; a view through a camera keeps only what that camera `Sees` and lays one
                       see-through view per overlay camera over itself, each handed its view and frame and
                       drawn like any other, #302), gamePointer (the mouse handed to a running world, in world units and on the main camera's picture, which `World.pointerThrough` turns into any other camera's view with `pointerThrough` in `core/properties/camera`, #304), projectFiles (the file
                       libraries pointed at the server, and what a project names fetched ahead),
                       fullscreen + FullscreenCorner (the browser's Fullscreen API, when the corner button shows,
                       and the button itself, shared by the Game tab and the released game, #267),
                       view (transforms), items (what to draw), symbols (how to draw it),
                       symbolIcons (the SVG files in icons/, parsed once, in the theme's colours, #175),
                       FileLibrary (load once, never retry a failure, tell the listeners) ← TextureLibrary, FontLibrary;
                       generatedPictures (the drawn half of the picture path, #185: one picture per asset kept
                       under its revision key, the older one freed when the newer is drawn — or, while a shader
                       still names it, right after the redraw that asked or on a later ask, #293: the mesh of an
                       object with a shader asset keeps its picture bound until it syncs, and a source destroyed
                       under it made Pixi warn "destroyed while still bound to a shader" with every change to the
                       drawing; `TextureLibrary` tells by the bind group listening for `change` on the source,
                       of the picture or of any frame cut from it, as a sprite wearing pixel art names a cut; who
                       draws which asset type, and the factory that looks an asset up in the open project) — no
                       GPU, so it is measured without a browser,
                       vectorArtPicture (the rasteriser of #129: the draw commands of
                       core/model/vectorPath painted onto a render texture of the canvas size, at twice its
                       resolution; registers vector art in both halves of the picture path; PaintCache keeps a
                       painter's gradients and hands the ones it no longer draws to Pixi's texture garbage
                       collector, and a texture fill waits for its picture instead of drawing without it, #186),
                       VideoLibrary (#402: one `<video>` per key, its picture a Pixi texture made only once
                       the video has shown its first frame (`openVideo`), a clip sent back to its start at
                       its end, a run that starts every player over and hears it through the master, a
                       player nobody asked for in 2 s let go) + the poster library of the video picker,
                       videoFollow (#404: how a driven video keeps up with the time its wearer asks for, playing
                       at that speed and nudged, holding and seeking only where it stands or jumps) + videoSeek
                       (only the newest wish waits while a seek runs),
                       GameRecorder (#405: the game view recorded by a behavior — the picture read back from
                       the GPU, `PictureReader`, encoded in a `RecordingTake`, the sound from a tap of the
                       master or, not in real time, from `SteadySound` on an offline context, #423) +
                       backgroundBeat + backgroundTimers + timerWorker (a recording goes on behind another
                       tab: a worker's beat ticks the editor and a worker's timers keep their pace),
                       PixelCels (the pixels of pixel art in memory: a painted cel is named at once, the ones the
                       server lacks are uploaded before a save, #130) + pixelArtPicture (every frame composed on the
                       CPU and laid side by side onto one canvas, so a frame is cut out like a texture frame),
                       SoundLibrary + fxNodes (what each stage of a sound FX becomes as Web Audio nodes, and
                       how the knobs of a playing voice move) + spectrum (#374: the taps of the voices and
                       buses playing, and the analysers they feed while a node is listened to, several nodes at once, and the
                       loudest sample of the last stretch on each side, #378) + fxWorklet (the AudioWorklet of the effects Web Audio has no node for: a
                       `Source` plays a sound of its own, a way with a Pitch, #317, or a Chip, #329; an `Effect`
                       sits between two stages, the pitch of a bus, #346, a Bitcrusher, a Phaser, a Flanger, a
                       Limiter, a Tremolo or a Vibrato; `Start` keeps either silent until its voice starts,
                       #332; what each of them works out is in core/soundfx; a note of an instrument is one
                       voice of what reaches its Output, each way with its own file, #331; a voice can start and
                       be released at a second of the audio clock, #332; a clip of a song from a second into its
                       sound until its end, a bus per track of a song with its level and pan, #334; the buses of
                       the mix and the Master routing, #325/#351; a bake as one voice on the master of its
                       playing, and a library of its own on an offline context to bake with, #340; a channel
                       probe that says whether a bake is mono; a sound or a bake streamed from its copy by a
                       `stream-player`, #349), streaming (#349: a copy fetched by Range, its head decoded
                       ahead, the rest decoded by WebCodecs a few seconds before it is heard),
                       bakeRender (#340: a pattern or song played
                       offline, what reaches its master taken, encoded as Opus with WebCodecs), music (the scheduler on
                       the library's clock, its notes played by the project's instruments and its clips by
                       the library: the player of the Pattern and Song tabs, #333/#334, and `__gm.music`) + musicTimer
                       (the Web Worker whose interval is the scheduler's beat, #332), takeRecorder + takeWorklet
                       (#342: the microphone on the library's clock, its samples with the frame they were taken
                       at), ShaderLibrary, ParticleLibrary,
                       particles (one ParticleContainer per source), tilemap (one container per layer),
                       shaderPicture (the picture a shader is laid over when it shades a camera's frame, #299, or
                       a particle field, #170: drawn anew each time into a render texture kept while its size
                       holds, the replaced one freed once the shader holds the new; cameraShade and the field
                       shader both draw through it), shaderPasses (the pictures of a shader's Pass nodes, #301),
                       quadPadding (the quad a shader's Padding grows, and the plain unit quad without one),
                       drawOrder (objects and tilemap layers by z, #174, rows of a layer sorted by Y, #228), activeCamera — no editor concepts,
                       so tests reach it freely
src/editor/            App → Editor (layout + ticker), PlaySession (the run),
                       ProjectSession (load/create/delete plus the top bar's one menu, #183),
                       OpenProjectDialog (the project picker behind "Open project…"),
                       SnapshotsDialog + snapshots (the list behind "Snapshots…" and the rule of when a
                       snapshot may be made: only with everything saved, #231),
                       ReleaseDialog + releases (the link behind "Release…", what the dialog says and warns about, #266),
                       ProjectSettingsDialog + settingsChanges (name, the start-scene switch and the collision
                       layers in one dialog; the pure rule of what Apply has to carry out),
                       GeneralSettingsDialog + generalSettings (#309: what the editor does for the person at
                       the screen, one entry in `localStorage`), viewTween (#309: a wheel step or Fit as a
                       transition; `tweenView` is the way between two views as a pure function, `ViewTween`
                       drives it on the ticker, and each zoomable surface hands it reading, writing and
                       drawing its own view; `axisTween` is one for a single axis, as the music tabs,
                       whose axes zoom apart, keep one per axis, #377),
                       EditorRun (run in editor), state, actions (what several places do alike: `removeSelection`
                       is Delete for every kind of selection, one case each, `removeShapes` the shapes of a vector
                       art), shortcuts, clipboard (#240: copy and paste of
                       objects and graph pieces — where a paste lands, into which parent, what a graph may take;
                       the snapshot is `core/model/clipboard`, the paste `core/commands/pasteCommands`),
                       objectGroups (#241: the group entries of the object menu and the group row's menu;
                       the commands are `core/commands/groupCommands`),
                       debugHook (installs `window.__gm`;
                       the hooks are grouped by area in debug/: ui, scene, graph, documents, pixel art, vector art,
                       render, sound, pattern, song, routing, bake, video, curve — `tools/` sees one flat object),
                       textProbe (finding widgets by their text — the ground floor of the test hook, shared with the sign-in screen),
                       auth/LoginScreen + auth/PasswordDialog + auth/loginRules + auth/FormStatus (the sign-in screen and the change-password dialog in the canvas, what either may complain about, and the warning line and busy state both show, #177/#182), session (what a guest or an account may do, `sessionAllows`, #428), guestStores (a guest's stores: they list what the server offers and refuse every write before a request is made, #428), autosave,
                       agent/ (#429: AgentLink, the tab's WebSocket to the MCP relay with its retries; agentView, what
                       `get_view` answers and the one line the agent is told when it changes, as plain functions;
                       agentTools, the tools of tools.json, which the relay reads as well, checked by argCheck;
                       commandCatalog, the commands an agent runs, each with its argument schema; editPlan, AgentEdit,
                       the steps of one call as one undo step, built one after another, all or nothing; readTools,
                       list_assets, get_asset, get_graph and catalog as plain functions of the project; editorToolHost,
                       the editor behind the tools and the question before a delete; screenshot; AgentTokensDialog),
                       polygonEdit (points of an outline: what is under the pointer, whether a point is inside,
                       put / move / take away — scene view and Texture tab; and the same for a ring of a vector art
                       shape, where a point may carry Bezier handles: `ringGripAt`, splitting a curved segment
                       without changing the curve, the four point types and how a dragged handle moves the other,
                       #129, #195), pointGrips + PointKindButton (a point with its handles as drawn, and the
                       header's point type — shared by the Vector art and Curve tabs),
                       fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport / videoImport,
                       FilePicker (the shared "+ → Texture / Font / Sound / Video" dialog; ImagePicker/FontPicker/SoundPicker/VideoPicker only describe their row;
                       the video one shows a conversion on its way and asks again until it is done, #402),
                       video/ (#402: VideoEditor + VideoCanvas, the Video tab — preview, ruler, clip strip and
                       track on the time axis of the Sound tab's `waveView`; videoInfo, what a video is in words),
                       EditorRecorder + EditorRecordingDialog + editorRecordingSettings + recordingUploads
                       (main menu → Start recording…: the whole editor recorded with the pointer drawn in,
                       the last choice kept in the browser, the file's way into the project),
                       editorCursors (the editor's pointer from the files a recording draws),
                       framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time; `dialogButtons` is the closing row every dialog shares, the pickers' included), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab — asset tabs, the plugins and accounts lists, NodeBoard; drawn by `drawHeaderBar` like the toolbar and the breadcrumb),
                       TabStrip + tabRow (the tab row and what it does when the tabs outgrow it; plugin tabs are inserted at run time),
                       assetTabs (which asset kind opens into which tab, and so which kind has an editor at all),
                       TabButton (how a tab is drawn: the card of the one showing, plain weight, no pressed fill, #176),
                       DocumentTab (a document's tab: kind, name, close cross, a right-click menu), documents (#171: where a new document's tab goes, which one shows
                       after a close, a tab going on to another document — pure, so checkable without a browser),
                       FloatWindow + floating (#371: a document's pane lent to a window above the editor and given back on Dock; where a window opens and how it stays on the screen; drawn by `drawFloatingBox` like a popup).
                       One editor per kind serves every open document of that kind: `Editor` points it at the one on
                       screen (`AssetTabEditor.showAsset`), and it keeps each document's view through `saveView`/`restoreView`
src/editor/plugins/    startPlugins (list, import, host; runs before the first project) → loader (PluginManager: syncTo makes the
                       running plugins match what the project asks for, order, checks, what became of each file), pluginApi
                       (API v1, the per-plugin scope), PluginsPane (the list of plugins), NodeBoard (api.ui)
src/editor/users/      the user management (#179): ManagementPane (the tab behind Menu → User management / Plugins,
                       a document named `management` rather than a fixed tab; accounts above, PluginsPane below),
                       UsersPane (one row per account, its projects with Open, the actions; reads the list again
                       after every change), AccountDialog (the one form class for new user, rename and password),
                       accountRules (what the forms say before sending, which buttons a row offers — pure).
                       The store is `core/store/UsersStore` over `?r=users`; a refusal arrives as the
                       server's own sentence. The server decides everything: `api/lib/AccountRules.php`
                       (not the own account, not the last administrator, not an account with projects),
                       `Auth::requireAdmin()` (403), `Store` with `seesAll` for an administrator
                       (reads and saves every project, deletes only their own: `NotOwnerException`, #272)
src/player/            GameScreen (the game on the page: Auto zoom, the caption before and after a run),
                       Player (loads, starts on a click, ticks, stops), playerHook (`window.__feuerwerkPlay`
                       for the check scripts) — a released game (#266), without the editor
src/editor/viewport/   FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas;
                       GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/.
                       All of them extend `src/viewport/WorldViewport`.
                       A pan only moves the world and redraws the screen-space overlays (`moveCamera`); a full
                       `redraw()` follows only when an object crosses the edge of the view.
                       The scene view's tools keep their own state and reach the view through `SceneViewHost` (and
                       `selectedItem`, the selected object as the view drew it):
                       tilemapPainting (brush, eraser, rectangle, fill and pick on the selected map),
                       polygonCorners (a polygon collider's corner handles, #151), tilemapSolids (where a placed
                       map blocks, outlined like a collider, #132/#173)
src/editor/inspector/  Inspector panel, host (what a section builds into, and `emptyHost`, the empty one the
                       Inspector, the Variables tab and the Tilemap panel each start from), field factory, previewHandlers (scrub → preview → command), valueSpecs
                       (where a value lives — shared with the inline lists),
                       one section per selection kind (asset/object/node/frame/shape), variableRows (one row shape for
                       every variable), shapeSection (the shapes of a vector drawing: the list, the four order
                       buttons and the style of the picked one, #129; Fill type and Line type with the stop bar,
                       the picked stop's rows or the texture, #186), portSection, aspectAction,
                       objectQueries/vectorRows/parentRow, cutSection (the Nine-slice and Sorting rows, shared by the frame and the drawing inspectors, #308), compositionSection (#417: a track, an item with its timing and audio, a clip),
                       SpritePreview (a sprite as `syncSpriteMesh` draws it in the scene) + previewFit (whole device
                       pixels per unit, the corner on one)
src/editor/panels/     AssetPanel, assetGroups (the categories, the type groups in them and their order, #176), assetIcons (one SVG per
                       asset type in icons/, shared by the panel, the + menu, the tabs and Scene objects,
                       plus the List, Game, Plugins and User management tabs' own four, which stand for no asset),
                       assetMenu (the categories of the + menu and their types), HierarchyPanel + hierarchyDrop (#241: what
                       dropping a row does, group rows included) + GroupNameDialog,
                       Toolbar + toolbarIcons + toolbarTips (icon buttons, no captions: menu, undo, redo, play, pause, #183, what each says as a tooltip, #209, and the eye
                       the scene tab's header wears, see ScenePane),
                       SceneVisibilityPopup (behind the eye: symbol, name and collider outline per type),
                       headerIcons (the pictures of the tab headers' switches: the magnet, #194),
                       markIcons (the kind marks, the row actions reset, apply and the two fit arrows, and a
                       transition's in/out arrow — drawn, because the bundled font has no such characters, #214),
                       TilemapPanel (tools, tiles and layers of the one selected tilemap, beside the scene view, #142;
                       painting itself is `viewport/tilemapPainting`)
src/editor/graph/      GraphEditor (header, breadcrumb), GraphCanvas and what it lends its gestures through
                       `GraphCanvasHost`: wiring (making edges — a drag from a pin, the palette a drop into
                       empty space opens, a transition armed from a state's menu), graphDrags (nodes, groups and
                       comments moved or resized, a node whose face may be resized by its corner, #385, each
                       gesture one undo step), annotationLayer (the boxes behind
                       the edges); graphView (zoom limits, grid, panAndZoom — shared with NodeBoard and the Tile tab's TileCanvas — and `inView`, which nodes are in the picture, #292), EdgeLayer (curves and fold marks), graphMenus,
                       NodeView (and a face instead of the rows, a widget between the inputs and the outputs, #380: `nodeFace.ts`) + portRow (how an input and an output share one row, #306), NodePalette + paletteEntries (which also ranks a search), folding (hidden links, collapsed nodes), Breadcrumb + trail (the path into nested behaviors), beads (the run's pulses and the clock that launches them),
                       AnnotationView + annotationDrag (groups and comments: what a drag carries, each thing moved once),
                       SnapToggle (the magnet every tab that snaps carries, the vector art tab included, #194; lit at first in the graph tabs, `GRAPH_SNAPS_AT_FIRST`, #391)
src/editor/container/  ContainerEditor (header), ContainerCanvas (a SceneViewport on a template)
src/editor/texture/    SheetCanvas (the shell of the Texture, Pixel art and Vector art canvases: ground, clip, hint, a middle or Shift drag that pans, wheel zoom, Fit and the view each document keeps, and the dashed nine-slice and sort lines of a drawing, #308), TextureEditor (header), TextureCanvas (picture + frames), SliceDialog (a FormDialog), GenerateCollisionDialog (#161: the rows, and a summary that traces while the numbers change; the texture library hands it the sheet's pixels), sheetView (the grid maths),
                       ImagePicker + imageGrid (the "+ → Texture" dialog), checker (the transparency board)
src/editor/pixelart/   PixelArtEditor (header: Resize…), PixelArtCanvas (one frame composed as the picture path composes it,
                       whole zoom steps, grid, onion skin and tile preview; a stroke paints a copy and becomes one command),
                       PixelArtSide (tools, palette, layers, frames, view, preview), toolOptions (#201: the options that change what a tool paints), PixelThumbs (the row pictures of the layer and frame lists, repainted only when their key changes), PaletteGrid, PixelPreview, ResizeCanvasDialog,
                       pixelActions (what the panel does; merge and resize work out the new cels), pixelView (zoom steps, the pixel
                       under the pointer), celUploadStore (the project store that uploads the cels before it saves)
src/editor/vectorart/  VectorArtEditor (header: the three tools, the picked point's type or, with Paint, whose handles, and the magnet), VectorArtCanvas (the drawing
                       painted from core/model/vectorPath onto its own Graphics, with points and handles over it;
                       the pen puts points down, the arrow drags points, handles and whole shapes, #129; the Paint
                       tool drags the two handles of a gradient or a texture tile, #186),
                       GradientBar (the colour stops of a gradient as a bar: pick, drag and add a stop, #186),
                       ShapeList (the shapes as rows with a preview each, in the inspector; a row may be dragged to
                       another place in the drawing order) + shapeRows (that list's arithmetic: the order is shown
                       upside down, and a drop counts the dragged row which the model has already taken out)
src/editor/animation/  AnimationEditor (header, preview over timeline), PreviewCanvas (the root at the playhead, a FreeViewport
                       over animationPreview.ts), TimelineCanvas (rows, keys, ruler, clipboard), timelineView (its geometry),
                       RowDialog + FramesDialog (FormDialogs)
src/editor/composition/ CompositionEditor (#417: header with Add track and Play, the picture above the timeline),
                       CompositionCanvas (what is alive at the playhead, drawn and edited like a container's
                       template), CompositionTimeline (heads, ruler, items and clips in one widget, assets
                       dropped in, the gestures and keys) + compositionView (its geometry, the Song tab's with
                       frames for ticks)
src/editor/curve/      CurveEditor (header: Preset, the picked point's type, Preview, #247), CurveCanvas (the curve as a
                       graph drawn from curveValue, points and handles edited with the vector art gestures, the
                       preview dot and its rail) + curveView (the curve laid onto a picture for those gestures and
                       read back, a point held between its neighbours, a point put down on free space)
src/editor/pattern/    PatternEditor (header: Play, Grid, Bake; plays through the scheduler and follows edits, #333),
                       PianoRoll (keys, ruler, notes and velocity lane in one widget, the gestures and keys) +
                       rollView (its geometry: rows, hits, bands) + rollEdit (what a gesture does to the notes)
src/editor/music/      timeView (the time axis: ticks to pixels, zoom, the lines of bars, beats and grid) + TimeRuler
                       (bars, loop, start and playhead) — for the piano roll and the song tab — + transport (one
                       piece plays at a time: a song or a pattern, #334) + BakeButton (#340: bakes the pattern
                       or song shown, lit while its bake holds) + dropWhenSaved (a file a bake or a stream copy
                       left behind, deleted once no saved project names it)
src/editor/song/       SongEditor (header: Play, Grid, Bake; plays through the scheduler, sets the tracks' buses and
                       follows edits, #334), SongTimeline (heads, ruler and lanes in one widget, dropping assets
                       in, the gestures and keys) + songView (its geometry: tracks, items and their ends, the
                       parts of a head) + songEdit (what a gesture does to the items) + songActions (deleting
                       tracks and items, shared with the inspector and Delete), meterView + meterDraw (#359:
                       the level meters of the heads), TakeMeter (#342: the input's level while recording),
                       ownFx (#355: a sound FX the song keeps for a song, a track or an item)
src/editor/sound/      SoundEditor (#343: header with Play, the clip's direction and fades), WaveCanvas (the
                       waveform, the clips as bands, the cut) + waveView (its time axis) + clipStrip (the
                       clips' names in their bands, shared with the Video tab), SoundPreview (play
                       and length in the inspector), SoundPicker (the "+ → Sound" rows with their play mark),
                       StreamKeeper (#349: the copies streamed sounds and every clip play from, made in the
                       browser and let go once no longer wanted)
src/editor/soundfx/    SoundFxEditor (#311: the graph, a sound to try it on, Play and Stop), PreviewParams (the
                       header's fields for the parameters a tab plays with, shared with the Instrument tab),
                       CurvePanel + BandCurve + eqView (#367, #385: the curve of filter bands and the picked
                       band's numbers, its handles on the picture, the spectrum behind it, #374, and what is
                       left out when the room is small: `fittingCount`, `shownRows`, `apartLabels`), EqPanel
                       and FilterPanel (#385: the Parametric EQ's bands and a Filter's one band on it),
                       EqEditor (#370: either panel in a tab of its own), CurveFace (#381, #385:
                       the panel in the node, resizable by its corner), CompressorEditor +
                       compressorView (#378: the Compressor's tab, its knobs, its curve with threshold and
                       ratio to drag, and the levels in and out tinted where it squeezes), CompressorPanel +
                       hardwareView (#379: the Compressor as a device, its meter, LEDs and plate worked
                       out apart from the drawing), CompressorFace + nodeFaces (#380: the panel as what a
                       Compressor node shows in the graph, #381 the EQ's as well), deviceParts (what every
                       device is built of: the brushed plate and its engraved names, a row of chrome knobs
                       with their captions, the bezel, lamps, levers and type buttons) + deviceFace (what
                       every face does with its node: finds it, tells what a link feeds, turns a knob as
                       one undo step, hears its tap, loads its sound's picture), KnobPanel + KnobFace (#384:
                       the Gain as an orange plate with one knob; #390 the Pitch in green and the Mix in
                       blue with a lever, #392 the Pan in cyan, each a `KnobDevice`),
                       ChipPanel + ChipFace + chipView (#386: the Chip of an instrument as a device, its
                       wave and its pitch over a note worked out apart from the drawing), EnvelopePanel +
                       EnvelopeFace + envelopeView (#388: the Envelope as a device, its curve over a note
                       with squeezed time worked out apart from the drawing), ReverbPanel + ReverbFace +
                       reverbView (#389: the Reverb as a device, its tail as rings on a round screen
                       worked out apart from the drawing), DelayPanel + DelayFace + delayView (#393: the
                       Delay as a device, its echoes as bars on a screen and the light that runs over them
                       at their beat worked out apart from the drawing), SaturationPanel +
                       SaturationFace + saturationView (#396: the Saturation as a device, the wave a sine
                       comes out as and the heat it glows with worked out apart from the drawing),
                       LimiterPanel + LimiterFace + limiterView (#397: the Limiter as a device, what it
                       takes away read from its taps and caught from the start of a sound, held once it
                       ends, worked out apart from the drawing),
                       ModulationPanel + ModulationFace + modulationView (#398: the Tremolo, the
                       Vibrato, the Phaser and the Flanger as one kind of device, the volume or the
                       pitch over time, or the response with a lane of where it goes, worked out
                       apart from the drawing against the worklets' own numbers),
                       CrusherPanel + CrusherFace + crusherView (#399: the Bitcrusher as a device,
                       a sine sent through the node's own `Crusher` and drawn in cells of its steps
                       and holds, and the bits it keeps, worked out apart from the drawing),
                       SamplePanel + SampleFace + sampleView (#400: the Sample as a device, the
                       waveform of its sound or clip from the library's peaks, cut into what is
                       played once, looped, played after the note and never heard, worked out
                       apart from the drawing), AcidPanel + AcidFace + acidView (#421: the Acid as
                       a device, the filter where it rests and at the top of a note's sweep),
                       `knobGroups` in hardwareView (the row of grouped small knobs the Chip, the Delay
                       and the Acid share),
                       nodeParamDrag (a knob's drag as one undo step, for the tab and the
                       node, several settings at once since #389), nodeTabs (which nodes open a tab of
                       their own)
src/editor/instrument/ InstrumentEditor (#331: the graph over the keys, Preset, Randomize, Mutate, Audition),
                       PianoKeyboard + keys (the keys drawn, played by the pointer and the computer keyboard)
src/editor/routing/    RoutingEditor (#351: the Master routing tab, one per project)
src/editor/tile/        TileEditor (header + kind), TileCanvas (every tile of the block and a sample map, animated),
                       PictureStrip (the pictures as animation steps; its reorder maths is `dropIndex` in ui/sortable),
                       TilePalette (the tiles as a grid of pictures in the tilemap panel, #139)
src/editor/statemachine/ StateMachineEditor (header, on the graph canvas)
src/editor/particles/  ParticlesEditor (graph canvas left of a preview panel, splitter between them),
                       ParticlePreview (the panel: the asset's own particles, wearing its own rows)
src/editor/shader/     ShaderEditor (graph canvas, preview panel with a slider per property, #168),
                       ShaderPreview (one sprite mesh wearing the shader through syncShader, own clock),
                       previewPicture (which selection has a picture, what it shows, slider start values,
                       the built-in test image)
src/editor/PreviewSplit.ts  graph left, preview panel right, the splitter between them (Particles, Shader)
src/editor/list/       InlineList (a list's items drawn where the list stands, a row per item, #252),
                       slot (where a list lives, and how it is written back)
src/editor/variables/  VariablesEditor (the Variables tab: every variable of an owner, or one list),
                       target (the document of a tab, how much of a list the inspector draws)
src/editor/console/    log (ring buffer, argument formatting, the console.* capture), ConsolePanel (the tail under the tabs)
api/                   PHP backend: index.php (routing and the sign-in gate; `routeFiles` serves images, fonts and
                       sounds alike, and only an administrator deletes one),
                       lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php
                       (Store: saves of one project take turns on `{id}.json.lock`, which stays;
                       the image, font and sound stores extend HashedFileStore — hashed names, keeping,
                       listing, deleting — and add only which bytes they take and what a record says),
                       lib/AtomicFile.php (every file the API writes: temp file, mode, rename; `HIDDEN` 0640 for
                       projects, snapshots, releases, accounts and sessions, so the web server does not hand them
                       out as files, `SERVED` for the uploads),
                       lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/)
                       lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file)
                       lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has; the same
                       class on data/demos/ holds the demo copies, #428)
                       lib/DemoFiles.php (#428: which uploaded files are offered as demo material, data/demo-files.json)
                       lib/LoginThrottle.php (#427: five wrong passwords from one address, then 15 minutes of
                       429; data/sign-ins/), lib/Quota.php (#427: 1 GB per account for uploads, projects and
                       snapshots, 413 for a write that would grow past it)
                       lib/AgentTokens.php (#429: agent tokens in data/agent-tokens/ and the tabs' one-minute tickets in
                       data/agent-tickets/, both under the hash of what they keep)
                       lib/{VideoStore,VideoQueue,VideoFormat,VideoWorker}.php (#402: a video named after its upload,
                       the queue in data/videos/queue/, the one target format as plain rules, the conversion with
                       ffmpeg; run by tools/video-worker.php in the container of tools/video/ and in CI)
                       data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ + data/videos/ content-addressed uploads,
                       data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written
                       in each project (#119)
tools/mcp/             the MCP relay (#429): relay (agents over Streamable HTTP, tabs over a WebSocket), websocket (the
                       server half of RFC 6455), api (its two questions to the API), serve (start from the environment),
                       compose.yml (the container feuerwerk-mcp on stein2)
tests/                 Vitest unit tests      tools/           seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet)

The picture is drawn when something changes, not sixty times a second (src/ui/render.ts). An idle editor on the ticker costs ~75 % of a core, and ~64 of those points are the drawing of a picture nobody changed. initRenderLoop() takes app.render off the ticker; the ticker itself keeps running (~3 points), so the clock, a running world and the panels still get their frame — only the drawing is skipped. Idle then costs ~10 % of a core, which is the browser's own floor.

What wakes it is deliberately not a strict dirty flag. requestRender() opens a short trailing window (200 ms) rather than asking for exactly one frame, and it is called from the few places everything passes through: every pointer, wheel and keyboard event (once, on the window, in initUI), Widget.invalidate() and Widget.resize(), Stack.relayout(), EditorState.emit(), and a texture or font arriving. A repaint that lands a few milliseconds after its cause — a microtask, a texture upload, a font resolving, one of the 70-odd direct redraw() calls that never went through invalidate() — is drawn without anyone having had to declare it. What moves by itself cannot be woken by an event, so Editor.tick asks for the next frame for as long as play or an editor run is on, a Time shader is on screen, or the particles or animation preview is showing (movesByItself()). The reason for the trailing window rather than the exact frame: a missing repaint is invisible to the checks. __gm.pixelAt re-renders the stage on demand, so every colour check would pass just as well on a screen that froze a minute ago. The drawn-frame counter is the only witness — __gm.renderStats() gives it, and step 33 is the check that the editor really does go quiet and really does wake up again.

Work that costs more per pointer event than a frame is worth waits for the frame — onFrame(key, fn) runs fn once before the next frame is drawn, however often it was asked for under that key, and cancelFrameWork(key) drops it. A drag writes the model on every pointer move, and a model event rebuilds the panels, the run in the editor and any picture drawn from the model: on a busy scene that measured 36 ms per pointer move, while a mouse reports around 125 times a second and the screen takes 60. Without coalescing the pointer outruns the drawing and the editor only catches up once the hand stops. The work runs before app.render(), not after, so what it writes is in the frame it waited for. The Vector art tab's drag uses it; while the gesture runs, its own copy of the rings is the truth and it never reads the model back.

Every other drag goes through editor/framePreview.ts (#213): FramePreview.request() in the move handler, cancel() on release, one instance per gesture owner. The model is still written on every pointer move, only state.preview() waits for the frame. The Scene view (moving objects, polygon corners, tile strokes) and the Texture tab (frames, slice edges, collision corners) use it; the graph canvases did until #292 and now follow a drag without a project event at all, see below. Measured with 120 extra objects in the scene and 120 pointer moves 8 ms apart: 26, 34 and 29 ms per move before, 18, 18 and 17 after (the probe's own pause and round trip are in every number). The Texture tab had been paying twice: Pixi repeats the last pointer move a few milliseconds later to update hover, with the same native event, and each one ran a full preview.

What nobody sees waits for the end of the drag (#394). While FramePreview announces its frame, isDragFrame() is true. Two listeners of the project event skip that frame: the Scene view behind another tab, which remembers it and draws on coming back (as it does while a game runs, #265), and preloadProjectFiles, which walks every asset. The command that ends the drag emits for itself, and a drag that changes nothing ends with a plain state.preview(), so both catch up then. Measured on a copy of a project with 162 assets, a band of a Parametric EQ dragged at 250 pointer moves a second: 17 ms of listeners per frame before, 7.4 ms after.

The inspector's rows and the gradient bar's colour stops go the same way (#274). previewingHandlers asks for the frame on every input, whether scrubbed, typed or picked, and cancels it before the command; the bar's preview handler in shapeSection.ts does the same. Both had been paying twice as well: the same probe counted 236 project events for 120 moves of a scrub and 238 for a colour stop, at 47 ms per move. Now it is 51 to 61 events and 22 ms per move. The Pixel art tab's side panel (the colour of a palette swatch, the opacity of a layer) and the Curve tab's t and v fields follow the same rule; those were changed without a measurement of their own.

A graph of many nodes is moved, not drawn again (#292). Five rules keep a graph of a few hundred nodes quick to pan, zoom and drag in, all in editor/graph/:

  • Panning and zooming move the picture. GraphCanvas.applyView sets the position and scale of the graph and draws anew only what is laid out on the screen: the grid and the rubber band. It used to call redraw(), which rebuilt every box and every curve on every pointer move.
  • A node draws its box and makes its texts only when what they show has changed. NodeView keeps a signature of everything redraw reads and one of everything rebuildTexts reads, and returns early while they hold. A model event still asks every node (GraphCanvas.sync), and a run still calls redraw() every frame, but a text made anew is a picture uploaded anew, and that was the cost: one node dragged remade around 1500 texts per frame in a graph of 388 nodes.
  • A drag does not announce a changed project. GraphDrags writes the model on every pointer move and asks the canvas once a frame to follow (GraphCanvasHost.moved): the views are put where their nodes are and the links drawn again. Where a node stands concerns no panel, no run and no picture, so state.preview() is left out until the command of the release.
  • Nodes outside the picture are not drawn. inView in graphView.ts is the rule, cull() sets visible after every redraw and every change of the view. At a working zoom that is most of a large graph.
  • The graph is a render group, and its shapes go into the batch. canvas.isRenderGroup lets Pixi move the whole graph as one and keeps a node entering the picture from rebuilding the drawing of the whole editor. The mask sits on a container above the group: on the group itself it is panned and zoomed along with the graph, which cut the picture down to a small window. Pixi draws a Graphics of more than 200 vertices on its own, and the round pins of a node are that, so box and pins set context.batchMode = 'batch': 474 draw calls a frame became 160.

Measured in the CI image with a behavior of 97 nodes and 135 links taken from a real project, and with four copies of it (388 nodes, 540 links), 240 pointer moves 16 ms apart, fitted to the canvas:

before after
pan, 97 nodes: script per move 6.7 ms 1.9 ms
pan, 388 nodes: script per move 25.2 ms 2.0 ms
pan, 388 nodes: frames per second 4.4 18.7
drag a node, 97 nodes: frames per second 4.4 33.4
drag a node, 388 nodes: frames per second 2.7 22.4
the still picture, 388 nodes: frames per second 9 21

Zoomed in to 1 the same gestures run at 59 frames a second with 97 nodes and at 29 to 38 with 388. The frame rates are those of the test browser, whose GPU sits behind WSL's d3d12 driver and pays for every draw call: with the graph hidden altogether the Behavior tab draws at 37 frames a second there, because the panels around it cost 148 draw calls of their own, one pair per mask. The script times are the part that carries over to another machine.

What a check can see of this: __gm.graph.made() counts how often texts, boxes and links were really made, and __gm.graph.nodesDrawn() how many nodes are left in the picture. A probe that wants the frame rate of a picture nobody changes has to ask for every frame itself (__gm.wake() in a requestAnimationFrame loop): the editor draws on demand.

The Texture tab draws its frames in two layers (#220). sheet holds every frame but the selected one with its outlines, overlay the selected frame, its handles and a frame being spanned. Both are drawn without the pan, in a container that sits at the pan. A pan only moves that container, and only the names (which stay on frames in view) are placed anew. sheet is rebuilt only when the zoom, the selection or one of its frames changes. A frame's outline list stands for its corners, because the model replaces the list instead of editing it. Whether an outline is a polygon is remembered per list too; it used to be worked out on every redraw by cutting the outline into convex pieces. With 1728 frames of one outline each (768 x 576 px, 16 px grid), per pointer move on the main thread: pan 77 → 5 ms, dragging a frame 52 → 10 ms, a wheel step 52 → 31 ms, selecting another frame 68 → 41 ms. The last two rebuild sheet on purpose, once per step or click. __gm.texture.drawInfo() counts the rebuilds for the checks.

The toolbar's right edge shows the frame rate in edit and play mode alike; __gm.fps() exposes the same number to tests. It counts drawn frames over the time the renderer was awake: idle seconds are not slow seconds, so the readout answers "how smooth is it while it moves" and holds its last answer while nothing needs drawing, instead of falling to zero when all is well.

The performance overlay of the Game tab (#275) is fed by FrameMeter (editor/frameMeter.ts, pure and unit tested). While playing, World.timeSystem is set and the world times each system it ticks with performance.now(); PlaySession sorts them into behaviors, physics and animation. GameViewport adds up what its redraw takes, and ui/render.ts keeps what the last app.render() took (lastRenderTime). Both together are Drawing. Editor.tick times itself; what is left of that once the systems and the game's redraw are taken off is Editor. The meter averages per frame over 0.5 s, the window of the fps readout. Possible on top is possibleFps: 1000 ms over the sum of the five areas, rounded down. The released game leaves timeSystem unset, so the world runs untimed there. Browsers round performance.now() (Chromium to 0.1 ms without cross-origin isolation), which the average over some 30 frames smooths out.

Writing the run into the model is World.syncToModel, and it runs after every frame of a run, so an edit made while playing lands on the current state. It does not resolve every object each time (#287): for each entity the world keeps what the model said when it last read the object (syncFromModel, spawn) or wrote it, and a frame only compares the entity with that. A place that differs is written as three numbers. A value that differs is written as an override, and that one object is resolved again, because an override can bring rows of its own (a sprite's shader). The objects of entities that left the run are dropped only in a frame in which one left. The contract behind it: during a run nothing but the world writes the model without having it read again. The editor does that on every project event (Editor, EditorRun.refresh). On the reference project of #284 (222 entities) this took syncToModel from 0.92 to 0.12 ms a frame, and the Editor line of the overlay from 1.2 to 0.26 ms.

Rules that keep it organised: core/ imports nothing from other layers; ui/ never sees assets; position exists only on scene objects; overrides are merged in exactly one place (core/properties/resolve.ts); the inspector is generated from PropertySchema — a new property is one schema line; a new node is one file plus a row in the table in tests/core/node-catalog.test.ts, which fails if a node has no case; a canvas that pans or zooms puts everything the view moves into one masked container, not just the picture — an outline, a name plate or a checkerboard left outside it is drawn over the panel next door. A rule that produces something over time is written as a function of the clock, not stepped per frame — core/particles/emission.ts says which particles are alive at second t and core/particles/compile.ts turns a graph into what one of them looks like at its age, so a field of 500 particles is reproducible, holds still for a check, and needs no state beyond where its source stood when each one was born. Both are pure and tested without a browser. The other side of that choice: freezing such a thing means moving its origin, not pausing a counter — the clock belongs to the library and keeps running, so a preview let go after a minute on hold has to be rebased to the second it was stopped at, or it resumes a minute further on. The good side: a row that changes when rather than what costs nothing but a term on that one argument — a particle source's Time scale is a factor on it and Prewarm a summand, so neither needs a frame of simulation nor a line in the drawing code. Which clock, though, is one decision for the whole program. core/time.ts holds it: it accumulates dt * scale per frame instead of reading the wall clock, and the world's delta is the very step it took — so a global time scale is a single multiplication that reaches behaviours, particle age, shader time and the animation preview alike, and a pause is that scale at 0 rather than four things being frozen apart. Anything in the program that moves by itself takes its step from there; a second clock beside it is the thing to avoid, not to add. Nothing needs rebasing under it, because the clock itself stands still. The price is the other way round: whoever keeps an absolute second of a clock has to survive that clock going back to zero, which it does at both ends of a run. A particle field keeps its origin that way, so one in the future means the clock was pulled out from under it and it starts over. The time scale is deliberately not part of one of them: pitch. A game in slow motion keeps its sounds at normal speed — a tape being slowed is a sound effect of its own, not the meaning of "half speed" — so the audio device is left alone and only what the world does is scaled. A pause is the exception, because a voice playing over a still image is nobody's idea of paused: the audio context is suspended and resumes mid-note. The game's own pause is a flag on the world, not a second clock (#167). Pause game sets World.paused; World.tick reads it once per frame (frozen), and a frozen frame neither moves the world time nor ticks a system unless it says runsWhilePaused — only the BehaviorSystem does, and it then runs just the instances whose object has runWhilePaused on, without collision and animation events. The editor holds the global clock for such a frame too, so particles and shaders freeze with the world, and hands the behaviors the step the clock would have taken (PlaySession.tick(step, awake)). Set time scale writes GameClock.scale through the clock a played game gives the BehaviorSystem (GameSystemOptions.clock); the editor run gets none, which is how it ignores the node. The sounds stay out of it: a pause menu may have its music.

Everything the graph does to the outside world goes through the context, audio included. NodeContext.playSound takes an asset id, not a file: resolving it — and folding the asset's own volume and loop setting together with the node's (loop is boolean | undefined, and undefined is what leaves the answer to the asset) — is the BehaviorSystem's job, since it is the one holding the project. Below that line, AudioSink (runtime/systems/Audio.ts) knows only files and voices, and viewport/SoundLibrary implements it. That keeps core/ free of the browser the way it is free of Pixi, lets a test run a world that plays into a recorder instead of a device, and is the reason a run started before anything wired a sink in plays silently rather than crashing.

A voice has an owner, so nobody has to remember to stop it. AudioSink.play takes the id of the behavior that asked, and destroying that behavior silences its voices — the reaping pass in BehaviorSystem does it right after On destroy, so a graph still gets its last word. That single rule is also what ends the sounds of a scene a Load scene swaps away, without the swap knowing anything about audio. detached gives the ownership up on purpose, for the death sound of an object that destroys itself; an empty owner is deliberately nobody, never a group stopOwner('') could silence by accident. Cancellation is counted rather than flagged (per file, per owner, and for "everything"), because a play whose bytes are still in flight has to know whether a stop happened after it was asked for — a flag cannot tell that from one that happened long before.

A scene swap is a new world inside the same run. Load scene sets a flag on the world (loadSceneRequested) the way Stop game and Restart game do, and the editor acts on it once the tick is over: half a frame in each of two worlds is undebuggable. What survives is one object — the SharedState handed to the next BehaviorSystem through carryOver — which is exactly the line between "belongs to the run" and "belongs to the scene". Carrying it over also suppresses the entry event of the global machines, because standing where you already stand is not a move.

A kept object is the same entity in the next world (#165). GameRun.loadScene does it in four steps, and the rules are pure functions in runtime/keptObjects.ts. keptEntities picks the top-level entities with the switch on and everything below them. objectsToBring copies their stored objects, parented as the run has them — a released game never writes the run back, so the scene's own parentId may be stale. bringObjects puts the copies into the next scene and takes out what they stand in for (same id, or a kept top-level object of the same asset). Then World.instantiate is handed the entities: when the scene's object for one comes up, the world takes the entity as it is instead of making one, and only notes what the scene says of it — so the next syncToModel writes the run's values into the scene rather than the scene's replacing them. The ids the scene left out answer through findByObjectId as the entity standing in.

What is not on the entity is handed over next to it, as one KeptState: BehaviorSystem.release takes the running instances out before the old world is destroyed — which is all it takes for them to hear no On destroy and keep their sounds — together with the graph index their interpreters read and, from the physics, how their dynamic bodies move. The next system adopts them before it starts, so they are no new instances and hear no On start; their trace is cleared and timed by the new world, which starts at second zero. PhysicsSystem.resume gives each body the velocity when it is made, and forgets the rest after the first sync.

A behavior heard by an On behavior node is an instance of its own (#294), on the entity of the behavior holding the node: its own GraphInterpreter, trace, local states and variables (a copy of the heard behavior's defaults; behaviorContext hands these out instead of the entity's props), and a host naming the instance and the node that hear it. BehaviorSystem.syncHeard builds them when an instance is born and again after every edit to the project, following the chain of behaviors hearing each other and stopping where one would hear itself; a node that is gone takes its instance along, with On disable and On destroy. Because they stand in the same list as every other instance, the event kinds of a frame, the state events, pause and the scene swap reach them without a second code path. runPulses does the two things that differ: before a pulse it reads the inputs of the hearing node into the interpreter's given, which the start node hands out, and after it surface asks the interpreter for its ending — the flow input the end node was reached by last — and pulses the hearing node through the output of that name, and so on up the chain. A call learns the same way which output to take first: call returns the Ending of the nested frame, and CallResult.entry names the start node's output it begins at.

A run keeps what each scene stored before it built it. GameRun copies a scene's objects and groups the first time it builds it, and puts them back before it builds it again: the run writes into the scene it is in (syncToModel, baking, spawn), and a level loaded twice must not start from what was left of it. GameRun.end puts every such scene back, which the player calls on stop because it plays the project itself; the editor's Play runs on a copy it throws away.

Where a run begins and which of the two questions Play asks are both project fields. project.startSceneId is content — it travels with the project, and a dangling id reads as none rather than being cleaned up, so undoing a deletion brings it back. project.playFrom (the switch) and project.gameScale (the game view's scale) live in the project too since #226, read by core/model/playSettings.ts; anything unknown reads as the default. The switch is an undo step like every project setting. The scale is saved but never pushed on the undo stack (EditorState.setGameScale), and one picked while playing is written into the saved model as well, so Stop does not throw it away. project.gameLinkZoom (#279, 'auto' or 'fit') is read the same way and is the one thing the page of a released game takes from these settings: GameScreen hands it to cameraView where it used to pass a fixed 'auto'. It is deliberately not gameScale, which changes whenever somebody zooms the Game tab. None of these fields needed a format bump: a document without them reads as the defaults, which is precisely what older projects meant. The only thing still kept in the browser is the save of a persistent Variables asset (editor/saves.ts).

A predicate named after what a thing looks like will be asked what a thing is. isSymbolType reads as "draws an icon", but four callers use it for "is editor decoration": the running world, the Game tab, the draw order and the upright placement all key off it. Giving a particle source an icon by adding it there would have taken its particles out of the game. The two questions are now two functions — hasSceneIcon for the drawing, isSymbolType for the decoration — and the icon itself is a decoration of SceneViewport, which is editor-only for free because GameViewport never syncs any. Where a shader is cheap depends on what already breaks the batch. A sprite wearing one leaves Pixi's batched draw and costs a call of its own; a particle field is a single call for its whole field either way, because the pipe breaks the batch for it regardless — so the same feature that is expensive per sprite is free per field. The fragment the compiler emits needed no change for it, only a second vertex stage: Pixi's particle vertex is GLSL ES 1.00 and ours is ES 3.00, so it is translated rather than invented, and what it hands on is named what the fragment already reads. Where rows come from two places, one of them has to be the owner. A source is the only asset whose rows are drawn from its own graph and a worn shader's, so a name declared twice is a row with two meanings — #63 again. The source's own row wins and the uniform keeps its default: the shader cannot be blamed for a collision that only exists at this one wearer. Comments state contracts and reasons, not what the code does: a /** */ on every export whose name does not say it all, // only for a decision or gotcha.