fix(render): clip grid, traces, regions and cursors to the plot area
This commit is contained in:
1 parent
d853db62ae
commit
2d819c4228
3 files changed
+610
-7
No files matched your search
+79
-5
@@ -279,6 +279,10 @@ void Renderer::begin_frame(u32 fb_w, u32 fb_h, Rect viewport_px) noexcept {
|
||||
// framebuffer coordinates and is not affected by glViewport, which is why the
|
||||
// clear happens before the viewport is set.
|
||||
//
|
||||
// The same clip stays engaged for the whole frame as its baseline -- set here, and put
|
||||
// back by `clear_clip()` -- so a pass that is not narrowed to the axes is still bounded
|
||||
// by the region the host gave the renderer. `end_frame()` turns the test off again.
|
||||
//
|
||||
// The host's clear colour is queried once, before the first frame sets its own:
|
||||
// GL keeps no history for it, so this is the only moment the value can be
|
||||
// recovered for `end_frame()` to hand back. A host that changes its own clear
|
||||
@@ -291,12 +295,10 @@ void Renderer::begin_frame(u32 fb_w, u32 fb_h, Rect viewport_px) noexcept {
|
||||
host_clear_ = Color{rgba[0], rgba[1], rgba[2], rgba[3]};
|
||||
host_clear_saved_ = true;
|
||||
}
|
||||
gl::glEnable(gl::GL_SCISSOR_TEST);
|
||||
gl::glScissor(vx, vy, vw, vh);
|
||||
set_clip(viewport_px);
|
||||
const Color& background = style_.background;
|
||||
gl::glClearColor(background.r, background.g, background.b, background.a);
|
||||
gl::glClear(gl::GL_COLOR_BUFFER_BIT);
|
||||
gl::glDisable(gl::GL_SCISSOR_TEST);
|
||||
gl::glViewport(vx, vy, vw, vh);
|
||||
|
||||
// The pipelines write straight alpha and rely on blending being on; setting it
|
||||
@@ -306,6 +308,53 @@ void Renderer::begin_frame(u32 fb_w, u32 fb_h, Rect viewport_px) noexcept {
|
||||
gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- the frame's clip
|
||||
|
||||
void Renderer::set_clip(const Rect& rect) noexcept {
|
||||
// Nested, never replacing: the box is the *intersection* of `rect` with the frame's
|
||||
// viewport, so the clip can only ever be smaller than the region this frame owns --
|
||||
// which is the same region `begin_frame()`'s clear covered and `clear_clip()` puts back.
|
||||
clip_to(std::max(rect.x, viewport_.x), std::max(rect.y, viewport_.y),
|
||||
std::min(rect.right(), viewport_.right()),
|
||||
std::min(rect.bottom(), viewport_.bottom()));
|
||||
}
|
||||
|
||||
void Renderer::clear_clip() noexcept {
|
||||
clip_to(viewport_.x, viewport_.y, viewport_.right(), viewport_.bottom());
|
||||
}
|
||||
|
||||
void Renderer::clip_to(f32 x0, f32 y0, f32 x1, f32 y1) noexcept {
|
||||
// The one conversion of a rect to a scissor box, and both halves of it matter here: GL's
|
||||
// box is integral and its origin is bottom-left, while a `Rect` is fractional and
|
||||
// top-left. The rounding is *inward* -- `ceil` the near edges, `floor` the far ones --
|
||||
// so the box holds only the pixels `rect` entirely contains. Rounding outward would let a
|
||||
// trace's half-pixel fringe over the edge it is being kept behind (a `Figure` row
|
||||
// bleeding a single row into its neighbour is exactly that, and so is a marker's fringe
|
||||
// over a label strip), while rounding inward at most drops the boundary half-pixel of a
|
||||
// stroke that a fractional axes rect does not fully contain either.
|
||||
//
|
||||
// An unordered rect -- empty, inverted, or NaN, whose every comparison is false -- sets
|
||||
// an empty box, which rejects every fragment: the honest reading of "clip to nothing",
|
||||
// and the reason a degenerate axes draws no data instead of drawing all of it.
|
||||
gl::GLint left = 0;
|
||||
gl::GLint top = 0;
|
||||
gl::GLsizei w = 0;
|
||||
gl::GLsizei h = 0;
|
||||
if (x0 <= x1 && y0 <= y1) {
|
||||
const gl::GLint x_min = static_cast<gl::GLint>(std::ceil(x0));
|
||||
const gl::GLint x_max = static_cast<gl::GLint>(std::floor(x1));
|
||||
const gl::GLint y_min = static_cast<gl::GLint>(std::ceil(y0));
|
||||
const gl::GLint y_max = static_cast<gl::GLint>(std::floor(y1));
|
||||
left = x_min;
|
||||
top = y_min;
|
||||
w = static_cast<gl::GLsizei>(std::max(0, x_max - x_min));
|
||||
h = static_cast<gl::GLsizei>(std::max(0, y_max - y_min));
|
||||
}
|
||||
gl::glEnable(gl::GL_SCISSOR_TEST);
|
||||
// Top-left to bottom-left: the frame's last row is GL's first.
|
||||
gl::glScissor(left, static_cast<gl::GLint>(fb_h_) - top - h, w, h);
|
||||
}
|
||||
|
||||
bool Renderer::begin_frame_text() noexcept {
|
||||
// The plot reserves its label strips from TextBackend metrics, so it must
|
||||
// measure with the size the text pipeline draws with, and re-read the style
|
||||
@@ -416,10 +465,27 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
||||
// after this returns, and `draw(Figure&)` after it has drawn every row and the
|
||||
// figure's shared x axis, so a figure is one text draw call rather than one per
|
||||
// row -- and the shared axis' labels join the same batch as the labels above them.
|
||||
//
|
||||
// The clip alternates around those passes, and every transition is one of the two
|
||||
// helpers -- no scissor is set anywhere else -- so the sequence is this: the passes that
|
||||
// draw *inside* the axes run narrowed to `layout.axes` (intersected with the frame's
|
||||
// viewport, which is the only clip a hostile rect could otherwise escape), the passes
|
||||
// that draw outside it 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, is
|
||||
// therefore never drawn over the label strips or, in a `Figure`, into the row above.
|
||||
//
|
||||
// Two of the three releases are load-bearing rather than tidiness. The axis lines are
|
||||
// chrome drawn *on* the axes' edge: half of each stroke lies outside the rect by
|
||||
// construction, so clipping them would thin them at the frame's border. The tick labels
|
||||
// and the axis titles live outside `axes` by design, and clipping them would erase them.
|
||||
// (They are staged last here, but their pixels are not: text is one batch flushed after
|
||||
// the frame's line geometry, in the order it was staged, so reordering these two calls
|
||||
// against the line passes would not change a pixel.)
|
||||
set_clip(layout.axes);
|
||||
draw_grid(plot);
|
||||
clear_clip();
|
||||
draw_axis_lines(plot);
|
||||
draw_tick_labels(plot, layout);
|
||||
draw_titles(plot, layout);
|
||||
set_clip(layout.axes);
|
||||
draw_traces(plot, layout);
|
||||
// Regions over the traces and under the cursors: an area of interest the data is
|
||||
// still legible through, with the markers that bound it on top of it.
|
||||
@@ -428,6 +494,9 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
||||
draw_measures(plot, layout);
|
||||
draw_annotations(plot);
|
||||
draw_legend(plot, layout);
|
||||
clear_clip();
|
||||
draw_tick_labels(plot, layout);
|
||||
draw_titles(plot, layout);
|
||||
}
|
||||
|
||||
void Renderer::end_frame() noexcept {
|
||||
@@ -458,6 +527,11 @@ void Renderer::end_frame() noexcept {
|
||||
gl::glActiveTexture(gl::GL_TEXTURE0);
|
||||
gl::glBindTexture(gl::GL_TEXTURE_2D, 0);
|
||||
gl::glDisable(gl::GL_BLEND);
|
||||
// The scissor, too: the frame engaged it (its baseline clip) and each plot narrowed and
|
||||
// released it, so what is left behind is the test *off* -- the state the residue list
|
||||
// promises -- rather than whichever rect the last pass was clipped to. The box stays
|
||||
// where `clear_clip()` put it, the frame's viewport rect.
|
||||
gl::glDisable(gl::GL_SCISSOR_TEST);
|
||||
if (host_clear_saved_) {
|
||||
gl::glClearColor(host_clear_.r, host_clear_.g, host_clear_.b, host_clear_.a);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user