fix(plot,figure): re-index hot region, logical row floor, refuse live on a linked axis
Three smaller findings at the plot/figure seam: * `Plot::remove_region()` re-indexed `region_stats_` but left `hot_.region` naming the old index, so a frame after a removal highlighted the wrong region's bound and an open drag edited a different region. The published handle (and its zone) is now cleared or decremented with the cache, and the contract documents that a host must not remove a region mid-drag. * `Figure`'s `kMinRowHeightPx` was applied as a *device*-pixel floor while chrome is logical, so at 2x the guarantee was half as strong exactly where the chrome is twice as large. It is now a logical constant converted through the row's own `ui_scale()`, 1:1 for a non-positive scale as `Style::device()` does. * `Figure::link_x()` refused a member whose axis 0 was `XFollow::Live`, but nothing refused `set_x_follow(0u, XFollow::Live)` *after* linking. The group then wrote its range into an axis that reinterpreted it against its own live edge, read it back as an external change, and tracked one member instead of the union. `Plot` now carries a linkage flag (`set_x_link_member()`), set by `link_x()` and cleared by `unlink_row()`, and `set_x_follow()` refuses `Live` on a linked axis. Tests: arming `Live` on a linked member leaves it `Off` and unlinking restores the ability to arm it; the row-height floor is 24 and 48 device pixels at 1x and 2x respectively.
This commit is contained in:
1 parent
2fbd1de7d4
commit
1f5ce0d4a5
5 files changed
+123
-12
No files matched your search
@@ -294,6 +294,26 @@ PLOT_TEST(every_row_keeps_a_positive_height) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The row-height floor is chrome-sized, so it must be *logical*: it is applied in
|
||||
/// device pixels, but the 24 it was written as is a logical size like every other chrome
|
||||
/// metric, and at 2x a device-pixel floor is half the physical height -- exactly where the
|
||||
/// chrome it protects is twice as large. It is converted through the row's own
|
||||
/// `ui_scale()`, so the guarantee is the same physical height at any DPI.
|
||||
PLOT_TEST(the_row_height_floor_is_logical_and_converts_with_the_display) {
|
||||
const float weights[] = {1.0f, 1.0f, 1e-30f};
|
||||
for (const plot::f32 scale : {1.0f, 2.0f}) {
|
||||
plot::Figure figure(3u);
|
||||
figure.set_viewport({0.0f, 0.0f, 400.0f, 600.0f});
|
||||
for (plot::u32 row = 0; row < 3u; ++row) figure.plot(row).set_ui_scale(scale);
|
||||
PLOT_CHECK(figure.set_row_weights(weights));
|
||||
const std::vector<plot::Rect> rows = figure.layout().rows;
|
||||
PLOT_CHECK(rows.size() == 3u);
|
||||
// The underflowing-weight row gets the floor and nothing else: the leftover after
|
||||
// the floor is shared by weight, and 1e-30 of it is zero in float.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rows[2].h), 24.0 * static_cast<plot::f64>(scale), 1e-2);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- y gutter alignment
|
||||
|
||||
/// Spec §16, the invariant: a figure gives its rows one common left gutter, so every
|
||||
@@ -641,6 +661,30 @@ PLOT_TEST(linking_a_live_axis_is_refused_with_a_status) {
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).max, 4.0, 1e-12);
|
||||
}
|
||||
|
||||
/// The post-link hole the door refusal cannot close: `set_x_follow(Live)` on a member
|
||||
/// *after* it linked. The group owns the shared window and the follow state, so arming
|
||||
/// the member's axis would make `push_link()` write the group's range into an axis that
|
||||
/// reinterprets it against its own live edge, and `adopt_member_changes()` read it back
|
||||
/// as an external change -- the group then tracking one member instead of the union. The
|
||||
/// arm is refused, and taking the row out of the group restores the ability to arm it.
|
||||
PLOT_TEST(arming_live_on_a_linked_member_is_refused) {
|
||||
plot::Figure figure = figure_with_axes(2u);
|
||||
const plot::u32 group = figure.add_x_link();
|
||||
PLOT_CHECK(figure.link_x(0u, group));
|
||||
PLOT_CHECK(figure.link_x(1u, group));
|
||||
|
||||
figure.plot(0u).set_x_follow(0u, plot::XFollow::Live);
|
||||
PLOT_CHECK(figure.plot(0u).x_follow(0u) == plot::XFollow::Off); // refused, not armed
|
||||
PLOT_CHECK(figure.plot(1u).x_follow(0u) == plot::XFollow::Off);
|
||||
|
||||
// Arming `Off` is not refused, and leaving the group frees the axis again: the
|
||||
// refusal is about the membership, not about the call.
|
||||
figure.plot(0u).set_x_follow(0u, plot::XFollow::Off);
|
||||
PLOT_CHECK(figure.link_x(0u, 0u));
|
||||
figure.plot(0u).set_x_follow(0u, plot::XFollow::Live);
|
||||
PLOT_CHECK(figure.plot(0u).x_follow(0u) == plot::XFollow::Live);
|
||||
}
|
||||
|
||||
/// A following group advances every row together: the window is derived once per
|
||||
/// frame from the newest x across the members' traces, exactly as one `Live` axis'
|
||||
/// would be, so a streaming multi-row figure has one window and no per-row drift --
|
||||
|
||||
Reference in new issue
Block a user