feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable
This commit is contained in:
1 parent
cae672bc99
commit
eee7c34dd5
12 files changed
+165
-11
No files matched your search
@@ -494,12 +494,15 @@ void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
|
|||||||
if (hovered >= row_count) return;
|
if (hovered >= row_count) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applied to the whole figure: one x, every row. A row whose controller has *no* crosshair (the
|
// Applied to the whole figure: one x, every row; and the pointer row's **own** y, only to that
|
||||||
// pointer left its axes, which is the only thing that clears one) clears the figure's, so the
|
// row -- so the row under the pointer gets the one horizontal line, and no row ever shows
|
||||||
// rows cannot be left showing a hover the pointer has abandoned.
|
// another row's y (spec §17). The y is the row's own data coordinate, placed by its controller
|
||||||
|
// from the pointer's pixel row, and it is never spread to a sibling. A row whose controller has
|
||||||
|
// *no* crosshair (the pointer left its axes, which is the only thing that clears one) clears the
|
||||||
|
// figure's, so the rows cannot be left showing a hover the pointer has abandoned.
|
||||||
const Plot& row = figure_->plot(hovered);
|
const Plot& row = figure_->plot(hovered);
|
||||||
if (row.crosshair().active) {
|
if (row.crosshair().active) {
|
||||||
figure_->set_crosshair(row.crosshair().x, true);
|
figure_->set_crosshair(row.crosshair().x, hovered, row.crosshair().y, true);
|
||||||
spread_x_ = row.crosshair().x;
|
spread_x_ = row.crosshair().x;
|
||||||
spread_active_ = true;
|
spread_active_ = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,22 @@
|
|||||||
|
|
||||||
namespace plot {
|
namespace plot {
|
||||||
|
|
||||||
|
/// A per-`Crosshair` override of the three style knobs spec §17 adds, in **logical**
|
||||||
|
/// pixels, so one plot's crosshair can differ -- be thicker, or fatter-dashed -- without a
|
||||||
|
/// caller touching the global `render::Style` the rest of the frame draws with.
|
||||||
|
///
|
||||||
|
/// A negative value means "not overridden": the field falls back to the matching
|
||||||
|
/// `Style::crosshair_*` metric, so a default-constructed `CrosshairStyle` (every field
|
||||||
|
/// `-1`) is exactly the global style and the common case costs nothing. `width`, `dash_on`
|
||||||
|
/// and `dash_off` are independent, so a caller may override only the dash, or only the
|
||||||
|
/// width, and leave the rest to the style.
|
||||||
|
struct CrosshairStyle {
|
||||||
|
f32 width = -1.0f; ///< logical stroke width; `< 0` takes `Style::crosshair_width`.
|
||||||
|
f32 dash_on = -1.0f; ///< logical ink length; `< 0` takes `Style::crosshair_dash_on`.
|
||||||
|
f32 dash_off = -1.0f; ///< logical gap; `< 0` takes `Style::crosshair_dash_off`, and `0`
|
||||||
|
///< (once resolved) means solid.
|
||||||
|
};
|
||||||
|
|
||||||
/// The crosshair's state: where it is, which of its two lines it draws, and whether the
|
/// The crosshair's state: where it is, which of its two lines it draws, and whether the
|
||||||
/// per-signal readout is wanted.
|
/// per-signal readout is wanted.
|
||||||
///
|
///
|
||||||
@@ -46,6 +62,10 @@ struct Crosshair {
|
|||||||
/// The data y of the horizontal line, on y axis 0. Meaningless for a vertical-only
|
/// The data y of the horizontal line, on y axis 0. Meaningless for a vertical-only
|
||||||
/// crosshair, which draws no row and anchors its readout at the top of the axes.
|
/// crosshair, which draws no row and anchors its readout at the top of the axes.
|
||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
|
/// This plot's own stroke and dash, when it wants to differ from the global style; a
|
||||||
|
/// default-constructed `CrosshairStyle` defers every field to `render::Style` (see
|
||||||
|
/// `CrosshairStyle`). Set through `Plot::set_crosshair_style()`.
|
||||||
|
CrosshairStyle style{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The two samples of a uniform-grid series that a data `x` lies between: what the
|
/// The two samples of a uniform-grid series that a data `x` lies between: what the
|
||||||
@@ -177,6 +197,16 @@ struct CrosshairLayout {
|
|||||||
/// The horizontal line's stroke box, the same instrument turned a quarter turn. Zero-sized
|
/// The horizontal line's stroke box, the same instrument turned a quarter turn. Zero-sized
|
||||||
/// when the horizontal line is not drawn.
|
/// when the horizontal line is not drawn.
|
||||||
Rect horizontal{};
|
Rect horizontal{};
|
||||||
|
/// The stroke width the two lines are drawn at, in **device** pixels: the crosshair's own
|
||||||
|
/// `CrosshairStyle::width` when it overrides it, else `Style::crosshair_width`, both through
|
||||||
|
/// `Style::device()` -- resolved here once, so the stroke boxes above and the renderer's
|
||||||
|
/// strokes are one number rather than two that must agree.
|
||||||
|
f32 stroke = 0.0f;
|
||||||
|
/// The dash pattern in **device** pixels: `x` the ink length, `y` the gap. `y <= 0` means
|
||||||
|
/// solid -- the state whenever no dash is asked for, which is what keeps the shared line
|
||||||
|
/// pipeline drawing every other stroke in the library unchanged. The dash is a fragment-shader
|
||||||
|
/// effect on `v_along`, so this is the only CPU-side change it makes (spec §17).
|
||||||
|
Vec2 dash{};
|
||||||
/// The readout panel, inside `axes`. Zero-sized when there is nothing to read out (no
|
/// The readout panel, inside `axes`. Zero-sized when there is nothing to read out (no
|
||||||
/// visible trace, nothing to measure text with, no room for even one row), when
|
/// visible trace, nothing to measure text with, no room for even one row), when
|
||||||
/// `Crosshair::show_readout` is off, or when the panel could not be placed inside the
|
/// `Crosshair::show_readout` is off, or when the panel could not be placed inside the
|
||||||
|
|||||||
@@ -161,10 +161,33 @@ class Figure {
|
|||||||
/// different things in each row. A row whose series has no sample at `x` lists the trace
|
/// different things in each row. A row whose series has no sample at `x` lists the trace
|
||||||
/// with no value rather than inventing one.
|
/// with no value rather than inventing one.
|
||||||
///
|
///
|
||||||
|
/// A host that *does* know which row the pointer is in -- every adapter does, from its own
|
||||||
|
/// pointer position -- uses the four-argument overload below instead, which adds the pointer
|
||||||
|
/// row's own horizontal line; this overload is for a caller that has only a coordinate.
|
||||||
|
///
|
||||||
/// A row is placed with `Plot::set_crosshair(x, active)`, which refuses a coordinate that
|
/// A row is placed with `Plot::set_crosshair(x, active)`, which refuses a coordinate that
|
||||||
/// is not a number; such a call therefore leaves every row's crosshair as it was.
|
/// is not a number; such a call therefore leaves every row's crosshair as it was.
|
||||||
void set_crosshair(f64 x, bool active);
|
void set_crosshair(f64 x, bool active);
|
||||||
|
|
||||||
|
/// Places the crosshair with the pointer's **row** and that row's local y (spec §17): the
|
||||||
|
/// shared x goes to every row -- vertical lines everywhere, as the plain overload does -- and
|
||||||
|
/// the horizontal line is drawn **only in `hovered_row`**, where its y is a value of *that*
|
||||||
|
/// row's own scale. It is the fix for the reported defect -- a figure showing no horizontal
|
||||||
|
/// line at all -- without inventing a figure-wide y, which would mean a different value in
|
||||||
|
/// every row: the line appears where the pointer is, and no row ever shows another row's y.
|
||||||
|
///
|
||||||
|
/// `y_in_row` is a data coordinate on `hovered_row`'s y axis 0, exactly as `Crosshair::y`'s
|
||||||
|
/// two-coordinate placement is; a host reads it from the row's own y transform at the
|
||||||
|
/// pointer's pixel row. `hovered_row` outside `[0, row_count())` is treated as "no row under
|
||||||
|
/// the pointer": every row gets the vertical line and none gets a horizontal one, the same
|
||||||
|
/// state the single-coordinate overload produces.
|
||||||
|
///
|
||||||
|
/// Refused, changing nothing, when `x` is not a number -- matching `set_crosshair(x, active)`
|
||||||
|
/// and `Plot::set_crosshair()`'s own rule. A non-finite `y_in_row` leaves `hovered_row` with
|
||||||
|
/// the vertical line only (the y is dropped, the hover is not), because a row with no valid y
|
||||||
|
/// is still a row the pointer is in.
|
||||||
|
void set_crosshair(f64 x, u32 hovered_row, f64 y_in_row, bool active);
|
||||||
|
|
||||||
/// Clears the crosshair on every row. A row that never had one is unaffected.
|
/// Clears the crosshair on every row. A row that never had one is unaffected.
|
||||||
void clear_crosshair();
|
void clear_crosshair();
|
||||||
|
|
||||||
|
|||||||
@@ -591,6 +591,17 @@ class Plot {
|
|||||||
/// **not** part of `set_crosshair()`, so placing a hover cannot silently change it.
|
/// **not** part of `set_crosshair()`, so placing a hover cannot silently change it.
|
||||||
void set_crosshair_readout(bool on) noexcept { crosshair_.show_readout = on; }
|
void set_crosshair_readout(bool on) noexcept { crosshair_.show_readout = on; }
|
||||||
|
|
||||||
|
/// Overrides this plot's crosshair stroke and dash, in logical pixels, without touching the
|
||||||
|
/// global style every other plot draws with (spec §17): each field left negative in `s`
|
||||||
|
/// defers to the matching `Style::crosshair_*` metric, so `CrosshairStyle{}` (the default)
|
||||||
|
/// is exactly the global style. Set it once, like a colour -- it is not part of
|
||||||
|
/// `set_crosshair()`, so placing a hover cannot silently change how the stroke looks. A
|
||||||
|
/// `dash_off` that resolves to `0` draws a solid line.
|
||||||
|
void set_crosshair_style(const CrosshairStyle& s) noexcept { crosshair_.style = s; }
|
||||||
|
/// The crosshair's style override, as set -- read back rather than the *resolved* stroke and
|
||||||
|
/// dash, which live on `CrosshairLayout` because they depend on `ui_scale`.
|
||||||
|
const CrosshairStyle& crosshair_style() const noexcept { return crosshair_.style; }
|
||||||
|
|
||||||
/// The crosshair's state: where it is, which lines it draws and whether the readout is
|
/// The crosshair's state: where it is, which lines it draws and whether the readout is
|
||||||
/// on. Read-only -- `set_crosshair()` and `clear_crosshair()` are how it moves -- so the
|
/// on. Read-only -- `set_crosshair()` and `clear_crosshair()` are how it moves -- so the
|
||||||
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ class LineProgram {
|
|||||||
void set_viewport(Vec2 px) const noexcept;
|
void set_viewport(Vec2 px) const noexcept;
|
||||||
/// `u_width`: stroke width in pixels.
|
/// `u_width`: stroke width in pixels.
|
||||||
void set_width(f32 px) const noexcept;
|
void set_width(f32 px) const noexcept;
|
||||||
|
/// `u_dash`: the dash pattern in pixels -- `x` the ink length, `y` the gap, and a
|
||||||
|
/// non-positive `y` (the default a caller passes) means **solid**. It is a fragment-shader
|
||||||
|
/// discard on the interpolated `v_along`, so it costs no geometry, and it is the one uniform
|
||||||
|
/// that distinguishes the crosshair's stroke from every other line in the library.
|
||||||
|
void set_dash(Vec2 on_off) const noexcept;
|
||||||
/// `u_color`: stroke colour, alpha included.
|
/// `u_color`: stroke colour, alpha included.
|
||||||
void set_color(Color color) const noexcept;
|
void set_color(Color color) const noexcept;
|
||||||
|
|
||||||
|
|||||||
@@ -365,7 +365,12 @@ class Renderer {
|
|||||||
/// Uploads the staged line batch, maps it with (`scale`, `offset`), rasterises
|
/// Uploads the staged line batch, maps it with (`scale`, `offset`), rasterises
|
||||||
/// it in one colour and empties the batch. A no-op for an empty batch, so the
|
/// it in one colour and empties the batch. A no-op for an empty batch, so the
|
||||||
/// caller can flush unconditionally.
|
/// caller can flush unconditionally.
|
||||||
void flush_lines(const Mapping& map, Color color, f32 width) noexcept;
|
///
|
||||||
|
/// `dash` is the pattern in pixels for the fragment shader's discard: `x` the ink
|
||||||
|
/// length, `y` the gap, and a non-positive `y` -- the default every caller but the
|
||||||
|
/// crosshair passes -- means **solid**. It is set on every flush, so one draw's dash
|
||||||
|
/// can never leak into the next.
|
||||||
|
void flush_lines(const Mapping& map, Color color, f32 width, Vec2 dash = Vec2{}) noexcept;
|
||||||
/// Draws every staged glyph in `style().axis_text` and empties the batch. Text
|
/// Draws every staged glyph in `style().axis_text` and empties the batch. Text
|
||||||
/// is texture-bound, so this runs once per frame, after all line geometry.
|
/// is texture-bound, so this runs once per frame, after all line geometry.
|
||||||
void flush_text() noexcept;
|
void flush_text() noexcept;
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ struct Style {
|
|||||||
/// already distinct, so no dashes.
|
/// already distinct, so no dashes.
|
||||||
Color crosshair = Color::hex(0xE6EBF2);
|
Color crosshair = Color::hex(0xE6EBF2);
|
||||||
|
|
||||||
|
/// The crosshair's stroke and dash, in logical pixels (spec §17). The crosshair is a
|
||||||
|
/// *passive* hover instrument drawn over the data, so its default **recedes**: a
|
||||||
|
/// thinner stroke than a trace's (`Trace::width`'s 1.5) so it reads as chrome rather
|
||||||
|
/// than as data, and a dash so it is distinguishable from the grid and the traces at
|
||||||
|
/// a glance.
|
||||||
|
///
|
||||||
|
/// `crosshair_dash_on` is the ink length and `crosshair_dash_off` the gap; **`off == 0`
|
||||||
|
/// means solid**, which is the value every *other* line in the library is drawn at, so
|
||||||
|
/// the one pipeline serves them unchanged (the dash is a fragment-shader discard on
|
||||||
|
/// `v_along`, so no geometry is added). A `Crosshair` may override all three for one
|
||||||
|
/// plot without touching this global style.
|
||||||
|
f32 crosshair_width = 0.75f;
|
||||||
|
f32 crosshair_dash_on = 4.0f;
|
||||||
|
f32 crosshair_dash_off = 4.0f;
|
||||||
|
|
||||||
// --- the scale ----------------------------------------------------------
|
// --- the scale ----------------------------------------------------------
|
||||||
|
|
||||||
/// Device pixels per logical unit. 1.0 is the 1x display every default here was
|
/// Device pixels per logical unit. 1.0 is the 1x display every default here was
|
||||||
|
|||||||
@@ -449,6 +449,22 @@ void Figure::set_crosshair(f64 x, bool active) {
|
|||||||
for (Plot& row : plots_) row.set_crosshair(x, active);
|
for (Plot& row : plots_) row.set_crosshair(x, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Figure::set_crosshair(f64 x, u32 hovered_row, f64 y_in_row, bool active) {
|
||||||
|
// The shared x first, on every row -- the same state the single-coordinate overload leaves,
|
||||||
|
// so the vertical lines and every row's readout are placed by one rule whether or not the
|
||||||
|
// pointer's row is known. A bad x is refused by the row call, which changes nothing, so the
|
||||||
|
// y below must not run either or the hover would be half-placed.
|
||||||
|
if (active && !std::isfinite(x)) return;
|
||||||
|
for (Plot& row : plots_) row.set_crosshair(x, active);
|
||||||
|
// Then the pointer row's own y: the *only* row that gets a horizontal line, because the y is
|
||||||
|
// a value on that row's scale and no other row's. A row that does not exist (the pointer is in
|
||||||
|
// the gap between rows) leaves every row vertical-only, which is the honest answer -- there is
|
||||||
|
// no row under the pointer to mark -- and a non-finite y leaves the row vertical-only rather
|
||||||
|
// than clearing a hover the pointer still has.
|
||||||
|
if (!active || hovered_row >= plots_.size()) return;
|
||||||
|
plots_[hovered_row].set_crosshair(x, y_in_row, true);
|
||||||
|
}
|
||||||
|
|
||||||
void Figure::clear_crosshair() {
|
void Figure::clear_crosshair() {
|
||||||
for (Plot& row : plots_) row.clear_crosshair();
|
for (Plot& row : plots_) row.clear_crosshair();
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -1265,6 +1265,8 @@ void Plot::build_crosshair() {
|
|||||||
crosshair.active = crosshair_.active;
|
crosshair.active = crosshair_.active;
|
||||||
crosshair.vertical = Rect{};
|
crosshair.vertical = Rect{};
|
||||||
crosshair.horizontal = Rect{};
|
crosshair.horizontal = Rect{};
|
||||||
|
crosshair.stroke = 0.0f;
|
||||||
|
crosshair.dash = Vec2{};
|
||||||
crosshair.panel = Rect{};
|
crosshair.panel = Rect{};
|
||||||
if (!crosshair_.active) {
|
if (!crosshair_.active) {
|
||||||
crosshair.rows.clear();
|
crosshair.rows.clear();
|
||||||
@@ -1286,7 +1288,18 @@ void Plot::build_crosshair() {
|
|||||||
// rule: the box is where the line may be drawn, and Task 1's clip never has to trim one
|
// rule: the box is where the line may be drawn, and Task 1's clip never has to trim one
|
||||||
// that already fits. The lines are placed through the primary axis pair, like the grid and
|
// that already fits. The lines are placed through the primary axis pair, like the grid and
|
||||||
// a region's fill -- one crosshair is one column and one row of the data area.
|
// a region's fill -- one crosshair is one column and one row of the data area.
|
||||||
const f32 stroke = style_.device(style_.line_width_px);
|
//
|
||||||
|
// The stroke and the dash are resolved *here*, once, from the crosshair's own override where
|
||||||
|
// it has one and the global style otherwise, and converted to device pixels through
|
||||||
|
// `device()`. The boxes below and the renderer's strokes are then one set of numbers rather
|
||||||
|
// than two that must be kept in step (spec §17): `stroke` is the device width, `dash` the
|
||||||
|
// device (on, off) pair with `off <= 0` meaning solid.
|
||||||
|
const CrosshairStyle& cs = crosshair_.style;
|
||||||
|
const f32 stroke = style_.device(cs.width >= 0.0f ? cs.width : style_.crosshair_width);
|
||||||
|
crosshair.stroke = stroke;
|
||||||
|
crosshair.dash =
|
||||||
|
Vec2{style_.device(cs.dash_on >= 0.0f ? cs.dash_on : style_.crosshair_dash_on),
|
||||||
|
style_.device(cs.dash_off >= 0.0f ? cs.dash_off : style_.crosshair_dash_off)};
|
||||||
const f32 half = 0.5f * stroke;
|
const f32 half = 0.5f * stroke;
|
||||||
if (crosshair_.vertical) {
|
if (crosshair_.vertical) {
|
||||||
const f32 at =
|
const f32 at =
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ void LineProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u
|
|||||||
|
|
||||||
void LineProgram::set_width(f32 px) const noexcept { program_.set_uniform("u_width", px); }
|
void LineProgram::set_width(f32 px) const noexcept { program_.set_uniform("u_width", px); }
|
||||||
|
|
||||||
|
void LineProgram::set_dash(Vec2 on_off) const noexcept {
|
||||||
|
program_.set_uniform("u_dash", on_off);
|
||||||
|
}
|
||||||
|
|
||||||
void LineProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
|
void LineProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
|
||||||
|
|
||||||
bool TextProgram::create(Status* st) noexcept {
|
bool TextProgram::create(Status* st) noexcept {
|
||||||
|
|||||||
+13
-4
@@ -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));
|
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();
|
const u32 segments = lines_.segment_count();
|
||||||
if (segments != 0 && ready_) {
|
if (segments != 0 && ready_) {
|
||||||
upload_vertices(lines_.data());
|
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_offset(map.offset);
|
||||||
line_program_.set_viewport({viewport_.w, viewport_.h});
|
line_program_.set_viewport({viewport_.w, viewport_.h});
|
||||||
line_program_.set_width(width);
|
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_program_.set_color(color);
|
||||||
line_vao_.bind();
|
line_vao_.bind();
|
||||||
// One triangle strip per instance, four corners from gl_VertexID.
|
// 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;
|
const CrosshairLayout& crosshair = layout.crosshair;
|
||||||
if (crosshair.empty()) return;
|
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 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
|
// 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`,
|
// 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.
|
// 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);
|
const Mapping data = mapping(plot, 0u, 0u);
|
||||||
if (crosshair.vertical.w > 0.0f) {
|
if (crosshair.vertical.w > 0.0f) {
|
||||||
const f64 x = plot.x_transform(0u).to_data_x(
|
const f64 x = plot.x_transform(0u).to_data_x(
|
||||||
static_cast<f64>(crosshair.vertical.x + 0.5f * crosshair.vertical.w));
|
static_cast<f64>(crosshair.vertical.x + 0.5f * crosshair.vertical.w));
|
||||||
const Range y_range = plot.y_range(0u);
|
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);
|
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) {
|
if (crosshair.horizontal.h > 0.0f) {
|
||||||
const f64 y = plot.y_transform(0u).to_data_y(
|
const f64 y = plot.y_transform(0u).to_data_y(
|
||||||
static_cast<f64>(crosshair.horizontal.y + 0.5f * crosshair.horizontal.h));
|
static_cast<f64>(crosshair.horizontal.y + 0.5f * crosshair.horizontal.h));
|
||||||
const Range x_range = plot.x_range(0u);
|
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);
|
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
|
// A small cross at each trace's intersection, in the trace's own colour -- opaque, like a
|
||||||
|
|||||||
+21
-1
@@ -18,7 +18,12 @@ uniform vec2 u_viewport; // render target size, pixels
|
|||||||
uniform float u_width; // stroke width, pixels
|
uniform float u_width; // stroke width, pixels
|
||||||
|
|
||||||
out float v_dist; // signed distance to the segment axis, pixels
|
out float v_dist; // signed distance to the segment axis, pixels
|
||||||
out float v_along; // distance travelled from a_p0, pixels
|
// Distance travelled from a_p0, pixels -- and the dash's phase anchor. Both of the
|
||||||
|
// crosshair's lines are staged from an *axes edge* (its vertical from `y_range.min`,
|
||||||
|
// the axes' bottom; its horizontal from `x_range.min`, the axes' left), so a dash measured
|
||||||
|
// from here is measured from a fixed edge of the rect: it does not crawl as the view pans
|
||||||
|
// or zooms, and a stationary crosshair renders identically frame to frame.
|
||||||
|
out float v_along;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 p0 = a_p0 * u_scale + u_offset;
|
vec2 p0 = a_p0 * u_scale + u_offset;
|
||||||
@@ -53,13 +58,28 @@ void main() {
|
|||||||
|
|
||||||
const char* const kLineFS = R"(#version 330 core
|
const char* const kLineFS = R"(#version 330 core
|
||||||
in float v_dist; // signed distance to the segment axis, pixels
|
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_width; // stroke width, pixels
|
||||||
uniform vec4 u_color;
|
uniform vec4 u_color;
|
||||||
|
uniform vec2 u_dash; // dash pattern in pixels: x = ink, y = gap; y <= 0 is solid
|
||||||
|
|
||||||
out vec4 o_color;
|
out vec4 o_color;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// The dash (spec §17): a fragment-shader discard, never new geometry -- the vertices
|
||||||
|
// stay in data space and the pattern is one per-draw uniform. `v_along` interpolates the
|
||||||
|
// distance from `a_p0` across the quad in pixels, so `mod` measures the pattern along the
|
||||||
|
// line rather than in screen space, which is what keeps it anchored to the segment while
|
||||||
|
// the view pans. `y <= 0` is solid, so every other stroke in the library -- grid, traces,
|
||||||
|
// axis lines, regions, cursors, the legend border -- draws unchanged by this uniform's
|
||||||
|
// default.
|
||||||
|
if (u_dash.y > 0.0) {
|
||||||
|
float period = u_dash.x + u_dash.y;
|
||||||
|
if (period > 0.0 && mod(v_along, period) >= u_dash.x) {
|
||||||
|
discard; // in the gap of the pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
float half_w = u_width * 0.5;
|
float half_w = u_width * 0.5;
|
||||||
float d = abs(v_dist);
|
float d = abs(v_dist);
|
||||||
if (d > half_w + 1.0) {
|
if (d > half_w + 1.0) {
|
||||||
|
|||||||
Reference in new issue
Block a user