rasterrocket_render/path/mod.rs
1//! PDF path geometry: points, flags, subpath state machine, and builder API.
2//!
3//! The `pts` and `flags` arrays are parallel `SoA` (one entry per index). This
4//! layout originates in `SplashPath` from `splash/SplashPath.{h,cc}`, but is
5//! preserved deliberately rather than ported: `xpath::XPath::new` transforms
6//! the entire `pts` array under a CTM in one pass before anything reads
7//! `flags`, and the contiguous f64-pair layout keeps that pre-pass tight.
8//! Walkers that read both arrays together (`xpath.rs`, `stroke/path.rs`)
9//! pay one extra L1-resident byte per index, which is negligible.
10//!
11//! ## Subpath state machine
12//!
13//! A [`Path`] is always in one of three states (matching the C++ comments in
14//! `SplashPath.cc`):
15//!
16//! | State | Condition | Meaning |
17//! |------------------|----------------------------------|----------------------------------|
18//! | No current point | `cur_subpath == pts.len()` | Fresh path or just after close |
19//! | One-point subpath| `cur_subpath == pts.len() - 1` | After moveTo, before lineTo |
20//! | Open subpath | `cur_subpath < pts.len() - 1` | Active path with ≥ 2 points |
21//!
22//! The `one_point_subpath` and `open_subpath` predicates both guard against
23//! `pts.is_empty()` before comparing with `pts.len() - 1`, so neither can
24//! underflow on an empty vector.
25
26pub mod adjust;
27pub mod flatten;
28
29use bitflags::bitflags;
30
31// ── Types ─────────────────────────────────────────────────────────────────────
32
33/// A 2-D point in path space (f64 coordinates, matching `SplashPathPoint`).
34#[derive(Copy, Clone, Debug, PartialEq)]
35pub struct PathPoint {
36 /// Horizontal coordinate.
37 pub x: f64,
38 /// Vertical coordinate.
39 pub y: f64,
40}
41
42impl PathPoint {
43 /// Construct a new point.
44 #[must_use]
45 pub const fn new(x: f64, y: f64) -> Self {
46 Self { x, y }
47 }
48}
49
50impl From<(f64, f64)> for PathPoint {
51 /// Convert a `(x, y)` tuple into a [`PathPoint`].
52 fn from((x, y): (f64, f64)) -> Self {
53 Self { x, y }
54 }
55}
56
57impl From<PathPoint> for (f64, f64) {
58 /// Destructure a [`PathPoint`] into a `(x, y)` tuple.
59 fn from(p: PathPoint) -> Self {
60 (p.x, p.y)
61 }
62}
63
64bitflags! {
65 /// Per-point flags stored in the parallel `flags` array of a [`Path`].
66 ///
67 /// Matches the `splashPathFirst/Last/Closed/Curve` constants in
68 /// `SplashPath.h`.
69 #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
70 pub struct PathFlags: u8 {
71 /// First point of a subpath (set on the `moveTo` point).
72 const FIRST = 0x01;
73 /// Last point of a subpath (set on every newly appended endpoint).
74 const LAST = 0x02;
75 /// Subpath is closed (set on both the first **and** last point).
76 const CLOSED = 0x04;
77 /// This point is a cubic Bezier control point, not an on-curve endpoint.
78 const CURVE = 0x08;
79 }
80}
81
82impl PathFlags {
83 /// Returns `true` if this point is the first of its subpath.
84 #[must_use]
85 pub const fn is_first(self) -> bool {
86 self.contains(Self::FIRST)
87 }
88
89 /// Returns `true` if this point is the last of its subpath.
90 #[must_use]
91 pub const fn is_last(self) -> bool {
92 self.contains(Self::LAST)
93 }
94
95 /// Returns `true` if the subpath containing this point is closed.
96 #[must_use]
97 pub const fn is_closed(self) -> bool {
98 self.contains(Self::CLOSED)
99 }
100
101 /// Returns `true` if this point is a Bezier control point (off-curve).
102 #[must_use]
103 pub const fn is_curve(self) -> bool {
104 self.contains(Self::CURVE)
105 }
106}
107
108/// Stroke-adjust hint: a pair of path segments that should be snapped to
109/// integer pixel boundaries to avoid seams between adjacent filled rectangles.
110///
111/// Matches `SplashPathHint` in `SplashPath.h`.
112#[derive(Copy, Clone, Debug)]
113pub struct StrokeAdjustHint {
114 /// Index (into [`Path::pts`]) of the first control segment.
115 pub ctrl0: usize,
116 /// Index (into [`Path::pts`]) of the second control segment.
117 pub ctrl1: usize,
118 /// First point of the range to adjust (inclusive, index into [`Path::pts`]).
119 pub first_pt: usize,
120 /// Last point of the range to adjust (inclusive, index into [`Path::pts`]).
121 pub last_pt: usize,
122}
123
124/// Errors returned by [`PathBuilder`] construction methods.
125#[derive(Copy, Clone, Debug, PartialEq, Eq)]
126pub enum PathError {
127 /// `lineTo`, `curveTo`, or `close` was called when there is no current
128 /// point (the path is fresh or was just closed).
129 ///
130 /// Callers should ensure a preceding `move_to` succeeded before calling
131 /// drawing operators.
132 NoCurPt,
133 /// `moveTo` was called while a one-point subpath is active (a `moveTo`
134 /// was immediately followed by another `moveTo` with no drawing operator
135 /// in between).
136 ///
137 /// Callers should either draw at least one segment or close the current
138 /// subpath before beginning a new one.
139 BogusPath,
140}
141
142impl std::fmt::Display for PathError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 Self::NoCurPt => f.write_str(
146 "path error: no current point \
147 (call move_to before line_to, curve_to, or close)",
148 ),
149 Self::BogusPath => f.write_str(
150 "path error: consecutive moveTo without a drawing operator \
151 (a one-point subpath is already active)",
152 ),
153 }
154 }
155}
156
157impl std::error::Error for PathError {}
158
159// ── Path ──────────────────────────────────────────────────────────────────────
160
161/// A PDF graphics path: an ordered sequence of subpaths built from lines and
162/// cubic Bezier curves.
163///
164/// # Invariants
165///
166/// These match `SplashPath.cc`:
167///
168/// - `pts.len() == flags.len()` at all times.
169/// - `cur_subpath` is the index of the first point of the currently open
170/// subpath, or equals `pts.len()` when there is no current point.
171/// - Control points (flag [`PathFlags::CURVE`]) always appear in groups of
172/// three between two on-curve points.
173#[derive(Clone, Debug, Default)]
174pub struct Path {
175 /// Ordered sequence of path points.
176 pub pts: Vec<PathPoint>,
177 /// Per-point flags, parallel to [`Self::pts`].
178 pub flags: Vec<PathFlags>,
179 /// Optional stroke-adjust hints.
180 pub hints: Vec<StrokeAdjustHint>,
181 /// Index of the first point of the currently open subpath.
182 ///
183 /// Equals `pts.len()` when there is no current point (fresh path or
184 /// immediately after a `close`).
185 pub cur_subpath: usize,
186}
187
188impl Path {
189 /// Create an empty path with no current point.
190 #[must_use]
191 pub fn new() -> Self {
192 Self::default()
193 }
194
195 // ── State predicates ──────────────────────────────────────────────────────
196
197 /// Returns `true` when there is no current point.
198 ///
199 /// This is the case for a freshly created path and immediately after
200 /// [`PathBuilder::close`] completes.
201 #[inline]
202 #[must_use]
203 pub const fn no_current_point(&self) -> bool {
204 self.cur_subpath == self.pts.len()
205 }
206
207 /// Returns `true` after exactly one [`PathBuilder::move_to`] with no
208 /// subsequent [`PathBuilder::line_to`] or [`PathBuilder::curve_to`].
209 ///
210 /// # Underflow safety
211 ///
212 /// The `!self.pts.is_empty()` guard ensures `pts.len() - 1` is evaluated
213 /// only when `pts` has at least one element, so no wrapping subtraction
214 /// can occur.
215 #[inline]
216 #[must_use]
217 pub const fn one_point_subpath(&self) -> bool {
218 !self.pts.is_empty() && self.cur_subpath == self.pts.len() - 1
219 }
220
221 /// Returns `true` when the current subpath has at least two points.
222 #[inline]
223 #[must_use]
224 pub const fn open_subpath(&self) -> bool {
225 !self.pts.is_empty() && self.cur_subpath < self.pts.len() - 1
226 }
227
228 /// Returns the current point (last appended endpoint), if any.
229 ///
230 /// Returns `None` when [`Self::no_current_point`] is true — i.e. on a
231 /// fresh path **and** immediately after a `close` (because `close` sets
232 /// `cur_subpath` to `pts.len()`).
233 #[must_use]
234 pub fn current_point(&self) -> Option<PathPoint> {
235 if self.no_current_point() {
236 None
237 } else {
238 self.pts.last().copied()
239 }
240 }
241
242 // ── Geometry ──────────────────────────────────────────────────────────────
243
244 /// Translate every point in the path by `(dx, dy)`.
245 pub fn offset(&mut self, dx: f64, dy: f64) {
246 for p in &mut self.pts {
247 p.x += dx;
248 p.y += dy;
249 }
250 }
251
252 /// Append all points and hints from `other` into `self`.
253 ///
254 /// `cur_subpath` is set to `self.pts.len() + other.cur_subpath` **before**
255 /// appending, matching `SplashPath::append`.
256 ///
257 /// # Edge cases
258 ///
259 /// If `other` is empty (`other.pts.is_empty()`), then
260 /// `other.cur_subpath == 0` (the default) and
261 /// `self.cur_subpath` is set to `self.pts.len()` — the "no current point"
262 /// sentinel — which is correct: appending an empty path does not create a
263 /// current point.
264 pub fn append(&mut self, other: &Self) {
265 debug_assert!(
266 other.cur_subpath <= other.pts.len(),
267 "append: other.cur_subpath ({}) exceeds other.pts.len() ({}); invariant broken",
268 other.cur_subpath,
269 other.pts.len()
270 );
271 let base = self.pts.len();
272 self.cur_subpath = base + other.cur_subpath;
273 self.pts.extend_from_slice(&other.pts);
274 self.flags.extend_from_slice(&other.flags);
275 for h in &other.hints {
276 self.hints.push(StrokeAdjustHint {
277 ctrl0: h.ctrl0 + base,
278 ctrl1: h.ctrl1 + base,
279 first_pt: h.first_pt + base,
280 last_pt: h.last_pt + base,
281 });
282 }
283 }
284}
285
286// ── PathBuilder ───────────────────────────────────────────────────────────────
287
288/// Ergonomic builder for [`Path`] implementing the PDF path construction
289/// operators (`m`, `l`, `c`, `h`) with the same state-machine semantics as
290/// `SplashPath::moveTo` / `lineTo` / `curveTo` / `close`.
291pub struct PathBuilder {
292 path: Path,
293}
294
295impl PathBuilder {
296 /// Create a new, empty builder with no current point.
297 #[must_use]
298 pub fn new() -> Self {
299 Self { path: Path::new() }
300 }
301
302 /// Begin a new subpath at `(x, y)`. Equivalent to the PDF `m` operator.
303 ///
304 /// # Errors
305 ///
306 /// Returns [`PathError::BogusPath`] if a one-point subpath is already
307 /// active (i.e. the previous operation was also a `move_to` with no
308 /// drawing operator in between). Callers must not silently ignore this
309 /// error: it indicates a malformed path construction sequence.
310 pub fn move_to(&mut self, x: f64, y: f64) -> Result<(), PathError> {
311 if self.path.one_point_subpath() {
312 return Err(PathError::BogusPath);
313 }
314 let len = self.path.pts.len();
315 self.path.pts.push(PathPoint::new(x, y));
316 self.path.flags.push(PathFlags::FIRST | PathFlags::LAST);
317 self.path.cur_subpath = len;
318 Ok(())
319 }
320
321 /// Add a line segment from the current point to `(x, y)`.
322 ///
323 /// Equivalent to the PDF `l` operator.
324 ///
325 /// # Errors
326 ///
327 /// Returns [`PathError::NoCurPt`] if there is no current point. Callers
328 /// must ensure a successful [`Self::move_to`] precedes this call.
329 ///
330 /// # Panics
331 ///
332 /// Panics if `flags` is empty despite a current point existing, which
333 /// would indicate a broken `Path` invariant (`pts.len() == flags.len()`).
334 pub fn line_to(&mut self, x: f64, y: f64) -> Result<(), PathError> {
335 if self.path.no_current_point() {
336 return Err(PathError::NoCurPt);
337 }
338 // Clear LAST on the previous endpoint before appending the new one.
339 let last = self.path.flags.last_mut().unwrap();
340 last.remove(PathFlags::LAST);
341 self.path.pts.push(PathPoint::new(x, y));
342 self.path.flags.push(PathFlags::LAST);
343 Ok(())
344 }
345
346 /// Add a cubic Bezier curve. Equivalent to the PDF `c` operator.
347 ///
348 /// `(x1, y1)` and `(x2, y2)` are the two off-curve control points;
349 /// `(x3, y3)` is the on-curve endpoint. Three points are always appended:
350 /// the control points receive [`PathFlags::CURVE`] and the endpoint
351 /// receives [`PathFlags::LAST`].
352 ///
353 /// # Errors
354 ///
355 /// Returns [`PathError::NoCurPt`] if there is no current point. Callers
356 /// must ensure a successful [`Self::move_to`] precedes this call.
357 ///
358 /// # Panics
359 ///
360 /// Panics if `flags` is empty despite a current point existing, which
361 /// would indicate a broken `Path` invariant (`pts.len() == flags.len()`).
362 pub fn curve_to(
363 &mut self,
364 x1: f64,
365 y1: f64,
366 x2: f64,
367 y2: f64,
368 x3: f64,
369 y3: f64,
370 ) -> Result<(), PathError> {
371 if self.path.no_current_point() {
372 return Err(PathError::NoCurPt);
373 }
374 // Clear LAST on the previous endpoint.
375 let last = self.path.flags.last_mut().unwrap();
376 last.remove(PathFlags::LAST);
377 // Two off-curve control points tagged CURVE, then the on-curve endpoint.
378 self.path.pts.push(PathPoint::new(x1, y1));
379 self.path.flags.push(PathFlags::CURVE);
380 self.path.pts.push(PathPoint::new(x2, y2));
381 self.path.flags.push(PathFlags::CURVE);
382 self.path.pts.push(PathPoint::new(x3, y3));
383 self.path.flags.push(PathFlags::LAST);
384 Ok(())
385 }
386
387 /// Close the current subpath. Equivalent to the PDF `h` operator.
388 ///
389 /// Behaviour:
390 ///
391 /// - If `force` is `true`, a closing `lineTo(first)` is **always**
392 /// appended.
393 /// - If `sp == last_idx` the subpath consists of exactly one point (the
394 /// `moveTo` with no drawing operators). In this degenerate case the
395 /// subpath is trivially "closed" (first == last by identity), so no
396 /// extra `lineTo` is needed — the single point has [`PathFlags::CLOSED`]
397 /// set on itself. This matches the C++ `SplashPath::close` behaviour.
398 /// - Otherwise, a closing `lineTo(first)` is appended only when
399 /// `first != last` (the path is not already closed geometrically).
400 ///
401 /// After closing, `cur_subpath` is advanced to `pts.len()` (the
402 /// "no current point" sentinel), so [`Path::current_point`] returns `None`
403 /// until the next `move_to`.
404 ///
405 /// # Errors
406 ///
407 /// Returns [`PathError::NoCurPt`] if there is no current point. The `?`
408 /// inside this method propagates any error from the internal `line_to`
409 /// call; since `line_to` only errors on `NoCurPt` and we have already
410 /// verified a current point exists at entry, that propagation path is only
411 /// reachable if an invariant is broken.
412 pub fn close(&mut self, force: bool) -> Result<(), PathError> {
413 if self.path.no_current_point() {
414 return Err(PathError::NoCurPt);
415 }
416 let sp = self.path.cur_subpath;
417 let last_idx = self.path.pts.len() - 1;
418 let first = self.path.pts[sp];
419 let last = self.path.pts[last_idx];
420
421 // Add a closing lineTo(first) when:
422 // • `force` is set, OR
423 // • the subpath has exactly one point (sp == last_idx) — no lineTo
424 // is needed but we still fall through to stamp CLOSED, OR
425 // • first != last (not yet geometrically closed).
426 //
427 // The `sp == last_idx` branch skips the `line_to` call because the
428 // condition is placed *before* the `first != last` check. The `?`
429 // propagation is correct: we hold a current point so `line_to` can
430 // only fail if the invariant `pts.len() == flags.len()` is broken.
431 if force || (sp != last_idx && first != last) {
432 self.line_to(first.x, first.y)?;
433 }
434 debug_assert_eq!(
435 self.path.pts.len(),
436 self.path.flags.len(),
437 "close: pts/flags length invariant violated"
438 );
439
440 // Stamp CLOSED on the first and last point of the subpath.
441 let new_last = self.path.pts.len() - 1;
442 self.path.flags[sp].insert(PathFlags::CLOSED);
443 self.path.flags[new_last].insert(PathFlags::CLOSED);
444
445 // Advance past this subpath → "no current point" state.
446 self.path.cur_subpath = self.path.pts.len();
447 Ok(())
448 }
449
450 /// Add a stroke-adjust hint referencing existing point indices.
451 ///
452 /// Indices refer to positions in [`Path::pts`] at build time.
453 pub fn add_stroke_adjust_hint(
454 &mut self,
455 ctrl0: usize,
456 ctrl1: usize,
457 first_pt: usize,
458 last_pt: usize,
459 ) {
460 self.path.hints.push(StrokeAdjustHint {
461 ctrl0,
462 ctrl1,
463 first_pt,
464 last_pt,
465 });
466 }
467
468 /// Returns the current point (last appended endpoint), if any.
469 ///
470 /// Returns `None` when there is no current point — i.e. on a freshly
471 /// created builder or immediately after a successful [`Self::close`].
472 /// Delegates to [`Path::current_point`].
473 #[must_use]
474 pub fn cur_pt(&self) -> Option<PathPoint> {
475 self.path.current_point()
476 }
477
478 /// Returns the number of points accumulated in the builder so far.
479 ///
480 /// This is a read-only view used by callers that need to record point
481 /// indices for stroke-adjustment hints (e.g. `raster::stroke::make_stroke_path`).
482 #[must_use]
483 pub const fn pts_len(&self) -> usize {
484 self.path.pts.len()
485 }
486
487 /// Translate every point accumulated so far by `(dx, dy)`.
488 pub fn offset(&mut self, dx: f64, dy: f64) {
489 self.path.offset(dx, dy);
490 }
491
492 /// Consume the builder and return the completed [`Path`].
493 #[must_use]
494 pub fn build(self) -> Path {
495 self.path
496 }
497}
498
499impl Default for PathBuilder {
500 fn default() -> Self {
501 Self::new()
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 // ── State-machine basics ───────────────────────────────────────────────────
510
511 #[test]
512 fn initial_state() {
513 let p = Path::new();
514 assert!(p.no_current_point());
515 assert!(!p.one_point_subpath());
516 assert!(!p.open_subpath());
517 }
518
519 #[test]
520 fn move_to_gives_one_point() {
521 let mut b = PathBuilder::new();
522 b.move_to(1.0, 2.0).unwrap();
523 assert!(b.path.one_point_subpath());
524 assert!(!b.path.open_subpath());
525 }
526
527 #[test]
528 fn line_to_opens_subpath() {
529 let mut b = PathBuilder::new();
530 b.move_to(0.0, 0.0).unwrap();
531 b.line_to(10.0, 0.0).unwrap();
532 assert!(b.path.open_subpath());
533 assert_eq!(b.path.pts.len(), 2);
534 }
535
536 // ── curve_to ──────────────────────────────────────────────────────────────
537
538 #[test]
539 fn curve_to_adds_three_points() {
540 let mut b = PathBuilder::new();
541 b.move_to(0.0, 0.0).unwrap();
542 b.curve_to(1.0, 2.0, 3.0, 4.0, 5.0, 0.0).unwrap();
543 // moveTo + 2 control points + 1 endpoint = 4
544 assert_eq!(b.path.pts.len(), 4);
545 assert!(b.path.flags[1].is_curve());
546 assert!(b.path.flags[2].is_curve());
547 assert!(b.path.flags[3].is_last());
548 assert!(!b.path.flags[3].is_curve());
549 }
550
551 // ── close ─────────────────────────────────────────────────────────────────
552
553 #[test]
554 fn close_sets_closed_flag() {
555 let mut b = PathBuilder::new();
556 b.move_to(0.0, 0.0).unwrap();
557 b.line_to(10.0, 0.0).unwrap();
558 b.line_to(5.0, 5.0).unwrap();
559 b.close(false).unwrap();
560 assert!(b.path.flags[0].is_closed());
561 assert!(b.path.flags.last().unwrap().is_closed());
562 assert!(b.path.no_current_point());
563 }
564
565 /// After a close, `current_point()` must return `None` because `close`
566 /// advances `cur_subpath` to `pts.len()`.
567 #[test]
568 fn after_close_current_point_is_none() {
569 let mut b = PathBuilder::new();
570 b.move_to(0.0, 0.0).unwrap();
571 b.line_to(1.0, 0.0).unwrap();
572 b.close(false).unwrap();
573 assert_eq!(b.path.current_point(), None);
574 }
575
576 /// A one-point subpath (`moveTo` with no drawing operators) should have
577 /// [`PathFlags::CLOSED`] set on that single point when `close` is called.
578 /// The single point acts as both first and last, so both writes target
579 /// `pts[sp]` — which is correct per `SplashPath::close`.
580 #[test]
581 fn close_one_point_subpath_sets_closed_flag() {
582 let mut b = PathBuilder::new();
583 b.move_to(3.0, 4.0).unwrap();
584 assert!(b.path.one_point_subpath());
585 b.close(false).unwrap();
586 // The single point must carry CLOSED.
587 assert!(b.path.flags[0].is_closed());
588 // After close there is no current point.
589 assert!(b.path.no_current_point());
590 // No extra point should have been appended.
591 assert_eq!(b.path.pts.len(), 1);
592 }
593
594 // ── Error paths ───────────────────────────────────────────────────────────
595
596 #[test]
597 fn no_cur_pt_errors() {
598 let mut b = PathBuilder::new();
599 assert_eq!(b.line_to(1.0, 1.0), Err(PathError::NoCurPt));
600 assert_eq!(
601 b.curve_to(1.0, 1.0, 2.0, 2.0, 3.0, 3.0),
602 Err(PathError::NoCurPt)
603 );
604 assert_eq!(b.close(false), Err(PathError::NoCurPt));
605 }
606
607 #[test]
608 fn bogus_path_on_double_moveto() {
609 let mut b = PathBuilder::new();
610 b.move_to(0.0, 0.0).unwrap();
611 assert_eq!(b.move_to(1.0, 1.0), Err(PathError::BogusPath));
612 }
613
614 // ── PathError::Display ────────────────────────────────────────────────────
615
616 #[test]
617 fn path_error_display() {
618 let no_pt = PathError::NoCurPt.to_string();
619 assert!(
620 no_pt.contains("no current point"),
621 "NoCurPt display should mention 'no current point', got: {no_pt}"
622 );
623
624 let bogus = PathError::BogusPath.to_string();
625 assert!(
626 bogus.contains("consecutive moveTo"),
627 "BogusPath display should mention 'consecutive moveTo', got: {bogus}"
628 );
629 }
630
631 // ── From<(f64, f64)> / From<PathPoint> ───────────────────────────────────
632
633 #[test]
634 #[expect(
635 clippy::float_cmp,
636 reason = "testing exact round-trip identity through From impls, not approximate equality"
637 )]
638 fn from_tuple_pathpoint() {
639 let p: PathPoint = (1.5_f64, 2.5_f64).into();
640 assert_eq!(p.x, 1.5);
641 assert_eq!(p.y, 2.5);
642
643 let t: (f64, f64) = p.into();
644 assert_eq!(t, (1.5, 2.5));
645 }
646
647 // ── PathFlags helpers ─────────────────────────────────────────────────────
648
649 #[test]
650 fn path_flags_helpers() {
651 let f = PathFlags::FIRST | PathFlags::LAST | PathFlags::CLOSED | PathFlags::CURVE;
652 assert!(f.is_first());
653 assert!(f.is_last());
654 assert!(f.is_closed());
655 assert!(f.is_curve());
656
657 let empty = PathFlags::empty();
658 assert!(!empty.is_first());
659 assert!(!empty.is_last());
660 assert!(!empty.is_closed());
661 assert!(!empty.is_curve());
662 }
663
664 // ── Path::append ──────────────────────────────────────────────────────────
665
666 #[test]
667 fn append_adjusts_hints() {
668 let mut a = PathBuilder::new();
669 a.move_to(0.0, 0.0).unwrap();
670 a.line_to(1.0, 0.0).unwrap();
671 let pa = a.build();
672
673 let mut b_path = pa.clone();
674 let mut other = pa;
675 other.hints.push(StrokeAdjustHint {
676 ctrl0: 0,
677 ctrl1: 1,
678 first_pt: 0,
679 last_pt: 1,
680 });
681 b_path.append(&other);
682 // Hint indices should be offset by original pa.pts.len() = 2.
683 assert_eq!(b_path.hints[0].ctrl0, 2);
684 assert_eq!(b_path.hints[0].first_pt, 2);
685 }
686
687 /// Appending an empty path must leave `self` in the no-current-point state
688 /// and must not panic.
689 #[test]
690 fn append_empty_other_is_safe() {
691 let mut base = PathBuilder::new();
692 base.move_to(0.0, 0.0).unwrap();
693 base.line_to(1.0, 0.0).unwrap();
694 let mut p = base.build();
695 let original_len = p.pts.len();
696
697 p.append(&Path::new());
698
699 assert_eq!(p.pts.len(), original_len);
700 // cur_subpath == pts.len() → no current point.
701 assert!(p.no_current_point());
702 }
703}