fix(editor): resnap works with Snap off and snaps BOTH edges to the subdivision guidelines - #263
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughResnap selection now explicitly quantizes selected note starts and sustained ends to grid guidelines, including when Snap is off. The operation preserves short and zero-length notes, applies one undoable compound change, reports its result, and adds deterministic tests for the updated snapping behavior. ChangesResnap selection
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Editor as _editorResnapSelection
participant Edges as _resnapEdgesPure
participant Grid as snapTime and snapGuidelineAfter
participant History as Compound undo command
Editor->>Edges: resnap selected note edges
Edges->>Grid: force-quantize starts and sustained ends
Grid-->>Edges: snapped times and sustain lengths
Edges-->>Editor: changed note values
Editor->>History: apply moves and conditional sustain resizes
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/loop.js (1)
420-426: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect, well-tested; minor duplication with
snapTime.
snapGuidelineAfterre-derivessv/substhe same waysnapTimedoes (lines 402/409). Not a bug, but if the two ever need different subdivision rules this duplication could quietly drift.♻️ Optional: share the sv/subs derivation
+function _effectiveSubs(force) { + const sv = _editorEffectiveSnapValuePure(S.snapEnabled || force, SNAP_VALUES[S.snapIdx]); + return sv ? _editorSnapSubdivisionsPure(sv) : 0; +} + export function snapGuidelineAfter(t, force = false) { - const sv = _editorEffectiveSnapValuePure(S.snapEnabled || force, SNAP_VALUES[S.snapIdx]); - if (!sv || S.beats.length < 2) return t; - const subs = _editorSnapSubdivisionsPure(sv); + const subs = _effectiveSubs(force); + if (!subs || S.beats.length < 2) return t; const q = Math.floor(beatOf(S.beats, t) * subs + 1e-6) + 1; return timeOf(S.beats, q / subs); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/loop.js` around lines 420 - 426, Optionally extract the shared snap-value and subdivision derivation from snapTime and snapGuidelineAfter into a reusable helper, then use it in both functions. Preserve each function’s existing snapping behavior and early-return conditions.src/input.js (1)
503-543: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSolid wiring; matches the tested
_resnapEdgesPurecontract and PR's undo-grouping objective.One gap:
_editorResnapSelectionitself (single-undo-entry grouping, the "already on the grid" branch, touched-count message) is only covered by manual/live verification per the PR description — the automated suite only exercises the pure helpers it calls. Given this function is the one place the "one undoable step" guarantee is assembled, a small integration test (export it, stubS.history/notes()/host) would catch a future regression that the pure-function tests can't see.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/input.js` around lines 503 - 543, Add an integration test for _editorResnapSelection that stubs notes(), host, and S.history, then verifies the already-on-grid path and that changed selections execute exactly one grouped undoable command with the expected status/update behavior. Export _editorResnapSelection as needed for test access, while preserving the existing pure-helper tests and production behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/input.js`:
- Around line 503-543: Add an integration test for _editorResnapSelection that
stubs notes(), host, and S.history, then verifies the already-on-grid path and
that changed selections execute exactly one grouped undoable command with the
expected status/update behavior. Export _editorResnapSelection as needed for
test access, while preserving the existing pure-helper tests and production
behavior.
In `@src/loop.js`:
- Around line 420-426: Optionally extract the shared snap-value and subdivision
derivation from snapTime and snapGuidelineAfter into a reusable helper, then use
it in both functions. Preserve each function’s existing snapping behavior and
early-return conditions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 967063ff-8d2e-4ed0-829b-65042945b5d2
📒 Files selected for processing (4)
CHANGELOG.mdsrc/input.jssrc/loop.jstests/resnap_explicit.test.mjs
b140a97 to
ca1d650
Compare
snapTime gains a force arg (explicit verbs bypass the live-placement toggle; the snap VALUE/mode still apply — default unchanged for every interactive caller). New snapGuidelineAfter supplies the end-edge minimum. _resnapEdgesPure snaps starts AND sustained end edges to the current-subdivision guidelines (piano-roll model), never collapsing a sustained note or inflating a chip; one undoable composite step and an honest status line either way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…d Inspector changelog bullet The end-edge bound falls back to afterFn (the first guideline after the new start) when the snapped end lands on or before it. But afterFn is the IDENTITY when there is no usable grid (fewer than two beats, or the snap value off) — and in onset mode snapFn still snaps, so both edges of a short note can land on the same onset with no guideline to push the end past it. That returned a sustain of 0: a quantize silently turning a sustained note into a chip. With no positive bound to offer, keep the authored length. The changelog entry had also swallowed the first line of the Inspector technique-undo bullet, orphaning its body onto the resnap entry.
The history gate already refused the verb there (its MoveNoteCmd half is not pitchPreserving), but it refuses SILENTLY — and the new status line ran unconditionally after exec, overwriting the roll's lock notice with 'Snapped N of M notes' when nothing had moved. Guard up front, the way the other roll-locked verbs in this file do.
An agent worktree symlinked node_modules; .gitignore only lists node_modules/ (trailing slash), which matches a directory but not a symlink, so git add -A tracked it. The symlink pointed at a local absolute path and would break any other checkout.
3e0172d to
2f3b2db
Compare
What
From a tester report this morning ("I miss a way to snap the selected notes to the grid. Like after setting the tempo map and importing from gp-file"): the feature exists (Edit ▸ Resnap selection / its shortcut) — but
snapTimehonoured the live Snap toggle, so with Snap off the explicit quantize silently moved nothing, and it printed no status either way. A silent no-op is indistinguishable from a missing feature.snapTimegains an opt-inforcearg: explicit quantize verbs bypass the ON/OFF toggle while the selected snap subdivision (and Onset mode) still applies — the toggle governs live interactive placement only. Default unchanged, so every interactive caller keeps honouring the toggle bit-exactly.ResizeSustainGroupCmd. A sustained note never collapses onto its start (newsnapGuidelineAftersupplies the one-subdivision minimum), and a zero-length chip is never inflated — length is authored intent.Tests
tests/resnap_explicit.test.mjs(8): the toggle-honouring identity is pinned unchanged; force lands on the grid with Snap off (fails on main — the arg is ignored there); force/no-force agree with Snap on; Onset-mode fallback;snapGuidelineAfterstrictly-after semantics; both-edges quantise; the collapse guard; the chip guard. Full suite 147 green, lint 0 errors,routes.pyuntouched.Live-verified
Real library pak, Snap toggled OFF, all 1,114 notes selected, resnap fired by its shortcut: 842 notes touched (698 starts moved, 581 end edges re-lengthed), zero chips inflated, exactly one undo entry, Ctrl+Z restores every time and sustain bit-exact. Zero page errors.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary by CodeRabbit
New Features
Bug Fixes