fix(score): rebuild viewer beat positions when a score is re-imported - #583
Open
FelipeDefensor wants to merge 2 commits into
Open
fix(score): rebuild viewer beat positions when a score is re-imported#583FelipeDefensor wants to merge 2 commits into
FelipeDefensor wants to merge 2 commits into
Conversation
`SvgViewer.load_svg_data` only computed the beat-to-x mapping when `viewer_beat_x` was empty, and nothing ever emptied it. Importing a second musicXML into a score timeline that already had one kept the first score's mapping, so `scroll_to_time`, `_get_closest_time`, the measure tracker and every stavenote's `seek_x` all pointed at the wrong places. The mapping is now rebuilt from the SVG on every load, which needs the data markers to still be there: `load_svg_data` no longer writes the stripped SVG back over `svg_data`. A stored mapping is still honoured as a fallback, for files saved before this change. `viewer_beat_x` had a setter that ignored falsy values, so the mapping could not be cleared through `set_data` at all. It can now, and clearing a score timeline discards both halves of the pair together — keeping either one alone would leave the timeline describing a score that is no longer there. Restoring a timeline state sets SERIALIZABLE attributes one at a time, `svg_data` before `viewer_beat_x`, so the viewer used to reload while only half of the pair had been applied. `viewer_beat_x` joins UPDATE_TRIGGERS and both updaters go through `_reload_svg_view`, so whichever attribute arrives second leaves the viewer consistent; an empty `svg_data` closes the viewer instead of being handed to `etree.fromstring`. `reset_svg` no longer reaches the viewer through the `svg_view` property, which would build one — re-registering its toolbar commands against a throwaway widget — only to destroy it again.
Every SVG load goes through `_get_beat_x_pos`, which removes the markers as it reads them, so `_strip_beat_x_markers` has no callers left. Its tests move over to `_get_beat_x_pos`, which becomes a staticmethod like the extractor it wraps.
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.
The bug
SvgViewer.load_svg_dataonly computed the beat-to-x mapping whenviewer_beat_xwas falsy, and nothing ever cleared it. Importing a second musicXML into a score timeline that already had one kept the first score's mapping.Reproduced by loading an SVG whose markers sit at x=100/150 and then one at x=900/950:
timeline.viewer_beat_xandviewer.beat_x_positionstill read{1.0: 100.0, 1.5: 150.0}. Everything downstream is then wrong for the new score —scroll_to_time,_get_closest_time, the measure tracker, and every stavenote'sseek_x.The fix
The mapping is rebuilt from the SVG on every load. That needs the data markers to still be there, so
load_svg_datano longer writes the stripped SVG back oversvg_data. Stripping is cheap and now runs on every load anyway; a stored mapping is still honoured as a fallback, for files saved before this change.Two things made the pair awkward to decouple, and both are handled:
The
viewer_beat_xsetter ignored falsy values, soset_data("viewer_beat_x", {})was a silent no-op and the mapping could not be cleared through the normal path. It assignsx_pos or {}now, andScoreTimeline.clear()discards both halves together — keeping either one alone would leave the timeline describing a score that is no longer there, and would make an undo overwrite the value it just restored._restore_timeline_staterestores SERIALIZABLE attributes one at a time,svg_databeforeviewer_beat_x, so the viewer reloads while only half of the pair has been applied.viewer_beat_xjoinsUPDATE_TRIGGERSand both updaters go through_reload_svg_view, so whichever attribute arrives second leaves the viewer consistent. An emptysvg_datacloses the viewer instead of being handed toetree.fromstring.SERIALIZABLEis untouched — the per-timelinehashbuilt from it goes into the.tlafile.Plus one small related change:
reset_svgno longer reaches the viewer through thesvg_viewproperty, which would build one — re-registering its toolbar commands against a throwaway widget — only to destroy it again.The second commit removes
_strip_beat_x_markers, which has no callers left now that every load goes through_get_beat_x_pos, and moves its tests over.Tests
Eight new tests in
TestViewerBeatPositions: clearing drops the mapping; a score imported after a clear uses its own beat positions; the same for a score imported over another without a clear; undo/redo across both transitions displays no error and leaves the viewer consistent; saving and reloading preserves the positions; a marker-less SVG falls back to the stored mapping; and an SVG with no source at all reportsSCORE_SVG_CREATE_ERROR. Four of them fail ondev.Full suite green (1835 passed). The
tests/test_app.pyfailures under-n autoare the pre-existing xdist flake — all 90 pass serially.Notes for the reviewer
svg_datanow keeps its markers, so.tlafiles get bigger. Files written before this change still load, through the stored-mapping fallback.SCORE_SVG_CREATE_ERRORwas unreachable ondev:validate_pre_validatedalways returnsTrue, so thesuccessflag that guarded it was always true. This PR makes the error genuinely reachable — when neither markers nor a stored mapping are available — and covers it with a test.fix/472-score-clear-svg-viewer, which also discardssvg_dataon clear and closes the viewer. That branch sits on a different base wheresvg_viewreturnsNonerather than lazily building a viewer, so the two will conflict in the same three places.🤖 Generated with Claude Code