Commit Graph
16 Commits
Author SHA1 Message Date
Martino Ferrari ea70cf4fca feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable 2026-09-28 20:33:35 +02:00
Martino Ferrari f8a364e963 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.
2026-09-28 20:29:09 +02:00
Martino Ferrari daff60e3f0 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.
2026-09-28 15:52:08 +02:00
Martino Ferrari a70c381a1e fix(examples): keep the glfw demo's region off the pan path, and print the multiplot's region geometry
Adding a grabbable region across the middle of the glfw demo turned the pan
gesture PLOT_DEMO_FOLLOW makes -- and a user's own centre drag -- into a region
drag, which that mode's self-check reported as 'distance behind only by the pan:
NO' and exit 1. The region now rides the left sixth of the live window, so a
press in the middle pans exactly as it always did, and the reason is written
where the bounds are placed.

The multiplot demo also prints its region's last-frame fill rect and bound
columns, so the exit frame's region pixels can be read against the geometry
Plot::layout() measured.

Report: .superpowers/sdd/p2-task-7-report.md
2026-09-28 15:36:25 +02:00
Martino Ferrari 942f88c568 feat(examples): a three-row synced Figure demo, on a GLFW host that draws one
The multi-plot example the phase exists for: three plots stacked in one
Figure, sharing one x link with following armed, each row carrying its own
named signals and its own legend, driven by six StreamSeries at 500 kS/s.
Two cursors and a region with an exact statistics readout reach the direct
manipulation of spec §5 in the same frame, and PLOT_DEMO_SYNC=1 is a
self-checking mode that exits non-zero unless all three rows hold one range
and the shared right edge keeps tracking the newest sample through a zoom, a
pan on another row and more arriving data.

The GLFW host gains the figure mode the demo needs: set_figure() makes the
run loop draw the Figure instead of its own plot and route each pointer,
wheel and key event to the row it lands in, through one Controller per row
(row_controller()). All of it is opt-in; the single-plot path is untouched.
2026-09-28 15:24:22 +02:00
Martino Ferrari 4681d7015b feat(examples): drive a region through the glfw demo and verify its geometry 2026-09-28 13:54:59 +02:00
Martino Ferrari 354e79eea2 feat(input): draggable cursors, regions and measurement-band edges 2026-09-28 13:47:35 +02:00
Martino Ferrari 7d2349555d fix(plot): a degenerate range cannot strand a live axis, and the demo's follow check fails the run 2026-09-28 13:14:50 +02:00
Martino Ferrari 77e861f79e feat(input): live x follow with relative zoom and mouse panning 2026-09-28 13:01:48 +02:00
Martino Ferrari 7500fc90d2 feat(render): logical style metrics with a UI scale, so chrome has a consistent physical size 2026-09-28 12:17:53 +02:00
Martino Ferrari 377cca8e9b fix(gtk_demo): assert the recompute invariant, not a strip width; make exit idempotent
The exit-frame self-check compared the y label strip to the pixel and printed a
FAIL at GDK_SCALE=2 where it measured 174 -> 172. Measured (temporary probe,
reverted): the strip is `widest major tick label + padding`, and the tick step
follows the axes length, which the resize changes -- 18 majors over 1440 device px
with "-800m" widest, 8 over 1080 with "-500m", 78 vs 76 px in the bitmap font. The
layout was recomputed; the equality was the wrong assertion. The check now asserts
what actually distinguishes a recompute from a scaled stale layout: the axes and
its strips tile the *current* viewport, and the strip is nearer the previous strip
than that strip scaled by the resize ratio (2 px of drift against a scaled 118).

`on_exit_timer`'s main-context pump can re-enter its own 20 ms timeout, which ran
the exit block twice in one observed run; `on_teardown` is guarded the same way so
the producer join, the window destruction and the quit happen exactly once. A window
closed by hand takes the same ordering through `GtkWindow::close-request`, which
`gtk_window_destroy()` does not emit.
2026-09-28 03:04:15 +02:00
Martino Ferrari 0445a1861b fix(gtk4): derive the device-pixel factor from GTK's two sizes, and four more review fixes
The factor both the viewport and every forwarded input coordinate use came from
`gtk_widget_get_scale_factor()`, which GTK documents as "the next higher integer
value" under fractional scaling. It is now derived by `DeviceMapping::from_sizes()`
from the widget's logical allocation and the device size `GtkGLArea::resize`
reports -- the viewport that signal's default handler sets with
`glViewport(0, 0, w, h)` -- so the viewport, the renderer and input are one value
by construction. The public `int scale_factor()` became `f32 device_scale()`.

`kSurfacePixelsPerNotch` is now 100.0 and documented as a *calibration* with a
stated assumption, not a unit conversion attributed to axis_value120/Qt.

`::unrealize` releases the renderer unconditionally, after making the context
current: the error it used to check says nothing about whether GL names exist
(creation failure is the path that returns before a renderer is built), and
skipping the release left the deletes to the wrapper's destructor, with no
context.

The drag anchor caches its press point and offsets in logical pixels and scales
the difference by the factor in force now, so a scale change mid-drag cannot
become a phantom pan.

The demo stops its producer in `on_teardown`, before destroying the window whose
renderer the stream feeds.
2026-09-28 02:57:17 +02:00
Martino Ferrari 36cd3421f3 feat(gtk4): GtkGLArea-based plot area and demo 2026-09-28 02:09:31 +02:00
Martino Ferrari 1ee2dd9c3d feat(qt6): QOpenGLWidget-based plot widget and demo 2026-09-28 01:17:45 +02:00
Martino Ferrari 15931a2405 feat(imgui): embeddable plot widget and demo 2026-09-28 00:38:31 +02:00
Martino Ferrari 4b649a2783 feat(glfw): adapter and 1 MS/s streaming demo 2026-09-27 23:44:30 +02:00