feat(editor): author per-note bend shape (bt + bnv) (PR-C) - #13
Merged
Conversation
PR-C of the bend-shape feature (feedpak §6.2.1). The editor authored only the scalar bend peak; this adds the bend intent (bt) and the time-stamped bend curve (bnv), round-tripping through the wire shape. Backend (routes.py): - `_arr_dict_to_wire._note` emits `bt` (default-omit when 0) and `bnv` (via the new `_safe_bend_curve`, default-omit when empty), matching core's note_to_wire. Chord member notes inherit through `_note_in_chord`. - `_NOTE_TECH_FIELDS` gains `bend_intent` (drives the generic load `_tech_dict` + content signature). `bend_values` is deliberately kept OUT of that tuple — a list would break the hashable content-signature tuples — and carried explicitly in `_tech_dict` instead, so an imported/authored curve loads back. Frontend (screen.js): - A bend modal (`_editorBendModal`): peak input, intent dropdown, and an interactive drag-point curve editor (left-click adds, drag moves, right-click removes; x = time across the note, y = semitones), plus intent-presets. - `promptBend` opens it for the right-clicked note (or the whole selection) and commits via `SetBendShapeCmd` (one undo step, snapshot/restore of bn/bt/bnv). - Inspector gains a bend-intent dropdown (`SetBendIntentCmd` — changes only bt so a multi-select edit doesn't clobber each note's peak/curve) and an "Edit curve…" button. - Pure helpers `bendPresetCurve` / `sanitizeBendCurve` (empty -> null, never []). Tests: tests/bend_shape.test.js (preset generation + sanitization) and tests/test_xml_export.py (wire emit, default-omit, sanitization, chord-note inheritance, field-set invariant). Editor pytest 62 passed; all JS tests pass; node --check screen.js clean. Part of got-feedBack/feedBack#334 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 20, 2026
byrongamatos
added a commit
that referenced
this pull request
Jun 21, 2026
…6.2.2) (#16) Author the three optional per-note teaching marks, mirroring the bend-shape authoring (#13). Display only — these never affect grading. - screen.js: a @pure:teaching-marks block (FRET_FINGER_OPTIONS + nextUnusedStrumGroup) and SetTeachingMarkCmd (one undoable batch edit per field, snapshot/rollback per note). Inspector gains a fret-hand-finger picker (fg, -1..4), a scale-degree override field (sd, -1..11; blank/-1 = auto), and Group/Ungroup-as-strum buttons (ch — Group assigns the next unused key across the selection, Ungroup clears it). All routed through S.history.exec. - routes.py: fret_finger/strum_group/scale_degree join _NOTE_TECH_FIELDS so they load (_tech_dict) and feed the content signature; the wire serializer emits fg/ch/sd default-omitted (matching core), and the chart-XML export writes fretFinger (core's _parse_note reads it). getattr-over-fields sites default to -1 so a core build predating the marks doesn't break load/alignment. Tests: tests/teaching_marks.test.js (@pure extract — FRET_FINGER_OPTIONS + nextUnusedStrumGroup edge cases); test_xml_export.py round-trip for fg/ch/sd through the wire + fretFinger through chart XML, plus default-omit assertions. Part of got-feedBack/feedBack#334 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos
added a commit
that referenced
this pull request
Jun 21, 2026
…eservation (feedpak-spec §8) (#12) * feat(editor): read + write .jsonc arrangements (feedpak-spec §8) feedpak-spec §8 (FEP #3 / PR #13) allows .jsonc arrangements (JSON with C-style // line and /* */ block comments). The editor reads the existing arrangement on save to preserve anchors/handshapes/phrases/tones, and re-writes the arrangement file — so both sides need comment handling for .jsonc. Read path (routes.py, the save_song preserve-on-save read): - _parse_jsonc(text) + _load_arrangement_json(path): string-aware strip regex (mirrors the spec reference validator + core lib/jsonc.py); .jsonc by suffix, plain .json through json.loads. Replaces the json.loads(...read_text...) at the preserve-on-save read site. Write path (routes.py, the arrangement file write in save_song): - _extract_jsonc_top_comments + _preserve_jsonc_comments: capture header, before-top-level-key, trailing (before closing }), and footer comments from the original .jsonc text and re-insert them into the freshly serialized (indent=2) JSON. Phase-1 scope: top-level comments only — comments inside nested arrays/objects (e.g. inside notes[]) are dropped, the documented limitation (the editor replaces note content, so re-anchoring nested comments to the right note is unreliable). A phase-2 swap can replace _preserve_jsonc_comments without touching call sites. - .jsonc writes serialize indent=2 (so comments land at top-level-key line boundaries); .json writes stay byte-identical (compact separators=(',',':')). - Orphan cleanup now also globs *.jsonc so a removed .jsonc arrangement doesn't linger on disk. Tests: tests/test_jsonc_save.py (18 tests) — _parse_jsonc unit cases, _load_arrangement_json (.jsonc/.json/anchors-through-comments), _extract_jsonc_top_comments (header/footer/before-key/trailing/nested-drop), _preserve_jsonc_comments round-trips (full preservation, removed-key drop, identity-no-comments, nested-drop), and a combined read→modify→write round trip simulating save_song on a .jsonc arrangement (comments survive at top level, preserved anchors survive in data, edited note count correct). 75 editor tests pass (2 skipped, pre-existing). * feat(editor): preserve fully-nested .jsonc comments on save (phase 2) Phase 1 preserved only top-level comments (header / before-key / trailing / footer) and dropped anything inside nested arrays/objects. Phase 2 preserves nested comments too, on a best-effort basis: each comment is anchored to a structural identity in the original and re-inserted at the matching spot in the freshly serialized (indent=2) JSON. Anchoring (routes.py, replacing the phase-1 top-level extractor): - Header / footer comments re-insert at the file head / tail. - A comment before a key (at any depth) anchors to (*path, 'before-key', key) and re-inserts on its own line before the key, indented to match. - A comment before an array element anchors to (*path, 'before-elem', sig) where sig is the element's content signature: objects with a numeric 't' key anchor by t=<json value> (the primary key for notes/beats/sections/ anchors — stable across edits to the element's OTHER fields); other objects anchor by a compact sorted-JSON hash; scalars/strings anchor by their JSON rendering. Because the anchor is content identity (not index), the comment follows the element across reorders / additions / removals of other elements. Editing the anchored element's t (or any field of a t-less element) changes its signature and the comment drifts or drops — the documented best-effort limit (a hand-author who nests a comment deep inside an array of notes owns the consequence of editing that note). - A trailing comment (before a closing }/]) anchors to (*path, 'trailing') and re-inserts one indent level deeper than the close bracket (matching the element/key indent, as a hand-author writes it). Empty containers rendered inline ([]/{}) are expanded so a // line comment doesn't eat the close bracket ([ // c ] strips to [ , which is invalid). - A comment between : and a value (rare) anchors to (*path, 'before-value', key) and re-inserts inline after :. Path components for array-element levels are element signatures (not indices), so a comment nested inside an array element (e.g. a before-key comment on a note object inside notes[]) re-anchors correctly even when notes are added/removed before it. Implementation: a single-pass JSONC scanner (_jsonc_skip_ws_comments, _jsonc_read_string, _jsonc_read_scalar, _jsonc_find_matching) shared by the build pass (_jsonc_extract_comments / _jsonc_process_obj / _jsonc_process_arr) and the re-emit pass (_jsonc_collect_obj / _jsonc_collect_arr), plus _jsonc_elem_signature and _preserve_jsonc_comments. _parse_jsonc / _load_arrangement_json (the read helpers) are unchanged. Tests: tests/test_jsonc_save.py rewritten for phase 2 (26 tests) — parse_jsonc / load_arrangement_json unit cases; _jsonc_extract_comments anchor structure (header/footer/before-key/trailing/before-elem/nested- before-key/nested-trailing); top-level round trips; nested round trips (before-key inside a note survives a non-t field edit; before-elem survives unchanged; comment follows element across an insertion; comment drops when the anchored note's t is edited; deeply nested t-less chord-note comment survives unchanged / drops when edited; trailing in nested array at element indent; empty-array trailing expands); and the combined read->modify->write save round trip with a nested before-key comment surviving. 83 editor tests pass (2 skipped, pre-existing). * fix(editor): never let .jsonc comment preservation corrupt a save (#12 review) Post-review of the .jsonc save path: _preserve_jsonc_comments could emit text that no longer parses (an inline comment between a key's ':' and value yields `"capo": // pickme 0,`, which strips to invalid JSON), and the write site had no try/except, no fallback, and no round-trip validation — so a comment-placement edge silently wrote an unreadable arrangement (data loss). - New _jsonc_save_text(original, wire): the single writer for .jsonc arrangements. Re-inserts comments best-effort but NEVER returns text that fails to parse back to `wire` — on any throw or round-trip mismatch it falls back to the plain comment-less pretty JSON. A dropped comment is fine; an unreadable file is not. The save site now delegates to it. - New _jsonc_inline_comment(c): renders a `//` line comment for INLINE placement as a CLOSED `/* … */` block comment. The old `c.replace("//","/*",1)` dropped the closing `*/`, producing an unterminated comment that ate the rest of the file; both inline-fallback sites now use this. - Removed the stale "phase-1 limitation, nested dropped" write-site comment (nested preservation is implemented). +9 tests: the guard always round-trips (incl. the previously-corrupting before-value case → safe), safe comments are still preserved, and the inline renderer closes line comments. 35 jsonc tests + full editor suite pass. --------- Co-authored-by: byrongamatos <xasiklas@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR-C (final) of the per-note bend-shape feature (feedpak spec §6.2.1). The editor authored only the scalar bend peak (
bn); this adds authoring of the bend intent (bt) and the time-stamped bend curve (bnv), round-tripping through the wire shape. Builds on PR-A (#531, wire+import) and PR-B (#532, highway render).Changes
Backend —
routes.py_arr_dict_to_wire._noteemitsbt(default-omit when0) andbnv(via the new_safe_bend_curve, default-omit when empty), matching core'snote_to_wire. Chord member notes inherit through_note_in_chord._NOTE_TECH_FIELDSgainsbend_intent(drives the generic load_tech_dict+ the content signature).bend_valuesis deliberately kept out of that tuple — a list would break the hashable content-signature tuples (_obj_note_sig/_dict_note_sig) — and is carried explicitly in_tech_dict, so an imported/authored curve loads back into the editor.Frontend —
screen.js_editorBendModal): peak input, intent dropdown, and an interactive drag-point curve editor — left-click adds a point, drag moves it, right-click removes it; x = time across the note, y = semitones — plus intent presets and a clear button.promptBendopens it for the right-clicked note (or the whole selection when it's part of it) and commits viaSetBendShapeCmd(one undo step; snapshot/restore ofbn/bt/bnv).SetBendIntentCmd— changes onlybt, so a multi-select edit doesn't flatten each note's distinct peak/curve) and an "Edit curve…" button.bendPresetCurve(intent→curve) andsanitizeBendCurve(empty →null, never[]; no magnitude clamp, mirroring core).Scope note
Per the agreed decision, this covers the wire/sloppak round-trip only. The archive-XML path (
_note_attrs_xml) still emits the scalarbendas before — a bend-curve XML representation is a follow-up.Testing
tests/bend_shape.test.js(new) — preset generation for all five intents + curve sanitization (drop-bad, sort, round, empty→null).tests/test_xml_export.py— wire emit, default-omit, sanitization, chord-note inheritance, and the_NOTE_TECH_FIELDSfield-set invariant.pytest→ 62 passed, 2 skipped; all JS test files pass;node --check screen.jsclean.Caveat
Live UI verification of the curve-editor modal was not performed (it needs the running editor in a browser). Coverage here is the pure-helper unit tests + backend wire round-trip tests +
node --check. Worth driving the modal in a build to confirm the canvas interactions feel right.Part of got-feedBack/feedBack#334
🤖 Generated with Claude Code