docs: close out Θ-W5, and record two spline-overlay UX deferrals

Collapse Θ-W5 to wave-level in PLAN.md, append its narrative to COMPLETED.md, fix a stale resolvePlay reference in core/instrument/CLAUDE.md, and file the drag-off-margin and AttackEnd/Origin shadow warts in TODO.md.
This commit is contained in:
2026-08-01 00:29:40 -04:00
parent 85cf34f858
commit 213ecfafe6
4 changed files with 103 additions and 107 deletions
+23 -105
View File
@@ -218,108 +218,24 @@ curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline
only a real, testable rule once Gate has something to be unavailable *for*. It also
depends on W3's radio switch, which is how a spline contour reaches the overlay at all.
**One track.** The spline algorithm is one implementation with several consumers; a
second concurrent writer to it collides by construction.
#### Θ-W5-T1 — `spline-egs`
**Goal.** Offer a free-drawn alternative to every staged envelope: switch any EG from
Staged to Spline and draw the contour directly — with the monotone-spline machinery
already proven by the velocity curve, enhanced so sharp corners are possible rather than
everything smoothed.
**Consolidates item 3.**
**Surface boundary — owns:** `core/instrument/engine/velocity_curve` (the shared spline
implementation — hard points), the spline evaluator on the voice-render path,
`core/instrument/ui/curve_popup` + the overlay's spline mode
(`envelope_overlay`/`envelope_edit`), `core/instrument/map/component_state_io` (dual
state persistence), and the deck band's **disabled-knob rendering only**.
**Behavior.**
- **Mode toggle per EG.** Every processor that has an envelope (pitch, filter, amp) can
switch its Staged EG to a **Spline EG**.
- **Dual state — save but inactive.** Both the Staged and the Spline state are persisted;
switching modes keeps the inactive one **saved but inactive**. No conversion, no
discard — round-tripping Staged↔Spline restores the other mode's shape untouched.
- **Gate unavailable in Spline mode.** Gate mode is **not available while a Spline EG is
active**; the spline **always covers the full sample length** — a pure time function
over the sample, i.e. the Trigger/one-shot playback model.
- **Time axis: normalized, visually 1:1.** The contour is normalized to the full sample
length and the overlay represents **the time axis of the actual sample visually 1:1**.
Consequently a different-length capture rescales the stored contour to its own length,
shape preserved proportionally.
- **Point-editing grammar. Left-click adds a point; right-click deletes a point;
control-click toggles a point hard/smooth.** (The source doc's second follow-up said
alt-click delete; the third **supersedes** it with right-click — matching the
velocity-curve popup's already-shipped right-click node delete, giving **one
point-editing grammar across both spline consumers, with no migration on either side**.)
- **Point-count ceiling: 128.** Floor: the two endpoints implied by full-length coverage.
Daniel floated 64 and raised it to 128 explicitly so the cap does not limit **long
rhythmic phrases, which require high resolution to be interesting** — at roughly two
points per articulation event, 64 is about two bars of 16ths and 128 about four. **The
ceiling is a musical bound, not a performance one** (segment lookup is logarithmic; the
editor's 8 px minimum node separation is the practical density limit anyway). **An
engineer tempted to lower this number must read that motivation first.**
- **Staged controls disabled while Spline is active.** That envelope's staged segment
knobs are **disabled — inert, not merely inaudible** — including their inner curve dials
(the dial is part of the knob). The dormant staged state is edited only by switching
back to Staged.
- **Not globally monotone.** Contours don't have to rise and fall and are not globally
monotone; the monotone guarantee is **per-segment** — all soft points between any hard
points are smooth/monotone, with no overshoot between adjacent points.
- **Default contour: a smooth y = 1 x** — a smooth downward slope over the full sample
length. (The Spline-EG default; not a change to the velocity→amp curve's existing
default.)
- **Hard points — the algorithm enhancement.** The one spline implementation is enhanced
to support **multiple segments that don't minimally smooth**, so hard points are
possible: the contour is defined by **one or more monotone spline functions** which
together form the full time function — e.g. the first three points form a curved segment
connecting **at a sharp angle** to the next three, finishing the contour over the full
sample length. A hard point does no curve smoothing on either adjacent segment; the
natural sharp angle stands instead of a continuous derivative.
- **Shared across consumers.** The enhancement applies to the one implementation and flows
to every consumer: the **velocity→amp transfer curve** gains hard-point support by the
same change, as do the pitch, filter, and amp EGs in spline mode.
- **Hot-path constraint.** Spline evaluation runs per-voice per-sample. It is an indexed
binary search over a point array plus a polynomial evaluation — **not** a polymorphic
curve object, and not a per-sample allocation. With a 128-point ceiling the search is
≤7 steps; cache the segment index across samples where the read position is monotone.
**Acceptance criteria.**
- Each of the pitch, filter, and amp EGs offers a Staged/Spline mode switch; in Spline
mode the overlay (via the W3 radio switch) shows and edits the drawn contour, and played
notes audibly follow it.
- Left-click on the contour adds a point; right-click on a point deletes it; control-click
toggles it hard/smooth. Points are smooth by default; a hard point renders a **visible
sharp angle with no smoothing on either adjacent segment**, and the discontinuous slope
is audible where the modulation target makes it so (e.g. a pitch EG corner).
- A contour accepts points up to the **128-point ceiling**; attempting to add beyond it is
refused **without disturbing the existing contour**. The two endpoints cannot be deleted.
- A contour of several segments joined at hard points plays back over the full sample
length exactly as drawn — including contours that rise and fall freely (no
globally-monotone restriction), with **no overshoot between any adjacent pair of
points**.
- A freshly created Spline EG shows the smooth y = 1 x default contour.
- While a Spline EG is active, **Gate mode is not selectable**; the spline plays as a pure
time function over the full sample length.
- The overlay contour spans the full displayed sample width, 1:1 with the sample's time
axis; loading a different-length capture rescales the contour to the new length with the
drawn shape preserved proportionally.
- While a Spline EG is active, that envelope's staged segment knobs **and their inner
curve dials** render disabled and reject edits; switching back to Staged re-enables them
with values exactly as left.
- **Staged↔Spline round-trip preserves both states**: switch to Spline, draw, switch back
— staged values exactly as left; switch forward again — the contour exactly as drawn.
Both survive save/reload.
- The velocity→amp transfer-curve editor supports the same control-click hard/smooth
toggle with identical rendering behavior, and its existing right-click node delete
matches the Spline EG's delete gesture unchanged — **one point-editing grammar across
both consumers.**
- Spline evaluation adds no allocation and no virtual dispatch to `process()`.
**Open questions.** None — item 3's two remaining questions (point-count bounds; gesture
convergence) closed in the source doc's third follow-up round.
**Θ-W5-T1 has landed** `spline-egs` — see `docs/COMPLETED.md` for the full narrative.
It shipped a free-drawn alternative to every staged envelope: pitch, filter, and amp
EGs can switch Staged → Spline and have their contour drawn directly on the waveform
overlay. The one shared monotone-spline implementation gained hard points — sharp
corners, no smoothing on either adjacent segment — flowing to every consumer including
the existing velocity→amp transfer curve, no fork. Both Staged and Spline state persist
simultaneously (saved-but-inactive, lossless round-trip); params payload reached v13,
and v12 projects still load. Gate is unavailable while a Spline EG is active; the
contour is a pure time function over the full sample length, normalized and drawn 1:1
with the sample's time axis. Point grammar converged on left-click add / right-click
delete / control-click hard-smooth toggle, one grammar across both spline consumers.
Staged segment knobs and their inner curve dials render disabled and reject edits while
Spline is active. Point-count ceiling: 128, a musical bound rather than a performance
one. A follow-on change in the same track reworked deck cell width: `-1` in `cellIds`
now means one cell's width, reserved and redistributed, rather than a blank cell holding
geometry — a Trigger face that drops Sustain and Release gets wider cells instead of
dead slots; group widths, row packing, deck height, and Gate-mode cell widths are
unchanged.
---
@@ -328,9 +244,11 @@ convergence) closed in the source doc's third follow-up round.
**Depends on W5 for:** the last change to a drawn surface. Item 13 is an audit whose
output is a disposition list over "every class of drawn surface," and item 10's sizing
pass is judged by eye over the finished layout — running either while the spline contour,
the disabled-knob state, or the deck inventory is still moving means auditing and then
re-auditing. The source doc names this sequencing as an observation (item 13 after the
layout/knob work); this plan adopts it as the boundary.
the disabled-knob state, or the deck inventory was still moving would have meant
auditing and then re-auditing. Θ-W5-T1 has landed (see `docs/COMPLETED.md`), so that
surface has stopped moving and this dependency is satisfied. The source doc names this
sequencing as an observation (item 13 after the layout/knob work); this plan adopts it
as the boundary.
**One track.** Both items repaint essentially every surface in the editor; concurrent
tracks would collide everywhere.