feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
Breaking: Series::gather emits x relative to x0() and takes a GatherMode; consumers reconstruct absolute x in f64 before mapping (series.hpp documents the contract). Bugs, each pinned by a fail-before regression test: - render: f32 staging quantised epoch-time x into 128 px columns; every data-space draw now stages to_space(x) - to_space(origin) with the origin folded into the offset uniform in f64 (epoch/zero-origin twins rasterise identically) - ticks: major test was ULP-fragile and blanked epoch axes; majors are an integer-index test and are never truncated by the minor cap - gather: LOD envelope emitted buckets straddling [begin,end); ceil/floor bucket edges + raw lead/trail spans - live follow: log x axes held a linear span and zoomed every frame; span and offset now live in axis space (plot + both figure link-group paths) - input: a zero-notch wheel disarmed auto_scale on every axis; dy==0 pans x, factor == 1 / inf refused in wheel and drag zoom - render: the single text flush painted legend names over the crosshair readout panel; two batches, under flushed before the panel - data: NaN dropped from min/max unless it seeded the fold; non-finite samples are now excluded from every aggregate and from cached bounds Robustness: viewport normalisation (no clamp UB), non-finite range refusal, pyramid level cap at 57 (no shift/div UB through public gather), rounded ring capacity documented + pre-sized, GL blend func and UNPACK_ALIGNMENT save/restore, Renderer::release_gl() teardown, u64 buffer epochs, annotation captions clamped, button-aware GLFW row capture, cached figure row rects (no per-event layout), ruler-pair measurement refused, span-nonfinite guards in axis_ends. Features: XY series (sorted x, binary-search neighbours/snap, LOD over sample index); StreamSeries::freeze(); ArraySeries::push(); per-trace dash and markers (scatter at width 0) with auto or literal stride; GatherMode Envelope/Nearest/Mean via bucket_sum/sumsq; Style::line_aa_px; per-glyph text colour (readout values in trace colour); wheel-dx x pan; arrow-key pan; Escape cancels a drag and restores captured ranges; modifier-masked box zoom (Shift both, Ctrl x, Alt y); double-click reset via PointerEvent::time_s; ImGui key repeat unified; pinch gestures on Qt and GTK; pixelDelta wheel path and DevicePixelRatioChange handling; GNUInstallDirs, SOVERSION, SameMinorVersion, generated plot/version.hpp, relocatable plot.pc, licence install, FILES_MATCHING, GitHub Actions CI (debug/coverage/asan). Suite 283 -> 350 cases, 0 failed; coverage 93.9% (3730/3971) over 24 files, floor 80/90 held; ASAN preset suite green; all four demos run headless (the GTK scripted-resize check needs a window manager and fails identically on the parent commit); readme_main.cpp committed and compiles with the README command; pkg-config and find_package consumption verified from a relocated install prefix.
This commit is contained in:
1 parent
f132a5ec92
commit
38ef459897
77 files changed
+5905
-688
No files matched your search
@@ -0,0 +1,305 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
# Hardening and features v0.2 — shared contracts
|
||||
|
||||
Design record for the bug-fix + robustness + feature wave. **This file is the
|
||||
interface bible for the five parallel streams.** File ownership is exclusive:
|
||||
a stream edits only the files listed under it. Everything not listed is the
|
||||
integrator's.
|
||||
|
||||
## Bug list (B1–B7) and robustness list (R1–R9) being fixed
|
||||
|
||||
- **B1** f32 staging of large-magnitude x destroys geometry (epoch axes).
|
||||
- **B2** tick major/minor classification ULP-fragile (`ticks.cpp:176`).
|
||||
- **B3** LOD gather emits buckets straddling `[begin,end)` (`series.cpp:168`).
|
||||
- **B4** `XFollow::Live` on log x holds linear span (`plot.cpp:494`, `figure.cpp`).
|
||||
- **B5** zero-notch wheel disarms auto_scale; `factor==inf` unguarded
|
||||
(`controller.cpp:158`, `glfw` forwards dy=0).
|
||||
- **B6** single text flush paints legend text over the crosshair readout panel.
|
||||
- **B7** NaN policy incoherent (min/max drop unless seeding; sum propagates).
|
||||
- **R1** `std::clamp` UB on negative viewport (`plot.cpp:886`).
|
||||
- **R2** `set_x_range`/`set_y_range` store non-finite verbatim (`plot.cpp:377-384`).
|
||||
- **R3** `kBucket << level` UB for level >= 58 via public `gather`.
|
||||
- **R4** ring capacity rounds up but docs/reserve assume raw capacity.
|
||||
- **R5** GLFW figure capture cleared by any button release.
|
||||
- **R6** renderer residue: blend func, UNPACK_ALIGNMENT, no GL shutdown,
|
||||
epoch() wrap, annotation caption overflow.
|
||||
- **R7** GLFW host re-lays out the figure on every event.
|
||||
- **R8** measure accepts horizontal rulers, silently measures [0,0].
|
||||
- **R9** minor-tick cap truncates the far end of the axis, majors included.
|
||||
|
||||
## Cross-cutting decisions
|
||||
|
||||
- **NaN policy (library-wide, documented):** a non-finite sample is *excluded*
|
||||
from every aggregate (min, max, sum, sumsq) and from cached bounds. A window
|
||||
with no finite samples aggregates to a zero-span Range / zero sums. Applies
|
||||
in `MinMaxPyramid::append`, `Series::reduce/sum/sumsq` edge folds,
|
||||
`ArraySeries::commit`, `StreamSeries::acquire` bounds. `docs/data-and-performance.md`
|
||||
gets a "Non-finite samples" paragraph.
|
||||
- **Gather x contract (breaking, B1):** `Series::gather` now emits x
|
||||
**relative to `x0()`** (exactly `i*dx` for uniform series, `x_at(i)-x0()` for
|
||||
XY), never absolute x. y is unchanged. The renderer reconstructs absolute
|
||||
data x in f64 (`x0() + rel`) before transforming, so no precision is lost at
|
||||
the f32 narrowing: the staged f32 is `to_space(x_abs) - to_space(origin)`,
|
||||
which is O(visible pixels), not O(epoch).
|
||||
- **Live follow on log axes (B4):** `Axis::offset` and the live-window span
|
||||
live in **axis space** (`log10` when `log`), matching how the controller's
|
||||
gestures already work (`to_space`/`from_space`). `place_live_x_window` and
|
||||
the figure link-group copies hold the log-space span constant.
|
||||
- **Key-repeat policy (library-wide):** a held key repeats in every adapter
|
||||
(GLFW/Qt/GTK already do; ImGui switches to `repeat=true`).
|
||||
- **Modifiers for box zoom:** Shift+Left = both axes (unchanged, default);
|
||||
Ctrl+Left = x-only; Alt+Left = y-only. Other modifier combos = both.
|
||||
|
||||
## Shared-header changes (made by the integrator before the fan-out)
|
||||
|
||||
- `include/plot/input/events.hpp`
|
||||
- `PointerEvent` gains `f64 time_s = 0.0;` (host-supplied monotonic seconds;
|
||||
powers double-click). Hosts that leave 0 simply never double-click.
|
||||
- New `GestureEvent { f32 x, y; f32 scale = 1.0f; f32 dx = 0.0f, dy = 0.0f; }`
|
||||
— pinch: zoom about (x, y) by `scale`, pan by (dx, dy). `Controller`
|
||||
gains `void on_gesture(const GestureEvent&)`.
|
||||
- New key constants: `kKeyEscape = 27`, `kKeyLeft = 256`, `kKeyRight = 257`,
|
||||
`kKeyUp = 258`, `kKeyDown = 259` (host-translated, like the ASCII ones).
|
||||
- `include/plot/data/series.hpp`
|
||||
- `enum class GatherMode { Envelope, Nearest, Mean };` — downsample policy.
|
||||
`Envelope` = today's min/max pairs. `Nearest` = one raw sample per bucket
|
||||
(via `snap`), x at the sample. `Mean` = one point per bucket, y = mean
|
||||
(bucket `sum` / count), x at bucket centre.
|
||||
- `virtual void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const` — signature change; every
|
||||
override updates; renderer passes the mode explicitly.
|
||||
- New virtuals (uniform default, XY override):
|
||||
`virtual f64 x_at(u64 i) const` (`x0() + i*dx()`),
|
||||
`virtual bool neighbours(f64 x, u64& i0, u64& i1) const` (false when x is
|
||||
outside `[x_at(0), x_at(n-1)]`; else the bracketing indices),
|
||||
`virtual u64 snap_index(f64 x) const` (nearest index, clamped).
|
||||
- `MinMaxPyramid` gains `f64 bucket_sum(u32 l, u64 i) const` and
|
||||
`f64 bucket_sumsq(u32 l, u64 i) const` (Mean/RMS need them).
|
||||
- `include/plot/plot/trace.hpp` — `Trace`/`TraceDesc` gain (trailing, defaults
|
||||
keep aggregate init source-compatible):
|
||||
`GatherMode mode = GatherMode::Envelope;`
|
||||
`f32 dash_on = 0.0f, dash_off = 0.0f;` (logical px, 0 = solid),
|
||||
`enum class Marker { None, Cross, Circle, Square }`, `Marker marker = Marker::None;`
|
||||
`f32 marker_size_px = 3.0f; u32 marker_every = 0;` (0 = auto: stride
|
||||
`ceil(samples_per_pixel)`, so markers only appear where samples are sparse).
|
||||
`width == 0` draws markers only (scatter) — documented.
|
||||
- `include/plot/render/style.hpp` — gains `f32 line_aa_px = 1.0f;` (logical,
|
||||
the AA fringe half-width, clamped >= 0.25f at use). No text-colour fields:
|
||||
text colour becomes a per-call parameter (see C).
|
||||
- `include/plot/plot/plot.hpp` — `set_viewport` normalises negative w/h to 0 (R1).
|
||||
- ~~`include/plot/plot/crosshair.hpp` — `CrosshairRow` gains `Color color`~~
|
||||
DROPPED: `CrosshairRow` already names its trace; the renderer resolves the
|
||||
colour as `plot.trace(row.trace).color` at draw time. No field added.
|
||||
|
||||
## Stream A — data layer (exclusive)
|
||||
|
||||
Files: `include/plot/data/*`, `src/data/*`, `tests/test_series.cpp`,
|
||||
`tests/test_ring_buffer.cpp`, `tests/test_minmax_pyramid.cpp`,
|
||||
`tests/test_xy_series.cpp` (new), plus the `Series::gather` override
|
||||
forwarders and `docs/data-and-performance.md` NaN/policy paragraphs.
|
||||
|
||||
1. **B3**: bucket-edge selection uses `first = max(ceil_div(origin+b, span), lbase)`,
|
||||
`last = min((origin+e)/span, lend)`; lead/trail raw spans cover the rest.
|
||||
Regression: gather at unaligned `begin`/`end` mid-window asserts no emitted
|
||||
x outside `[begin, end)` and envelope values equal exact reduce over the range.
|
||||
2. **B7**: NaN policy per above; regression: NaN at first/middle/last position
|
||||
— bounds and reduce stay finite, sum skips NaN.
|
||||
3. **R3**: `level_span` caps at level 57 (span 2^63); `gather(level > 57)`
|
||||
clamps; no UB/div-zero for any u32 level.
|
||||
4. **R4**: docs say "at most `capacity()` rounded up to a power of two";
|
||||
`StreamSeries` reserves the *rounded* `ring_.capacity()`.
|
||||
5. **GatherMode** implementation in `gather_impl` (Mean/Nearest via the new
|
||||
pyramid accessors); `XY`/`Array`/`Stream` forwarders updated; relative-x
|
||||
contract implemented everywhere (x emitted as `x_at(i) - x0()`).
|
||||
6. **XY series** (full feature): `include/plot/data/xy_series.hpp`,
|
||||
`src/data/xy_series.cpp`, target `plot_core`. Sorted-ascending x
|
||||
(documented precondition, `assert` in debug); binary-search
|
||||
`neighbours`/`snap_index`; pyramid over sample index; `commit()` rebuilds.
|
||||
7. **Snapshot**: `ArraySeries StreamSeries::freeze() const` — consumer-thread
|
||||
only; copies the retained window (`copy_raw`) into a fresh ArraySeries with
|
||||
this window's `x0()`/`dx()`. Freezes against the *last acquire* (documented).
|
||||
8. **Incremental append**: `void ArraySeries::push(f32)` — appends, folds into
|
||||
the pyramid (`append`), extends bounds incrementally (NaN-aware), extends
|
||||
`x_bounds_`. `commit()` stays the wholesale path.
|
||||
Acceptance: new tests for every item above; `plot_tests` builds and passes.
|
||||
|
||||
## Stream B — plot + input (exclusive)
|
||||
|
||||
Files: `src/plot/ticks.cpp`, `src/plot/plot.cpp`, `src/plot/figure.cpp`,
|
||||
`src/input/controller.cpp`, `include/plot/input/controller.hpp`,
|
||||
`include/plot/plot/axis.hpp` (offset doc), `tests/test_ticks.cpp`,
|
||||
`tests/test_controller.cpp`, `tests/test_plot.cpp`, `tests/test_figure.cpp`,
|
||||
`tests/test_measure.cpp`, and the plot-layer consumers of the new `Series`
|
||||
virtuals (`crosshair_value`, `crosshair_span`, `covered_samples`,
|
||||
`measurement` — route through `x_at`/`neighbours`/`snap_index` so an XY series
|
||||
crosshairs and measures correctly).
|
||||
|
||||
1. **B2**: major = integer-index test (`k % ratio == 0` on the minor index,
|
||||
`k` derived from `(v - first_major)/minor_step` rounded); no float tolerance.
|
||||
Regression: epoch range 1.75e9 ± 300 s at 800 px produces labelled majors.
|
||||
2. **R9**: majors are never truncated; only minors count against `kMaxTicks`.
|
||||
3. **B4**: live window placement in axis space (log-aware span/offset) in
|
||||
`Plot::place_live_x_window`, `Plot::set_x_range` live branch, and the two
|
||||
figure link-group copies. Regression: streaming log axis holds visible
|
||||
decades constant across samples; scrolled-back window keeps span.
|
||||
4. **B5**: `on_wheel` — `dy == 0 && dx == 0` returns; `dy == 0, dx != 0` pans x
|
||||
only (`dx` notches, 5% of the x span per notch, log-aware via `to_space`);
|
||||
zoom path rejects `factor == 1` and non-finite. Same guard in the
|
||||
middle-drag `axis_zoom`.
|
||||
5. **R1**: `layout()` clamp guards `hi >= lo` before clamping (no UB even for a
|
||||
hostile viewport; `set_viewport` normalises, already done by integrator).
|
||||
6. **R2**: range setters refuse non-finite endpoints (Off branch + y), keep the
|
||||
previous range and `auto_scale`.
|
||||
7. **R8**: `add_measure`/`measurement` refuse horizontal-ruler (and
|
||||
vertical-ruler dead-field) cursors — `valid` stays false, `add_measure`
|
||||
returns an id that reports invalid.
|
||||
8. **Interaction features**:
|
||||
- Keyboard pan: Left/Right/Up/Down pan 10% of the span per press/repeat
|
||||
(x-only for Left/Right, y-only for Up/Down; log-aware).
|
||||
- Escape (`kKeyEscape`): cancels the active drag — restores the ranges the
|
||||
axes had at `begin_drag` (captured then), drops box-zoom without applying,
|
||||
clears grab state.
|
||||
- Box-zoom masks per the modifier table above; applied per-axis at release.
|
||||
- Double-click reset: two presses of the same button within 0.35 s and 4 px
|
||||
(uses `PointerEvent::time_s`) re-fits every axis (`auto_scale = true` +
|
||||
fit). Second press does not begin a drag.
|
||||
Acceptance: controller tests for each gesture incl. cancel-restore; tick
|
||||
regressions; XY crosshair/measure unit tests via an `XY` fixture.
|
||||
|
||||
## Stream C — render (exclusive)
|
||||
|
||||
Files: `src/render/*`, `include/plot/render/*` (renderer/primitives/text/buffer
|
||||
interfaces), `src/gl/handle.cpp`, `include/plot/gl/handle.hpp`,
|
||||
`tests/test_render_smoke.cpp`, `tests/test_primitives.cpp`.
|
||||
|
||||
1. **B1**: implement the relative-x staging contract (see Cross-cutting):
|
||||
trace staging reconstructs `x_abs = series.x0() + rel` in f64, stages
|
||||
`f32(to_space(x_abs) - to_space(origin))`; every data-space line draw
|
||||
(grid, cursors, regions, measures, crosshair, annotations) picks a natural
|
||||
origin (its own reference data x/y) and `mapping()` folds
|
||||
`origin` into the offset uniform in f64 before the single narrowing.
|
||||
Regression: EGL smoke renders an epoch-window series and a zero-origin
|
||||
twin and asserts both rasterise to the same pixel column set (tolerance).
|
||||
2. **B6**: two text batches — `text_under_` (legend names, cursor labels,
|
||||
measure labels, annotation captions) flushed *immediately before*
|
||||
`draw_crosshair_readout`; `text_over_` (readout text, tick labels, titles)
|
||||
flushed at `end_frame`. The readout panel occludes under-batch text.
|
||||
Regression: legend + overlapping readout frame — legend name pixels are not
|
||||
lit inside the readout panel rect.
|
||||
3. **R6**: `begin_frame` saves `GL_BLEND_SRC/_DST` via `glGetIntegerv` and
|
||||
`end_frame` restores function + enable bit; `Texture::upload_rgba`
|
||||
saves/restores `GL_UNPACK_ALIGNMENT`; `Renderer::release_gl()` (public)
|
||||
destroys all GL handles explicitly, called first by the destructor (safe
|
||||
when the context is current) and documented for host teardown order;
|
||||
`epoch()` widened to u64 end-to-end (buffer generation + slot epoch +
|
||||
VAO cached epoch); annotation captions clamp their box inside `axes`
|
||||
(skip the caption when it cannot fit).
|
||||
4. **Dash**: trace dash reaches `u_dash` per trace flush (device px, logical
|
||||
converted); legend swatch samples the dash. `dash_off == 0` = solid.
|
||||
5. **Markers**: staged as line segments through the existing batch
|
||||
(Cross 2 segs, Circle 8-gon, Square 4 segs), colour = trace colour, size =
|
||||
`device(marker_size_px)`, stride `marker_every` or auto `ceil(spp)`;
|
||||
`width == 0` draws markers only. Render-smoke regression: a marker'd trace
|
||||
lights vertices a plain line does not, and a `marker_every = 3` trace
|
||||
stages 1/3 the marker count.
|
||||
6. **AA knob**: `u_aa` uniform = `max(0.25, device(style.line_aa_px))`; shaders
|
||||
drop the hardcoded fringe. Default output unchanged (1.0 logical).
|
||||
7. **Text colour**: per-vertex `a_colour` (vec4); `stage_text(..., Color)`;
|
||||
`flush_text` stops setting `u_color`; crosshair readout value rows draw in
|
||||
their row's trace colour (`CrosshairRow::color`), labels in `axis_text`.
|
||||
Smoke regression: a coloured readout row differs from the axis-text pass.
|
||||
8. **GatherMode**: renderer passes `trace.mode` to `gather`.
|
||||
Acceptance: smoke tests above; full suite green; no uniform/attribute drift
|
||||
(cross-check against GLSL in the same PR).
|
||||
|
||||
## Stream D — adapters (exclusive)
|
||||
|
||||
Files: `adapters/**` only, `tests/test_glfw_host.cpp`, `tests/test_qt_pointer.cpp`,
|
||||
`tests/test_gtk_input.cpp`, `tests/test_imgui_widget.cpp`,
|
||||
`docs/embedding.md` (shared conventions: key repeat, touch parity, DPR note).
|
||||
|
||||
1. **R5**: GLFW figure capture records the button; only the owning button's
|
||||
release ends it; a second button does not steal or clear the capture
|
||||
(documented single-gesture policy). Test: press left row0, press right row1,
|
||||
release right — left drag still routes to row0.
|
||||
2. **R7**: `Host` caches row rects; invalidated on resize + `set_figure`,
|
||||
refreshed once per frame; event handlers read the cache (one-frame-stale
|
||||
rows during resize — documented).
|
||||
3. Key repeat: ImGui `IsKeyPressed(..., repeat=true)`; policy comment.
|
||||
4. **Timestamps**: all four adapters fill `PointerEvent::time_s`
|
||||
(glfwGetTime / QEvent timestamps /1000 / gdk time /1000 / ImGui::GetTime).
|
||||
5. **New keys**: GLFW maps arrows + Escape to the new constants (with
|
||||
`GLFW_KEY_LEFT` etc.); Qt maps `Key_Left...`; GTK maps `GDK_KEY_Left...`;
|
||||
ImGui `ImGuiKey_LeftArrow...`, `ImGuiKey_Escape`.
|
||||
6. **Qt**: `wheelEvent` uses `pixelDelta()/40` notches when `angleDelta` is 0
|
||||
(pixelDelta path); `DevicePixelRatioChange` refreshes the controller
|
||||
viewport + scales; pinch via `grabGesture(Qt::PinchGesture)` ->
|
||||
`GestureEvent`.
|
||||
7. **GTK4**: `GtkGestureZoom` attached in the plot area factory ->
|
||||
`GestureEvent` (scale + focal delta); scroll `ScrollUnit::Surface` path
|
||||
already exists — keep.
|
||||
8. **GLFW**: `on_scroll` forwards unchanged (controller owns the dy=0 case).
|
||||
Acceptance: adapter-gated tests updated; toolkit-free seams (qt_pointer,
|
||||
gtk_input) carry the new logic so headless tests cover it.
|
||||
|
||||
## Stream E — build, packaging, docs, CI (exclusive)
|
||||
|
||||
Files: root `CMakeLists.txt`, `cmake/*`, `CMakePresets.json`,
|
||||
`.github/workflows/ci.yml` (new), `plot.pc.in` (new),
|
||||
`docs/building-and-testing.md`, `docs/getting-started.md`, `README.md`
|
||||
(feature list + versioning), `tests/test_series.cpp` (delete the wall-clock
|
||||
steady-state case only), `readme_main.cpp` (new, referenced by README).
|
||||
|
||||
1. `GNUInstallDirs` for every install path; `VERSION`/`SOVERSION` on
|
||||
`plot_core` when shared; `SameMinorVersion` compatibility (0.x reality);
|
||||
generated `include/plot/version.hpp` (`configure_file`, installed, added to
|
||||
the umbrella + exemption handling as needed); `plot.pc` pkg-config file;
|
||||
install `LICENSE` + `licences/OFL-1.1.txt` under
|
||||
`${CMAKE_INSTALL_DATADIR}/licenses/plot/`; `install(DIRECTORY)` gets
|
||||
`FILES_MATCHING PATTERN "*.hpp"`.
|
||||
2. `PLOT_BUILD_TTF_FONT` moves to `cmake/plot_options.cmake` (docs match);
|
||||
coverage-flag detection switches to a `PLOT_COVERAGE` option check on the
|
||||
config profile, not a `CMAKE_CXX_FLAGS` string match.
|
||||
3. CI: `.github/workflows/ci.yml` — ubuntu-latest, apt deps (libglfw3-dev,
|
||||
qt6-base-dev, libgtk-4-dev, libegl1, libglvnd-dev, xvfb, lcov,
|
||||
python3-fonttools), jobs: debug (configure/build/xvfb ctest), coverage
|
||||
(tools/coverage.sh, uploads the table), asan (linux-asan preset). `continue-on-error`
|
||||
on the ImGui fetch is not needed (network is available on GH runners).
|
||||
4. Docs: correct the case-count math (recount from the tree at integration
|
||||
time), remove the stale hard-coded coverage figures from README and
|
||||
building-and-testing (coverage.md stays the single source, regenerated),
|
||||
document the new keys/gestures/touch parity in `interaction.md` +
|
||||
`embedding.md` (note: B/C own feature *code*; E only documents what the
|
||||
contract above specifies).
|
||||
Acceptance: `cmake --install` smoke into a temp prefix finds `plot` via
|
||||
`find_package` in a throwaway consumer; umbrella tests stay green; suite
|
||||
counts match docs.
|
||||
|
||||
## Integration (integrator only)
|
||||
|
||||
Merge all streams; reconcile `include/plot/plot.hpp` umbrella; full
|
||||
configure+build+ctest; regenerate coverage via `tools/coverage.sh`; update the
|
||||
per-file coverage table and README numbers from the fresh run; run the GLFW
|
||||
demo headless under Xvfb as the final smoke; update this design record's
|
||||
status.
|
||||
|
||||
## Status: COMPLETE (2026-09-30)
|
||||
|
||||
All five streams landed and were integrated on `main` (uncommitted working
|
||||
tree). `Controller::on_gesture` was implemented by the integrator (Stream B was
|
||||
cut off before it). Incident: the root filesystem hit 100% mid-wave and
|
||||
truncated `src/render/renderer.cpp` to 0 bytes; Stream C restored it from its
|
||||
transcript and re-verified. Final state:
|
||||
|
||||
- `linux-debug` suite: **350 cases, 0 failed** (was 283); 4/4 ctest entries pass.
|
||||
- Coverage: **93.9% aggregate (3730/3971) over 24 files**, every file >= 80%
|
||||
(floor 80/90) — regenerated in `docs/coverage.md` by `tools/coverage.sh`.
|
||||
- Headless smoke: `plot_glfw_demo` + `plot_multiplot_demo` under `xvfb-run`,
|
||||
exit 0, screenshots contain rendered ink (65.4% / 20.0% non-background at
|
||||
1280x720); the multiplot run reports **20,901 staged GPU segments for
|
||||
~4.5M visible samples, GL error 0** (README paragraph updated).
|
||||
- Known follow-ups: real touch hardware has never exercised the Qt/GTK pinch
|
||||
paths; macOS/Windows remain untested; CI YAML is written but has run only
|
||||
locally in structure (no GitHub remote execution during this wave).
|
||||
Reference in new issue
Block a user