From 50078f0217ecd69aa5dede19d23b40e372f6e38c Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Tue, 3 Nov 2026 14:57:53 +0100 Subject: [PATCH] docs: design spec for the horizontal plot split Co-Authored-By: Claude Opus 4.6 --- .../2026-11-03-horizontal-split-design.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/superpowers/specs/2026-11-03-horizontal-split-design.md diff --git a/docs/superpowers/specs/2026-11-03-horizontal-split-design.md b/docs/superpowers/specs/2026-11-03-horizontal-split-design.md new file mode 100644 index 0000000..91be061 --- /dev/null +++ b/docs/superpowers/specs/2026-11-03-horizontal-split-design.md @@ -0,0 +1,92 @@ +# Horizontal plot split -- design + +## Goal + +Let each row of the stacked plots be split into several plots side by side. All plots share one +time window (one x link, one zoom/pan, one crosshair). A split only arranges more plots on screen; it +is not an independent time window and not an XY plot. + +## Decisions + +1. **Shared time window.** Cells in a row, and rows in the figure, are all members of the one x link + group. No new time semantics. +2. **Per-row split.** Each figure row holds 1..4 cells, independently of the other rows. +3. **Draggable cell widths**, saved with the session, mirroring row heights (`weight`). +4. **Caps.** At most 4 cells per row and at most 8 plots in total (`RowSet::kMaxRows` keeps its value + and now bounds plots, not figure rows). Every per-plot structure sized for 8 stays as is. +5. **Approach.** Extend the vendored plotter's `Figure` so a figure row holds several plots; the host + treats each cell as one plot. Rejected: several `Figure`/`PlotArea`s linked by hand (cannot express a + per-row split, reintroduces manual x/crosshair sync) and a general tiling tree (not asked for). + +## Plotter (`deps/plotter`) + +- `Figure` is constructed from cells-per-row (e.g. `{1,2,1}`). `Figure(u32 rows)` stays and means one + cell per row, so existing behaviour and `tests/test_figure.cpp` are unchanged. +- `plot(i)` indexes the flattened cells, top to bottom, left to right. `sync_x_links()` and the + crosshair already work on any set of plots and need no change. +- New: `set_cell_weights(row, weights)` and `set_col_gap(px)`. Widths use the height scheme: a floor + per cell, the weights share the rest, the last cell closes the row exactly (no seam). +- `FigureLayout` gains `cells` (one `Rect` per plot, flattened). `rows` stays the full-width rect per + figure row. +- Only the bottom figure row's cells carry x labels and the x title; other cells get `XStrip::None`. +- The left y gutter stays one common maximum for the whole figure with the same reserved floor. A + per-column gutter would make cell width depend on content, which the layout rule forbids. +- A figure too narrow for the floor widths shrinks the cells proportionally and drops the column gap, + as heights do; no rect is ever negative. +- Pointer hit-testing finds the plot under the cursor by its cell rect. +- The change is documented in `docs/claude/build-and-run.md` with the other vendored changes. + +## Host (`src/plot_host.*`, `src/ui_layout.cpp`, `src/layout_store.*`, `src/ui.cpp`) + +- Everything keyed by a plot index keeps meaning "flattened plot index": cursors, measurements, + `bind()`, readouts, persistence layers, `cursors_pane_bound`. +- `RowSet` gains `cells_per_row_` and per-figure-row cell weights, re-applied after every rebuild like + `row_weights_`. `split_cell(i)` / `remove_cell(i)` change a row's cell count; a removed cell drops its + traces, a new cell starts empty. `insert_row` / `remove_row` act on figure rows and carry their cells. +- `row_count()` stays the plot count; `figure_row_count()` is added. The plan may rename if this reads + badly. +- Cursors: the vertical pair is one shared time drawn in every cell; horizontal rulers are per cell. + The crosshair is one x on every cell, the hovered cell shows its y. +- Y axes are per cell (auto or manual); fit-all fits each; wheel and axis drags act on the cell under + the pointer; an x gesture anywhere moves the shared window. +- A signal dropped on a cell lands in that cell with the same slot rule as a row. The signals page's + "place on row" choice lists cells ("row 2, left") only for split rows. +- Persistence layers fill free slots of the same cell. + +## Layout editor + +Mirrors the row controls: + +- Each vertical boundary between two cells gets a `+` (insert a cell) and a drag arrow (resize the two + neighbours). Each row's left and right edge gets a `+`, so a one-cell row can be split from either side. +- Each cell keeps its top-left `×`: in a multi-cell row it removes the cell; in a one-cell row it + removes the row, as today. Insensitive when it would leave no plot. +- Cell `+` is insensitive at 4 cells in the row or 8 plots in total; the row `+` is insensitive at 8 + plots in total. +- A divider drag moves the two neighbouring widths, each floored at about 96 px logical, relative to the + weights at drag start (as the row drag is). Saved on drag end. +- Outside the editor nothing changes except the extra plots. + +## Session file + +`[[plot]]` tables gain `row` (the figure row) and `width` (the cell's weight). A session without `row` +loads as one cell per row, so existing sessions open unchanged. Row `weight` stays per figure row (carried +on the row's first cell). + +## Verification + +- `tests/test_figure.cpp`: `{1}` identical to today; `{1,2}` tiles exactly; weights and floors respected; + a narrow figure degrades without negative rects; x link and crosshair span all cells. +- New headless probe `UDPSCOPE_PROBE_SPLIT_MS`: split a row, drop a signal in each cell, drag a divider; + check the cells tile the width, the shared x range is identical, both vertical cursors line up in every + cell, widths survive a session round-trip, and a session without `row` still loads. Expect + `gtk warnings: 0`, no `Gtk-CRITICAL`. +- Regression: layout, cursors, measurements, readouts and E-1608 probes read as before; the baseline file + is re-baselined only where this feature explains a difference. +- Layout dumps and probes that print per-row layout gain a cells list and a column-weights line. + +## Docs + +`README.md`, `docs/USER-GUIDE.md`, `help/C/org.udpscope.app/` (layout page), `docs/claude/probe-readings.md`, +`docs/claude/verification.md`, `docs/claude/invariants-plot-and-layout.md` (cell rules), +`docs/claude/build-and-run.md` (plotter change), and `ui_context_help.cpp` where the layout editor is described.