Skip to content

fix(score): rebuild viewer beat positions when a score is re-imported - #583

Open
FelipeDefensor wants to merge 2 commits into
devfrom
fix/score-viewer-beat-x-reimport
Open

fix(score): rebuild viewer beat positions when a score is re-imported#583
FelipeDefensor wants to merge 2 commits into
devfrom
fix/score-viewer-beat-x-reimport

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

The bug

SvgViewer.load_svg_data only computed the beat-to-x mapping when viewer_beat_x was 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_x and viewer.beat_x_position still 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's seek_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_data no longer writes the stripped SVG back over svg_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:

  1. The viewer_beat_x setter ignored falsy values, so set_data("viewer_beat_x", {}) was a silent no-op and the mapping could not be cleared through the normal path. It assigns x_pos or {} now, and ScoreTimeline.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.

  2. _restore_timeline_state restores SERIALIZABLE attributes one at a time, svg_data before viewer_beat_x, so the viewer reloads while only half of the pair has 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. SERIALIZABLE is untouched — the per-timeline hash built from it goes into the .tla file.

Plus one small related change: 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.

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 reports SCORE_SVG_CREATE_ERROR. Four of them fail on dev.

Full suite green (1835 passed). The tests/test_app.py failures under -n auto are the pre-existing xdist flake — all 90 pass serially.

Notes for the reviewer

  • svg_data now keeps its markers, so .tla files get bigger. Files written before this change still load, through the stored-mapping fallback.
  • SCORE_SVG_CREATE_ERROR was unreachable on dev: validate_pre_validated always returns True, so the success flag 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.
  • Overlaps with fix/472-score-clear-svg-viewer, which also discards svg_data on clear and closes the viewer. That branch sits on a different base where svg_view returns None rather than lazily building a viewer, so the two will conflict in the same three places.

🤖 Generated with Claude Code

`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant