Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
path.hpp
Go to the documentation of this file.
1#pragma once
2
3// Path — a retained 2D vector path VALUE type.
4//
5// A path is fundamentally a value: you build an icon once (in a constructor,
6// or from an SVG string at load time), store it as a member, transform copies
7// of it, and fill it every frame. The immediate-mode builder on `Canvas`
8// (`begin_path` / `move_to` / ... / `fill_current_path`) cannot express that —
9// it streams geometry into the backend and forgets it. `Path` is the retained
10// half of the pair.
11//
12// Path icon = Path::from_svg_string("M0 0 L10 0 L5 8 Z"); // once
13// ...
14// void paint(Canvas& c) { // every frame
15// c.set_fill_color(color);
16// c.fill_path(icon, FillRule::nonzero);
17// }
18//
19// ── Copy cost ────────────────────────────────────────────────────────────
20// Copying a Path is O(1): the geometry sits behind a copy-on-write buffer, so
21// a copy is a refcount bump, not a memcpy. Mutating a shared Path detaches it
22// first (O(n), once). This is deliberate — if copying were O(n) every caller
23// would hoist paths out of `paint()` and the value semantics would be a lie.
24// `test_canvas_path.cpp` pins this with a benchmark.
25//
26// ── Curve representation ─────────────────────────────────────────────────
27// Every curve is stored as a line, quadratic, or cubic. Arcs, ellipses, and
28// SVG elliptical-arc segments are flattened to cubics AT BUILD TIME, so the
29// verb set is closed at five and every backend (Skia, CoreGraphics, the
30// recording target) replays byte-identical geometry. There is no conic verb
31// and no backend-specific escape hatch.
32//
33// ── Threading ────────────────────────────────────────────────────────────
34// Standard value-type contract, same as std::vector: concurrent reads of one
35// Path are safe; concurrent mutation of the SAME Path object is not. Copies
36// handed to other threads are independent even though they share a buffer —
37// the copy-on-write detach makes the sharing invisible.
38
40#include <pulp/canvas/rectangle_list.hpp> // pulp::canvas::Rect2D
41
42#include <cstdint>
43#include <memory>
44#include <optional>
45#include <string>
46#include <vector>
47
48namespace pulp::canvas {
49
54enum class FillRule { nonzero, evenodd };
55
57enum class LineCap { butt, round, square };
58
60enum class LineJoin { miter, round, bevel };
61
68 float width = 1.0f;
71 float miter_limit = 4.0f;
72
76 std::vector<float> dash;
77 float dash_phase = 0.0f;
78
79 bool operator==(const StrokeStyle& o) const {
80 return width == o.width && cap == o.cap && join == o.join &&
81 miter_limit == o.miter_limit && dash == o.dash &&
82 dash_phase == o.dash_phase;
83 }
84 bool operator!=(const StrokeStyle& o) const { return !(*this == o); }
85};
86
87class Path {
88public:
90 enum class Verb : uint8_t {
96 };
97
99 static constexpr int points_for(Verb v) {
100 switch (v) {
101 case Verb::move: return 1;
102 case Verb::line: return 1;
103 case Verb::quad: return 2;
104 case Verb::cubic: return 3;
105 case Verb::close: return 0;
106 }
107 return 0;
108 }
109
111 struct Element {
113 const Point2D* points = nullptr;
114 int count = 0;
115 };
116
117 Path() = default;
118
119 // Value semantics. Copy is O(1) (copy-on-write); see the header comment.
120 Path(const Path&) = default;
121 Path& operator=(const Path&) = default;
122 Path(Path&&) noexcept = default;
123 Path& operator=(Path&&) noexcept = default;
124 ~Path() = default;
125
128 bool operator==(const Path& o) const;
129 bool operator!=(const Path& o) const { return !(*this == o); }
130
131 // ── Building ─────────────────────────────────────────────────────────
132 Path& move_to(float x, float y);
133 Path& line_to(float x, float y);
134 Path& quad_to(float cx, float cy, float x, float y);
135 Path& cubic_to(float c1x, float c1y, float c2x, float c2y, float x, float y);
136
143 Path& arc_to(float x1, float y1, float x2, float y2, float radius);
144
146
148 void clear();
149
150 // ── Shape helpers ────────────────────────────────────────────────────
151 Path& add_rect(float x, float y, float w, float h);
152 Path& add_rect(const Rect2D& r) { return add_rect(r.x, r.y, r.width, r.height); }
153
155 Path& add_rounded_rect(float x, float y, float w, float h, float radius);
156
160 Path& add_rounded_rect(float x, float y, float w, float h,
161 float top_left, float top_right,
162 float bottom_right, float bottom_left);
163
164 Path& add_ellipse(float cx, float cy, float rx, float ry);
165 Path& add_circle(float cx, float cy, float radius) {
166 return add_ellipse(cx, cy, radius, radius);
167 }
168
172 Path& add_arc(float cx, float cy, float rx, float ry,
173 float start_angle, float sweep_angle);
174
176 Path& add_pie(float cx, float cy, float rx, float ry,
177 float start_angle, float sweep_angle);
178
180 Path& add_polygon(const Point2D* points, size_t count);
181
184 Path& add_path(const Path& other,
185 const AffineTransform& transform = AffineTransform::identity());
186
187 // ── Transforms ───────────────────────────────────────────────────────
190
193
209 AffineTransform transform_to_fit(float x, float y, float w, float h,
210 bool preserve_proportions) const;
211
213 void scale_to_fit(float x, float y, float w, float h,
214 bool preserve_proportions);
215
216 // ── Queries ──────────────────────────────────────────────────────────
217 bool is_empty() const { return !data_ || data_->verbs.empty(); }
218
220 size_t verb_count() const { return data_ ? data_->verbs.size() : 0; }
221
230 Rect2D bounds() const;
231
239
243 bool contains(float x, float y, FillRule rule = FillRule::nonzero) const {
244 return contains(Point2D{x, y}, rule);
245 }
246
248 std::optional<Point2D> current_point() const;
249
250 // ── Iteration ────────────────────────────────────────────────────────
254 class Iterator {
255 public:
256 Iterator() = default;
257 Iterator(const Path* path, size_t verb_index, size_t point_index)
258 : path_(path), verb_(verb_index), point_(point_index) {}
259
262 bool operator==(const Iterator& o) const { return verb_ == o.verb_ && path_ == o.path_; }
263 bool operator!=(const Iterator& o) const { return !(*this == o); }
264
265 private:
266 const Path* path_ = nullptr;
267 size_t verb_ = 0;
268 size_t point_ = 0;
269 };
270
271 Iterator begin() const { return Iterator(this, 0, 0); }
272 Iterator end() const { return Iterator(this, verb_count(), 0); }
273
277 template <typename Sink>
278 void replay(Sink& sink) const {
279 for (Element el : *this) {
280 switch (el.verb) {
281 case Verb::move:
282 sink.move_to(el.points[0].x, el.points[0].y);
283 break;
284 case Verb::line:
285 sink.line_to(el.points[0].x, el.points[0].y);
286 break;
287 case Verb::quad:
288 sink.quad_to(el.points[0].x, el.points[0].y,
289 el.points[1].x, el.points[1].y);
290 break;
291 case Verb::cubic:
292 sink.cubic_to(el.points[0].x, el.points[0].y,
293 el.points[1].x, el.points[1].y,
294 el.points[2].x, el.points[2].y);
295 break;
296 case Verb::close:
297 sink.close_path();
298 break;
299 }
300 }
301 }
302
306 struct FlatSubpath {
307 std::vector<Point2D> points;
308 bool closed = false;
309 };
310 std::vector<FlatSubpath> flatten(float tolerance = 0.25f) const;
311
312 // ── SVG ──────────────────────────────────────────────────────────────
317 static Path from_svg_string(const std::string& d);
318
321 std::string to_svg_string() const;
322
323private:
324 // Copy-on-write geometry buffer. Verbs and points are stored in parallel
325 // arrays rather than as a fat 32-byte command struct: a `line_to` costs
326 // 1 byte + 8 bytes here, versus 32 bytes in a tagged-union layout.
327 struct Data {
328 std::vector<Verb> verbs;
329 std::vector<Point2D> points;
330 };
331
333 Data& mutable_data();
334
340 void append_arc_cubics(float cx, float cy, float rx, float ry,
341 float start_angle, float sweep_angle,
343
350 void append_svg_arc(Point2D from, float rx, float ry, float x_axis_rotation_deg,
351 bool large_arc, bool sweep, Point2D to);
352
353 std::shared_ptr<Data> data_;
354
355 // The bounds cache lives on the Path, not in the shared Data, so two
356 // copies sharing one buffer can never race computing it. Copies inherit a
357 // valid cache — the geometry is identical, after all.
358 mutable bool bounds_valid_ = false;
359 mutable Rect2D bounds_cache_{};
360
361 friend class Iterator;
362};
363
364} // namespace pulp::canvas
Definition affine_transform.hpp:48
static constexpr AffineTransform identity()
Definition affine_transform.hpp:60
Forward iterator over the verbs.
Definition path.hpp:254
Iterator(const Path *path, size_t verb_index, size_t point_index)
Definition path.hpp:257
bool operator!=(const Iterator &o) const
Definition path.hpp:263
bool operator==(const Iterator &o) const
Definition path.hpp:262
Path & move_to(float x, float y)
Rect2D control_bounds() const
Cheap conservative bounds: the AABB of the control points.
Path(const Path &)=default
void scale_to_fit(float x, float y, float w, float h, bool preserve_proportions)
apply_transform(transform_to_fit(...)).
Iterator end() const
Definition path.hpp:272
Path & add_path(const Path &other, const AffineTransform &transform=AffineTransform::identity())
Append other, optionally transformed.
Path & add_circle(float cx, float cy, float radius)
Definition path.hpp:165
Path & add_rounded_rect(float x, float y, float w, float h, float top_left, float top_right, float bottom_right, float bottom_left)
Per-corner radii, clockwise from the top-left.
std::string to_svg_string() const
Emit a standard SVG d string using absolute commands (M/L/Q/C/Z).
Path & quad_to(float cx, float cy, float x, float y)
Path & line_to(float x, float y)
Path & add_ellipse(float cx, float cy, float rx, float ry)
void replay(Sink &sink) const
Replay this path into any sink that exposes the Canvas path-builder verbs.
Definition path.hpp:278
static Path from_svg_string(const std::string &d)
Parse an SVG path d string.
std::optional< Point2D > current_point() const
The last point of the path, or nullopt if there is no current point.
Path & arc_to(float x1, float y1, float x2, float y2, float radius)
Tangent arc, matching Canvas2D ctx.arcTo(x1, y1, x2, y2, radius): the arc of radius tangent to both t...
void apply_transform(const AffineTransform &t)
Transform every point in place.
std::vector< FlatSubpath > flatten(float tolerance=0.25f) const
AffineTransform transform_to_fit(float x, float y, float w, float h, bool preserve_proportions) const
The transform that would scale this path to fit (x, y, w, h).
Path & add_polygon(const Point2D *points, size_t count)
Closed polygon through count points. Fewer than 2 points draws nothing.
Path & add_arc(float cx, float cy, float rx, float ry, float start_angle, float sweep_angle)
Arc as its own subpath (an implicit move_to to the arc's start).
Path & add_rounded_rect(float x, float y, float w, float h, float radius)
Uniform corner radius, clamped to half the shorter side.
Path & cubic_to(float c1x, float c1y, float c2x, float c2y, float x, float y)
Path & operator=(const Path &)=default
bool contains(Point2D p, FillRule rule=FillRule::nonzero) const
Is p inside the path under rule?
Path transformed(const AffineTransform &t) const
A transformed copy; leaves this path alone.
friend class Iterator
Definition path.hpp:361
Iterator begin() const
Definition path.hpp:271
Path & add_rect(const Rect2D &r)
Definition path.hpp:152
size_t verb_count() const
Number of verbs.
Definition path.hpp:220
static constexpr int points_for(Verb v)
How many points a verb consumes.
Definition path.hpp:99
bool contains(float x, float y, FillRule rule=FillRule::nonzero) const
Definition path.hpp:243
Path & add_rect(float x, float y, float w, float h)
Verb
The closed verb set. Arcs and ellipses flatten to cubic at build time.
Definition path.hpp:90
@ move
1 point: the new subpath's start
Definition path.hpp:91
@ line
1 point: the endpoint
Definition path.hpp:92
@ close
0 points: close the current subpath
Definition path.hpp:95
@ cubic
3 points: control 1, control 2, endpoint
Definition path.hpp:94
@ quad
2 points: control, endpoint
Definition path.hpp:93
Rect2D bounds() const
TIGHT bounds: the true visual bounding box, computed from each curve's extrema (the roots of its deri...
void clear()
Drop every verb. Keeps any allocated capacity for reuse.
Path & add_pie(float cx, float cy, float rx, float ry, float start_angle, float sweep_angle)
A pie slice / wedge: center → arc → back to center, closed.
bool is_empty() const
Definition path.hpp:217
Path(Path &&) noexcept=default
Definition affine_transform.hpp:24
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
FillRule
Winding rule used to decide what counts as "inside" a path.
Definition path.hpp:54
@ nonzero
Definition path.hpp:54
@ evenodd
Definition path.hpp:54
LineJoin
Join applied where two segments meet when stroking.
Definition path.hpp:60
@ miter
Definition path.hpp:60
@ bevel
Definition path.hpp:60
@ path
Definition scene_node.hpp:93
LineCap
Line cap applied to the ends of open subpaths when stroking.
Definition path.hpp:57
@ square
Definition path.hpp:57
@ butt
Definition path.hpp:57
@ round
Definition path.hpp:57
One step of an iteration over the path.
Definition path.hpp:111
int count
Definition path.hpp:114
const Point2D * points
count points, or null for close
Definition path.hpp:113
Verb verb
Definition path.hpp:112
Flatten every curve to line segments, within tolerance user units of the true curve.
Definition path.hpp:306
bool closed
Definition path.hpp:308
std::vector< Point2D > points
Definition path.hpp:307
A 2D point.
Definition affine_transform.hpp:37
float x
Definition affine_transform.hpp:38
float y
Definition affine_transform.hpp:39
Axis-aligned rectangle in canvas coordinates.
Definition rectangle_list.hpp:28
Everything needed to stroke a path, as one value.
Definition path.hpp:67
LineCap cap
Definition path.hpp:69
float miter_limit
Definition path.hpp:71
float dash_phase
Definition path.hpp:77
float width
Definition path.hpp:68
bool operator!=(const StrokeStyle &o) const
Definition path.hpp:84
bool operator==(const StrokeStyle &o) const
Definition path.hpp:79
std::vector< float > dash
Dash pattern: alternating on/off lengths in user units.
Definition path.hpp:76
LineJoin join
Definition path.hpp:70