feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s

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:
Martino Ferrari committed 2026-09-30 17:32:06 +02:00
1 parent f132a5ec92
commit 38ef459897
77 files changed
+5905 -688

No files matched your search

+17 -9
View File
@@ -19,6 +19,7 @@ uniform vec2 u_scale; // data -> pixels
uniform vec2 u_offset; // pixels
uniform vec2 u_viewport; // render target size, pixels
uniform float u_width; // stroke width, pixels
uniform float u_aa; // AA fringe half-width, pixels
out float v_dist; // signed distance to the segment axis, pixels
// Distance travelled from a_p0, pixels -- and the dash's phase anchor. Both of the
@@ -33,7 +34,7 @@ void main() {
vec2 p1 = a_p1 * u_scale + u_offset;
vec2 d = p1 - p0;
float len = length(d);
float half_w = u_width * 0.5 + 1.0; // + one pixel of AA fringe
float half_w = u_width * 0.5 + u_aa; // + the AA fringe
// A degenerate segment has no direction: pin the normal, and give the quad
// area by stretching its two ends apart along x. Collapsing all four corners
// onto p0 would rasterise nothing, and a flat trace staged as p == p (the
@@ -64,6 +65,7 @@ in float v_dist; // signed distance to the segment axis, pixels
in float v_along; // distance travelled from a_p0, pixels
uniform float u_width; // stroke width, pixels
uniform float u_aa; // AA fringe half-width, pixels
uniform vec4 u_color;
uniform vec2 u_dash; // dash pattern in pixels: x = ink, y = gap; y <= 0 is solid
@@ -85,12 +87,15 @@ void main() {
}
float half_w = u_width * 0.5;
float d = abs(v_dist);
if (d > half_w + 1.0) {
if (d > half_w + u_aa) {
discard; // outside the fringe; nothing to blend
}
// Ramp over the last pixel of the half width so a thin stroke keeps its
// weight instead of aliasing into a dashed line.
float alpha = 1.0 - smoothstep(half_w - 0.5, half_w + 0.5, d);
// Ramp over the fringe so a thin stroke keeps its weight instead of aliasing
// into a dashed line: the ramp is centred on `half_w` and spans `u_aa`, so
// `u_aa == 1.0` -- the style default -- reproduces the historical single-pixel
// ramp exactly. `u_aa` is the style's `line_aa_px` in device pixels, clamped
// at 0.25 by the renderer.
float alpha = 1.0 - smoothstep(half_w - 0.5 * u_aa, half_w + 0.5 * u_aa, d);
o_color = vec4(u_color.rgb, u_color.a * alpha);
}
)";
@@ -99,12 +104,14 @@ const char* const kTextVS = R"(#version 330 core
layout(location = 0) in vec2 a_pos; // per-instance glyph cell top-left, pixels
layout(location = 1) in vec2 a_uv; // per-instance glyph rect top-left, normalised
layout(location = 2) in vec2 a_axis; // per-instance cell +x axis, unit, pixels
layout(location = 3) in vec4 a_colour; // per-instance tint, straight alpha
uniform vec2 u_viewport; // render target size, pixels
uniform vec2 u_px_range; // glyph cell on screen, pixels (width, height)
uniform vec2 u_glyph_uv; // glyph cell in the atlas, normalised (width, height)
out vec2 v_uv; // normalised atlas coordinate of this fragment
out vec2 v_uv; // normalised atlas coordinate of this fragment
out vec4 v_colour; // the instance's tint
void main() {
// The cell's two axes: the run direction the instance carries and its
@@ -123,15 +130,16 @@ void main() {
// The same handedness as the cell, so the glyph's rect lands on it the right
// way round: u runs along the basis, v down the glyph.
v_uv = a_uv + corner * u_glyph_uv;
v_colour = a_colour;
}
)";
const char* const kTextFS = R"(#version 330 core
in vec2 v_uv; // normalised atlas coordinate of this fragment
in vec2 v_uv; // normalised atlas coordinate of this fragment
in vec4 v_colour; // the instance's tint
uniform sampler2D u_atlas; // GL_R8 glyph atlas, GL_NEAREST
uniform vec2 u_tex_size; // atlas size in texels
uniform vec4 u_color;
out vec4 o_color;
@@ -141,7 +149,7 @@ void main() {
// glyph cell; rounding to a centre removes the tie by construction.
vec2 texel = clamp(floor(v_uv * u_tex_size), vec2(0.0), u_tex_size - vec2(1.0));
float coverage = texture(u_atlas, (texel + vec2(0.5)) / u_tex_size).r;
o_color = vec4(u_color.rgb, u_color.a * coverage);
o_color = vec4(v_colour.rgb, v_colour.a * coverage);
}
)";