fix(render): clip grid, traces, regions and cursors to the plot area

This commit is contained in:
Martino Ferrari committed 2026-09-28 17:00:02 +02:00
1 parent d853db62ae
commit 2d819c4228
3 files changed
+610 -7

No files matched your search

+57 -2
View File
@@ -111,6 +111,12 @@ class Renderer {
/// bottom-left origin once, clears that region to `style().background` and
/// empties the staging batches.
///
/// The scissor that bounds the clear is the frame's *baseline clip*: it stays engaged,
/// set to the same viewport rect, so every pass of the frame is bounded by the region
/// the host gave the renderer -- and `draw()` narrows and releases it again around the
/// passes that draw inside a plot's axes (see `set_clip()`). `end_frame()` turns the
/// scissor test off again, so a host never inherits a clip it did not ask for.
///
/// It is also where the instance stores wait -- once each -- for the previous
/// frame's draws to retire the ring they read, before this frame writes any
/// slot (see `InstanceStore::begin_frame`): a frame never synchronises with the
@@ -152,14 +158,30 @@ class Renderer {
/// line geometry staged with everything else, the names are glyphs on the text
/// path, and both read the boxes `PlotLayout::legend` measured. It is the last
/// line batch, so the legend cannot be drawn under a trace, and it leaves no GL
/// state of its own -- the frame's blend and its neighbours' scissor already say
/// everything a fill and a hairline border need (see end_frame's residue list).
/// state of its own: it is drawn inside the same axes clip the interior passes share,
/// and the frame's blend already says everything a fill and a hairline border need
/// (see end_frame's residue list).
///
/// Regions are part of that same evidence: the fill is one instance of the line
/// pipeline (`fill_pixels()`, the primitive the legend panel uses), the bounds are
/// vertical strokes and the label is a staged glyph run, so a frame with regions
/// leaves behind exactly what a frame without them does.
void draw(Plot& plot);
///
/// **The clip** is the one piece of state the frame moves to keep data inside the plot
/// area (spec §13): the passes that draw *inside* `axes` -- grid, traces, regions,
/// cursors, the measurement band, annotations and the legend -- run with the scissor
/// narrowed to `layout().axes`, intersected with the frame's viewport, while the axis
/// lines (chrome drawn on the rect's own edge, which overhangs it by half a stroke) and
/// the tick labels and titles (outside the rect by design) run with the frame's viewport
/// clip. A sample outside the visible range, or a segment the gathered window's slack
/// hangs past the end of the window, therefore cannot be drawn over the label strips --
/// or, in a `Figure`, into the row above.
///
/// Nothing moves to make that happen: the clip is a GL state change and not a geometry
/// one, so the vertices staged, the transforms and the data-space invariant are exactly
/// what they were.
/// Draws a whole `Figure`: every row in its own rect, then the figure's one shared
/// x axis.
@@ -192,6 +214,16 @@ class Renderer {
/// vertex array 0, program 0, `GL_ARRAY_BUFFER` 0, texture unit 0 active and
/// nothing bound to `GL_TEXTURE_2D` on it, blending off, scissoring off, and the
/// viewport left at the frame's rect (the caller sets its own for the next pass).
///
/// The scissor is the other piece of state a frame moves, and it ends the frame *off*:
/// `begin_frame()` engages it on the frame's viewport -- the box the clear uses --
/// `draw()` narrows it to a plot's `axes` for the interior passes and releases it back to
/// the viewport for the labels and titles, and this turns the test off. A host is
/// therefore never left holding a clip it did not ask for: not the axes rect of the last
/// interior pass, and not the viewport's either. The box itself is left at the viewport's
/// own rect, which is where that release puts it, so a host that enables the scissor
/// again without setting one gets the region the frame drew in.
/// The one piece of global state the frame *changed* is handed back rather than
/// left where the frame wanted it: the clear colour, which is
/// `style().background` only for the duration of the frame. GL keeps no history
@@ -338,6 +370,29 @@ class Renderer {
/// is texture-bound, so this runs once per frame, after all line geometry.
void flush_text() noexcept;
/// Narrows the frame's clip to `rect` -- framebuffer pixels, top-left origin, like every
/// `Rect` here -- and *intersects* it with the frame's viewport, so the clip can only
/// ever be smaller than the region the frame owns: a rect the layout got wrong, or one
/// that reaches past the frame, clips the interior passes away rather than letting them
/// paint over the host's surface. Enables `GL_SCISSOR_TEST`; `clear_clip()` puts it back.
///
/// The box is integral and GL's origin is bottom-left, so this is also where a `Rect`
/// becomes a scissor box, and it rounds *inward* -- see `clip_to()`. Called once per
/// plot with `layout().axes`, and released again before the passes that draw outside the
/// axes (see `draw_plot()`).
void set_clip(const Rect& rect) noexcept;
/// Puts the frame's clip back to its viewport: the state `begin_frame()` leaves, and the
/// one the axis lines, the tick labels and the axis titles run with -- chrome that lives
/// outside `axes` by design, and would be erased under the axes clip.
void clear_clip() noexcept;
/// The one place a scissor box is computed from a rect's four edges: clamped to the
/// framebuffer's rows, rounded *inward* (`ceil` the near edges, `floor` the far ones) so
/// the box holds only pixels the rect entirely contains -- rounding outward would let a
/// trace's half-pixel fringe past the edge it is being kept behind -- and flipped to GL's
/// bottom-left origin. An empty or unordered rect (a degenerate axes, or a NaN) sets an
/// empty box, which rejects every fragment: the honest meaning of "clip to nothing".
void clip_to(f32 x0, f32 y0, f32 x1, f32 y1) noexcept;
/// Fills `rect` -- in framebuffer pixels, like `Rect` -- with `color`, in one
/// instance: a horizontal stroke whose width is the rect's height *is* the rect,
/// because a stroke's quad is its perpendicular width and the fragment shader