fix(input,examples): a linked figure's wheel anchor uses the group's live edge, and every demo verdict reaches its exit status
Review fix pass 1 on the task 7 work; four findings, all closed. 1. The Qt demo's region sat across the middle of the window, which is exactly where its own scripted input check pans: after the check's box zoom the press opened DragMode::MoveRegion, the x range never moved, and the check printed 'input check pan FAIL' -- under an exit 0, because the verdict was printed and not returned. The region now rides the left sixth (as the GLFW demo's does), both checks pass, and the verdict is wired to the exit status. The same audit found two more printed-only verdicts -- the GTK demo's scripted-resize layout check and its screenshot write (and Qt's) -- now wired too. What stays printed-only is the screenshot line of the GLFW host and the ImGui widget, where the dump happens inside the adapter's frame loop. 2. A follow-ing link group was invisible to the input layer: a member's own axis must be XFollow::Off (link_x() refuses a Live member), so Controller's x_zoom_anchor() extrapolated a coordinate from the pointer instead of using the live edge and a wheel over a row's axis strip walked the window back by 19% of its width (behind 0.000000 -> 0.285809 s). The group now publishes its own edge to every member (Axis::live_owner + Plot::set_x_live_owner, written by Figure::push_link, cleared when the group stops following or the row unlinks), and x_zoom_anchor() reads it. Public-header addition, called out. Measured after: behind 0.012992 s and the window's right edge bit-identical to the edge the group published at the gesture. 3. The multiplot summary printed x_link_count() -- a number of groups -- under 'rows in it'. It now reports the members and the group count separately, and every row line carries its own group id. 4. Host::controller_at() promised to clamp a pixel in a row gap to the nearest row and only clamped below the last one, so a gap pixel went to row 0. It now selects by distance, and tests/test_glfw_host.cpp pins it with a 40 px gap -- the first coverage of the host's figure routing, which needs no window. New cases: tests/test_controller.cpp x2 (the anchor and the hint's lifecycle), tests/test_glfw_host.cpp x3 (routing, the gap, the single-plot fallback): 229 cases, 0 failed, ASan clean. All five demos exit 0, the three self-checking modes exit 0, and the amended assertions were confirmed failing against the pre-fix behaviour.
This commit is contained in:
1 parent
a70c381a1e
commit
daff60e3f0
14 files changed
+441
-55
No files matched your search
@@ -564,10 +564,18 @@ f64 Controller::x_zoom_anchor(u32 axis, f32 px, f32 py) const {
|
||||
// off the plot keep the window ending at the newest sample instead of drifting
|
||||
// away from it. Any other axis keeps the extrapolated coordinate, and `Off` is
|
||||
// the default, so nothing changes for a plot that is not following.
|
||||
if (!std::as_const(*plot_).layout().axes.contains(px, py) &&
|
||||
plot_->x_follow(axis) == XFollow::Live) {
|
||||
//
|
||||
// A figure row's axis is `Off` even while its window follows, because a link group
|
||||
// owns the shared window *and* the follow state (`Figure::link_x()` refuses a `Live`
|
||||
// member) -- so the axis' own follow flag is not the whole answer. A following group
|
||||
// publishes its edge to every member (`Plot::x_live_owner()`), and that edge is read
|
||||
// second: it is the group's -- the union of its members' newest x -- which is the
|
||||
// coordinate the window actually ends at, and the member's own series would be a
|
||||
// different answer in a figure whose rows are fed at different rates.
|
||||
if (!std::as_const(*plot_).layout().axes.contains(px, py)) {
|
||||
f64 live = 0.0;
|
||||
if (plot_->x_live_edge(axis, live)) return live;
|
||||
if (plot_->x_follow(axis) == XFollow::Live && plot_->x_live_edge(axis, live)) return live;
|
||||
if (plot_->x_live_owner(axis, live)) return live;
|
||||
}
|
||||
return plot_->x_transform(axis).to_data_x(static_cast<f64>(px));
|
||||
}
|
||||
|
||||
+26
-3
@@ -67,6 +67,10 @@ u32 Figure::x_link(u32 row) const noexcept {
|
||||
}
|
||||
|
||||
void Figure::unlink_row(u32 row) noexcept {
|
||||
// A row that leaves its group stops following anything, so the hint a following group
|
||||
// published to it goes with the membership: leaving `has_live` set behind would leave a
|
||||
// former member anchoring a zoom on an edge it no longer follows.
|
||||
if (row < plots_.size()) plots_[row].set_x_live_owner(0u, false, 0.0);
|
||||
for (XLink& group : links_) {
|
||||
for (std::size_t i = 0; i < group.rows.size(); ++i) {
|
||||
if (group.rows[i] != row) continue;
|
||||
@@ -172,8 +176,12 @@ Status Figure::set_x_link_follow(u32 group, XFollow follow) {
|
||||
XLink& g = links_[group - 1u];
|
||||
g.follow = follow;
|
||||
if (follow != XFollow::Live) {
|
||||
// Disarming leaves the range exactly where it is, as it does on a single axis:
|
||||
// the last live window becomes an ordinary stored range.
|
||||
// Disarming leaves the range exactly where it is, as it does on a single axis: the
|
||||
// last live window becomes an ordinary stored range. The live edge goes with the
|
||||
// follow state -- nothing may anchor on an edge no group is following any more -- and
|
||||
// this path does not go through `push_link()`, which is the other place the hint is
|
||||
// written, so it is cleared here.
|
||||
for (const u32 row : g.rows) plots_[row].set_x_live_owner(0u, false, 0.0);
|
||||
return Status{};
|
||||
}
|
||||
// Arming starts from the span the group already holds -- as `Plot::set_x_follow()`
|
||||
@@ -220,6 +228,10 @@ bool Figure::live_edge(const XLink& group, Range& out) const {
|
||||
}
|
||||
|
||||
bool Figure::place_live_window(XLink& group) {
|
||||
// Every early return leaves the group with no live edge published: a group that is not
|
||||
// following, one whose members hold nothing streaming, and one whose span is not a span
|
||||
// all have no edge for a member's controller to anchor on.
|
||||
group.has_live = false;
|
||||
if (group.follow != XFollow::Live) return false;
|
||||
Range live{};
|
||||
if (!live_edge(group, live)) return false;
|
||||
@@ -234,13 +246,24 @@ bool Figure::place_live_window(XLink& group) {
|
||||
// An offset past that would put the window's right edge behind the oldest sample
|
||||
// left, i.e. over data that is gone.
|
||||
group.offset = std::clamp(group.offset, 0.0, std::max(0.0, live.max - live.min));
|
||||
group.live_newest = live.max;
|
||||
group.has_live = true;
|
||||
group.range = Range{live.max - group.offset - group.span, live.max - group.offset};
|
||||
return true;
|
||||
}
|
||||
|
||||
void Figure::push_link(XLink& group) {
|
||||
// The member's window is the group's and the group is what follows, so the edge is
|
||||
// published with it: a gesture that asks where the live edge is (the controller's wheel
|
||||
// anchor, outside `axes`) reads the group's edge rather than extrapolating from the
|
||||
// pointer, which is the difference between a zoom that keeps the window on the newest
|
||||
// sample and one that walks it back. A group that is not following publishes "no owner",
|
||||
// which is what clears the hint the frame after `set_x_link_follow(g, Off)`.
|
||||
const bool owned = group.follow == XFollow::Live && group.has_live;
|
||||
for (std::size_t i = 0; i < group.rows.size(); ++i) {
|
||||
plots_[group.rows[i]].set_x_range(0u, group.range);
|
||||
Plot& p = plots_[group.rows[i]];
|
||||
p.set_x_range(0u, group.range);
|
||||
p.set_x_live_owner(0u, owned, group.live_newest);
|
||||
group.pushed[i] = group.range;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +327,18 @@ bool Plot::x_live_edge(u32 axis, f64& newest) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Plot::set_x_live_owner(u32 axis, bool live, f64 newest) noexcept {
|
||||
if (axis >= x_axes_.size()) return;
|
||||
x_axes_[axis].live_owner = live;
|
||||
x_axes_[axis].live_owner_newest = live ? newest : 0.0;
|
||||
}
|
||||
|
||||
bool Plot::x_live_owner(u32 axis, f64& newest) const noexcept {
|
||||
if (axis >= x_axes_.size() || !x_axes_[axis].live_owner) return false;
|
||||
newest = x_axes_[axis].live_owner_newest;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Plot::live_x_bounds(u32 axis, Range& out) const {
|
||||
bool any = false;
|
||||
for (const Trace& t : traces_) {
|
||||
|
||||
Reference in new issue
Block a user