aa097bd07638ce64b0f32e8ec9808e758e1a1063
365
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
f97961c8ff |
Play and stop a song from a behavior in the game #336
A song could only be heard in its tab. Now a game plays it through Play song and ends it through Stop song, with its tracks as they are set. A playing pattern or song also no longer undoes the pause of the toolbar. - Play song plays from the start of the song and then round the loop of its ruler, so an intro is heard once. Without loop it plays once to its end. Inputs are song, volume, fade in, loop (on by default) and detached (off by default). It gives out playback. - Stop song stops one song, every song for none, or one playback by its number, over its fade out. Stop sound with no sound named stops songs too. Stop pattern with no pattern named stops only patterns. - Songs play side by side, as patterns do. They are owned like a sound, so leaving the scene ends one unless it is detached. Pause game and the time scale leave the music alone. - Each playing is heard through a master of its own, silent until its first note. The fades move it, and a song's tracks sit inside it on buses of their own. They go once the playing and its voices have. - Notes and clips are struck the same way in the editor and the game (strikeNote, playClip). What a sequence needs comes from one place (core/music/needs). Findings: - Reading the audio clock resumed a suspended context. A playing pattern read it every 25 ms and so undid the toolbar's pause. The library now leaves a context alone while the toolbar holds it. - A pattern played once to its end asked the list of playings about its voices, and that list asked the pattern again. Once its last voice was gone this ran until the stack overflowed. The scheduler of a playing now asks only after voices. - e2e 64 reads the playings through __gm.sound.playbacks() now. Tests: - song-play.test.ts (new): 8 tests, node catalog +8 cases - sound-library.test.ts: 66 tests, 4 new - e2e 64 (melektaus-atx): 23 of 23 checks passed - e2e 65 (melektaus-atx): ALL PASSED, 22 checks (+4) - Gate (melektaus-atx): CHECK EXIT 0, 251 files, 3903 tests - Counter-checks: the old way of asking after voices, a clock read that wakes a held context, and a start after a fade out began each fail 1 test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
2a3b90b017 |
Play and stop a pattern from a behavior in the game #336
A pattern could only be heard in its tab. Songs come with #334, and only then can #336 play them in a game. Until then a game plays a single pattern, looped or once, through two new nodes. - Play pattern plays the whole pattern through its instrument at its own tempo. The loop on the ruler is only for editing. Inputs are pattern, volume, loop (on by default) and detached. It gives out playback, 0 where there is no instrument or no note. - Stop pattern stops one pattern, every pattern for none, or one playback by its number. Let ring lets notes with an Envelope fall away in their release. - A playing is numbered and owned like a voice. Destroying the object, Stop sound with nothing named and the end of the run stop it. Stop sound with a voice takes a playback too. - Each playing runs on a scheduler of its own (core/music/playbacks). It waits for its files first, and a stop that came earlier wins. - The scheduler has an end for a sequence that does not loop. It stops waking there and is done once its last notes are. - The worker clock moved to viewport/wake.ts, so SoundLibrary can use it without importing music.ts. Tests: - pattern-play.test.ts (new): 8 tests, node catalog +7 cases - e2e 64 (melektaus-atx): ALL PASSED, 23 checks (+3) - Gate (melektaus-atx): CHECK EXIT 0, 248 files, 3854 tests - Counter-checks: no end in the scheduler, and no owner for the playing, each fail 1 test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
9c71eb1a0d |
Shape notes with an Envelope and move numbers with its curve #335
Without an envelope a note set in hard and broke off 10 ms after its end. An instrument now has an Envelope node with attack, decay, sustain and release. On the way of the sound it is the volume. Its output "value" moves a number of the note with the same curve, from "From" to "To", for a filter sweep among others. A new Filter node gives that sweep something to move. - Envelope (instrument only): attack a straight line, decay and release ease out and are 60 dB of the way at their time. A release takes 10 ms at least. Defaults 0.005 / 0.1 / 1 / 0.1. - The end of a note goes to the Envelope instead of the 10 ms fade. A Gate or Loop Sample under it plays on through the release, "Loop, then release" leaves its loop too, and the voice ends with the release. A One-shot under an Envelope of its own plays through. - A note ending before attack and decay releases from the level it has reached. Chrome holds it with cancelAndHoldAtTime, Firefox gets the level worked out from the shape. - New port type "envelope" (coral). It fits only a number input that says it can follow one (PortDef.followsEnvelope): Filter cutoff and resonance, Pitch, Gain, the EQ bands and Mix. Not the Delay, not the Envelope's own numbers. portsMeet() decides it for ConnectCommand, the drag and the drawn edges. - On the audio thread every Envelope of a note is one ConstantSourceNode. A number it moves gets the curve through a WaveShaper, in equal ratios for a cutoff and a gain. A number an Envelope moves is no longer reached by a parameter. - Filter (sound FX and instrument): low-, high- or band-pass, cutoff in Hz, resonance 0 to 1 as a Q. A low- or high-pass is flat at 0 and rises 20 dB at 1, a band-pass goes from Q 0.7 to 20. - Stop sound with "let ring" lets a note with an Envelope fall away in its release. Without it, and with Stop in the tab, it is cut. - The same note again during a release is a voice of its own. - The Envelope's numbers are set when the note starts. They can come from velocity or a parameter, later changes do not bend the note. Tests: - envelope.test.ts (new, 12): curve, ratios, Filter Q, compiling an Envelope, its value into each number, a Delay refused - sound-library +7: attack and decay on the clock, release from the level reached, One-shot held, Stop sound with and without let ring, a cutoff and a pitch moved by the curve - e2e 34: ALL PASSED, 50 checks (+2). Offline in Chrome the curve rendered 0.5 / 1 / 0.5005 at 0.05 / 0.1 / 0.3 s, released at 0.5 s it was 0.0158 at 0.65 s and 0.0005 at 0.8 s, released mid-attack it fell from 0.5 to 0.05 in 0.1 s. A Gain moved 0.25 to 1 was 0.5 halfway. A looped note of 0.2 s with a release of 1 s was gone after 1.32 s. - Five counter-checks (release end, One-shot hold, the follows check, ratios, pitch sum) failed 3, 1, 1, 2 and 2 tests. - Gate on melektaus-atx: CHECK EXIT 0, 245 files, 3812 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fb4d65a2ab |
Keep both sounds full at the middle of a Mix with a curve #331
A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds in an instrument now goes through a Mix ( |
||
|
|
23bdc3f775 |
Play notes in time on the audio clock with a scheduler #332
Patterns and songs (#333, #334) have to play sample-accurate in time,
whatever the frame rate does. What is counted in game time runs away
from the sound. This lays the ground: musical time, a scheduler that
plans notes ahead on the audio clock, and a player that starts and ends
a note at a given second. Nothing in the editor plays a sequence yet,
the check scripts drive it through `__gm.music`.
- Ticks, 480 to the quarter as in MIDI files (decided in #332).
Triplets and quintuplets land on whole ticks. Any meter, a beat being
the note value under the line. The tempo always counts quarters.
- `core/music/sequence.ts` is a pure rule: the notes starting in a
window of playing time. Across the end of a loop it looks into the
next round, so the seam has no gap. A note past the loop end ends
there. Windows are whole ticks, and the seam is stepped exactly, so
no rounding can drop the first note of a round.
- `core/music/Scheduler.ts` plans the next 100 ms every 25 ms through a
host, so it is tested with a clock of its own. The first note sounds
50 ms after the start. Changes to the sequence are heard from the
next window on. Stop and seek fade what it started. A new tempo or
loop counts from what is planned, and the playhead keeps its old
pace until the clock gets there.
- The position is the audio clock less `outputLatency`, as bar, beat
and tick.
- The beat comes from a Web Worker (`viewport/musicTimer.ts`), which
keeps its pace in a hidden tab.
- The sound library starts a note at `timing.at`: a buffer source by
`start(when)`, the pitch worklet by counting down inside the render
block it falls in. A note's end is set on the clock at once, its
fade to the sample. Leaving a loop still waits for the page.
- Delay synced to beats (decided in #332, noted from #327): a setting
`Sync` (Seconds / Beats), `time` then in quarter notes, held to the
2 s of the line. Play sound and Play note have an input `tempo`,
120 BPM where nothing is connected. A scheduled note carries the
tempo of its sequence.
- Swing is left for #333/#334, as decided.
Also fixes the red suite of
|
||
|
|
6f1f31516f |
Find asset types below the fold of the + menu in e2e #331
Since the Instrument joined the + menu, its last entries can sit
below the fold of the test window. CI for
|
||
|
|
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
|
||
|
|
cf6799a784 |
Let echoes ring on with a Delay node in sound FX #324 #327
A voice used to end the moment its sources did, and Stop sound cut it dead. An echo has to outlive its sound, and a stop must not click. - Delay node: a delay line with a damped feedback loop, added at mix next to the untouched sound. Time 0.01 to 2 s, feedback up to 0.95, damping 20 kHz to 500 Hz evenly in octaves, all fed by parameters and moved by Set sound FX parameters like any other number. - Ping-pong is a setting of the node. It takes the sound into the left of the line and swaps the sides every round. - A voice with an echo rings on for its tail: until the last echo is 60 dB below the sound, from time, feedback and mix (fxTailOf). It stays reachable meanwhile, and a change counts the tail again. - The end of a tail is timed on the audio clock with a silent ConstantSourceNode, so a paused run holds it. Then the voice is disconnected, since an echo is a loop of nodes. - Stop sound fades a voice out in 10 ms instead of cutting it. Let ring fades only its sources and keeps the voice for its echoes. Where the context is suspended it cuts at once. - Stop sound takes a voice. Connected, it stops only that play, and a 0 from a Play sound that played nothing stops nothing. - Play sound hands out tail: length plus the echoes at its start values. - Destroying a behavior and ending a run still cut, echoes included. - No limit on voices. The emit of a sound FX node now also gets the node's settings. Finding: in Chrome every round of a feedback loop comes one render quantum late (128 samples, 2.7 ms at 48 kHz), measured as 0, 128, 256, 384 for the first four echoes. Left so, since a browser without it would then hear the echoes early. Written in the wiki. Tests: - sound-library, soundfx, node-catalog, sounds, keep-on-scene-load: 489 tests green. New: fade on stop, cut while suspended, echo and ping-pong wiring, ringing until the tail, tail counted again, let ring, stop by voice, echo knobs, delay compile, tail sums. - Counter-checks: no tail, a stop without fade, a connected voice 0 read as unconnected. Each fails its tests, files back by cmp. - e2e 34 (melektaus-atx): ALL PASSED, 37 checks. A blip through a ping-pong Delay (0.3 s, 0.5, 0.5) rang 2.97 s, 2.95 s worked out. Let ring kept the voice, a stop after it cut it. Rendered offline with the player's own stageNodes the echoes are 0.25 left, 0.125 right, 0.063 left. 64 stereo voices of it took about 0.3 s for 2 s, so about 400 play at once. - Gate (melektaus-atx): CHECK EXIT 0, 242 test files, 3713 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6e70421b92 |
Keep the length of a sound whatever its pitch does #317
A Pitch used to keep the length only for the pitch a voice started at. Set sound FX parameters changed it by speed, so a sweep made the sound longer or shorter, and length into time no longer spanned the sound. Now a way with a Pitch is played by an AudioWorklet, a phase vocoder whose pitch is an AudioParam in semitones. The length never changes, and a pitch glides, ramps and eases like a Gain. - PitchPlayer (core/soundfx) is the vocoder as a plain class, so it runs in a unit test. Frames of about 43 ms, four overlapping, a real FFT done as a complex one of half the size. - Peaks are moved with the bins around them (Laroche and Dolson's phase locking), and frames are turned by half their size so a phase is the one at the middle. Without both, a tone beat against itself and dipped to 0.54 of its loudness while the pitch moved. - It reads the sound itself rather than being fed it. It looks ahead as far as a frame reaches, so a voice starts with its first sample and every way of a Mix stays in step. A change of pitch is heard about half a frame (20 ms) late. - At exactly 0 a frame is the sound itself and costs next to nothing. Coming back to 0, the phases are pulled round to the sound's for about half a second before it switches, so the switch does not dip. - pitchWorklet (viewport) is the processor, bundled by Vite through ?worker&url. It is loaded with the first play that has a Pitch, which waits for it like for its bytes. A file's samples are sent with its first voice only and kept in the worklet's scope. - A way has the worklet when its plan has a Pitch, a typed-in 0 excepted (FxChain.shifted). A way without one stays a plain buffer source. Without AudioWorklet a Pitch is not heard, and the console says so. - The pitched copies (WSOLA, pitchShift.ts) and their cache are gone. - Decisions of the user: the player at the source rather than an effect in the chain, a phase vocoder rather than WSOLA, no limit on voices, no range tighter than the node's -64 to +64. - stats() says whether the worklet is there, levels() gives a pitch in semitones, and __gm.sound.pitchWorkletUrl() lets a check run it. Tests: - pitch-player.test.ts (new, replaces pitch-shift.test.ts): 11 tests. Pitches from -30.5 to +12 within 2 %, the length to the sample, 0 exact to 1e-5, a sweep half a frame behind, 8 kHz sounds, loops, ramps there and back without a dip. - sound-library.test.ts: 23 tests (+4), soundfx.test.ts: 31 (+1). - Counter-checks: no worklet in the player, shifted taken from the value, no pull back to 0. 15 tests fail, files restored by cmp. - e2e 34 on melektaus-atx: ALL PASSED, 34 checks (+1). In real Chrome the worklet is there and the pitch param glides to 3.0007. Offline: 880.0 Hz for 440 an octave up, 48000 of 48000 samples, a sweep 0 to -12 at 392 then 248 Hz, still 48000 samples. - Gate on melektaus-atx: 242 test files, 3690 tests, CHECK EXIT 0. - Cost: 53 ms per second of a stereo voice at +5 in Node on stein2, 5 ms at 0. In headless Chrome 16 voices for 2 s took 1.3 s, about 24 fill the audio thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
297fca3e94 |
Show Direction and Curve of a node only where they matter
An animation key shows Direction only for one of the ten curves and the Curve row only for Curve. Lerp and Set sound FX parameters hid them on the node body, but the inspector still offered both rows for every easing, so a linear Lerp had a Direction to pick that did nothing. - A node definition can name `idleParams`: settings that do nothing at the current values. The node body and the inspector both leave them out. - `hiddenParams` stays as it was. It only trims the node body, the inspector keeps those rows (the Order of an event, the mode of Find), so it could not take this job. - Lerp and Set sound FX parameters use `idleEasing` (formerly `hiddenEasing`) for it. - The idle settings are part of the node signature of the inspector. Picking an easing that needs Direction or Curve rebuilds the section, and the row appears. Tests: - easing-curves: 14 tests (+1), node-catalog: 381, green - e2e 06 (149 checks, green) checks that a new linear Lerp has no Direction and no Curve row in the inspector. Picking Cubic brings the Direction row, and Out is picked there as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e7a360db73 |
Offer the easings of an animation key on Lerp and sound FX ramps
A timed Set sound FX parameters ran only linearly, and a Lerp had four quadratic eases of its own. An animation key already had the full set. Now all three offer the same choice: Linear, Step, the ten curves from Sine to Bounce with In, Out or In-out, and a drawn Curve asset. - easingParams in src/core/graph/nodes/easing.ts gives a node the fields Easing, Direction and Curve from the key's own lists, and easingOfParams reads them back as a key interpolation. - There is no migration. A Lerp saved with the old in, out or inOut runs linear until its easing is picked again. - Set sound FX parameters hands the easing to the player. Linear and Step stay ramps of the audio clock, every other easing goes to setValueCurveAtTime as 200 samples a second, in semitones and dB. - Step now reaches its end at t 1. A key never asked for that, it reads the next key itself, so animations play as before. - The curve-in-use count and the warning for a gone curve cover the sound node too. Tests: - node-catalog: Lerp with cubic in, quad out and in-out, a curve without a direction, step, an unknown easing as linear. The sound node hands on a curve, step and a gone curve as linear. - sound-library: an eased change becomes a curve in semitones and decibels, a step a jump at the end. - node-catalog and curve after dropping the old values: 399, green. - e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a 2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the end, a 1 s Step held 1 and then jumped to 0.5. - Counter-check: with curves sent as linear ramps, the curve test of the sound library fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
501a1524ed |
Ramp sound FX parameters over a time on the audio clock
A pitch sweep counted down in On update or On fixed update runs in game time, while the sound runs on the clock of the audio device. A short sound magnifies any offset between the two, so the sweep came out different on every play and most of all on the first. - Set sound FX parameters has a new input time in seconds. The values run evenly to their target in that time, scheduled on the audio clock with linearRampToValueAtTime or exponentialRampToValueAtTime. - Evenly in their own unit: a pitch in semitones and a gain in decibels, an EQ band and a Mix share linearly in their value. - A time of 0 or below keeps the quick glide of about 0.1 s. - Each change starts from where the voice is now. Whatever was still planned is dropped, so a change never queues behind an older one. Firefox lacks cancelAndHoldAtTime, there the value is held by hand. - A voice still waiting on its bytes starts a timed change with the sound, so the whole sweep is heard. Tests: - node-catalog: time is handed on, a time below 0 is a quick glide. - sound-library: semitones and decibels ramp in their own steps, a timed change on a waiting voice starts with the sound. - sound-library + node-catalog + soundfx: 419 tests, green. - e2e 34 on melektaus-atx: ALL PASSED, 31 checks. A 2 s ramp from depth 7 to -5 read speed 0.979, 0.926 and 0.778 half a second on. - Counter-check: folding a timed change on a waiting voice into its start values fails the new test. - The gate did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c7e9769f90 |
Change the sound FX parameters of a sound while it plays #315
Until now a value handed to a sound FX counted from the start of the voice only. An engine whose pitch follows the speed, or a loop that gets quieter, could not be built. Every play of Play sound now gives out the number of its voice, and a new node lets the knobs of that one voice glide to new values while it plays. - Play sound has a new output voice: the number of this one play, or 0 where nothing was played. Kept in a Number variable it reaches the voice from another event. Two plays of one sound are two voices. - New action node Set sound FX parameters (exec, voice, fx params). It reaches exactly the voice it is given, a detached one too. A voice that has ended, or voice 0, is not reached. - AudioSink.play returns the voice number and takes the plan and the values (FxVoice) instead of a finished chain. AudioSink.tune takes a voice number and new values. A name the voice's FX does not have is left out, one not given keeps its value. - SoundLibrary hands out the number at once, before the bytes are there, and never twice. A voice still waiting for its bytes starts with the values handed in meanwhile. - Every knob of a voice moves with setTargetAtTime (30 ms time constant). A knob whose value did not change is not sent again, so a tune every frame costs next to nothing. - A pitch on a playing voice glides by playbackRate. A new WSOLA copy in the middle of a sound is not worked out, so the voice gets shorter or longer while it is moved. On a loop that is not heard. - fxChainOf now gives a chain whose shape depends on the plan alone. A stage fed by a parameter stays at 0 dB, and a Mix fed by a parameter keeps both ways. A typed-in 0 or Mix at 0/1 is still left out by the compiler. - Every way of a voice has a source of its own, also at the same pitch, so each can glide on its own. The pitched copy is shared. - The tab keeps the numbers of what it plays, so moving a parameter field is heard at once. - Test hook __gm.sound.levels(file) reads the knobs of every voice of a file, oldest first, and stats() counts the voices tuned. Tests: - soundfx.test.ts: 29 tests (+2 for the node in a running world: the voice of the first of two plays of one sound, and a play that played nothing) - sound-library.test.ts: 15 tests (+3: glide, reach by number, pending voice) - node-catalog.test.ts: 370 tests (+2, Play sound checks its voice) - sounds + keep-on-scene-load: 31 tests, unchanged - Counter-test: a tune that reaches every voice of the same file makes the reach test fail. - e2e 34 (melektaus-atx): ALL PASSED - 30 checks (+1). In Chrome two looping voices of one sound start at depth 7. The one whose number sits in a variable glides to 0.7938 (2^(-4/12) = 0.7937), the other stays at 1. Both gains stay at 0.5012. - The gate ran before voices got a number (CHECK EXIT 0, 241 files, 3658 tests), not after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
072537b2e8 |
Blend two ways of a sound FX with a Mix node #316
A sound FX was a single line from Input to Output, so two versions of one sound could not be heard together. The new Mix node joins two ways again, and each way can carry its own effects, its own Pitch included. - Mix has the audio inputs a and b and a number input mix (0 to 1, default 0.5). At 0 only a is heard, at 1 only b. Like every number it can come from a parameter of the Input and is held to its range. - The compiled plan and the chain a voice gets are now a tree. A source carries a pitch and stages, a mix carries a, b and the stages after it. A Mix that ends at exactly 0 or 1 collapses to the one way heard, so the silent way is never worked out. - A Pitch is still worked out on the sound as it starts (#312). Behind a Mix it goes to both of its sounds. An effect before a Pitch therefore acts as if it came after it, the node now says so. - The player starts one source per pitch, all at once and all of the same length, and two ways at the same pitch share one. A mix becomes two gain nodes with 1 - mix and mix into a sum. A voice leaves the count when all of its sources have ended. - stats().last reports pitches (one per source) instead of pitch. Tests: - soundfx.test.ts: 27 tests (+5), sound-library.test.ts: 12 (+2), node-catalog.test.ts: 368, all green. Typecheck, ESLint, Prettier clean. - Counter-check: a Pitch behind the Mix given to a only, and b mixed at 1 - mix, each fails its new test. - e2e 34 on melektaus-atx: ALL PASSED, 29 checks (+1). The new check plays the pitched way mixed with the dry Input in the browser: two sources at 5 and 0 semitones, as long as the sound itself. - The full gate did not run before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
790e483198 |
Pitch a sound FX without changing its length #312 #313 #314
The Pitch node changed the playback speed, so a sound pitched up was also shorter and one pitched down longer. A pitch now keeps the length, takes fractions and reaches further. Gain and the EQ bands reach further down, and the EQ is renamed so a parametric one can come later without a mix-up. - Pitch shifting is the pure function pitchShift in core/soundfx/pitchShift.ts. It reads the sound faster or slower and stretches it back by WSOLA: 40 ms frames under a Hann window, each moved within 12.5 ms to where it continues the frame before it. A looping sound reads on across its end. - SoundLibrary plays a pitched copy of the decoded buffer instead of setting playbackRate. Copies are kept per file, pitch and loop, the last 32 of them. - stats().last tells the duration of what was played, so a check can see that the length is kept. - Pitch: -64 to +64 semitones, scrubbed in steps of 0.1 (#312). - Gain: -120 to +24 dB (#313). - EQ is now labelled "5-Band-EQ", its bands go -60 to +24 dB (#314). The node type stays SoundFxEq, so saved graphs load unchanged. Findings: - A plain granular shifter was tried first and missed the pitch: at +0.5 semitones it measured 438 Hz instead of 452.9 Hz, because every grain restarts at the phase of the original. WSOLA measures 452 Hz. - Far ends: +64 semitones from 440 Hz lands above half the sample rate and folds back, -64 is below what a 40 ms frame can follow. Nobody has listened to either. - Speed: 3 s of a 48 kHz channel take 47 ms at +3 and +64, 171 ms at -64 (measured in Node). Tests: - pitch-shift (new): 7 tests. A 440 Hz sine at +12, -12, +7, +0.5 and -30.5 semitones is within 2 % of the target and keeps its length. Level stays at most 1, a loop does not fade at its seam. - sound-library: 10 tests (+1), soundfx 22, node-catalog 368. - Counter-check: with the pitched copy switched off the new sound-library test fails. - e2e 34 (melektaus-atx): ALL PASSED - 28 checks. Its octave-down voice is now also checked for the length of the sound. - Typecheck, ESLint and Prettier clean. The gate did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45e45fb1df |
Count the Sound FX type in the + menu check
The suite run 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>
|
||
|
|
a5184b3368 |
Let the rows of a Variables node be dragged into a new order
The Variables and Properties nodes listed their picked rows in the inspector, but unlike Start, End and struct rows they could not be reordered. The drag dots also sat right on the edge of the row and touched the kind mark. - buildPickedPortSection gathers each row with its warning line as one block and hands the blocks to sortBlocks. A move reorders the ports on the node and is one undo step. Properties uses the same section and gains the same reordering. - Grip draws the dots 3 px in from the left edge and moves the kind mark 3 px clear of them (DOTS_PAD). The column keeps its width, so the name fields do not shift. Rows without dots are unchanged. Tests: - block-drag, reorder, variable-choices: 3 files, 13 tests, all green. block-drag now expects the mark 11 px in with dots, 0 without. - e2e 23 on melektaus-atx: ALL PASSED, 32 checks (+3 for dragging vy before vx and undoing it). It ran before the padding change, which touches no checks there. - The gate did not run before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2da07599d8 |
Let Get position give only the local x and y
Get world position now covers the world coordinates. Two nodes that both offer world x and y made it unclear which one to take. No project uses the world outputs of Get position any more. - Get position drops its world x and world y outputs and its description points to Get world position. - Old snapshots with edges on those outputs still load. The edges hang on ghost ports and the node warns about them. - The unit tests in hierarchy and raycast and the graphs in e2e 43 use Get world position where they read the world coordinates. Tests: - node-catalog, hierarchy, raycast: 3 files, 417 tests, all green - e2e 43 on melektaus-atx: ALL PASSED, 17 checks 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> |
||
|
|
92395f6773 |
Find the render layer log pair in e2e 63c3 instead of the tail (#305)
The suite went red three times in a row (tasks 488, 491, 492) on one check only: 63c3 read `named: ["",""]` while the picture half passed. Alone the module was green. In the full run objects left by earlier modules are still in the scene, and their behaviors log two empty lines after the OnStart of the check. `log.slice(-2)` took those. - The check now searches the log for the pair "UI", "1". Both lines come from one exec chain, so they stand next to each other. - Without the pair it shows the last 6 lines for diagnosis. - Not found out: which earlier module writes the empty lines. Tests: - e2e 63 on melektaus-atx with a temporary behavior that logs two empty lines after the switch: ALL PASSED, 13 checks. - The same with the old `slice(-2)`: 1 FAILED, `named: ["",""]`, the picture from CI. The disturbing behavior is removed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f57cf731d |
Keep a pixel frame worn through a shader alive while it is drawn
Changing a palette colour of pixel art flooded the console with Pixi warnings: "[BindGroup] a 'textureSource' was destroyed while still bound to a shader", and the same for its sampler. #293 had stopped this for a drawing, but a frame of pixel art is worn as a cut of the sheet. The cut has a source of its own, so the sheet looked unbound, was freed at once and took the cut with it under the shader. - The check whether a replaced picture is still in use gets its key too. TextureLibrary then asks the picture and every cut taken from it. - A sheet with a bound cut waits like a bound picture did since #293, and both go together once the shader has taken the new frame. Found on the way: - The project the report came from has a sprite wearing a frame of an eleven-frame pixel art through a shader asset. - A nine-sliced sprite wearing the frame showed no warning, even without the change. The first try of the e2e check used it. Tests: - Gate on melektaus-atx: 236 files, 3577 tests (+1) - e2e 48: 45 checks (+1, sum 1741). 48l-iii puts a shader on the object wearing the second frame, changes a palette colour three times and counts the warnings. - Counter-checks bite: without the cuts in the check the unit test fails, and 48l-iii counts 6 warnings for 3 changes. 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> |
||
|
|
3e05cbbed5 |
Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing and sorting already read both off the TextureRef every picture hands out, but the generated pictures always said "no slice, no line". So the feature moves from the frame to the picture, without a second copy of any rule. - PictureCut (border, fill, sortLine) is one type. TextureFrame extends it, PixelArtAsset and VectorArtAsset carry it as a whole. The texture file format does not change, and no migration is needed. - The rules in texture.ts take a cut and a size: cutSortLine, setCutSlice, rawCutSlice and the rest replace the frame* versions. serialize.ts reads all three through one readCut. - cutTarget.ts resolves a target to cut and size: a texture frame, a pixel frame's size or the vector canvas. It lives apart from texture.ts because vectorart.ts reaches texture.ts itself. - SetSliceCommand and SetSortLineCommand take that target instead of (assetId, frameId). They refuse an asset that draws no picture. - generatedRef fills slice and sortLine from the drawing. The whole strip of a pixel art asset with several frames gets none, it is no picture of that size. With one frame asset and frame are the same. - Nine-slice scale is one shared schema row, now also on pixel art and vector art. - cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame inspector and the drawing inspector both build them. - SheetCanvas draws a drawing's lines that are set, dashed as in the Texture tab. They are not dragged there, the painting tools own the canvas (as proposed in the issue). - drawOrder falls back to the picture's height when a ref has no rect, or a sort line on vector art would have been ignored. Tests: - Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a drawing, a cut on the whole strip, no height fallback. - e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where 12-pixel borders at scale 2 put them. Without borders both spots read green. - Looked at: the Pixel art tab with the lines, and the sliced sprite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d9c6bbfa4d |
Add a node that names the render layer of an object (#305)
The issue asked for Set render layer, Set sees and Render layer of. As with #211, a probe showed that writing already works. A Properties node of the object's type writes Render layer during a run, Inherit included, and one of type Camera writes Sees. The next picture follows both. As agreed, only Render layer of is built, and writing stays with Properties. - Render layer of is a data node with the input object and the outputs name and number. It answers the layer that holds, so an object without a choice of its own names its parent's. - NodeContext.renderLayerOf answers the number with the same rule the drawing uses (renderLayerAlong). The new field renderLayers on GraphLookup brings the names to the node. - hasRenderLayer in PropertySchema says which types are drawn. - Decided here: for an object that is not drawn (a camera, a behavior, a collider) the name is empty and the number -1, as Layer of does without a collider. Changing the render layer or Sees during a run had no test so far. Three unit tests now hold the proof from the issue. Tests: - Gate: 231 files, 3534 tests (3526 + 8), exit 0 - e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1) - Unit: the coin moves into the overlay with Set and back with Inherit, a HUD container takes its sprite along, the main camera sees a second layer once its Sees is set, and Render layer of names the inherited layer, and none for a behavior - Node catalog: four cases for Render layer of - 63c3 moves a sprite onto UI in a Play run: the main camera no longer draws it, and the log reads "UI" and "1" - Three counter-probes fail 1, 1 and 2 tests: without inheriting in the node, without the check for a drawn type, and without inheriting in the drawing - "layer of" in the palette still finds Layer of first (6z-x) - The e2e sum is 1731 (1730 plus 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70f3608733 |
Let Mouse position and Is hovered see through any camera (#304)
An overlay camera from #302 can stand elsewhere and zoom differently.
A HUD button only it shows lay somewhere else in the world than the
mouse of the main camera, so Is hovered never found it. Both nodes now
take a camera and see the mouse through that camera's picture.
- New input `camera` on Mouse position and Is hovered. Left open, both
read the main camera exactly as before.
- `pointerThrough` in core/properties/camera is the rule as a pure
function. It fits the camera's picture into the main one and centres
it, the same way overlayView draws it.
- The game view reports where the mouse lies on the main picture, as a
part of its width and height. The world turns that into a camera's
view, so a main camera on the move does not drag the mouse across
the HUD while the mouse stands still.
- `in view` is true only on the camera's own picture. A camera that is
not drawn (switched off, a second plain one, not a camera) never has
the mouse on it.
- On click and Mouse button stay with the main camera, as agreed in
the issue.
Also fixes a unit test that
|
||
|
|
1d0fa7b947 |
Let the game view zoom up to 20x
A small camera on a large screen stopped at 6x. The steps now go on in whole numbers up to 20, both in the zoom menu of the Game tab and for Auto, which takes the largest step that fits whole. - GAME_SCALE_STEPS in playSettings.ts runs 0.25, 0.5, 1 to 20. Prettier wraps the longer line. - e2e 60 keeps its own copy of the steps to work out what Auto should pick. The copy follows the new list. Tests: - view.test.ts: 23 passed (every step is used as its own factor) - e2e 60: ALL PASSED - 19 checks (Auto still picks 4 at 1280x800) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b895ae99c0 |
Show the graph in the e2e 6 screenshot of the long name (#306)
The check for the long animation name ran after a play and stop, which leave the editor on the scene tab. Its screenshot showed the scene, not the node. The check itself read the row through the test hook and was not affected. - The step switches back to the graph tab before it renames the animation. Tests: - e2e 6: all passed, 149 checks. The screenshot shows the node, with "animation Swi…" ending before a whole "progress". 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> |
||
|
|
a40e629fde |
Make e2e 56 pass on its own, not only after module 55 (#307)
Check 56a-ii clicks the read-only value of a private variable and types into it. A read-only field takes no focus, so typeText found no input and threw when the module ran alone. In the full run the keystroke went into whatever input an earlier module had left focused, and the check was green by luck. - New harness helper typeBlind sends the keystrokes without checking where they land. It is meant for keystrokes that should be refused. - 56a-ii now also checks that no field holds the focus after the click (__gm.focusedRect() is null), the same way module 28 checks its read-only Asset row. - No other module types into a field that is meant to be locked. Tests: - e2e 56 alone: ALL PASSED, 10 checks (was E2E EXIT 1 before) - e2e 55,56: ALL PASSED, 27 checks - Counter-check with the second lock click left out: 56a-ii fails with a focused field and timer 9, three later checks follow - Gate: 229 files, 3514 tests, CHECK EXIT 0 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 ( |
||
|
|
a526e1b40f |
Use a shader graph as a node in other shaders (#303)
A calculation needed in several shaders (a greyscale, a noise, an outline) had to be built again in every one of them. A shader function keeps it as an asset of its own, called like a behavior is since #16. - New asset type `shaderfunction` ("Shader function"), edited in the Shader tab. The panel beside the graph shows no picture, the readout counts its inputs and outputs. - Two new nodes: `Function input` and `Function output`. Their declared ports, numbers or colours only, are the ports of every call. One of each per function, like Start and End. - The call is node `ShaderCall`, named after the function. The palette lists every function, it can be dragged in from the Assets panel, and a double-click opens it. - The compiler sets the function in where it is called (`inlineFunctions`). It renames the copied nodes `<call>/<node>` and turns an unconnected port into a constant. Passes, Time, the Edge cut and the corner walk then see a plain graph. - A problem inside a function is reported at the call, in the function's words. The function's own tab checks it on its own (`checkShaderFunction`). - `ShaderLibrary` keys a shader on its graph and on every function it calls, however deep, so a change reaches every caller. - The port commands mirror a new, renamed or removed port into every call, as for a behavior's Start and End. `NodeDef.portKinds` holds the two ends to Number and Color. - As agreed in the issue: no `Property`, `Color property`, Output or Vertex output in a function, and `Time` is the caller's clock. - Own decision: a `Pass` may stand in a function. It becomes a pass of the calling shader, so a wide blur can be a function. - A function calling itself, also through another, says so at the node and is not set in. The palette does not offer such a call. - The Shader tab showed only the compiler's problems. A call nothing reaches was never compiled and looked healthy, so a call now also shows its own problem there, as in a behavior. Tests: - Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in tests/core/shader-functions.test.ts, 2 for the new icon) - e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in place both #606060/#a2a2a2, halved in the function #303030/#515151. - Counter-check: a fingerprint without the called functions fails 1 unit test, and in the browser the change check (#606060 stays). - The first e2e runs found two faults, both fixed: a double-click only selected the function, and an unreached call of itself showed no problem. - e2e total 1728 is computed (1725 + 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
f03ca0d382 |
Draw a shader in passes with a Pass node (#301)
A Blur stops at radius 8 in one pass, and bloom needs a softened copy of the picture beside the original. A Pass node cuts the graph: what feeds it is drawn into a picture of its own first, and the nodes that read a picture read that one when it feeds them. - Pass has color and alpha like the Output, a Resolution setting (Full, Half, Quarter) and a picture output. Texture color, Texture alpha, Sample texture, Blur and Texture size get a picture input. Unconnected it is the wearer's own picture, so the original stays readable beside any pass. - picture is a port type of its own. It plugs only into a picture input, not even into an any port. - The walk shared by the compilers can stop at a node (cut). The shader compiler draws each Pass it meets as a fragment of its own, earlier passes first. A ring through a picture says Feeds itself, and more than 8 passes say so at the node. - A pass draws untinted. The tint comes on when the last part reads a picture, so a particle source draws its passes once for all of its particles (user decision). - A pass's picture is the wearer's picture and its Padding, at the pixels of the picture times Resolution, at most 2048 per side. It is read with the picture's own filter and continues past its frame as the Output's Edge says. - PassPictures keeps one render texture per pass and wearer. It draws again only when picture, size, Padding or a uniform changed. A field's picture is a render texture that changes under the same id, so a field draws its passes every time. - Sprite, sliced sprite, text, particle source, field shader and the Shader tab's preview all draw passes. syncShader takes a fixed picture for a 9-slice, which holds its corners but keeps the margin. - texelAt and blur take the sampler they read. Existing shaders compile to the same program apart from that argument. - A field shader's Blur and Texture size count a pixel as a unit of the frame (uPixelScale), not of the screen. Before, a field's blur grew when zooming out and shrank when zooming in. This came with #170 and #300 and is fixed here because passes inherit it. - tools/perf-shaders.mjs and tools/perf-field-shaders.mjs measure two passes with GM_PERF_PASSES=1 and GM_PERF_PASS_RESOLUTION. Below Full an open Pass shrinks the picture first, as the wiki's recipe does. Found on the way: - Destroying the pictures before the shader made Pixi warn that a texture was still bound. The shader goes first now, and a picture of a new size is resized rather than replaced. - Two passes in a row could draw plain white the first time. Pixi builds the code that binds a program's resources once and finds them by position. A pass of one graph is often the same program as a pass of another with fewer slots, so it bound a placeholder. Every shader now declares all 8 pass slots and its uniform group. Tests: - Gate on melektaus-atx: 225 files, 3480 tests (3467 + 13), exit 0. - e2e 28 and 31 on melektaus-atx: 97 checks (94 + 3) and 44 checks (43 + 1), all passed, no Pixi warning in the console. - 28l-vii: a Blur across, a Pass and a Blur down draw the 5x5 spot of a Blur of Both (difference 0.000). On a grey sprite a pass's picture added to the original is 231/60/60, each tinted once. Two Passes in a row after a graph with one show red and green, not white. - 31v-iii: a Blur of 8 fades a field's frame edge over 8.0 units at 50 % and at 200 % zoom. - Counter-checks: a tinted pass fails 1 unit test. Pictures never bound fail both e2e checks (spot 0.20 instead of 0.04, 243/158/158). Pass slots by the number of passes draw #ffffff, and a field without its pixel scale ramps over 16 and 4 units: exactly the two new checks fail. - e2e sum 1716 is counted, not yet measured (1712 + 4). - Measured on stein2 (HD 630, one run per row): 50 field shaders at radius 8 go from 11.4 to 41.3 fps in two passes, 44.4 at radius 16 (three passes at Half). 800 sprites at radius 8 go from 13.9 to 24.5 fps. At radius 4 one pass stays faster from 400 sprites on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
df61b08bd1 |
Soften a shader's picture in one pass with a Blur node (#300)
A blur already worked with Sample texture, but only by wiring every sample by hand: 25 nodes for a 5x5. Averaging their colours also gave a dark seam wherever a neighbour was transparent. The Blur node writes the loop itself and averages the right way. - Blur takes radius (pixels of the picture, 0 to 8, fractions allowed) and u/v like Sample texture, and gives color and alpha. - Kind is Box (default) or Gaussian, Direction is Both, Horizontal or Vertical. Both are arguments of one prelude helper, blur(). - The samples are averaged premultiplied and only then made straight, so a transparent neighbour lowers the opacity and adds no black. - The outermost ring counts by the part of the radius that reaches it, so a radius from a Property changes smoothly and compiles nothing again. - A node may now declare prepare, a vec4 the compiler works out once into a local. Color and alpha read that one sum instead of blurring twice. - A reached Blur counts as a shifted sample, so the picture comes with the Output's Edge. It cannot feed the Vertex output. - Both perf tools take GM_PERF_BLUR=<radius>. Wider blurs and bloom need several passes. That is #301. Measured on stein2 (HD 630, one run per value, fps), Box blur: - 48x48 sprites at 100/400/800: radius 2 59.8/58.4/55.6, radius 4 53.1/51.4/36.3, radius 8 46.6/24.4/14.4. - 256x256 field shaders at 1/10/50: radius 2 60.2/56.0/49.0, radius 4 60.1/49.0/27.1, radius 8 52.4/34.9/11.4. Tests: - Gate: 224 files, 3467 tests, CHECK EXIT 0 (+7). - e2e 28: ALL PASSED, 94 checks (+5 in 28l-vi). Pixels match the arithmetic to 1: the last red texel at radius 0.5/1/4 reads 187/173/154 (expected 188/173/154). - Counter-checks: Blur not counted as shifted fails 1 unit test. Blur output left premultiplied fails the seam and spot checks. - e2e total 1712 is computed (1707 + 5). 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> |
||
|
|
bf27e42e8a |
Let a shader declare a color the sprite sets (#298)
A shader could only ask its sprites for numbers. A tint, an outline colour or a glow colour had to be built from three Property nodes. - New node Color property next to Property: Name and Default (a colour, white unless picked), output `color`. It becomes a colour row below the Shader dropdown and a vec3 uniform in the program. - Number and colour properties share one set of names. A clash is the existing "Another property is named" problem. - Like Color it cannot feed the Vertex output. - Placed from the palette it is called tint, then tint2. - The preview in the Shader tab shows a colour field for it. Like the sliders it starts at the sprite's value and never writes into the project. - `declaredRows` takes the row builder, so the particle graph keeps its number rows unchanged. Tests: - Gate: 222 files, 3452 tests (3445 + 7) - e2e 28: 89 checks (28l-v: default colour drawn, then the colour the sprite sets) - e2e total 1704, counted (1703 + 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4f4c149211 |
Duplicate an asset from its right-click menu (#297)
A new asset that starts out like an existing one (a death animation
next to "Break", a second slime) had to be built again by hand. Now
the asset panel's right-click menu has Duplicate.
- The copy lands right below the original and is named the way a new
asset would be ("Crate 2"). It is selected and its tab opens where
it has one. A copied scene does not become the active one.
- One undo step takes it out again. A redo brings back the same id.
- The copy is a deep copy: values, graph, tracks, layers and objects
are its own, and editing it leaves the original alone.
- Frames of a texture or of pixel art get fresh ids, because a sprite
names a frame across the whole document.
- Objects of a scene or container get fresh ids the way pasted ones
do. Parents and object references among them follow the copy.
- Whatever pointed at the original keeps pointing at the original: a
sprite wearing it, an animation's root, a placed object.
Tests:
- 3 new unit tests (DuplicateAssetCommand). One counter-check run:
without renaming the references the scene test fails.
- e2e 04: 2 new checks with the real menu (copy named "Crate 2" right
behind the Crate and selected, one undo removes it).
- Gate: 221 files, 3445 tests, CHECK EXIT 0.
- e2e total 1703 is computed (1701 + 2). Full suite left to CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f1e3bd8378 |
Let a joint glide on a rail and drive it with a motor (#154)
Step 2 of the joints: a door that opens by itself, a lift and a piston need a joint that slides along one line and a motor that turns or pushes. A graph also needs to read how far such a joint stands. - Slider: a fifth joint type on Rapier's prismatic joint. Its rail is the joint's own x axis, so turning the joint turns the rail. Joints now carry Scale, Rotation and Mirror like a sprite. - Limit distance with Min and Max, in world units from where the object was placed. - Motor on hinge and slider: Off, Speed or Target, with Motor speed, Motor target and Max force (0 = no limit). Target goes there at no more than the speed and slows down for the last stretch. - Changing speed, target or force keeps the engine joint. Switching the motor on or off builds it again. - New node Joint position: value and speed of a hinge (degrees, counting past a full round) or a slider (world units). - Scene view draws a slider's rail dashed, with a tick at each limit. - shownFor in the schema takes a list of choices that must all hold. Findings, measured with a probe against Rapier 0.20: - A prismatic joint holds both bodies at one angle. Held to the scene or to an object without a rigidbody, the joint's own anchor body is turned by the placed difference, so nothing jumps. Between two rigidbodies the held one is turned to match, and the console says so once. - Rapier reads no joint angle back. The position is worked out from the two bodies. - A hinge motor is weak on a small body at the end of a long arm (box 20 on an arm of 100 does not lift itself). More solver iterations would help but cost every scene. A door of 100 x 20 reaches -90 and holds it. This is written in the wiki. Tests: - Unit: 16 new in joints.test.ts, 1 in joint-links, 3 catalog cases. - e2e 41: 27 checks (25 + 2), block 41f with a real mouse in the inspector and the motor in play (59.1 of 60 units in 1 s). - 9 of 9 unit counter-probes fail as they should. - Gate: 220 files, 3436 tests (3416 + 20), CHECK EXIT 0. - e2e total 1701 is counted (1699 + 2), the full suite did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4716726b6b |
Let a shader move the corners of the picture it wears (#79)
A shader could only say what colour a pixel has. Where the picture is drawn was fixed to the four corners of its rectangle, so nothing could wave, bend or wobble. - The new node Vertex output takes x and y, in pixels of the picture, and moves every corner of the wearer's grid by them. The UVs stay with the corners, so the picture bends along. - Subdivisions on that node says how fine the grid is: cells per axis, 1 to 64, 8 by default. 1 is the plain quad. A shader without the node keeps the quad every sprite shares. - A graph holds one Vertex output at most. UV, Time, Property, Number and the math nodes may feed it, and one node may feed both outputs. - Texture color, Texture alpha, Sample texture, Texture size and Color cannot feed it. The node says "Not available per corner" and the shader does not compile. - Sprites, texts and the preview in the shader tab wear the grid. A nine-slice sprite and particles draw as if the node were not there. - Picking, the selection frame, the size and colliders stay with the rectangle. A wearer with a Vertex output is never culled, as with Padding, and with Padding the grid covers the room around the picture too. - tools/perf-shaders.mjs takes GM_PERF_SUBDIVISIONS to measure a grid against the plain quad. Measured on stein2 (Intel HD 630), frames per second, one run per row: at 800 sprites 58 with a shader on the quad, 46 with 8 cells, 41 with 32. Up to 400 sprites a grid hardly shows. Every sprite holds its own grid. Tests: - Gate on melektaus-atx: 220 files, 3416 tests, CHECK EXIT 0. - 15 new unit tests for the compiler and the grid. - Four new e2e checks in modules 28 and 30: a fixed offset moves the picture, 8 cells bend only the lower half where 1 cell shears it all, Texture alpha is refused at the node, a text moves. - e2e 28,30 on melektaus-atx: 110 checks, all pass. - The e2e total of 1699 is counted (1695 + 4), the full suite did not run. - No counter-probe, no screenshot. Not looked at: the preview, Time in the browser, flipped sprites, Padding together with the grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
963f8a152a |
Open a scene framed, and thin out two inspector sections (#295)
Three small things asked for in one issue. A scene came up wherever the view happened to stand, and the inspector carried a button and a list that are better reached somewhere else. - A scene comes up framed as if Fit had been clicked: after a project is loaded and when another scene is opened. From then on the view belongs to the user. - An empty scene has nothing to frame and stands at the origin, one pixel per unit. A scene the running game loads is not framed anew. - The Container tab is left out. It keeps the view of each container as before (#171). - The Unpack button and the two lines under it are gone from the inspector of a placed container. Unpack container stays in the right-click menu of the hierarchy and of the scene view. - The corners of a polygon collider are no row in the inspector any more. They are edited on the outline in the scene view. The new schema option noRow keeps a property out of the inspector while it stays a property. Tests: - Gate on melektaus-atx: 219 files, 3401 tests, CHECK EXIT 0. - One new unit test in tests/core/polygon.test.ts: the corners are a property of the collider and no row. - Three new e2e checks: a loaded project is framed like Fit (module 03), an opened scene is (04), and after a real reload a click on Fit moves nothing (09). - Module 09 unpacks through the right-click menu now and checks that the inspector offers no button. - Full suite on melektaus-atx: 1695 checks, all pass. Smokes 24, 19 and 21, all pass. - No counter-probe evaluated, and nothing looked at after the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |