feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:17:01 +02:00
1 parent cae672bc99
commit eee7c34dd5
12 files changed
+166 -12

No files matched your search

+13 -4
View File
@@ -568,7 +568,7 @@ void Renderer::upload_text_vertices(std::span<const f32> interleaved) noexcept {
text_vao_.upload(interleaved.data(), static_cast<u32>(interleaved.size() / 6u));
}
void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept {
void Renderer::flush_lines(const Mapping& map, Color color, f32 width, Vec2 dash) noexcept {
const u32 segments = lines_.segment_count();
if (segments != 0 && ready_) {
upload_vertices(lines_.data());
@@ -577,6 +577,9 @@ void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept
line_program_.set_offset(map.offset);
line_program_.set_viewport({viewport_.w, viewport_.h});
line_program_.set_width(width);
// Set on *every* flush, solid included, so the crosshair's dash cannot leak into the
// grid, a trace or a cursor drawn after it (see draw_crosshair_lines()).
line_program_.set_dash(dash);
line_program_.set_color(color);
line_vao_.bind();
// One triangle strip per instance, four corners from gl_VertexID.
@@ -1192,25 +1195,31 @@ void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout)
const CrosshairLayout& crosshair = layout.crosshair;
if (crosshair.empty()) return;
const f32 stroke = style_.device(style_.line_width_px);
// The stroke and dash the layout already resolved from the crosshair's own override and the
// global style, in device pixels -- one source, so the boxes the layout held inside `axes` and
// the strokes drawn here cannot disagree (spec §17).
const f32 stroke = crosshair.stroke;
// The two lines. Each is staged as one data-space segment at the *centre* of the stroke box
// the layout placed, spanning that box's own extent, so the pixels this draws are the box
// rather than a second placement of the same rule: the layout held the box inside `axes`,
// and the clip keeps the half pixel the rasteriser puts outside the box's fractional edges.
//
// The dash rides on the same segment: `v_along` is measured from the axes edge the line is
// staged from, so no geometry is added and the phase does not depend on the transform.
const Mapping data = mapping(plot, 0u, 0u);
if (crosshair.vertical.w > 0.0f) {
const f64 x = plot.x_transform(0u).to_data_x(
static_cast<f64>(crosshair.vertical.x + 0.5f * crosshair.vertical.w));
const Range y_range = plot.y_range(0u);
stage_segment(x, y_range.min, x, y_range.max, plot.x_axis(0u).log, plot.y_axis(0u).log);
flush_lines(data, style_.crosshair, stroke);
flush_lines(data, style_.crosshair, stroke, crosshair.dash);
}
if (crosshair.horizontal.h > 0.0f) {
const f64 y = plot.y_transform(0u).to_data_y(
static_cast<f64>(crosshair.horizontal.y + 0.5f * crosshair.horizontal.h));
const Range x_range = plot.x_range(0u);
stage_segment(x_range.min, y, x_range.max, y, plot.x_axis(0u).log, plot.y_axis(0u).log);
flush_lines(data, style_.crosshair, stroke);
flush_lines(data, style_.crosshair, stroke, crosshair.dash);
}
// A small cross at each trace's intersection, in the trace's own colour -- opaque, like a