1f13794b0cc371195e625fd5711e69b7646b7501
86
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1f13794b0c |
Shape a sound with a Parametric EQ and its own curve editor (#367)
A sound FX or an instrument had a 5-Band-EQ with fixed bands and a Filter with one band. The Parametric EQ holds up to ten bands of any type, and a double-click on the node opens an editor that draws the curve they make together. - The bands are a list on the node (`bands`): type, frequency, gain, Q and slope. A hand-edited project is held to the editor's ranges and to ten bands. - Each band compiles to the existing `pass` stage, so the audio core is unchanged. A peak or a shelf at 0 dB is left out. - The curve is worked out in `parametricEq.ts` with the biquad formulas of the Web Audio spec, so the editor draws what is heard. - The editor is a window that floats over the editor without a backdrop, so the tab's Play stays usable. A double-click adds a peak, a drag moves a band, the wheel turns its Q, a right-click removes it. Fields below the curve set the picked band. It opens in the middle of the screen and moves by its title bar. - A drag or a scrub writes the node as a preview and makes one undo step on release. The panels hear of the preview once a frame, and the window lays itself out only when one of its texts changes. - The area under the curve is filled as one shape for each side of 0 dB. As a single shape it crossed itself and came apart. - The curve works out each band's coefficients once and adds the sum up from the bands. A drag took 295 ms of script time in a profile before and 64 ms after. Decided on my own: - Bands are added by a double-click, so a single click can let go of the picked band without adding one. - The cuts are labelled "Low cut" and "High cut" as in an EQ, not "High-pass" and "Low-pass" as on the Filter. - The bands have no input, so no parameter or Envelope moves them. Tests: - Unit parametric-eq (new) and soundfx: 69 green. Counter-probe with no stages emitted: the compile test red. Counter-probe with one fill shape: the fill test red. - e2e 34 (melektaus-atx): ALL PASSED, 96 checks (3 new). - Counter-probe e2e: no stages emitted, the playing check red. - Gate (melektaus-atx): CHECK EXIT 0, 274 files, 4230 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f840ec3c44 |
Snap a new song to 1/16, and route new assets as the others (#365)
A new song snapped to whole bars, too coarse to lay out a beat. And a new or duplicated sound, instrument, pattern or song went to the master alone, so every one had to be linked into the buses by hand again. - A song without a grid of its own snaps to 1/16. The songs in the user's project have Bar written in and keep it. - A new or duplicated sound, instrument, pattern or song gets the links of the one whose targets most others of its type share, at their volume. On a tie the latest of them decides, so a single song decides for the next one. - Where nothing of its type is linked yet, it goes as most of all that plays, and where nothing is, to the master. A new bus still goes to the master alone. - Assets made at once are matched against what stood before, not against each other. A project from before the routing still opens with everything on the master. - The song tab has a gridAt hook. e2e 65 checks the new grid and sets Bar, which its checks go by. Tests: - Unit master-routing (1 new) and song (1 rewritten): 26 green. Counter checks (rule off, grid back to Bar) red. - e2e 65 (melektaus-atx): ALL PASSED, 58 checks (1 new). - Gate (melektaus-atx): CHECK EXIT 0, 273 files, 4226 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0e73fd85b9 |
Route sounds, songs and buses in one Master routing graph (#351)
A project bus could only go into one other bus, and a sound, an
instrument, a pattern or a song into exactly one bus. A shared reverb
needs a sound to go there in part and on dry at the same time. Instead
of a list of sends on every asset, the project gets one routing graph
like the routing of a song (#350), one level up.
- Menu -> Master routing opens a tab with one graph per project. It has
a node per sound, instrument, pattern, song and bus, named after its
asset, and one Master. Embedded assets have none, their song routes
them.
- The nodes follow the assets. Every edit settles the routing in the
same undo step (KeepRoutingCommand), so an undone delete brings the
links of its asset back. A new asset gets a node and a link to the
master. What went only into a deleted bus goes to the master.
- New nodes start in three columns: what plays, the buses, and the
master a row above, so no link into it runs behind a bus.
- Links are sends: -60 to +12 dB, and a Mode on links out of a bus or a
song. Out of a sound, an instrument or a pattern a link always takes
the sound after its volume and FX. Loops are refused.
- A node without a link is silent and says "Goes nowhere".
- The rows Bus and Into are gone. Play nodes with a picked bus still
overrule the routing.
- A click on a node shows its asset in the inspector, the Master shows
the master volume and sound FX. A double click opens the asset's tab.
- The graph lives in project.routing and is held to the assets when a
project is read, but nothing is added there. The editor adds the
nodes when it opens a project. An asset without a node plays on the
master, so old projects sound as before without a migration.
- In the mix an asset with one link at 0 dB goes straight into that bus.
A sound or instrument with more gets a bus of its own with the sends.
A pattern or song sends from the master of its playing, which is
where its Mode taps. Buses of the mix can have sends now too.
Found:
- The check "the scheduler plans a looped bar" in e2e 34 wobbles. It
failed in 2 of 5 runs here and in CI task 580 at
|
||
|
|
5cb7ef7bed |
Show empty asset groups on request, and add by the heading (#361)
The asset panel only listed the categories and types that hold assets. To make the first asset of a kind you had to know its category in the `+` menu. A switch now lists all of them, and a right-click on a heading offers just what sits under it. - General settings has "Show empty asset categories and types", off by default and kept in this browser. Applying it refreshes the panel at once through a new `onGeneralSettings` listener. - `groupAssets(assets, empty)` gives every category and type when asked. The empty ones stand faint with a count of 0, and an empty type has no chevron, since there is nothing to unfold. - `ListItem.faint` draws a row's label, hint and icons at half opacity, like a disabled widget. - A right-click on a category heading offers the types of that category without a submenu, on a type heading only that type. The empty space below the rows still opens the whole `+` menu. `ListView.onListMenu` passes the heading's id for this. - Every menu of new assets says "Create asset" above its entries, so a click on a type reads as making one. `MenuOptions.title` draws that muted line. It is not an entry and matches no search. Tests: - Unit asset-groups and view-tween: 23 green, 2 new. Counter-test with the empty flag ignored: the new groups test fails. - e2e 04 (melektaus-atx): ALL PASSED, 67 checks, 4 new and 1 rewritten. Counter-test with the heading id dropped: the 3 menu checks fail. The title is checked on the `+` button and on a type heading (2 checks extended), the rerun passed with 67 checks. - Gate (melektaus-atx): CHECK EXIT 0, 272 files, 4212 tests. It ran before the menu title. Afterwards only the menu and asset-groups units, typecheck and lint ran, all clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf96442835 |
Keep the Sound tab smooth while it pans (#360)
Panning the Sound tab drew the whole wave anew on every move, one filled rectangle per pixel column, and gave the ruler's labels new texts whenever a tick left the area. At 2560 px across that took the tab down to 16 fps. - The wave is drawn half the area's width past either side of it. A pan within that moves the drawing. Zoom, height, size or a pan past the margin draw it anew. - The bars of the wave share one fill instead of one fill each. - A ruler label keeps the second it already shows. Before, a pan shifted every label by one and each had to be rendered anew. - A clip's name is given its style only when the style changes. - The rule for what a drawing covers is pure in `waveView.ts` (`shownSeconds`, `drawnSeconds`, `coversShown`). Measured with a copy of the user's project, "Take 1" with 4 clips, 240 pan moves with the middle button, headless on stein2: - 2560x1440: main thread 712 -> 373 ms/s, texts rendered 694 -> 65 - 1280x800: main thread 429 -> 225 ms/s What is left in the profile is Pixi's render pass and the pointer events, which hovering pays as well. Tests: - Unit wave-view: 12 green (1 new), counter-probe red - e2e 34 (stein2, melektaus-atx was down): ALL PASSED, 92 checks (1 new). Counter-probe with the labels given out by position: red, 7 labels set anew instead of 1. - Gate (stein2): 272 files, 4210 tests green Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
490fcba88f |
Meter each track and the song, and limit the output at -1 dBFS (#359)
A song could clip at the output without anyone noticing: a track and a sound item at +12 dB each add up to +24 dB. The Song tab now shows how loud each track and the whole song are, and a fixed limiter behind the master keeps every sample under -1 dBFS in the editor and in the game. - Each track head has a stereo meter up its right edge. The head is 190 px wide instead of 176. - Out sits in the corner above the heads, with the loudest peak since Play in dB. In the tab header it made the row too wide for a 1280 px window, and the inspector lost its fields. - The meters read AnalyserNodes beside the buses, one per side after a channel splitter, so a peak panned hard is not halved. They are tapped only while the song plays and its tab is shown. - A clip lamp lights past 0 dBFS and stays through Stop. The next Play or a click on the meter puts it out. - The meter of a track that mute or solo keeps from being heard is grey. - Music always plays through a bus of its own, with or without a sound FX. That bus is what Out measures. - The limiter is the one from #326 and needs the FX worklet. Until that is loaded the master goes straight out. - It is 5 ms late. latency() counts that in, so the playhead and the alignment of a recorded take follow it. - Unit tests build SoundLibrary without the limiter, as their wiring checks follow every node of a voice. - Out shows "-inf" while nothing was loud. The UI font has no infinity sign, which the font test found. Tests: - Unit meter-view 8 new, sound-library 112 (2 new), 3 counter-tests each red - e2e 65 (melektaus-atx): ALL PASSED, 55 checks (3 new). Out wrote +16.6 dB, behind the limiter the loudest sample was 0.8913 - Gate (melektaus-atx): 272 files, 4209 tests, one red in the font test. Fixed after it, then the font and meter tests ran green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
95edf9959b |
Show the clips not picked faint over a sound's wave (#357)
Only the picked clip showed in the wave, between its handles. Where the other clips lay could only be read from the strip above, so cutting one next to another meant looking up and down all the time. - Every clip but the picked one is drawn over the wave as a pale tint with a thin edge, as high as the file reaches. - With the sound picked, all its clips are faint. While a drag spans a clip, that one is left out. - Test hook soundTab.faintClips() names the faint clips. Tests: - e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 extended) - Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4195 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
42a3759d9d |
Cut a sound into named clips, as a texture into frames (#357)
A sound could only be cut to one stretch, set on the asset itself. To
play two parts of one file, the file had to be imported twice. Now a
sound is always the whole file, and its clips are the stretches of it,
each with a name and a cut of its own.
- A clip is { id, name, props } on SoundAsset.clips. Its id shares the
generator with asset ids, so a sound field holds a sound or a clip,
as a texture field holds a texture or a frame.
- props are the cut: start, end, the fades and their curves, reverse.
Volume, loop, sound FX and bus stay on the sound for all its clips.
The cut rows left the sound's schema for CLIP_ROWS.
- findSound resolves either id. Play sound, the Sample of an
instrument, a Reverb impulse, song items, the scheduler and the test
sound of the Sound FX tab all go through it.
- Every sound field offers each sound followed by its clips, "Kick",
"Kick / Clip 1". A clip that is gone reads as "<id>?".
- The Sound tab has a strip under the ruler with the clips as bands.
A drag there makes a clip, a click picks one, beside the bands the
sound. Only the picked clip has handles. Reverse, fades and curves
in the header work on it and are greyed out without one.
- Whole file is gone from the header, as the user asked.
- Picking a clip is a selection of its own, like a frame. The
inspector shows its name, its cut rows and Delete clip. Delete
takes it away without asking, as with a frame. Ctrl+Z brings it
back.
- Copies get fresh clip ids: Duplicate, Embed a copy, a song
duplicated with its takes, and the copy a split or paste makes of
an embedded take. An item on a clip plays the same clip of its copy.
- A double-click on a song item playing a clip opens its sound with
the clip picked. The Sound row of an item on an embedded take
offers the take and its clips, which no list shows.
- A new take stays the whole sound. Clips are made by hand, as the
user decided.
- No migration in the code, as the user decided. The one project
with cut sounds was changed over by hand.
Deviation from the issue: deleting a used clip does not warn. A frame
does not warn either, and the issue had promised "like a frame".
Tests:
- Unit sound-clips (new): 11 green, 7 counter-checks each red
- Unit sounds, embedded: adjusted to clips, green
- e2e 34 (melektaus-atx): ALL PASSED, 90 checks (3 new, 12 rewritten)
- Counter-check in the browser: clips left out of the sound fields,
the picker check fails
- Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4194 tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c7fa1d15b0 |
Zoom the height of a sound's wave over its level scale (#357)
The Sound tab always drew the wave at the full height of the tab. Quiet stretches were hard to see, and there was no room above a loud wave. - A level scale in dB stands left of the wave, a mark every 6 dB down from 0, as long as a mark lies inside the area and at least 14 px from the middle. - The wheel over the scale zooms the height, from 0.25x to 16x. Over the wave it zooms the time as before. - What is louder than the area stops at its edge. The fade ramps grow with the wave. - Fit puts the height back to 1x. The tab keeps the height with its view when another tab is shown. - The rules live in waveView.ts (zoomLevel, levelMarks), the hooks give the height, the scale and the top of a full-scale wave. Tests: - Unit wave-view: 10 green (2 new), 2 counter-checks each red - e2e 34 (melektaus-atx): ALL PASSED, 87 checks (1 new) - Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4183 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
25c1d02b06 |
Keep the audio thread free of ended voices and too many vocoders (#356)
A song that had been played a few times in a long session stuttered at regular intervals until the page was reloaded. A quick run up the keys of a sampled instrument made every sound drop out for a second or two. Both came from work the audio thread should not have had. - Every worklet node of a voice listened on its port. Chrome never collects a port that is listened to, and through its listener not the voice either. Every node of the voice stayed on its bus and was worked on in every block. A way or stage on the worklet now closes its port once it ends or is stopped, and the processor closes its own side when it is told to stop. - A voice without echoes now leaves its bus as soon as its sound and what its stages hold back are through. Before, only a voice with echoes did. - At most 12 ways play through the pitch worklet at once. One more cuts the oldest in 10 ms, a note already let go before one still held. Each runs a phase vocoder, whose first block alone took 2.7 ms of the 2.67 ms a block has on stein2. - Play for the game ends everything the tabs play: the piece holding the transport and every key or preview (`stopPreviews`). - Closing a Sound, Sound FX, Instrument, Pattern or Song tab stops what it plays of that asset (`AssetTabEditor.closed`). - A preset in the Instrument tab plays nothing any more. With Audition on it is heard like any other change. - Test hooks: `__gm.instrument.play`, `__gm.sound.playing`, and `vocoders` in the stats of the sound library. Measured: - A copy of the project, its Song 2 played for 12 s and stopped, six times (Chrome on stein2, CDP WebAudio): before, 746 nodes were left after the first time and 4469 after the sixth, the load at rest grew from 0.01 to 0.06. After, 7 nodes are left every time (the master and one bus) and the load at rest stays at 0.01 to 0.02. - Pitch-player voices live in Chrome on melektaus-atx: 12 take 0.50 of the audio thread, 16 take 0.71, 24 take 0.99. 30 struck 40 ms apart held it at 0.99 from the first second on. Tests: - Unit sound-library: 110 green (3 new), 4 counter-probes each red - e2e 34 (melektaus-atx): ALL PASSED, 86 checks (3 new, 2 changed) - e2e counter-probe with game start and preset undone: their 3 checks red, and the old check of Play note in the game as well, which now counts on the preview being gone - e2e counter-probe with tab close undone: exactly its check red. A first form of it held a key and stayed green, since the keys let go of a held key by themselves once the tab shows no asset - Gate (melektaus-atx): 270 files, 4181 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8c0d1cfd3f |
Snap the routing graph of a song with the header's magnet (#350)
The routing of a song is the same graph editor as a sound FX, but its tab had no magnet, so its nodes could not be put on the grid. - The Song tab carries the magnet next to Routing. It is off at first and acts on the routing graph. - It is greyed out on the timeline, as Grid is greyed out in the routing. Switching the view only updates these two buttons, so a playing song is not handed its sequence again. - The check hooks give the magnet and the snapping of the routing. Tests: - e2e 65 (melektaus-atx): ALL PASSED, 52 checks (1 new) - Counter-check (magnet enabled on the timeline): exactly this check red - Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4178 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7a17030472 |
Give every sound FX effect a Bypass and a Wet (#354)
A node could only be taken out of a sound FX by unplugging it, and only six of them could be blended with the sound they get. Every effect now has both, in the sound FX graph and in the instrument graph alike. The Mix, the Input and the Output have neither. - Bypass is a checkbox setting. A bypassed node is left out when the graph is compiled, so it costs nothing and holds nothing back. The graph draws it faded, and its body says "Bypass: true". - Wet is a number input like the others. It can be a parameter, be moved by Set sound FX parameters and, where the node allows it for its other numbers, follow an Envelope. - A blend is a stage of its own (`wet`) around what the node adds. The sound as it came in is held back as far as the node is late, so a Compressor, a Limiter or a Vibrato do not comb it out. - The six nodes that had a Mix keep their key `mix` and only call it Wet. Saved values, parameters and edges stay as they were. - The Delay crossfades now: the sound alone at 0, the sound and its echoes in full at 0.5, the echoes alone at 1. Before, the sound was always in full and `mix` was the echoes. - The Pitch takes its semitones by Wet instead of blending, through a share `times` on the amount that an instrument binds as well. Findings: - A Delay saved before this sounds different. It sounds the same at half its old value. The two chip presets with a Delay (Echo Lead, Bell) are halved to keep their sound. - Project cbbf160bb7e8 has two Delays (mix 1 and 0.5). They are left as they are. Tests: - Unit: soundfx 65 (8 new), sound-library 107 (3 new), instrument 35 (1 new), envelope 13. Two delay tests and the filter ports test follow the new numbers. - Counter-checks: 8, each turns its test red. - e2e 34 (melektaus-atx): ALL PASSED, 83 checks (2 new). The ping-pong check at wet 0.25 measures what it did at mix 0.5. - Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4154 tests. - After the gate the derived ports of a blend are asked for once instead of twice: typecheck, lint and the three unit files green again (113 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
daeffe0f9d |
Route the tracks of a song through a graph (#350)
Every track of a song went straight into its output. Now the Song tab has a second view, Routing: a graph with a node per track and one Output, the links between them sends. A track can go into other tracks, into several at once, and a link has its own volume and the point of the track it taps. - The song asset has a graph like an instrument (SongTrack, SongOutput). heldSongGraph keeps it in step with the tracks on load and in SetSongCommand: a node per track named after it, one output, a new track and every track of an old song linked to the output. Links of gone tracks, doubled links and links closing a loop are dropped. - A link carries an optional send (volume in dB, tap). The inspector shows Volume and Mode for a picked link of a song: Post volume (Post pan), Pre volume (Post FX), Pre volume (Pre FX). SetSendCommand. - NodeDef has two new flags. `fixed` nodes are never added or deleted by hand. Links between `acyclic` nodes may not close a loop, checked in canConnect, dropTargetPort and ConnectCommand. - songBuses gives each track its sends instead of one `into`, and a bus `song:<song>:<track>:#items` for its own items. That bus is what solo closes on a track that only passes a soloed one on. - Mute and solo now act on the sends (trackGates). A muted track passes nothing on, on any tap. A soloed track is heard with what goes into it, through the tracks it goes through, their own items silent. Only sends on a way through a soloed track stay open, so a track going into it and into the output is heard through it only. - SoundLibrary: a bus has either `into` or sends. Each send is tap, delay, level, target. busLatency takes the latest way (arrivalOf), and each send is held back so all ways of a track arrive together (sendDelays). Taps are wired again whenever a chain comes or goes. - The Song tab has Timeline and Routing in its header. The routing uses GraphCanvas with an empty palette. A track linked nowhere shows "Goes nowhere". Delete takes links, never a track node. - Game playings route the same way, the output being the master of the playing. Decided without asking: a send ranges -60 to +12 dB like a track, default 0 dB Post. Mute closes the sends, the track's own level stays. Found on the way: the inspector built the Mode row with the send as it was when the rows were made. Setting Volume and then Mode wrote the old volume back. Both rows now read the current send when they change. Tests: - Unit song-routing (new): 13 green. Sound library: 4 new, 94 green. 11 counter-probes (loop on load, canConnect, solo, solo past the soloed track, mute, preFx latency, graph sync, delete guard, delays, taps, glide), each red. - Existing tests adjusted to the new bus names and send shape: song, bus-fx, song-play, graph registry, edge description. - e2e 65 on melektaus-atx: ALL PASSED, 43 checks (5 new). The first run caught the inspector bug. Counter-probe with the old closure: 2 FAILED. - Gate on melektaus-atx: typecheck, lint, format clean, 2 tests red (registry rule, edge shape), fixed and rerun locally: 56 green. - Not rerun after the per-send solo fix: e2e 65 and the gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3a52f2290f |
Mix sounds through buses and a master (#325)
Until now every sound went straight to the speakers. There was no point where all of them came together, so nothing could turn the music down against the effects, and a compressor or a shared echo over everything had nowhere to sit. The bus technique of #346 was there already, only the end of the chain was missing. - Bus asset (Audio): Volume, Sound FX and Into. Into is empty for the master and never offers the bus itself nor a bus that already goes into it. The list is worked out when the menu opens, so a change made meanwhile counts. A loop that is in a project anyway sends the buses on it into the master. - The master is no asset. Its volume and sound FX are two new rows under Master in Project settings, one undo step each. A project written before has 1 and none, which is how it sounded. - Sound, instrument, pattern and song have a row Bus, empty for the master. Play sound, Play note, Play pattern and Play song have an input bus: From the asset, Master or a bus. Inside a song or a pattern only the bus of the song or pattern counts. - Set bus volume (bus, volume, time, easing) and Bus volume. An empty bus is the master. The volume set holds for the whole run, across scenes, over that of the asset. GameRun resets it at start and end. Set sound FX parameters without a voice now reaches the sound FX of the bus picked in its new bus input. - SoundLibrary: the master is the bus "mix", each bus "mix:<id>". Every bus, the playing masters of #346 included, ends there, and only the master is heard. setMix sets them up once a context exists, so an editor nobody listened to opens no audio device. The same mix again changes nothing, which lets the editor hand it over on every change of the project. - The tabs play as the game does: Sound, Sound FX, Instrument, Pattern and Song tab through the bus of their asset and the master. - Pause game does nothing to the buses (the user's answer 4b). A game that wants quiet in its pause menu sets a bus volume itself. - Streaming long music files is #349, split off as asked. Finding on the way: the Into menu first worked from a list made when the inspector row was built. After another bus was sent into the open one, e2e 34 saw it still offered. It is now asked on open. The SoundLibrary tests expected voices to end at the destination. They now end in the master, which adds its input, level and pan to every wiring and two gains to every list. Those expectations are updated, nothing else in them changed. Tests: - Unit buses (new): 15 green. Ten counter-checks, each red on its own: loop check, bus picked, reset at run start, bus of Set sound FX parameters, master volume in the settings, and in SoundLibrary default into the master, voices into the master, run volume over the mix, no context before a play, a playing on its bus. - Unit sound-library: 90 green (5 new). Node catalog: 416 green (9 new cases). - e2e 34 (melektaus-atx): ALL PASSED, 75 checks (3 new). The voice of the Sound tab was heard on mix:<bus>, that bus into the other, that one at 0.5 into the master. Master volume 0.6 via the dialog, undo back to 1. - Gate (melektaus-atx): CHECK EXIT 0, 262 files, 4072 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a848cf8469 |
Pick a new asset from category submenus in the + menu
The + menu was one long list in two groups, placeable types and library ones. That split is no longer wanted. The menu now lists the categories of the asset panel, and each opens its types beside it, so a new asset is filed where the panel shows it. - Menu learns submenus: an entry with submenu instead of onSelect opens a second Menu right of the first, or left of it where the canvas ends. Its first entry lines up with the row that opened it. - A submenu opens on hover or click. Resting on another entry swaps or closes it after 150 ms, so a pointer cutting across the rows on its way into a submenu does not lose it. - The row of an open submenu stays lit, and a triangle pointing right marks every such row. - A submenu has no backdrop and no Escape of its own. Popup takes a backdrop option for that. The first menu's backdrop and Escape close the whole stack, and picking an entry does the same. - assetMenuCategories replaces assetMenuGroups and takes categories and their order from ASSET_GROUPS. The types are sorted by label. - New test hooks __gm.menuSubmenus() and __gm.addMenuCategory(). pickAssetType opens the category first, so the modules that pick a type through it need no change of their own. Modules 04, 10, 47, 48, 53 and 54 open the category themselves where they read the menu. Tests: - Unit asset-menu: 4 green - e2e 04: ALL PASSED, 63 checks. New: categories in panel order, each type once and sorted, submenu placement, hover swaps the submenu, picking closes menu and submenu - Gate: CHECK EXIT 0, 261 files, 4040 tests - The other modules that use the + menu did not run here Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5e09ba9a50 |
Group the asset panel by category, then by type
The asset panel listed one heading per type, 30 of them side by side. Now the types sit under five categories, and each type heading reads like its entry in the + menu. - ASSET_GROUPS replaces ASSET_GROUP_LABELS and gives every type a name and a category: Basics, Visuals, Logics, Physics or Audio. - The categories keep that fixed order. The groups in a category and the assets in a group stay sorted by name. - A category heading folds like a group heading. A project opens with every category open and every group folded, as before. - Selecting an asset unfolds its category and its group. Folding the category of the selected asset lets go of the selection. - The headings are singular now (Scene, Sprite, Sound FX). The old plural names only kept them apart from the + menu entries. - locate looks in the popup layer first. A click by text meant for a menu entry hit the heading of the same name, since the panel is drawn first. Below an open menu a click only closes the menu anyway. - New test hook __gm.assetCategories(). Tests: - Unit asset-groups: 6 green, counter-check (empty categories kept) 1 red - e2e 04: ALL PASSED, 63 checks (1 new). Counter-check with locate taking the first hit again: 2 red, the click on Behavior created no asset - Gate: CHECK EXIT 0, 261 files, 4040 tests - The category names got their final plural form after these runs. The unit test ran again on them (6 green), e2e 04 did not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4c7861b5b8 |
Keep the frame rate up while painting a big tilemap (#347)
Painting on the 60x60 map of a project with fifty objects dropped the editor to 20 to 39 fps. A profile of a brush stroke showed four causes, none of them the brush itself. - Each frame of a stroke previews the model. The asset and hierarchy panels then built every row anew, so every label became a new text and a new upload to the GPU. One run spent 46 % of the main thread in those uploads. ListView.setItems now keeps a row whose item draws the same (sameListItem) and builds only the rows that changed. - The brush drew the whole scene on every pointer event, on top of the preview. Now the preview draws what the stroke changed, once a frame. The rectangle tool asks for one drawing a frame for its box. - An animated tile made the view draw in the tick and the preview drew it again in the same frame. animate now asks just before the frame is drawn and stays out when the view was drawn since the tick began. - layoutSymbols ran on every drawing and costs up to the cube of the symbols. SymbolLayoutMemo keeps the spots while no anchor moves. - A painted cell made the map read every "x,y" key again, and once more for each of the eight neighbours of every cell. layerCells reads a layer once per content into an index by place. layerTiles and the blocking cells take their neighbours from there (neighboursIn). parseCellKey reads a key without split, with the same results. The test hook sceneRedraws counts how often the scene view was drawn. Not done: a painted cell still works out the tiles and blocking cells of the whole map again, so that part grows with the map. Tests: - Brush stroke on a copy of the project (stein2, Chrome): 20 to 39 fps before, 47.5 after, 55 to 57 without painting. Labels drawn again during the stroke 2012 before, none after. Scene drawings per frame 1.32 before, 1 after. - One painted cell, headless: 28 to 16 ms on 60x60, 109 to 49 ms on the same map doubled to 120x120. - frame-preview: 2 new, red with the old pointer redraw. - tilemap: 3 new (key reading, index, refresh), list-ping 1 new, symbol-layout 1 new. 5 single reversals turned each red. - e2e 39,40 (melektaus-atx): ALL PASSED, 107 checks (1 new, a fast stroke: 6 drawings in 6 frames, no label drawn again). With the row reuse and the pointer redraw reverted it fails: 24 drawings in 3 frames, and the panels draw their labels again. - Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4038 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
101b12504e |
Play a sound backwards with a Reverse switch on the asset (#321)
A sound asset can now play its stretch backwards. The switch sits on the asset, so every way the sound is heard plays it reversed: Play sound, a song item, an instrument's Sample and a Pitch in a sound FX. There is no Reverse node in the sound FX graph. The user dropped it, because playing backwards needs the whole sound in advance, which a bus or a Chip does not have. - The cut gets a `reverse` flag, stored as the asset property `reverse` (default off). The cut name ends in `~reverse` when it is on, so a reversed sound over the whole file is a cut of its own. - The handles keep marking the same part of the file. The reversed copy is made once, like any cut, and the fades follow what is heard: Fade in ramps at the end handle, Fade out at the start handle. - The Sound tab has a toggle with a left-pointing play triangle. Its playhead runs from right to left, and the wave draws the ramps at the edges they now belong to. The inspector has a Reverse row. - The icon is a tab header icon (`HEADER_ICON_SVG`, next to the snap magnet), not a top bar icon. The first gate run had it in the top bar set, and the test that fixes that set went red. - A song item draws the waveform of a reversed sound backwards. Lengths stay as they are. - The header label reads "Fade in" instead of "Fade in (ms)" and the fade fields are 8 px narrower. With the new button the readout was cut to "0.03 s of 0..." at 1280 px, now it shows in full. The inspector keeps the unit. Tests: - Unit sounds 22, sound-library 85 (1 new each). Counter-checks: without turning the samples round, without moving the fade in to the end, and without mirroring the peaks, all red. - e2e 34: ALL PASSED, 72 checks (2 new). The reversed stretch starts with the last samples of the forward one, and the playhead ran 0.75, 0.60, 0.45, 0.30, 0.15 over the whole file. - Gate: first run red, 1 of 4031 (the top bar icon list). After moving the icon, CHECK EXIT 0, 260 files, 4031 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d505f6ed27 |
Show what the song plays around a pattern as ghost notes (#339)
A bass line is written against the chords it goes with. Until now the piano roll showed only the pattern itself, so writing a part to fit the rest of a song meant switching between the Pattern and the Song tab. - A double-click on a pattern item remembers song, item and the round clicked into for that pattern. The place lives in the session, not in the project, and goes by the item's id, so moving the item moves the ghost notes along. - Opened from the Assets panel, the pattern shows the item of the last double-click. Without one, a pattern that lies in exactly one item of all the songs shows that item's first round. - Ghost notes are the pattern items of the other tracks all along the song, and on the own track the items before and after and the other rounds of the item. The round edited itself does not count. Sound items have no notes. - A ghost note sits on the row it sounds with: its own transpose, less the transpose of the item the pattern was opened from. - The roll draws them grey, without an edge, under the notes. Resting on one names its track and its pattern. - The header has a Ghosts switch, on by default, and names where they come from, e.g. "Tune · Bass · bar 5". - The time axis pans back to the start of the song, with bar and grid lines before the pattern. Fit still frames only the pattern. - A click or a paste outside the pattern no longer lands. Before, only the time left of bar 1 was closed, notes could still be drawn past the end, where they are kept but never played. - The rounds of a pattern item are now worked out in one place, itemNotes() in song.ts, which songSequence() and the ghost notes share. Tests: - Unit ghosts (new): 5 green. Counter-check with the round edited not left out and the own transpose not taken off: 2 red. - Unit piano-roll: 1 new (pan back to the song, lines before bar 1). Counter-check without the song's start: red. - e2e 65 (melektaus-atx): ALL PASSED - 38 checks (3 new). The first run was red on the tooltip check: the ghost note it rested on lay a fifth up, outside the rows Fit frames. The test now uses a tone. - e2e 64 (melektaus-atx): ALL PASSED - 30 checks. - Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4027 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
29b604dc07 |
Fade a sound's start and end apart, each along its own curve (#345)
A cut sound had one edge fade for both ends, always a straight ramp. Now its start and its end each have a fade of their own, and each fade has a curve of its own. - Fade in (ms) and Fade out (ms) replace Edge fade (ms), 5 ms each by default. Like before they apply only to an edge inside the file. - Fade in curve and Fade out curve each offer Linear (the default), Equal power, S-curve, Exponential and Logarithmic. A fade out runs its curve backwards. - The Sound tab's header has a field and a curve dropdown per edge. Each dropdown shows its curve as a small picture, rising or falling, and names it in its tooltip. With two named dropdowns the header was too wide at 1280 px and cut off the readout on the right. - The inspector shows the same four rows, the curves by name. - The wave draws each ramp in its curve rather than as a line. - A cut plays by <file>#<start>~<end>~<fadeIn>~<fadeOut>~<curveIn>~<curveOut>. - No migration, as asked in the issue: the old fade key is no longer read, so saved sounds start with 5 ms each and Linear on both. Tests: - Unit sounds, sound-library: 96 tests - Counter-check (fade out along the fade in's curve): 1 red - e2e 34 (melektaus-atx): ALL PASSED - 69 checks - Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3978 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f3ae19431a |
Make an 8-bit sound with a Preset, Randomize or Mutate (#329)
A Chip could play a sound, but finding a good one meant typing a dozen numbers. sfxr's buttons do that in one click: a kind of sound drawn anew every time, a wild draw, or a small nudge. The Instrument tab now has the same, and plays the result at once. - Preset menu with Pickup, Laser, Explosion, Powerup, Hit, Jump and Blip. It puts a new graph in place of the instrument's, as one undo step: Notes for all 128 notes, Chip, Envelope, Filter, Bitcrusher, Output. The Chip keeps its own frequency with follow note off, so every key plays the same effect, and sustain 0 ends it by itself. - The presets are sfxr's generators, worked out into Hz, semitones and seconds with sfxr's own formulas, in chipPresets.ts. sfxr's envelope holds and falls in a line, ours falls quick at first. So the decay is three times sfxr's hold and fall. - Randomize draws every number of the Chips, Envelopes, Filters and Bitcrushers anew, as sfxr's randomizer does. Waves, filter types, the Bitcrusher's mix and an Envelope's from and to stay. - Mutate moves each of those numbers a little, one in two. A 0 stays 0, so nothing that was off comes on. - An input fed by an edge is left alone by both. Both are grey while the graph has none of those nodes. - Audition, off by default as in the Pattern tab, plays the note last played again 150 ms after a change to the graph rests. - Preset, Randomize and Mutate always play that note, for up to 2 s. - ReplaceGraphCommand swaps nodes and edges and keeps groups and notes. - The info on the right of the header kept 220 px and was cut off with the four new buttons. 140 px still fit "No sound picked". Not done: punch, filter sweeps and the phaser of sfxr (#341). Tests: - Unit chip-presets (new) 7. Counter-check: a wrong Hz formula and fed inputs not left alone turn 3 of them red. Unit tests of the instrument tab, graph commands and sound FX nodes: 49 files, 851 green. - e2e 34 (melektaus-atx): ALL PASSED, 64 checks (4 new): Preset, Randomize and Mutate, Audition, three undos back to the Sample. - Screenshot with a Laser preset: the header fits at 1280 px, the chain is fitted, the Chip is a saw with its duty grey. - Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3969 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f1f4a57589 |
Cut a sound to a stretch of its file in its own Sound tab (#343)
A sound asset now opens in a tab that shows its file as a waveform. Two handles set where the sound starts and ends in the file, and that stretch is the sample: Play sound, a song item and an instrument's Sample all play it and nothing else. The file itself is never changed. - The asset has three new rows: Start (s), End (s) and Edge fade (ms). End 0 is the end of the file, the fade is 10 ms by default. Older assets have no values and play the whole file as before. - Every way of playing asks playedSoundOf() for a name instead of the file: the file, or <file>#<start>~<end>~<fade> for a cut. - SoundLibrary makes the cut from the decoded file the first time that name plays and ramps each edge inside the file over the fade. Length and peaks of a cut are read from the file without a copy. - Cut buffers can be made again at any time, so the library lets the oldest go past 64 Mi samples. A voice holds its buffers itself. - The tab draws min/max per pixel from 256-sample block peaks, and the samples as a line when zoomed in far. Wheel zooms, the middle button pans, as in the music tabs. Wave, cut and ruler sit under a mask, so a handle panned out of view does not draw over the panels beside. - Dragging a handle moves that edge, dragging elsewhere spans a new stretch. One drag is one undo step for both edges. Whole file puts the handles back, Play or Space plays the stretch with a playhead. - The inspector preview and the Sound FX preview play the cut too, and the song lanes draw and repeat it. Tests: - Unit: sounds 20, wave-view 8 (new), sound-library 68, plus 258 in the touched areas (instrument, song, pattern, inspector, tabs) - e2e 34 on melektaus-atx: ALL PASSED, 57 checks (7 new) - Gate on melektaus-atx: CHECK EXIT 0, 253 files, 3936 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2fdf97f7de |
Record a take from the microphone in the Song tab (#342)
A song could only be filled with sounds that were made somewhere else. Record plays the song from the cursor and takes the input meanwhile, so a part played along lands on the bar it was played to. - An audio worklet takes the input's samples on the clock the song plays on. Each run carries the frame it was taken at, so the take is cut where the song was heard to start. The output's and the input's latency are taken off, as far as the browser reports them. - The take goes up as mono 16-bit WAV. Sound, a new track "Recording" under the track clicked last and the item at the cursor are one undo step. The sound is named "Song – Take 1", 2, 3 and so on. - While recording the take grows red with its waveform on its track, and a meter beside the input dropdown shows the level. The lanes take no edits then, only a middle-drag pans. - The loop is not used while recording. The song plays once through and on past its end until Stop, Play or Space keep the take. Esc throws it away and nothing goes up. - Echo cancelling, noise suppression and auto gain are off, nothing is played back. The input picked is kept in localStorage. - A take stops by itself at the size a sound may have. A failed upload offers Retry, Download WAV and Discard. - The API takes sounds up to 256 MB and bodies of the same size. nginx and PHP were raised to match outside the repo (body 256m, upload and post 256M, memory_limit 512M). - The pure rules (cut, cap, WAV header, name, track) live in core/music/take.ts. The scheduler tells where it started on the audio clock. - The e2e browser gets chromium's fake microphone and skips the permission prompt. Each run leaves its take in the sound store. Found on the way: - The header was too full with the new widgets and cut the track and item count on the right. Input dropdown and meter are narrower now. Tests: - Unit take.test.ts: 7 new, svg-icons adjusted to 8 toolbar icons - e2e 65 (melektaus-atx): ALL PASSED – 31 checks (+4) - Gate (melektaus-atx): CHECK EXIT 0, 252 files, 3923 tests - Probe on stein2: a 3 s take of the test tone, peak 32767, 285100 bytes, waveform seen in the screenshot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a3101c590c |
Size notes by either end and pan before bar one in music tabs
A round of small fixes to the Pattern and Song tabs: what the ruler and a pan do in both, and a few things the tabs carried that were in the way. - A middle-drag can take bar 1 up to half the grid or lanes in. The time before it is drawn dark on the ruler, the grid and the lanes. Nothing lands there: a click draws no note, a drop shows no frame. - A press or drag on the ruler puts the start on the nearest grid line, no longer on the one before the pointer. - A new song has one track instead of four. The "+ Track" row under the heads is gone, a head's menu adds tracks above or below. A song still keeps at least one, so that menu is always there. - A note can be sized by its left end too: the start goes to the nearest grid line and the end stays put, never before tick 0. - An end of a note under the pointer lights up and shows a resize cursor, as on an item of the Song tab. Tests: - Unit: piano-roll, song-edit, song, song-play 52 tests. Two new cases for the left end and the pan, the song fixtures add their tracks themselves now. - e2e 64+65 on melektaus-atx: ALL PASSED - 50 checks (+2). e2e 65 adds its tracks through the head's menu and clicks the ruler three quarters into a bar, which only the nearest line passes. - Gate on melektaus-atx: 251 files, 3909 tests, CHECK EXIT 0. - A probe shot of the piano roll panned in: the time before bar 1 dark, the left end of a note lit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa097bd076 |
Zoom with the wheel and set the loop on its own strip in music tabs
The ruler of the Pattern and Song tabs did two things with one drag,
and the wheel did three things with Alt and Shift. Both now do one
thing each, and the Song tab gets a few edits it was missing.
- The ruler has a loop strip along its top. A drag there sets the
loop, a press or drag below it moves only where playing starts.
- The wheel only zooms: the time over the grid, lanes and ruler, the
rows over the keys, the tracks over their heads. Alt and Shift no
longer change it. A middle-drag still pans.
- Tracks can be zoomed from 28 to 160 px. Below 46 px a head shows
only its name, M and S. An old saved view gets the old height.
- A click on nothing in the lanes picks the song itself.
- A right-click on an item opens a menu with Delete instead of
deleting it. The inspector has no delete buttons any more.
- A sound item goes up to +12 dB, a track up to +12 dB. A pattern
item stays at 0 dB, since a note is held to unity.
- An end of an item under the pointer lights up and shows a resize
cursor. A long item marks every point where it starts over.
- The Pattern tab has an Audition switch in its header, off at first.
Only with it on does a note sound when drawn, grabbed or moved.
The keys down the left sound either way.
- A looped sound item draws every round alike. Each round is drawn
from its own start, and each column shows the loudest part of the
sound it covers instead of one sampled bucket.
- A song is at least one bar long instead of four. An empty song
fits that bar across, and Play song ends after the last item.
- The ghost notes held back in #334 got their own issue, #339.
Finding: suite 542 for
|
||
|
|
8f9c250eb6 |
Lay patterns and sounds out on tracks and play them as a song #334
Patterns could only be heard one at a time, in their own tab or from a behavior. A song lays patterns and sounds out on tracks along one time axis and plays them through the scheduler of #332, the third piece of the music tools of #330. - New asset type song under the heading Songs, with four empty tracks, 120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows in the inspector. Tracks, items and the loop are the asset's data, held to their rules on load and in every command. - A track has a name, volume, pan, mute and solo. Each is heard on a bus of its own, so a change reaches what is sounding at once. - A pattern item repeats its pattern for as long as it is, with a transpose and another instrument if wanted. A sound item plays its sound as a clip at its own speed, round and round, through a sound FX if wanted. Both have a volume on top of their track's. - Cut back at the left end or split, an item starts as far into what it plays as the cut is: ticks into a pattern, seconds into a sound. - The tab shows the heads with M, S, volume and pan, the ruler of the Pattern tab and the items, a pattern with its notes, a sound with its waveform, a missing asset red. Patterns and sounds are dragged in from the Assets panel. Body, right and left end move, lengthen and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits where playing starts, Delete, arrows. A double-click opens a pattern item in its tab. - What is picked is a new selection of its own, so the inspector shows the item or the track, with a button to delete it. - The scheduler plans clips beside notes. Where the playhead starts or jumps back inside a clip, it is taken up that far into its sound. A clip fades out at the end of its item or of the loop. - The editor plays one piece at a time: a pattern started stops the song and the other way round. - The rules are pure functions in core/music/song, editor/song/ songView and songEdit. Findings: - The suites of |
||
|
|
4ea752feef |
Draw notes for an instrument in a piano roll and loop them #333
Notes could only be played one at a time, by a key or a behavior. A pattern is a stretch of notes for one instrument, drawn in a piano roll and played in time through the scheduler of #332. Songs (#334) will lay patterns out on tracks. - New asset type pattern under the heading Patterns. Instruments are listed under Instruments now, since a heading is one per type. - Notes are a start and a length in ticks, a note number and a velocity, kept to their rules on load and in every command. The instrument, the length in bars, the grid, the meter and the tempo are rows in the inspector. The loop is the asset's own data, the whole pattern while none is set. - Grids from 1/1 to 1/32 and the triplets 1/4T to 1/32T, all whole ticks. The meter only moves the bar lines, the notes stay put. - The tab shows the keys down the left, a ruler with bars, beats, loop and start along the top, the notes between them and a velocity lane below. Keys the instrument plays nothing for are grey, the names of a drum kit stand on its keys. - A click draws a note on the grid line before the pointer, as long as the last one drawn or lengthened and as hard as the last velocity set. The body moves a note, the right end lengthens it, Alt leaves the grid. A drag on empty grid picks with a band, Shift adds, a right-click deletes. - Keys: Space plays, Delete, Ctrl+C/V/D/A, the arrows by a semitone or a grid step and with Shift by an octave or a bar. A paste lands where playing starts. Every gesture is one undo step. - A click on the ruler sets where playing starts, a drag the loop or one of its ends, its menu loops a bar or the whole pattern. - The wheel scrolls, Shift across, on the ruler or with Alt it zooms. Ctrl+wheel stays with the browser, Pixi listens passively. - Play runs the loop through the scheduler. An edit goes to it on the next window without a restart, a new loop or tempo counts from what is planned. Another pattern shown or a project opened stops it. - The time axis and the ruler are their own pieces in editor/music for the song tab. The rules of the roll are pure functions in rollView and rollEdit. Findings: - Music.play could start after a stop that came while it was still loading its files. A run counter makes the late start give up. Music.edit now loads what a new note needs as well. - A click on an end of the loop would have set the loop again. A press there only moves the end once it is dragged. Tests: - pattern.test.ts (new, 9) and piano-roll.test.ts (new, 16): model, grids, meter, held notes, sequence, commands, loop, round trip, drum names, time lines, rows, hits, bands, lane, edit rules. - e2e 64 (new) on melektaus-atx: ALL PASSED, 20 checks. e2e 4 with 29 asset types: ALL PASSED, 62 checks. - Gate on melektaus-atx: CHECK EXIT 0, 247 test files, 3839 tests. - Counter-checks: the move without its clamp, the sequence without the end of the pattern, the duplicate offset without whole grid steps each fail 1 test. Without passing edits to the scheduler the e2e check for a note drawn while playing fails, the rest passes. - Probe on stein2: the tab with a drum kit, before and while playing, looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
144dee0a97 |
Let a looped sample play out past its loop when the note ends #331
With "Mode" and "Loop" as two fields, One-shot plus Loop ran until Stop was pressed: letting go of the key did nothing. The two fields are now one, "Play", with four entries that each say what happens while the note is held and when it is let go. - Gate and One-shot play the sound once, as before. Loop repeats it and fades out with the note. - "Loop, then release" is new: the sampler's sustain loop. Let go, it finishes the round it is in and plays what comes after "Loop end" to the end of the sound. - A buffer source stops looping and plays on by itself. The pitch worklet gets a "leave" message. The phase vocoder then lets the round finish that its newest frame has read into, so nothing read already changes under the frames still sounding. - Samples saved with "Mode" and "Loop" are turned into "Play" on load. Gate and One-shot stay, either with Loop becomes Loop, since playing until stopped is gone. - The keys of the Instrument tab stay half lit while a note let go is still heard. Esc on the keys stops everything, like Stop. - Striking a ringing key again plays a new note beside it rather than stopping the old one, so a drum can be hit quickly. This differs from what was proposed in the issue discussion. Tests: - instrument + pitch-player + sound-library + node-catalog: 477 tests, green (12 new). - e2e 34 (melektaus-atx): ALL PASSED, 45 checks (+1). Let go after a loop of 0.05 s, the note played out for 0.31 s on the audio clock, the key half lit meanwhile. - Counter-check: a release that does not leave the loop fails the 2 new player tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8a4e4a6762 |
Play sounds as notes of an instrument asset #331
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.
- New asset type instrument, under the heading Music, not placeable.
A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
only, the lowest C-1. Notes has From and To, outputs any, note,
frequency and velocity, and with Single notes a trigger per note,
each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
can take any number of edges now (PortDef.many): a trigger, and
the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
through the pitch worklet of #317, so it keeps its length. Gate
fades it out at the end of the note, One-shot plays through. Loop
runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
default. Sound FX sends the sound through a sound FX asset, its
parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
only for the notes it stands for, a silent source silences what it
alone feeds, a Mix with a silent side is the other side. velocity
and the parameters stay open for the voice. Builds are cached per
note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
with its own file and volume. release fades the gated ways out,
one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
detached, and hands out voice for Set sound FX parameters and
Stop sound. Velocity is always loudness. Instrument parameters
picks values for the instrument's parameters like Sound FX
parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
128 keys below. Keys without sound are grey. A key plays while it
is held. Focused, the computer keyboard plays as in a tracker by
KeyboardEvent.code, the arrows move the octave.
Findings:
- A label of the keys set fill on the shared small text style and
darkened every small label of the editor. Caught in a screenshot
before the commit, the keys have a style of their own now.
- Suite task 523 on
|
||
|
|
183f356c99 |
Let a game hand values to the parameters of a sound FX #311
A sound FX played the same every time: its numbers were settings of the nodes. Now the Input declares parameters with a default, every number of Gain, Pitch and EQ is an input they can feed, and a Play sound hands in values for them. - Input declares number parameters in the inspector, each with a Default (DeclaredPort.default, SetPortDefaultCommand). They are outputs of the Input. Input and Output count as terminals now, one of each per graph. - Gain, Pitch and EQ take their numbers at inputs under the keys the settings had, so saved graphs sound as before without a migration. Each value is held to its range however it arrives. - The compiler gives an FxPlan with the parameters still open. fxChainOf fills them in when a voice starts. The player still only sees an FxChain. - New behavior node Sound FX parameters: names an FX and picks its parameters like a Variables node picks variables. Each picked one is a number input that starts at the FX's default. Its output has the new port type fxparams, which only fits Play sound's fx params. - Play sound warns when the parameters name another FX than the one it plays. Names the FX does not know are left out. - Renaming a parameter at the Input renames it in every Sound FX parameters node naming that FX, edges and values included. - The Sound FX tab has a number field per parameter in its header. Play uses those values, they are never written into the asset. - A value counts from the start of a voice. Changing a playing voice is #315, a Mix node is #316. Tests: - soundfx.test.ts 22 (+11): parameters in the plan, held values, a 0-by-default stage, EQ band, save with defaults, wrong port types, default command with undo, Sound FX parameters into Play sound, defaults of picked ones, node problems, mismatch, rename with undo - node-catalog 368 (+3): Play sound fx params, Sound FX parameters - e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5, a Sound FX parameters node gives the game's voice depth 7) - Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test files, 3636 tests green - Counter-check: with fxChainOf ignoring the values and the rename not followed, 5 of the new tests fail. - Browser probe: header fields, inspector Default and the picked parameter looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
de14f0f9e2 |
Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.
- New asset type soundfx ("Sound FX"), not placeable, with a graph
and its own tab. The heading in the asset panel is "Sound effects",
so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
emitter. An input takes one edge, so the result is one line: a
pitch and a list of gain and filter stages. A node without input
or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
through the graph as it stands. Stop there ends only its own voice.
Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
test files green. The red one was sound-library (its fake
AudioContext had no playbackRate), fixed afterwards, gate not
re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
test fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
3f12a77aea |
Preview a sprite at the end of its inspector, placed or as an asset
A sprite's inspector listed its rows but never showed what they add up to. The picture, its frame, nine-slice, tint and shader could only be judged in the scene, often far from the object being edited. - SpritePreview draws the sprite through syncSpriteMesh, the function the scene draws it with. Slices, padding, tint and shader come out the same without a second path. syncSpriteMesh now takes only the three fields of a DrawItem it reads. - The subject is resolved by previewPicture from the Shader tab, so a placed sprite shows its own overrides. - previewFit is the rule for size and place: whole device pixels per unit from 1 on, at most 8, and the corner on a whole device pixel. Pixel art stays sharp. A sprite larger than the group shrinks freely. - A shader with a Time node keeps the preview redrawing once a frame while it is on screen. A folded group or a hidden panel stops it. - New test hook __gm.inspectorSpritePreview(). Finding: the preview redrew on a model change in a microtask at first. A drawn picture whose revision moved on is retried once, in a microtask queued by the scene's synchronous redraw. That retry ran before the preview had let go, and the old pixel art sheet stayed waiting (e2e 48l-iii read waiting [1,1,1]). The preview now redraws at once on a model change, as the scene does. Tests: - unit: 238 files, 3592 tests (+5 in preview-fit.test.ts), and the group list of a placed sprite in sections.test.ts ends in PREVIEW - e2e 48: ALL PASSED, 47 checks (+2: 48l-i asset preview with the frame, 48l-iii placed preview with its shader values) - counter-checks: without whole-pixel zoom 2 unit tests fail; with the microtask redraw 48l-iii fails with waiting [1,1,1] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1665576fb9 |
Let a pixel perfect camera draw on the game view's pixel grid
A camera that follows smoothly stands between pixels most of the time, and pixel art then wanders between them. The new camera switch draws what the camera shows on the game view's own grid, while positions in the model keep their fractions. - `Pixel perfect` is a camera row, off by default. It works at every fixed game scale and at Auto, but not at Fit and not in the Scene tab, where one works with the fractions. - The grid has one step per device pixel of a world unit. At 100% that is whole units, at 500% fifths. The view carries it (`View.grid`), so no view draws with the grid of another. - The view looks from a point that puts the world's origin on a whole device pixel (`lookFrom`). This also covers odd camera sizes and views that cut the picture to an odd size. - Sprites, texts and tilemaps snap their top left corner when they are unturned, so an odd size lands on whole pixels too. A turned object snaps its pivot (`snapPlacement` in `pixelGrid.ts`). - Under a camera shader the picture is taken the same way, and the objects in it use the picture's grid. With Camera pixels that is one step per camera pixel. - An overlay camera has its own switch and snaps only while the main picture is at a fixed scale. - Particles are drawn where they are. - New test hook `__gm.gameDrawnRect(id)` reads where a sprite's picture is drawn, which `gameRect` (the model's) cannot tell. Found: the e2e fixture at 300% drew the crate at y 205.5 device pixels without moving the camera, because the view cut to an odd height puts its centre on half a pixel. With the switch on it is at 206. Tests: - Gate: 237 files, 3587 tests (+10 in pixel-grid.test.ts) - e2e 2,3: ALL PASSED, 75 checks (+1, 3j) - Counter-checks: without the origin snap 3 unit tests fail, with the centre snapped instead of the corner 2 fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c74b6b1cab |
Show where an inspector row can be dragged and answer the pointer
After #310 rows could be dragged by their mark, but nothing said which marks take a drag, and the pointer on one changed nothing but the cursor. - A mark that drags is now a Grip: the same widget wrapped, the same size, so the value column stays where it was. Once sortBlocks takes it, it carries six faint dots at its left edge. - Marks that cannot drag stay plain: the variables of a placed object and the fields of a typed struct show no dots. - With the pointer on a grip its dots light up and the whole block that would move is washed in the accent colour, a struct with its fields. While the block is carried the wash gives way to the dimming, and the grip stays lit. - While a block is carried the other grips let the pointer pass: no lit dots, no wash. Every grip of the column shows grabbing instead of grab until the drop. - The wash first used controlHover like list rows. In the inspector it is darker than the panel and vanished next to the dark fields, so it became the accent at 15 percent, the colour of the drop line. - __gm.inspectorMark also reports the dots: none, idle or lit. Tests: - Gate on melektaus-atx: 236 files, 3576 tests (+4 in block-drag.test.ts, one of them a drag passing over another grip). - e2e 22: 40 checks (+1, sum 1740). 22f-iii reads idle dots, moves the mouse onto the mark and reads them lit. - Two counter-checks bite: no wash on hover fails 1 test, grips never made grabbable fail 3. A third: hover ignoring the drag fails 1. - Screenshots of a lit single row and a lit struct block looked at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2c19c89878 |
Let variables, fields, ports and list items be dragged into order (#310)
Variables of a behavior, the fields of a struct, the ports of a start or end node and the items of a list could only be added and removed. Changing their order meant removing and adding them again. Now each of them is dragged by the mark in front of its row. - sortBlocks (ui/blockDrag.ts) makes the rows of one column sortable. A block is every row it covers, so a struct moves with its fields and a list item with its nested list. The block is dimmed while it is carried, an accent line shows where it lands, and a scrolled panel follows the pointer at its edge. Less than 4 px stays a click. - The arithmetic is pure and lives in ui/sortable.ts: blockGap, gapLineY, dropSlot and edgeScroll. dropIndex of the tile strip moved there too, so the reorder maths sits in one file. - MoveVariableCommand, MoveStructFieldCommand and MovePortCommand build on the existing MoveCommand. Undo moves back. - Lists need no command. InlineList replaces the list as a whole, as it does for every other edit of its items. - The grip is the kind mark, for a list item its number. Moving only happens among siblings: a field stays in its struct. - On a Struct asset the move runs through WithStructTypes. Typed variables, Pack/Expand ports and split outputs follow the new order. - Values and edges hang on names, so only the order changes. Nodes running a behavior read their ports from the start node, the new order reaches them without a copy. - Not sortable by design: the variables of a placed object, the fields of a typed struct (moved on the Struct asset) and the picked ports of Variables/Properties nodes. - Test hook __gm.inspectorMark(label) finds the mark of a row. Tests: - Gate on melektaus-atx: 235 files, 3572 tests, CHECK EXIT 0 (+9). - tests/core/reorder.test.ts: a variable, a field two levels deep, a Struct asset reaching a typed variable and Pack, and start ports reaching the calling node with edges and result kept. - e2e 22: ALL PASSED, 39 checks (+4, sum 1739). 22f-iii drags a variable past a struct of several rows, then a field inside it. - Counter-probes: dropSlot by top edge instead of middle and a field move that ignores its path fail 3 unit tests. With sortBlocks switched off, the three order checks of 22f-iii fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf6043a066 |
Show a wheel step or Fit as a short zoom transition (#309)
The wheel and the Fit button used to jump to the new view. Now the view takes a short way there, 200 ms by default, everywhere the editor zooms. The zoom steps stay the same, only the change is shown. - viewTween: tweenView is the way between two views as a pure function. The scale moves by equal ratios, and the world point that sits at the same place on screen in both views stays there. After a wheel step that is the point under the pointer. - ViewTween drives it on the ticker. A surface hands it reading, writing and drawing its own view and wraps its wheel and Fit code in change(). A step during a transition starts from where that one was heading, so quick steps add up to exactly as many steps. - Anything else that moves the view ends the transition. A pan jumps to the target first, and saving a view saves the target. - Wired into FreeViewport (Scene, Container, animation preview), SheetCanvas (Texture, Curve, Pixel art, Vector art), GraphCanvas, panAndZoom (Tile board, plugin node board) and the timeline. - Menu > General settings... sets the time in ms, 0 jumps at once. It is kept in this browser like the panel sizes, not in the project, and it is not an undo step. - A document that opens still comes up framed at once. The animation preview frames a new subject itself instead of calling fit(). - The pixel art readout names the step the view is heading for. Measured in the scene view at 200 ms on melektaus-atx, where every frame of the transition is a full redraw: - 500 sprites: redraw 6.7 ms, 16 frames, mean 18 ms - 1000 sprites: redraw 13.0 ms, 9 frames, mean 27 ms - 2000 sprites: redraw 32.2 ms, 5 frames, mean 58 ms Tests: - The harness sets 0 ms on every load, so the existing modules read the step and not the way. - e2e 46i samples one wheel step at 1000 ms a frame later: zoom 1.033 between 1 and 2, the point under the pointer kept at (134, -40). It lands on the view a 0 ms jump reaches. - e2e 46 and 49 expect the new menu entry. - Gate (atx): 234 files, 3563 tests, CHECK EXIT 0 (+15) - e2e 46,49 (atx): ALL PASSED, 43 checks (+3, sum 1735) - Three counter-checks fail view-tween.test.ts as expected. e2e 46i fails when the scene view does not carry its centre along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2aa635f453 |
Keep an output's name readable beside a long inline value (#306)
An input and an output in the same port row were cut to fit each on its own. The input text could reach x = 123 and the output label could start at x = 83, so a long inline value ran under the output's name. At Animation playing a long animation name covered "progress". - The output label is placed first and keeps its room. The input text is cut with an ellipsis 6 px before it, as decided in the issue. - The rule is a pure function in src/editor/graph/portRow.ts. A row without an output keeps the input's old limit of 108 px. - The value a run shows beside an output uses the same rule. Its left edge can no longer lie right of its right edge. - New test hook __gm.graph.rowSpans(nodeId) gives each row's input and output text as cut and placed, in node units. - NODE_WIDTH stays as it is, as decided in the issue. Tests: - Gate: 230 files, 3520 tests (6 new in port-row.test.ts). - e2e 6: all passed, 149 checks (148 + 1). - Counter-check: with the rule switched off, the input text reaches x = 121.4 under "progress" at x = 114.1, and the new check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
808e1eefc2 |
Clean up duplicates and dead code, and fix the + menu check (#85)
Refactoring round 9 covers everything since round 8 ( |
||
|
|
2e55071fa5 |
Lay a shader over a camera's whole picture (#299)
A shader so far worked only on its own wearer. Effects over the whole game picture, such as a vignette, grey, scanlines or a red flash on a hit, had no place to live. A camera now wears one, drawn the way a field shader of a particle source is (#170). - The camera gets a Shader row and, while one is picked, a Shader resolution row. The shader's properties follow as camera rows. They load, animate and appear in a Properties node under Type Camera. - Every view keeps masks and objects in one container. CameraShade renders it into the camera's picture, cleared to the background, and draws the shader over the frame. Game tab and play.html take the container off the stage while the picture stands in for it. - Screen pixels gives the picture as many pixels as it covers on screen. Camera pixels uses View width x View height and scales up without smoothing. - A pixel counts as a camera pixel in both (the user's choice). The uniform uPixelScale makes Texture size read View width x View height, so a Blur keeps its reach at any game scale. - Each camera wears its own shader, overlay cameras included. A red flash on the game camera leaves the HUD of an overlay camera alone. - The scene view's eye has a Shader switch on the Camera row, off by default. With it on, the main camera's shader lies over its frame, below symbols and gizmos, and the world around it stays drawn. - New tool tools/perf-camera-shader.mjs measures the cost. Measured on stein2 (HD 630, Game tab at Fit 1310x679): a camera shader that hands the picture through stays inside the scene's own spread of 3 to 5 fps between runs. A Blur of 4 cost 4 to 8 fps. Tests: - Gate on melektaus-atx: 227 files, 3497 tests (3487 + 10). - e2e 63 alone: 11 checks (6 + 5), e2e sum 1725 counted. - 63e-63i: pass-through is byte-equal and upright at 100%, flat red reaches the corners while the overlay keeps its green, Texture size reads 640 x 360 in both resolutions at Fit, no shader draws as before, and the scene view shows it only with the switch on. - Counter-checks: a camera left out of the shader wearers fails 1 unit test. Counting screen pixels fails 63g with #ab6000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2326085ed |
Show each camera only its layers and lay overlay cameras on top (#302)
A camera could only show the whole scene, so a HUD drawn in the world would run through a camera shader (#299) with everything else. Render layers say which cameras see an object, and overlay cameras draw their own picture over the main one. - The project has a list of up to 16 render layers, edited in Project settings next to the collision layers. It is a list of its own, read by the same rules (`layerNamesOf`). - Sprites, texts, tilemaps, particle sources and containers carry a row `Render layer` under their switches. Its default `Inherit` takes the parent's layer, at the top the first one. - A camera has `Sees` (all layers by default) and `Overlay`. The main camera is the first one switched on without Overlay, so two plain cameras still draw through the first one only. - Every view through a camera keeps only what that camera sees and lays one see-through view per overlay camera over itself. That view is a `WorldViewport` of its own, handed its view and frame from below. - An overlay is fitted into the main camera's whole picture without distortion. One of the same size and zoom covers it exactly. - `z` sorts within one camera only. The pictures lie on top of each other as a whole, which is what a shader per camera (#299) needs. - The Game tab's preview now collects the scene once per redraw. Before it collected it twice, with the cameras it would have been five times. - `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which takes the list it sets. Follow-ups: #304 (the mouse through a chosen camera) and #305 (change the render layer at run time). Tests: - Gate: 226 files, 3487 tests (3480 + 7) - e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a red main sprite at z 1000, in the preview and in the running game. A `UI` sprite in front of the main camera and a `Default` one in front of the overlay stay away. Without Overlay the second camera draws nothing. The settings dialog adds a render layer at the end. - Counter-checks: inheriting from parents switched off fails 1 unit test. Overlays never drawn fails the two overlay checks in e2e 63. - e2e total 1720 (1716 + 4), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d6d38a6422 |
Lay a shader over a particle source's whole field (#170)
A shader on a source runs per particle and stops at each quad, so a glow or blur across the whole cloud was not possible. A source now has a second shader row that is laid over everything it draws at once. - New rows Field shader, Field width and Field height (256) on a particle source. The size rows show only while a field shader is picked (new `shownWhenSet` on a row definition). - The particles are drawn into a RenderTexture of the frame, and a sprite mesh wearing the field shader draws that picture. The frame is centred on the source and turns and grows with it. - UV run 0 to 1 across the frame. Texture color, Texture alpha and Sample texture read the picture of the particles. - The picture has as many device pixels as the frame covers on screen, at most 2048 per side. A frame off screen draws no pass. - The field shader's Padding and Vertex output do nothing: the frame is the edge, and its corners stay put as on a nine-sliced sprite. - A name both shaders declare is one row, and both uniforms read it. - The scene view draws the frame dashed round a selected source. The Particles tab preview wears the field shader too. - `tools/perf-field-shaders.mjs` measures 1, 10 and 50 sources. On the HD 630 they ran at 60.0, 60.0 and 57.6 fps against 60.1, 60.0 and 59.2 without, one run per row. Tests: - Gate: 223 files, 3460 tests (3452 + 8), CHECK EXIT 0. - e2e 31: 43 checks, 3 of them new. Sum 1707 is computed (1704 + 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7dd3ae1131 |
Tie two objects with a hinge, a spring, a rope or for good (#154)
Two bodies could not be connected: no door on a hinge, no lamp on
a rope, no wheel on an axle. This is the first step of the issue,
the four joints without a motor. The slider and the motors follow
in a second step under the same issue.
- Joint is a new asset type, placed as a child of the object it
holds, like a collider or a rigidbody. "Add joint…" in the
object menu makes one or places one the project has. An object
takes as many as it needs.
- Where the joint sits is the point it holds its object at. A
hinge and a fixed joint have that one point only. A spring and a
rope end at a connected anchor on the other object, counted in
that object's own units, or in the scene's without one.
- The rows follow the type: length, stiffness and damping for a
spring, length for a rope, an angle limit for a hinge. Length 0
takes the distance the two ends were placed at. The limit counts
from how the two were placed.
- The connected object is an object reference in the schema of the
type. Pasting, containers and removing treat it as they treat
every reference since #258.
- shownFor is a new schema option: a row is shown while an enum of
the same bag holds one of the named values.
- The object needs a rigidbody, its own or one further up. Without
one the joint holds nothing and the console says so once.
- A connected object without a rigidbody is a moving anchor: the
joint has a kinematic body of its own that follows it.
- A connected object destroyed while playing makes the joint let go
with a hint. Removed in the editor the reference empties, so the
joint holds on to the scene. The issue asked for "inactive" there.
- "Add joint" ties two objects while the game runs, from a joint
asset as the template. Destroy on the joint lets go, there is no
Break joint node. Destroy with keep children takes the joints
along with the object.
- The scene view draws a line from each joint to what it holds on
to and rings the held point. A selected spring or rope has a
round handle on its far end, and dragging it is one undo step.
- Joints hold in Play only. The editor run simulates nothing
(#148), and the inspector of a placed joint says so.
- Rapier counts a revolute joint's limits absolutely, not from the
start, so the placed angle is added. Measured with a probe.
- The + menu has 25 types now and no longer fits under its button
on a canvas 800 high. It scrolls there, and e2e 04 scrolls to
the last entry instead of expecting it in view.
Tests:
- Gate on melektaus-atx for this state: 218 files, 3390 tests
(3336 + 54), exit 0.
- e2e 1-16,24,39-46,51,52,57,58 on melektaus-atx: 831 checks, all
pass. Module 41 alone: 25 checks (21 + 4).
- 54 new unit tests: 40 in joints.test.ts (rope, spring, pendulum,
limit, fixed, the other end, the node, containers, paste), 6 for
the line the scene view draws, 4 catalog cases of the node. The
remaining 4 come from tests that make a case per asset type.
- Four new e2e checks in module 41: Add joint from the menu, the
rope to the hook with its line and handle, dragging the handle
with one undo, the bob hanging in Play next to one that falls.
- e2e 04, 40 and 46 count the new type in their lists.
- 21 of 22 unit counter-probes fail their tests. The other one is
equivalent: Rapier ignores removing a joint that left with its
body, the guard stays.
- Three browser counter-probes fail their checks (2, 1 and 2).
- One screenshot looked at: icon, line, handle and the rope rows.
- The e2e sum of 1691 is counted (1687 + 4, 1687 measured by CI
on
|
||
|
|
f02adabcf9 |
Show the type an arithmetic shader node really puts out (#219)
The result of Add, Multiply, Mix and the other arithmetic nodes of a shader graph was drawn as any, whatever went in. The compiler already lifts a colour on any side to a colour result, so the pin said less than the code knew. A colour result could also be wired into a number input, and the shader then failed to compile. - liftedType in core/shader/nodes.ts is the one rule. The compiler reads it when it lifts two operands, and the editor reads it through liftedPortType for the ports it draws. - A SharedPortGroup may bring its own rule as agree. Without one it is agreedType, as before. The thirteen arithmetic nodes share their type by liftedPortType. - Only the any inputs decide. An input nothing feeds counts as the number typed into it. t, edge, exponent, min and max have no say. - The type travels through a chain of such nodes without code of its own, because #210 hands a shared type on. - In a ring the result stays any, unless a colour has settled it. - A number may feed a colour input. Without that a number result no longer fitted Output.color, although the compiler spreads it over three channels. The rule sits in portsCompatible, but only the shader graph has colour ports. - A colour result no longer fits a number input. An edge laid before the colour came in stays and is drawn in the danger colour, and the node at its end names the problem. - The nine shaders in the stored projects (108 edges) all stay valid under the new rule. Tests: - Gate on melektaus-atx: 212 files, 3260 tests (3240 + 20), exit 0. The gate right before this commit ran on the tree of this and the next commit together, with the same numbers. - e2e 28 on melektaus-atx: 81 checks, all pass. - e2e 1-31: 1059 pass, 1 fail in module 18 (the editor clock read 1.97 s after a stop). A rerun of 1-18 passed with 602 checks and 0.18 s. 1-31 was not run in full again. - No full run and no smokes. The sum of 1676 (1673 + 3) is reckoned. - Twenty unit tests in shader-port-types.test.ts, one of them runs all thirteen nodes through every mix of number and colour and compares what is drawn with the code made. - Two tests in shaders.test.ts now lay the edge first and bring the colour in after, the only order that still wires such a graph. - Three e2e checks in module 28 with the real mouse: a colour into Multiply, the red edge with its message, and the colour result refused at alpha and taken at color. - Fifteen unit counter-probes and two browser counter-probes all fail their tests. - Not checked in the browser: the one-input nodes, Mix and the ring, and a number wired straight into a colour input. Nothing was looked at after the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c88f57cf0a |
Let a project choose how its game link is zoomed (#279)
The page of a released game always drew the camera at Auto, the
largest whole step that fits. A camera of 300x180 in a window of
1280x800 stands at step 4 and leaves a border left and right. The
issue asks for Fit next to it. Answered there: a setting of its own
rather than the scale of the Game tab, and no switch for the player.
- The project has a field gameLinkZoom, 'auto' or 'fit'. A document
without it reads as 'auto', so nothing changes for a project that
is there already. No format bump.
- Project settings has a row "Link zoom" with a menu of Auto and
Fit. It is an undo step of its own, SetGameLinkZoomCommand.
- GameScreen hands the field to cameraView where it passed a fixed
'auto'.
- gameScale, the scale of the Game tab, is not touched and does not
reach the link.
- The released copy is frozen, so the change reaches the link only
with "Update release".
Decided here:
- The row is called "Link zoom", not "Game link zoom" as proposed.
The longer label is wider than the label column of the dialog and
pushed its button 12 px out of line with the others.
- The undo step keeps the name "Game link zoom", which is what the
tooltip of the undo button shows.
- A note under the row says "The released game at the largest whole
step that fits, or filling the window."
Tests:
- Gate on melektaus-atx: 211 files, 3227 tests (3219 + 8), exit 0
- e2e 60 on melektaus-atx: 19 checks, all pass
- e2e 35,46,59,60: 74 checks, all pass
- No full run and no smokes. The sum of 1671 (1668 + 3) is reckoned,
1668 is what CI counted for
|
||
|
|
9e869df063 |
Free a redrawn picture only once no shader names it (#293)
Editing a vector drawing made Pixi warn "destroyed while still bound
to a shader", twice with every change, once it had begun. It began
where an object wears the drawing through a shader asset. Each
change draws the picture anew and destroyed the one before it, but
the mesh of that object keeps its picture bound until it syncs. The
issue also asks for the button "Add shape" to go: a shape without
points cannot be given any, the Pen draws a new one.
- GeneratedPictures takes two more callbacks. inUse says that a
picture cannot be freed yet, soon runs a retry once whoever asked
is done.
- A replaced picture that is still in use waits in a list of its
own. It is freed right after the redraw that asked, as a rule. On
a later ask where its wearer was not part of that redraw.
- TextureLibrary tells a bound picture by the listeners for change
on its source. Under WebGL the bind groups are the only ones that
listen there. The retry is a microtask.
- The rule is asked for every drawn picture alike, whoever wears it.
- The inspector of a drawing and of a shape has no "Add shape" any
more. "Delete shapes" for several selected shapes stays.
Decided here:
- Under the list of shapes a second note says "The Pen draws a new
shape." while only the asset is selected.
Found on the way:
- Reproduced on a throwaway copy of the project the report came
from: 6 warnings for 3 changes with the shader override on the
object, none without it. After the change none, and no replaced
picture is left waiting.
- The fixture project of the tests never showed the warning. No
object there wore a drawing through a shader.
Tests:
- Gate on melektaus-atx: 211 files, 3219 tests (3209 + 10), exit 0
- e2e 47,48 on melektaus-atx: 113 checks, all pass
- e2e 28,30,31,39,40,47,48: 350 checks, all pass, run before the
wording of the new note changed
- The first gate was red: an existing test read the wording of the
note that had changed. Only three test files had been run after
the change.
- No full run and no smokes. The sum of 1668 (1667 + 1) is reckoned,
1667 is what CI counted for
|
||
|
|
ec2fd9084f |
Say what a button without words does when the pointer rests (#209)
The top bar, the magnet, the eye and many marks of the inspector are
pictures without a caption. Whoever did not know one could only try
it. The toolkit had no hint on hover, which also forced the point
menu of #195 to be words. The answers in the issue settle the open
points: a cut tab name shows in full, shortcuts are part of the
text, the point menu stays as it is.
- Button takes a tooltip, a text or a function asked each time. It
shows on a small sign once the pointer has rested for half a
second.
- There is one sign for the page, a TooltipLayer on the overlay
above the menus. A control reports each pointer move and the
pointer leaving, the layer does the waiting.
- A press, a key, the wheel or a blur of the window takes the sign
away. initUI listens for them on the window.
- placeTooltip in ui/tooltip.ts is the rule for the place as a pure
function: under the control and flush left, flush right at the
right border, above the control at the bottom.
- toolbarTip says what the five buttons of the top bar do right now.
Undo and redo name their step from CommandStack.undoLabel and
redoLabel, as in "Undo Paint tiles (Ctrl+Z)".
- A SortableList reports its own marks with a key per mark: the eye,
the duplicate mark, the cross and the plus.
- The magnet, both fullscreen buttons, the eye of the scene tab, the
overflow of the tab strip, the plus of the assets panel, the marks
of the inspector rows and the nine anchors of the resize dialog
carry a text.
Decided here:
- Every button whose text is cut short shows the whole text, not
only a tab.
- A row action of the inspector names the one thing a click does. A
button that opens a menu says only Reset, Apply or Size from the
picture.
- A button just clicked stays quiet until the pointer has left it.
- A greyed out button shows no sign.
- The switches of the eye popup, the cross of a document tab and the
tools of pixel art have none. The tools carry words, other than
the issue assumes.
- The marks of a list say Show or Hide, Duplicate, Remove and Add,
whatever the list holds.
Found on the way:
- Signing out empties the overlay and would have destroyed a sign on
screen with it. The editor takes the tooltip away first. This has
no test of its own.
- The text hooks locate and has skip the sign, or it would answer
for the button under it.
Tests:
- Gate on melektaus-atx: 210 files, 3209 tests (3176 + 33), exit 0
- e2e 46 on melektaus-atx: 20 checks, all pass
- e2e 1-14 on melektaus-atx: 508 checks, all pass
- e2e 1-9,14,39,40,44-48: 685 pass, 2 fail, both in module 14. It
measures tabs that modules 10 to 13 open, and the chain 1-14 is
green. The cut chain was not run on the state before.
- No full run and no smokes. The sum of 1667 (1664 + 3) is reckoned,
1664 is what CI counted for
|
||
|
|
345289982b |
Scale, turn and mirror the shapes of a vector drawing (#291)
A shape could only be moved and reshaped point by point. Whoever
wanted it larger or turned pulled every point alone, and for several
shapes or a group there was no way at all. The answers in the issue
settle the rest: the change is baked into the points, nothing of it
is remembered, and the line width stays.
- With Select the selection stands in a frame of eight squares and a
knob, drawn like the one of the scene view. It holds one shape,
several shapes or a group, the same selection that is dragged as
one.
- A corner square scales from the opposite corner, a square on an
edge in its own direction. Shift keeps the proportions, Alt scales
about the middle.
- The knob turns round the middle of the frame, with Shift in steps
of 15 degrees. After the pull the frame stands upright again.
- The inspector has a band TRANSFORM with X, Y, Width, Height and
Rotate by, and the buttons Flip horizontal and Flip vertical.
- Points, handles, gradients and texture tiles are taken through the
same matrix. The rule is a pure function in vectorTransform.ts,
transformShapesCommand makes one undo step of any number of
shapes.
- A press goes to a point or a handle first, then to the frame, then
to a line. shapeFrame.ts holds where the handles are and what a
pull of each means.
Decided here:
- The frame stands 15 pixels off the shapes. The points of a shape
lie on its bounds, so a square on the bounds would cover them.
- The knob only turns. In the scene view it scales as well.
- A side cannot be pulled through the opposite one, the frame keeps
at least 1 pixel.
- Coordinates are rounded to a thousandth of a pixel, so a quarter
turn is exact.
- A texture tile stays upright under a turn and grows to the bounds
of the turned tile. The bands of a gradient stay square to its
axis under an uneven scale.
- The switch Scale stroke of the proposal is not built.
Found on the way:
- The inspector of a shape is now taller than the window. Two checks
of module 47 no longer found their row or button on the screen and
use the new hook inspectorButton, which scrolls there first.
- Not solved: with a shape at the top edge of the view the knob lies
outside the tab. The wiki names it under the limits.
Taken over from #217: the e2e check for two selected shapes without
a group, and unit tests for the snap of the common offset and for
the gradients of the shapes dragged along.
Tests:
- Gate on melektaus-atx: 209 files, 3176 tests (3121 + 55), exit 0
- e2e 47,48 on melektaus-atx: 112 checks, all pass
- No full run and no smokes. The sum of 1664 (1660 + 4) is reckoned,
1660 is what CI counted for
|
||
|
|
6d79cf59a3 |
Move a large graph instead of drawing it again (#292)
In a graph of many nodes the frame rate dropped while panning, and more so while one node was dragged. Every pointer move rebuilt every box, every text and every curve, and a drag also announced a changed project, so the panels rebuilt along with it. - Panning and zooming move the picture. GraphCanvas.applyView sets position and scale and draws anew only the grid and the rubber band. It used to call redraw(). - A node draws its box and makes its texts only when what they show has changed. NodeView keeps a signature of what redraw reads and one of what rebuildTexts reads, and returns early while they hold. - A drag does not announce a changed project. GraphDrags writes the model on every pointer move and asks the canvas once a frame to follow (GraphCanvasHost.moved). state.preview() waits for the command of the release. - Nodes outside the picture are not drawn. inView in graphView.ts is the rule, cull() applies it after every redraw and every change of the view. - The graph is a render group, and box and pins go into the batch. The mask sits on a container above the group, on the group itself it would be panned and zoomed along. - The rubber band and the drop mark of a dragged behavior are drawn on their own (drawBand), without a redraw of the graph. Measured in the CI image with 388 nodes and 540 links, fitted to the canvas, 240 pointer moves 16 ms apart: - Pan, script per move: 25.2 ms before, 2.0 ms after - Pan: 4.4 frames per second before, 18.7 after - Drag a node: 2.7 frames per second before, 22.4 after - The still picture: 9 frames per second before, 21 after Not solved: the still picture of such a graph stays at 21 frames per second in the test browser. What is left is the drawing itself, and the frame rates are those of that browser, not of a desktop GPU. Tests: - Gate on melektaus-atx: 207 files, 3121 tests (3114 + 7), exit 0 - e2e 4,6,18,19,20,23,24,26,28,31,36,43,51,53 on melektaus-atx: 542 pass, 1 fail. The fail is module 51 after module 43, an order the suite does not have, and it fails the same way without this change. Module 51 alone: 12 checks, all pass. - No full run and no smokes. The sum of 1660 (1656 + 4) is reckoned, and the CI workflow is switched off. - Seven unit tests: six for inView in tests/editor/graph-view.test.ts and one in frame-preview.test.ts - Four e2e checks in block 6z-xii of module 06: pan, drag, zoom in and fit, a changed value - Seven browser counter-probes fail 2 to 3 checks each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
32a0af6e2c |
Put the text and boxes of the interface on device pixels
On a screen scaled to 125 % or 150 % the editor looked soft beside a page the browser draws itself. The layout counts in whole CSS pixels, and most of those fall between two device pixels there. The GPU averages both: text loses its edges, and a 1 px line becomes a pixel and a half of mixed colour. Now text and boxes are rounded to device pixels, and a line is one device pixel wide. - ui/pixelGrid.ts holds the rule. crispText() makes a text with Pixi's roundPixels, crisp() does the same for a Graphics of upright boxes, and hairline() is the width of one device pixel. - Every text of the interface is made with crispText() instead of new Text(). - crisp() only rounds where the scale is not a whole number. At 100 % and 200 % nothing changes. - drawControlBox, drawHeaderBar and drawSectionBand draw their lines a hairline wide and flush with the edge. The widgets that draw lines of their own do the same. - Two bands share exactly the line they draw, and a column still ends on a whole pixel. - The stage is nudged by 1/32 px where the scale is not whole. At 150 % every odd pixel is an exact half, and without the nudge one band came out a row shorter than its neighbours. - Curves, icons and everything of the game stay unrounded. Rounding their points would bend them. - The harness takes deviceScaleFactor as an option, and the hooks deviceColumn, edgeContrast and pixelGrid read the frame as the screen gets it. Not in here: the subpixel smoothing a browser gives its own text. Pixi draws a text into a transparent picture first, and there only greyscale smoothing is possible. Measured at 150 %, edge contrast before and after: top bar 31.5 to 47.8, the lists on the left 68.6 to 86.4, inspector 27.6 to 38.4. At 100 % it is 46.5, 90.1 and 37.9, and the picture there is the same as before pixel for pixel, apart from the fps display. Tests: - Gate on melektaus-atx: 206 files, 3114 tests (3104 + 10), exit 0 - Full run on melektaus-atx: 1655 of 1656 checks passed, smokes 24/19/21. The one that failed was a condition of the new step 62 itself: it scrolled an inspector that had nothing to scroll. The step now makes the window lower for that reading. - e2e 61,62 after that fix: 14 checks. The full run was not repeated, and CI is switched off for now. - Ten unit tests, nine in tests/ui/pixel-grid.test.ts - Eight e2e checks in the new step 62, which runs at 150 % - Eleven unit counter-probes fail tests - Four browser counter-probes fail their checks. They ran before the fix to step 62 and were not repeated for it. - Screenshots looked at for 125 % and 150 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2ff3a78607 |
Put the shapes of a vector drawing on layers and in groups (#190)
A vector drawing was one flat list of shapes. With many shapes the list got hard to read, and several shapes could not be kept or moved together. Now the shapes stand on layers, as in pixel art, and can be put into groups that nest. - A drawing has layers, each with a name and an eye. A hidden layer is neither in the tab nor in the picture a sprite wears, and its shapes cannot be pressed. One layer is the one worked on: the pen and Add shape draw onto it, and SHAPES lists what is on it. - The inspector has a list LAYERS over the shapes. Its plus adds a layer, the cross on a row removes it with all on it, the last one stays. A double-click renames, a drag sorts. - Group (Ctrl+G) puts the selected shapes into a group, Ungroup (Ctrl+Shift+G) takes it away. A group stands over its rows in the list, the rows one step in, and a row is dragged into a group or out of it. - In the tab a press on a shape takes its outermost group, and a drag moves the group as one undo step. - The model has layers with a tree of shapes and groups instead of a flat shapes array. The rules are pure functions in core/model/vectorTree.ts. A drawing saved before opens with all its shapes on one layer. - SortableList takes rows with a depth and drops into a tree. The flat lists of tilemap and pixel art run as before. Decided here: - A group has no transformation of its own and cannot be hidden or locked. Rotating and scaling is the follow-up #291. - A group is never empty: the one that loses its last shape goes with it. - Several selected shapes are dragged together, grouped or not, and a click on a shape of the selection keeps the selection. This is what #217 asked for. - Shift+click selects like Ctrl+click. With Select, Shift pans only beside the shapes, as in the scene view. The middle button pans anywhere, and with the pen and Paint Shift pans as before. - A row of the list picks one shape of a group alone. - Shapes change their layer through a row Layer in the inspector, which is there from two layers on. Rows are not dragged between layers. - A group cannot span two layers, the button is off then. - The pen does not draw onto a hidden layer, the hint says so. - Groups are always shown unfolded. - The four order buttons work inside the own group or layer. Tests: - Gate: 204 files, 3067 tests (3008 + 59), exit 0 - e2e 47,48: 108 checks. e2e 39,40,47,48 before the Shift change: 209 checks. No full run, the sum of 1645 (1639 + 6) is reckoned and CI measures it. - 59 unit tests: 36 in tests/core/vector-tree.test.ts, 23 in tests/editor/vector-layers.test.ts - Six e2e checks with a real mouse in block 47j - 47 unit counter-probes for layers and groups and three for the Shift click all fail tests, run on stein2 - Three browser counter-probes fail their checks - Two screenshots looked at (group in the list, hidden layer) - Only unit tested: the row Layer, sorting and removing layers, copying a group, the Shift pan beside the shapes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |