Commit Graph
10 Commits
Author SHA1 Message Date
Martino Ferrari 38ef459897 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.
2026-09-30 17:32:06 +02:00
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.

Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.

README's provenance section now states MIT and links LICENSE.

No code, test or public API change.
2026-09-28 22:13:40 +02:00
Martino Ferrari 1cb5970757 fix(render): environment-aware atlas drift guard, pinned text scale, shipped-atlas coverage 2026-09-28 10:54:00 +02:00
Martino Ferrari 5ca3587325 feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed 2026-09-28 10:40:23 +02:00
Martino Ferrari 3ff7b9c978 fix(render): gate TextAtlas::sync on the loader, and cover three review findings
Review of the atlas refactor found one Important and three Minor issues:

- Important: TextAtlas::sync called glGetIntegerv before any available() check,
  so a host that synced before plot::gl::load() -- which is legal for a public
  class Tasks 2 and 8 build backends on -- segfaulted on a null entry point
  instead of receiving the Status the interface promises. It now follows the
  handle-constructor convention (available(), then the entry point itself) and
  reports Unsupported.
- empty() no longer means "a GL name exists": releasing one is a
  glDeleteTextures call, so the no-atlas path forgets the atlas and only resets
  the texture when there is a table to release it with.
- end_frame's comment still claimed the frame hands back the unpack alignment
  init() set, contradicting the code below it and renderer.hpp.
- The orientation case's texel probe mapped the atlas' u along the cell's +v
  axis and vice versa, which is a no-op for its two diagonal probes -- so it
  could not fail on a transposed UV mapping, and its own comment misstated the
  rule. Probes now follow the shader's rule and include an off-diagonal pair.
- The null-glyph path of add_text (and its twin in walk_text_clipped) was
  unreached by any case.

Cases: text_atlas_reports_a_status_when_the_entry_points_are_missing,
add_text_skips_a_codepoint_the_backend_cannot_draw.
2026-09-28 10:22:38 +02:00
Martino Ferrari ffc060d6ca refactor(render): the atlas is a declared fixed-cell grid, and staging checks it
The text pipeline stages a glyph's rect *origin* per instance and carries the
rect's *size* once per draw, so a backend whose glyphs have differing rect sizes
-- a TrueType packing trimmed to ink, which is Task 8's shape -- would have drawn
every glyph at whichever size it happened to be told, with nothing downstream
able to tell. Make that constraint structural rather than documented:

- TextBackend::cell_uv() declares the one cell size, and the contract (an atlas
  is a fixed-cell grid; a proportional face draws its glyphs *into* a uniform
  cell instead of trimming each rect to its ink, and why the pipeline needs it)
  is stated on the atlas accessors.
- glyph_is_cell() is the check; add_text()/set_cell() declare the cell from the
  backend, and the staging path asserts it for every glyph in a debug build.
  A release build draws on with the cell it was told, as before.
- Renderer::flush_text() takes u_glyph_uv from the backend, not from the batch,
  so the draw's one rect size can never come from whichever glyph was staged
  first.
2026-09-28 10:09:50 +02:00
Martino Ferrari e074f57ddf refactor(render): TextBackend owns its atlas, and staged glyphs carry a rotation basis 2026-09-28 09:57:28 +02:00
Martino Ferrari 3555ab63d7 feat(render): Renderer with LOD selection, 3.3 and 4.3+ upload paths, headless smoke test 2026-09-27 23:03:08 +02:00
Martino Ferrari b4e88fe0c1 fix(render): give degenerate segments area, pin font bit order, clarify vertex_count 2026-09-27 22:10:53 +02:00
Martino Ferrari 7cbc1c9955 feat(render): style, GLSL line/text pipelines, staging batches, bitmap font 2026-09-27 22:03:56 +02:00