Skip to content

curve: fix edition crashes, scale to 100k+ points, sampled segments and waveform rendering - #2300

Open
jcelerier wants to merge 3 commits into
masterfrom
curve-edition-fixes
Open

jcelerier wants to merge 3 commits into
masterfrom
curve-edition-fixes

Conversation

@jcelerier

Copy link
Copy Markdown
Member

Two commits, meant to be reviewed separately.

Depends on ossia/libossia#949 (the submodule points at it). ossia/score-addon-hdf5 has a companion PR that makes CSV/HDF5 drops use the new sampled segments.

1. curve: fix the crashes and corruptions of curve edition

Reproduced from the GUI through Curve::ToolPalette on a real Presenter/View, each case in a forked child:

  • The value tooltip was a function-local static that dangled once its view was destroyed mid-drag.
  • CurveEditor::paste indexed past the end when pasting at the start of a curve.
  • Move / suppress / cross on overlap left dangling links or used erased segments. MovePointCommandObject is rewritten on a vector.
  • Vertical steps (zero-width segments) aborted validation.
  • UpdateCurve::update() did not validate. Model::fromCurveData now validates before clearing the model.
  • IdContainer::at() returned a cached pointer to a replaced segment across back-to-back undo/redo.
  • Tool events carried raw item pointers; they now carry ids read at post time.
  • A lost mouse release left the tool mid-gesture (QEvent::UngrabMouse is now handled).
  • Point-array serialization was missing; PowerSegment misbehaved with 0 steps or zero width; recording a single value crashed; SegmentData lost its id through JSON.

2. curve: scale edition to 100k+ points, sampled segments, waveform rendering

  • Edition is proportional to the change. UpdateCurve stores only the changed segments. Model::applyChanges updates them in place, splices the order and reuses point objects. CommandBackupFile stops re-serializing the whole stack on every command. ScenarioValidity skips curves. Model destruction avoids Qt's O(N²) child removal.
  • Sampled segments. PointArraySegment is a real segment: shared copy-on-write samples, interpolated evaluation, cropping, serialization. Curve::curveFromSamples turns more than 10k values into one sampled segment. "Convert samples to editable points" simplifies them with Douglas–Peucker.
  • Rendering. A min/max pyramid gives the extremes of any range in O(log N). Dense curves draw as a waveform, one cosmetic line per device pixel column, so the cost scales with pixels rather than samples.
  • Execution. Scaled executor functions; the old automation curve is freed on the GUI thread (libossia swap_behavior).
  • Uses ossia::safe_isfinite and the ossia containers.

Release timings, 100k-segment automation, master → this branch:

master branch
load 15 s 138 ms
drag a point 22 s 2.4 ms
paste 48 s 110 ms
1M-sample import 34 ms, 31 MB
paint a 2000 px layer (1M samples) ~1.8 ms

Tests

  • test_unit_curve_edition: 42 cases covering crashes, invariants, undo/redo, backup-file equality, the executor swap, sampled segments and rendering.
  • test_unit_curve_bench: hidden benchmark (NO_CTEST), run with CURVE_BENCH_N=10000,100000 tests/unit/test_unit_curve_bench "[.curve_bench]".

🤖 Generated with Claude Code

https://claude.ai/code/session_01RskGW91A5EsEeuHyC91XHV

jcelerier and others added 2 commits September 23, 2026 12:33
Tested by tests/unit/CurveEditionTest.cpp, which drives Curve::ToolPalette
on a real presenter and view and runs each crash in a forked child.

- View: the value tooltip belongs to each view instead of a static that
  dangled once its view was destroyed mid-drag.
- CurveEditor::paste: splice in one pass, no more indexing past the end.
- MovePointCommandObject: rewritten on a vector (no pointers into a dense
  hash map across erase), with explicit move / suppress / cross cases and
  no validity checks halfway through an update; lock between points holds
  at neighbours sharing the point's x.
- Model::fromCurveData validates before clearing and orders segments by
  their chains; UpdateCurve validates in update() too; loading relinks a
  curve with broken links instead of asserting.
- IdContainer::at always looks the id up: the cached pointer could name a
  replaced, not yet deleted object.
- Tool events record ids when posted rather than an item pointer.
- A lost mouse release ends the gesture (UngrabMouse, and on the next
  press) instead of dragging the old point.
- PointArraySegment serializes its points; conversions need two points.
- JSON SegmentData reads its id; removeSegment no longer erases while
  iterating; power / easing segments interpolate both ends and survive a
  zero width; non-finite cursor positions are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ering

Edition:
- UpdateCurve stores the changed segments (before / after) instead of the
  whole curve; Model::applyChanges updates segments in place, splices the x
  order and reuses the point objects. An ongoing drag reverts to the
  original state on each update.
- The command objects work on a reused vector; overlap suppression, pen,
  paste and removal are single passes with batched erasure. Segment ids are
  stable across a gesture.
- CommandBackupFile keeps the serialized commands instead of re-serializing
  the stack on every command; ScenarioValidity does not walk curves; the
  model leaves its segments to QObject on destruction.
- Non-finite values go through ossia::safe_isfinite; ossia containers
  (hash_map / hash_set / flat_map / pod_vector) replace the std ones.

Execution:
- Automations and mappings read the model's sorted segments by reference,
  build scaled executor functions, and swap the automation behavior so the
  old curve dies on the GUI thread (needs libossia's swap_behavior).

Sampled segments:
- PointArraySegment is a real segment: shared copy-on-write samples,
  interpolated valueAt, cropping with setSegmentExtent, serialization.
- Curve::curveFromSamples turns more than 10k values into one sampled
  segment; "Convert samples to editable points" simplifies them with
  Douglas-Peucker. Recording builds sampled segments the same way.

Rendering:
- A min/max pyramid gives the extremes of any sample range in O(log N).
  Where there are more samples than pixels, a segment is drawn as a
  waveform, one cosmetic line per device pixel column; otherwise as the
  envelope polyline. Direct-draw curves use the same code.

Tests: test_unit_curve_edition covers the above; test_unit_curve_bench
(hidden, NO_CTEST) measures edition, paint and execution at N points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RskGW91A5EsEeuHyC91XHV
CurveModel.cpp also has an anonymous-namespace ChangeScratch; in a unity
build both land in the same translation unit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RskGW91A5EsEeuHyC91XHV
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 40.669% (+0.3%) from 40.337% — curve-edition-fixes into master

This branch has not been deployed

No deployments
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.

2 participants