From 133cd680ceebce6d50145e8c1fa5570ae6df2d01 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 2 Oct 2026 15:00:50 +0200 Subject: [PATCH] feat(plot): set_annotation(), so a readout annotation can be moved A host whose annotation is a readout -- the point where a cursor crosses a trace -- has to move it every frame, and `add_annotation()` only appends: the id would change every frame and the annotation count grow without bound. The setter keeps the id, ignores a stale one exactly as the cursor setters do, and pairs with parking (`visible = false`) since there is no `remove_annotation`. Tested by `an_annotation_can_be_moved_by_id` in tests/test_plot.cpp. --- include/plot/plot/plot.hpp | 7 +++++++ src/plot/plot.cpp | 5 +++++ tests/test_plot.cpp | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/include/plot/plot/plot.hpp b/include/plot/plot/plot.hpp index 80d0058..850a398 100644 --- a/include/plot/plot/plot.hpp +++ b/include/plot/plot/plot.hpp @@ -229,6 +229,13 @@ class Plot { /// Measures the interval between two cursors over the series of `trace`. u32 add_measure(u32 cursor_a, u32 cursor_b, u32 trace = 0); u32 add_annotation(const Annotation& annotation); + /// Moves an annotation that is already in the plot, keeping its id -- what a host whose + /// annotation is a *readout* needs (the point where a cursor crosses a trace moves with + /// the cursor). `Plot` has no `remove_annotation`, so the same parking discipline as the + /// cursors applies: set `visible = false` rather than dropping it, and re-place where it + /// belongs. An id that names no annotation is ignored, not an error, exactly as every + /// other setter here treats a stale id. + void set_annotation(u32 id, const Annotation& annotation); /// Adds a region. Its bounds are ordered and, when the region asks to snap, pulled /// onto the sample grid as it is added -- exactly as `add_cursor()` places a /// snapping cursor -- so a region handed to a plot that already holds samples is on diff --git a/src/plot/plot.cpp b/src/plot/plot.cpp index 0ac00c9..46c72b2 100644 --- a/src/plot/plot.cpp +++ b/src/plot/plot.cpp @@ -306,6 +306,11 @@ u32 Plot::add_annotation(const Annotation& annotation) { return static_cast(annotations_.size() - 1); } +void Plot::set_annotation(u32 id, const Annotation& annotation) { + if (id >= annotations_.size()) return; + annotations_[id] = annotation; +} + u32 Plot::add_region(const Region& region) { // A region whose bounds are not numbers is not an interval this plot can hold, and a // `Free` one stored that way could never be dragged back (every motion re-derives both diff --git a/tests/test_plot.cpp b/tests/test_plot.cpp index 4b6a753..5e441be 100644 --- a/tests/test_plot.cpp +++ b/tests/test_plot.cpp @@ -2584,3 +2584,37 @@ PLOT_TEST(xy_series_measurement_uses_the_true_nearest_samples) { PLOT_CHECK_NEAR(stats.min, 10.0, 1e-9); PLOT_CHECK_NEAR(stats.max, 10.0, 1e-9); } + +// A host whose annotation is a readout -- the point where a cursor crosses a trace -- +// moves it every frame. The id has to survive that, and a stale id must be ignored +// rather than read or written out of bounds, exactly as the cursors' setters behave. +PLOT_TEST(an_annotation_can_be_moved_by_id) { + plot::Plot p; + const plot::Annotation pin{1.0, 2.0, "first", plot::Color::hex(0xE8E8E8), 0u, 0u, true}; + const u32 id = p.add_annotation(pin); + PLOT_CHECK(p.annotation(id).x == 1.0); + PLOT_CHECK(p.annotation(id).text == std::string("first")); + + plot::Annotation moved = pin; + moved.x = 3.5; + moved.y = -1.25; + moved.text = "moved"; + p.set_annotation(id, moved); + PLOT_CHECK_NEAR(p.annotation(id).x, 3.5, 1e-12); + PLOT_CHECK_NEAR(p.annotation(id).y, -1.25, 1e-12); + PLOT_CHECK(p.annotation(id).text == std::string("moved")); + PLOT_CHECK(p.annotation_count() == 1u); // moved, not added + + // Parked rather than removed, like a cursor: the id stays valid and reads back hidden. + plot::Annotation parked = moved; + parked.visible = false; + p.set_annotation(id, parked); + PLOT_CHECK(!p.annotation(id).visible); + PLOT_CHECK(p.annotation_count() == 1u); + + // A stale id changes nothing (and reads a default, as the accessors promise). + plot::Annotation other = pin; + other.x = 99.0; + p.set_annotation(42u, other); + PLOT_CHECK_NEAR(p.annotation(id).x, 3.5, 1e-12); +}