// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include #include #include namespace plot { namespace { /// True when `v` lies in `r`, allowing the rounding slack a generated tick can /// carry at the range's edge: a tick that is within 1e-9 relative of the edge is /// the edge, and dropping it would leave the frame un-drawn. bool inside(f64 v, Range r) noexcept { const f64 tolerance = std::max(std::fabs(r.min), std::fabs(r.max)) * 1e-9 + 1e-12; return v >= r.min - tolerance && v <= r.max + tolerance; } } // namespace void Grid::build(const std::vector& x_ticks, Range x_range, const std::vector& y_ticks, Range y_range) { lines_.clear(); if (!std::isfinite(x_range.min) || !std::isfinite(x_range.max) || !std::isfinite(y_range.min) || !std::isfinite(y_range.max)) { return; } // Segments stay in data space: the renderer applies the transform, so a pan or // zoom needs no grid rebuild other than for a changed tick set. // // Both classes are built: the tick set is the only source of positions, and // `Style::grid_*_visible` decides what the renderer stages from here, so a class // is never filtered out of the tick set (that would be a second, geometry-side // switch for the same behaviour, and the grid would then disagree with the tick // list a caller can read). if (vertical) { for (const Tick& tick : x_ticks) { if (!std::isfinite(tick.value) || !inside(tick.value, x_range)) continue; lines_.push_back(GridLine{tick.value, y_range.min, tick.value, y_range.max, tick.major}); } } if (horizontal) { for (const Tick& tick : y_ticks) { if (!std::isfinite(tick.value) || !inside(tick.value, y_range)) continue; lines_.push_back(GridLine{x_range.min, tick.value, x_range.max, tick.value, tick.major}); } } } } // namespace plot