feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable

The figure sync learns which row the pointer is in and that row's local y, so the
shared x goes to every row and the horizontal line only to the hovered one -- a
crosshair that finally marks a row where its y means something. The stroke gains
Style::crosshair_width/_dash_on/_dash_off (logical; 0.75 / 4 / 4), a per-Crosshair
override, and a fragment-shader dash on v_along (off == 0 is solid), anchored to the
axes edge each line is staged from so the phase does not crawl.
This commit is contained in:
Martino Ferrari committed 2026-09-28 20:29:09 +02:00
1 parent eee7c34dd5
commit f8a364e963
3 files changed
+506

No files matched your search

+35
View File
@@ -545,6 +545,18 @@ int main() {
/// cursor or region drag on any row stops it where the user put it.
bool overlay_follows = true;
/// Optional scripted crosshair (spec §17): `PLOT_DEMO_CROSSHAIR=<row>` (default row 1)
/// places the figure crosshair every frame with the pointer in that row and that row's own
/// y -- the shared x going to all three rows, the horizontal line only to the hovered one.
/// Off unless the environment asks for it, like the sync and region scripts, so a headless
/// screenshot can show the one horizontal line the hovered row draws and none in the others.
u32 crosshair_row = kRows;
if (const char* v = std::getenv("PLOT_DEMO_CROSSHAIR");
v != nullptr && v[0] != '\0' && v[0] != '0') {
crosshair_row = static_cast<u32>(std::strtoul(v, nullptr, 10));
if (crosshair_row >= kRows) crosshair_row = 1u; // a row that does not exist means row 1
}
const plot::glfw::FrameFn frame = [&](plot::Plot&, plot::f64 dt) {
for (std::unique_ptr<plot::data::StreamSeries>& s : series) s->acquire(); // once per frame
@@ -556,6 +568,18 @@ int main() {
figure.plot(0).set_cursor_x(cursor_b, xr.min + 0.70 * xr.span());
figure.plot(0).set_region(region, xr.min + 0.42 * xr.span(), xr.min + 0.58 * xr.span());
}
if (crosshair_row < kRows) {
// The pointer at a fixed screen position in the hovered row, read back to *its* data
// coordinates: one x, spread to every row by the figure, and the hovered row's own y.
const plot::Plot& hovered = std::as_const(figure.plot(crosshair_row));
const plot::Rect a = hovered.layout().axes;
figure.set_crosshair(
hovered.x_transform(0u).to_data_x(static_cast<plot::f64>(a.x) + 0.60 * static_cast<plot::f64>(a.w)),
crosshair_row,
hovered.y_transform(0u).to_data_y(static_cast<plot::f64>(a.y) + 0.45 * static_cast<plot::f64>(a.h)),
true);
}
for (u32 row = 0; row < kRows; ++row) {
const plot::input::Controller* c = host.row_controller(row);
if (c == nullptr) continue;
@@ -682,6 +706,17 @@ int main() {
p.trace_count(), static_cast<u32>(p.layout().legend.rows.size()),
figure.x_link(row));
}
// The crosshair's geometry per row (spec §17), which is what the headless screenshot's ink is
// read against: with the scripted crosshair on row `PLOT_DEMO_CROSSHAIR`, that row has a
// horizontal line and the others do not, while every row has the shared vertical one.
for (u32 row = 0; row < kRows; ++row) {
const plot::CrosshairLayout& c = std::as_const(figure.plot(row)).layout().crosshair;
std::printf("[multiplot] crosshair row %u: active %d, vertical box w %.2f at x %.1f,"
" horizontal box h %.2f at y %.1f\n",
row, c.active ? 1 : 0, static_cast<plot::f64>(c.vertical.w),
static_cast<plot::f64>(c.vertical.x), static_cast<plot::f64>(c.horizontal.h),
static_cast<plot::f64>(c.horizontal.y));
}
std::printf("[multiplot] region %u: x [%g, %g] s, n %llu, mean %.4f, rms %.4f, min"
" %.4f, max %.4f, integral %.4f\n",
region, rs.x0, rs.x1, static_cast<unsigned long long>(rs.count), rs.mean, rs.rms,