Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
path_measure.hpp
Go to the documentation of this file.
1#pragma once
2
3// PathMeasure — arc length over a `Path`, measured once and queried forever.
4//
5// A `Path` knows its verbs; it does not know how long it is. Everything that
6// walks a path by DISTANCE rather than by verb — a knob's value arc, a ring
7// meter filling to 0.7, an envelope or waveform revealing left to right, a
8// label riding a curve — needs arc length, and each of those callers would
9// otherwise re-derive it, differently and wrongly.
10//
11// PathMeasure m{ring}; // once, when the ring changes
12// Path filled = m.segment(0.0f, m.length() * value); // every frame
13//
14// ── Why this is a value, not a method on Path ────────────────────────────
15// Measuring is the expensive half: every quad and cubic is flattened until it
16// is flat to `tolerance`, and the cumulative distance table is built. Querying
17// is a binary search over that table. Collapsing the two into
18// `Path::point_at()` would hide a full re-flatten inside something that reads
19// like an accessor, and a per-frame caller would pay it every frame. So the
20// expensive intermediate is the public value — the same shape as
21// `TextShaper::prepare()` returning `PreparedText`, and for the same reason.
22//
23// A PathMeasure holds a COPY of the path's geometry, so it stays valid if the
24// source `Path` is mutated or destroyed. Copying a Path is O(1), so this is
25// cheap.
26//
27// ── Accuracy ─────────────────────────────────────────────────────────────
28// `tolerance` is the maximum distance (in path units) between a flattened
29// chord and the true curve. Lengths are therefore slight UNDER-estimates of
30// THE PATH'S OWN arc length, converging as tolerance falls. The default of
31// 0.25 keeps that error far below a pixel at ordinary widget scale.
32//
33// "The path's own" is the part worth reading twice. A PathMeasure measures the
34// geometry the Path actually holds — never the ideal shape that geometry was
35// built to approximate. `Path::add_circle` flattens to four cubics at build
36// time (see path.hpp: the verb set is closed, with no conic), and those cubics
37// are very slightly longer than the circle they stand for. Measuring a
38// radius-100 circle therefore converges to 628.405, not 2*pi*100 = 628.319 —
39// about +0.014%, and it converges from BELOW, crossing the true circumference
40// once the tolerance is fine enough. Code that needs the analytic
41// circumference of a circle should compute it, not measure it.
42//
43// `segment()` is exact in a different sense: it splits the bracketing curve
44// with de Casteljau, so a trimmed cubic comes back as a cubic, not as a
45// polyline. Only the DISTANCE at which the split happens carries the
46// flattening error; the curve itself is not degraded.
47//
48// ── Threading ────────────────────────────────────────────────────────────
49// Standard value-type contract: concurrent reads of one PathMeasure are safe;
50// concurrent mutation is not. All query methods are const.
51
52#include <cstddef>
53#include <cstdint>
54#include <optional>
55#include <vector>
56
57#include <pulp/canvas/path.hpp>
58
59namespace pulp::canvas {
60
62struct PathPose {
67 Point2D tangent{1.0f, 0.0f};
68};
69
71 public:
73 static constexpr float default_tolerance = 0.25f;
74
75 PathMeasure() = default;
76
80 explicit PathMeasure(const Path& path, float tolerance = default_tolerance);
81
83 float length() const {
84 return total_length_;
85 }
86
87 bool is_empty() const {
88 return contours_.empty();
89 }
90
94 size_t contour_count() const {
95 return contours_.size();
96 }
97
98 float contour_length(size_t index) const;
99
101 bool contour_is_closed(size_t index) const;
102
105 float contour_start(size_t index) const;
106
114 std::optional<PathPose> pose_at(float distance) const;
115
117 std::optional<Point2D> position_at(float distance) const;
118
120 std::optional<Point2D> tangent_at(float distance) const;
121
131 Path segment(float start, float end) const;
132
133 private:
137 struct Sample {
138 float distance = 0.0f;
139 Point2D position{};
140 uint32_t segment_index = 0;
141 float t = 0.0f;
142 };
143
146 struct Segment {
147 Point2D points[4]{};
148 uint8_t count = 2;
149 uint32_t contour_index = 0;
150 };
151
152 struct Contour {
153 size_t first_sample = 0;
154 size_t sample_count = 0;
155 size_t first_segment = 0;
156 size_t segment_count = 0;
157 float start_distance = 0.0f;
158 float length = 0.0f;
159 bool closed = false;
160 };
161
162 std::vector<Sample> samples_;
163 std::vector<Segment> segments_;
164 std::vector<Contour> contours_;
165 float total_length_ = 0.0f;
166 float tolerance_ = default_tolerance;
167
169 size_t sample_index_for(float distance) const;
170};
171
172} // namespace pulp::canvas
size_t contour_count() const
Contours are the move_to-delimited subpaths, in path order.
Definition path_measure.hpp:94
Path segment(float start, float end) const
The part of the path between two distances, as a new Path.
std::optional< PathPose > pose_at(float distance) const
Position and unit tangent at distance along the path.
std::optional< Point2D > position_at(float distance) const
pose_at(distance)->position, for callers that do not need the tangent.
PathMeasure(const Path &path, float tolerance=default_tolerance)
Measure path.
bool is_empty() const
Definition path_measure.hpp:87
std::optional< Point2D > tangent_at(float distance) const
pose_at(distance)->tangent, for callers that do not need the position.
static constexpr float default_tolerance
The default flattening tolerance, in path units.
Definition path_measure.hpp:73
float length() const
Total length of every contour summed. 0 for an empty path.
Definition path_measure.hpp:83
bool contour_is_closed(size_t index) const
Whether contour index was closed with close().
float contour_length(size_t index) const
float contour_start(size_t index) const
Distance at which contour index begins, measured from the start of the path.
Definition path.hpp:87
Definition affine_transform.hpp:24
@ path
Definition scene_node.hpp:93
A position on a measured path, with the direction of travel there.
Definition path_measure.hpp:62
Point2D position
Definition path_measure.hpp:63
Point2D tangent
Unit tangent.
Definition path_measure.hpp:67
A 2D point.
Definition affine_transform.hpp:37