Skip to main content

skia_safe/core/
path_builder.rs

1use std::fmt;
2
3use crate::{
4    path, prelude::*, scalar, Matrix, Path, PathDirection, PathFillType, PathVerb, Point, RRect,
5    Rect, Vector,
6};
7use skia_bindings::{self as sb, SkPathBuilder};
8
9pub type ArcSize = sb::SkPathBuilder_ArcSize;
10variant_name!(ArcSize::Large);
11
12// PathBuilder can't be a Handle<>, because SkPathBuilder contains several STArrays with interior
13// pointers.
14//
15// See <https://github.com/rust-skia/rust-skia/pull/1195>.
16pub type PathBuilder = RefHandle<SkPathBuilder>;
17unsafe_send_sync!(PathBuilder);
18
19impl NativeDrop for SkPathBuilder {
20    fn drop(&mut self) {
21        unsafe { sb::C_SkPathBuilder_delete(self) }
22    }
23}
24
25impl NativePartialEq for SkPathBuilder {
26    fn eq(&self, rhs: &Self) -> bool {
27        unsafe { sb::C_SkPathBuilder_equals(self, rhs) }
28    }
29}
30
31impl Default for PathBuilder {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Clone for PathBuilder {
38    fn clone(&self) -> Self {
39        Self::from_ptr(unsafe { sb::C_SkPathBuilder_clone(self.native()) }).unwrap()
40    }
41}
42
43impl fmt::Debug for PathBuilder {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct("PathBuilder")
46            .field("fill_type", &self.fill_type())
47            .finish()
48    }
49}
50
51impl From<PathBuilder> for Path {
52    fn from(mut value: PathBuilder) -> Self {
53        value.detach()
54    }
55}
56
57impl PathBuilder {
58    /// Constructs an empty [`PathBuilder`]. By default, [`PathBuilder`] has no verbs, no [`Point`], and
59    /// no weights. [`PathFillType`] is set to [`PathFillType::Winding`].
60    ///
61    /// # Returns
62    /// empty [`PathBuilder`]
63    pub fn new() -> Self {
64        Self::from_ptr(unsafe { sb::C_SkPathBuilder_new() }).unwrap()
65    }
66
67    /// Constructs an empty [`PathBuilder`] with the given [`PathFillType`]. By default, [`PathBuilder`] has no
68    /// verbs, no [`Point`], and no weights.
69    ///
70    /// - `fill_type`: [`PathFillType`] to set on the [`PathBuilder`].
71    ///
72    /// # Returns
73    /// empty [`PathBuilder`]
74    pub fn new_with_fill_type(fill_type: PathFillType) -> Self {
75        Self::from_ptr(unsafe { sb::C_SkPathBuilder_newWithFillType(fill_type) }).unwrap()
76    }
77
78    /// Constructs a [`PathBuilder`] that is a copy of an existing [`Path`].
79    /// Copies the [`PathFillType`] and replays all of the verbs from the [`Path`] into the [`PathBuilder`].
80    ///
81    /// - `path`: [`Path`] to copy
82    ///
83    /// # Returns
84    /// [`PathBuilder`]
85    pub fn new_path(path: &Path) -> Self {
86        Self::from_ptr(unsafe { sb::C_SkPathBuilder_newFromPath(path.native()) }).unwrap()
87    }
88
89    /// Returns [`PathFillType`], the rule used to fill [`Path`].
90    ///
91    /// # Returns
92    /// current [`PathFillType`] setting
93    pub fn fill_type(&self) -> PathFillType {
94        self.native().fFillType
95    }
96
97    /// Returns minimum and maximum axes values of [`Point`] array.
98    /// Returns `None` if [`PathBuilder`] contains no points.
99    ///
100    /// [`Rect`] returned includes all [`Point`] added to [`PathBuilder`], including [`Point`] associated
101    /// with [`PathVerb::Move`] that define empty contours.
102    ///
103    /// If any of the points are non-finite, returns `None`.
104    ///
105    /// # Returns
106    /// Bounds of all [`Point`] in [`Point`] array, or `None`.
107    pub fn compute_finite_bounds(&self) -> Option<Rect> {
108        let mut rect = Rect::default();
109        unsafe { sb::C_SkPathBuilder_computeFiniteBounds(self.native(), rect.native_mut()) }
110            .then_some(rect)
111    }
112
113    /// Like [`Self::compute_finite_bounds()`] but returns a 'tight' bounds, meaning when there are curve
114    /// segments, this computes the X/Y limits of the curve itself, not the curve's control
115    /// point(s). For a polygon, this returns the same as [`Self::compute_finite_bounds()`].
116    pub fn compute_tight_bounds(&self) -> Option<Rect> {
117        let mut rect = Rect::default();
118        unsafe { sb::C_SkPathBuilder_computeTightBounds(self.native(), rect.native_mut()) }
119            .then_some(rect)
120    }
121
122    /// Returns minimum and maximum axes values of [`Point`] array.
123    ///
124    /// # Returns
125    /// Bounds of all [`Point`] in [`Point`] array, or an empty [`Rect`] if the bounds are non-finite.
126    ///
127    /// # Deprecated
128    /// Use [`Self::compute_finite_bounds()`] instead, which returns `None` when the bounds are non-finite.
129    #[deprecated(since = "0.91.0", note = "Use compute_finite_bounds() instead")]
130    pub fn compute_bounds(&self) -> Rect {
131        self.compute_finite_bounds().unwrap_or_else(Rect::new_empty)
132    }
133
134    /// Returns a [`Path`] representing the current state of the [`PathBuilder`]. The builder is
135    /// unchanged after returning the path.
136    ///
137    /// # Returns
138    /// [`Path`] representing the current state of the builder.
139    pub fn snapshot(&self) -> Path {
140        self.snapshot_and_transform(None)
141    }
142
143    /// Returns a [`Path`] representing the current state of the [`PathBuilder`]. The builder is
144    /// unchanged after returning the path.
145    ///
146    /// - `mx`: if present, applied to the points after they are copied into the resulting path.
147    ///
148    /// # Returns
149    /// [`Path`] representing the current state of the builder.
150    pub fn snapshot_and_transform<'m>(&self, mx: impl Into<Option<&'m Matrix>>) -> Path {
151        let mut p = Path::default();
152        unsafe {
153            sb::C_SkPathBuilder_snapshot(
154                self.native(),
155                mx.into().native_ptr_or_null(),
156                p.native_mut(),
157            )
158        };
159        p
160    }
161
162    /// Returns a [`Path`] representing the current state of the [`PathBuilder`]. The builder is
163    /// reset to empty after returning the path.
164    ///
165    /// # Returns
166    /// [`Path`] representing the current state of the builder.
167    pub fn detach(&mut self) -> Path {
168        self.detach_and_transform(None)
169    }
170
171    /// Returns a [`Path`] representing the current state of the [`PathBuilder`]. The builder is
172    /// reset to empty after returning the path.
173    ///
174    /// - `mx`: if present, applied to the points after they are copied into the resulting path.
175    ///
176    /// # Returns
177    /// [`Path`] representing the current state of the builder.
178    pub fn detach_and_transform<'m>(&mut self, mx: impl Into<Option<&'m Matrix>>) -> Path {
179        let mut p = Path::default();
180        unsafe {
181            sb::C_SkPathBuilder_detach(
182                self.native_mut(),
183                mx.into().native_ptr_or_null(),
184                p.native_mut(),
185            )
186        };
187        p
188    }
189
190    /// Sets [`PathFillType`], the rule used to fill [`Path`]. While there is no
191    /// check that `ft` is legal, values outside of [`PathFillType`] are not supported.
192    ///
193    /// - `ft`: [`PathFillType`] to be used by [`Path`]s generated from this builder.
194    ///
195    /// # Returns
196    /// reference to [`PathBuilder`]
197    pub fn set_fill_type(&mut self, ft: PathFillType) -> &mut Self {
198        self.native_mut().fFillType = ft;
199        self
200    }
201
202    /// Specifies whether [`Path`] is volatile; whether it will be altered or discarded
203    /// by the caller after it is drawn. [`Path`] by default have volatile set false, allowing
204    /// Skia to attach a cache of data which speeds repeated drawing.
205    ///
206    /// Mark temporary paths, discarded or modified after use, as volatile
207    /// to inform Skia that the path need not be cached.
208    ///
209    /// Mark animating [`Path`] volatile to improve performance.
210    /// Mark unchanging [`Path`] non-volatile to improve repeated rendering.
211    ///
212    /// raster surface [`Path`] draws are affected by volatile for some shadows.
213    /// GPU surface [`Path`] draws are affected by volatile for some shadows and concave geometries.
214    ///
215    /// - `is_volatile`: true if caller will alter [`Path`] after drawing
216    ///
217    /// # Returns
218    /// reference to [`PathBuilder`]
219    pub fn set_is_volatile(&mut self, is_volatile: bool) -> &mut Self {
220        self.native_mut().fIsVolatile = is_volatile;
221        self
222    }
223
224    /// Sets [`PathBuilder`] to its initial state.
225    /// Removes verb array, [`Point`] array, and weights, and sets [`PathFillType`] to [`PathFillType::Winding`].
226    /// Internal storage associated with [`PathBuilder`] is preserved.
227    ///
228    /// # Returns
229    /// reference to [`PathBuilder`]
230    pub fn reset(&mut self) -> &mut Self {
231        unsafe {
232            self.native_mut().reset();
233        }
234        self
235    }
236
237    /// Specifies the beginning of contour. If the previous verb was a "move" verb,
238    /// then this just replaces the point value of that move, otherwise it appends a new
239    /// "move" verb to the builder using the point.
240    ///
241    /// Thus, each contour can only have 1 move verb in it (the last one specified).
242    pub fn move_to(&mut self, pt: impl Into<Point>) -> &mut Self {
243        unsafe {
244            self.native_mut().moveTo(pt.into().into_native());
245        }
246        self
247    }
248
249    /// Adds line from last point to [`Point`] p. If [`PathBuilder`] is empty, or last [`PathVerb`] is
250    /// [`PathVerb::Close`], last point is set to (0, 0) before adding line.
251    ///
252    /// `line_to()` first appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array, if needed.
253    /// `line_to()` then appends [`PathVerb::Line`] to verb array and [`Point`] p to [`Point`] array.
254    ///
255    /// - `pt`: end [`Point`] of added line
256    ///
257    /// # Returns
258    /// reference to [`PathBuilder`]
259    pub fn line_to(&mut self, pt: impl Into<Point>) -> &mut Self {
260        unsafe {
261            self.native_mut().lineTo(pt.into().into_native());
262        }
263        self
264    }
265
266    /// Adds quad from last point towards [`Point`] p1, to [`Point`] p2.
267    /// If [`PathBuilder`] is empty, or last [`PathVerb`] is [`PathVerb::Close`], last point is set to (0, 0)
268    /// before adding quad.
269    ///
270    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array, if needed;
271    /// then appends [`PathVerb::Quad`] to verb array; and [`Point`] p1, p2
272    /// to [`Point`] array.
273    ///
274    /// - `p1`: control [`Point`] of added quad
275    /// - `p2`: end [`Point`] of added quad
276    ///
277    /// # Returns
278    /// reference to [`PathBuilder`]
279    pub fn quad_to(&mut self, p1: impl Into<Point>, p2: impl Into<Point>) -> &mut Self {
280        unsafe {
281            self.native_mut()
282                .quadTo(p1.into().into_native(), p2.into().into_native());
283        }
284        self
285    }
286
287    /// Adds conic from last point towards pt1, to pt2, weighted by w.
288    /// If [`PathBuilder`] is empty, or last [`PathVerb`] is [`PathVerb::Close`], last point is set to (0, 0)
289    /// before adding conic.
290    ///
291    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array, if needed.
292    ///
293    /// If w is finite and not one, appends [`PathVerb::Conic`] to verb array;
294    /// and pt1, pt2 to [`Point`] array; and w to conic weights.
295    ///
296    /// If w is one, appends [`PathVerb::Quad`] to verb array, and
297    /// pt1, pt2 to [`Point`] array.
298    ///
299    /// If w is not finite, appends [`PathVerb::Line`] twice to verb array, and
300    /// pt1, pt2 to [`Point`] array.
301    ///
302    /// - `pt1`: control [`Point`] of conic
303    /// - `pt2`: end [`Point`] of conic
304    /// - `w`: weight of added conic
305    ///
306    /// # Returns
307    /// reference to [`PathBuilder`]
308    pub fn conic_to(&mut self, p1: impl Into<Point>, p2: impl Into<Point>, w: scalar) -> &mut Self {
309        unsafe {
310            self.native_mut()
311                .conicTo(p1.into().into_native(), p2.into().into_native(), w);
312        }
313        self
314    }
315
316    /// Adds cubic from last point towards [`Point`] p1, then towards [`Point`] p2, ending at
317    /// [`Point`] p3. If [`PathBuilder`] is empty, or last [`PathVerb`] is [`PathVerb::Close`], last point is
318    /// set to (0, 0) before adding cubic.
319    ///
320    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array, if needed;
321    /// then appends [`PathVerb::Cubic`] to verb array; and [`Point`] p1, p2, p3
322    /// to [`Point`] array.
323    ///
324    /// - `p1`: first control [`Point`] of cubic
325    /// - `p2`: second control [`Point`] of cubic
326    /// - `p3`: end [`Point`] of cubic
327    ///
328    /// # Returns
329    /// reference to [`PathBuilder`]
330    pub fn cubic_to(
331        &mut self,
332        p1: impl Into<Point>,
333        p2: impl Into<Point>,
334        p3: impl Into<Point>,
335    ) -> &mut Self {
336        unsafe {
337            self.native_mut().cubicTo(
338                p1.into().into_native(),
339                p2.into().into_native(),
340                p3.into().into_native(),
341            )
342        };
343        self
344    }
345
346    /// Appends [`PathVerb::Close`] to [`PathBuilder`]. A closed contour connects the first and last [`Point`]
347    /// with line, forming a continuous loop. Open and closed contour draw the same
348    /// with [`crate::PaintStyle::Fill`]. With [`crate::PaintStyle::Stroke`], open contour draws
349    /// [`crate::PaintCap`] at contour start and end; closed contour draws
350    /// [`crate::PaintJoin`] at contour start and end.
351    ///
352    /// `close()` has no effect if [`PathBuilder`] is empty or last [`PathVerb`] is [`PathVerb::Close`].
353    ///
354    /// # Returns
355    /// reference to [`PathBuilder`]
356    pub fn close(&mut self) -> &mut Self {
357        unsafe {
358            self.native_mut().close();
359        }
360        self
361    }
362
363    /// Append a series of `line_to(...)`
364    ///
365    /// - `points`: array of [`Point`]
366    ///
367    /// # Returns
368    /// reference to [`PathBuilder`]
369    pub fn polyline_to(&mut self, points: &[Point]) -> &mut Self {
370        unsafe {
371            sb::C_SkPathBuilder_polylineTo(
372                self.native_mut(),
373                points.native().as_ptr(),
374                points.len(),
375            );
376        }
377        self
378    }
379
380    /// Adds beginning of contour relative to last point.
381    /// If [`PathBuilder`] is empty, starts contour at (dx, dy).
382    /// Otherwise, start contour at last point offset by (dx, dy).
383    /// Function name stands for "relative move to".
384    ///
385    /// - `pt`: vector offset from last point to contour start
386    ///
387    /// # Returns
388    /// reference to [`PathBuilder`]
389    pub fn r_move_to(&mut self, pt: impl Into<Vector>) -> &mut Self {
390        unsafe {
391            self.native_mut().rMoveTo(pt.into().into_native());
392        }
393        self
394    }
395
396    /// Adds line from last point to vector given by pt. If [`PathBuilder`] is empty, or last
397    /// [`PathVerb`] is [`PathVerb::Close`], last point is set to (0, 0) before adding line.
398    ///
399    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array, if needed;
400    /// then appends [`PathVerb::Line`] to verb array and line end to [`Point`] array.
401    /// Line end is last point plus vector given by pt.
402    /// Function name stands for "relative line to".
403    ///
404    /// - `pt`: vector offset from last point to line end
405    ///
406    /// # Returns
407    /// reference to [`PathBuilder`]
408    pub fn r_line_to(&mut self, pt: impl Into<Vector>) -> &mut Self {
409        unsafe {
410            self.native_mut().rLineTo(pt.into().into_native());
411        }
412        self
413    }
414
415    /// Adds quad from last point towards vector pt1, to vector pt2.
416    /// If [`PathBuilder`] is empty, or last [`PathVerb`]
417    /// is [`PathVerb::Close`], last point is set to (0, 0) before adding quad.
418    ///
419    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array,
420    /// if needed; then appends [`PathVerb::Quad`] to verb array; and appends quad
421    /// control and quad end to [`Point`] array.
422    /// Quad control is last point plus vector pt1.
423    /// Quad end is last point plus vector pt2.
424    /// Function name stands for "relative quad to".
425    ///
426    /// - `pt1`: offset vector from last point to quad control
427    /// - `pt2`: offset vector from last point to quad end
428    ///
429    /// # Returns
430    /// reference to [`PathBuilder`]
431    pub fn r_quad_to(&mut self, pt1: impl Into<Vector>, pt2: impl Into<Vector>) -> &mut Self {
432        unsafe {
433            self.native_mut()
434                .rQuadTo(pt1.into().into_native(), pt2.into().into_native());
435        }
436        self
437    }
438
439    /// Adds conic from last point towards vector p1, to vector p2,
440    /// weighted by w. If [`PathBuilder`] is empty, or last [`PathVerb`]
441    /// is [`PathVerb::Close`], last point is set to (0, 0) before adding conic.
442    ///
443    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array,
444    /// if needed.
445    ///
446    /// If w is finite and not one, next appends [`PathVerb::Conic`] to verb array,
447    /// and w is recorded as conic weight; otherwise, if w is one, appends
448    /// [`PathVerb::Quad`] to verb array; or if w is not finite, appends [`PathVerb::Line`]
449    /// twice to verb array.
450    ///
451    /// In all cases appends [`Point`] control and end to [`Point`] array.
452    /// control is last point plus vector p1.
453    /// end is last point plus vector p2.
454    ///
455    /// Function name stands for "relative conic to".
456    ///
457    /// - `pt1`: offset vector from last point to conic control
458    /// - `pt2`: offset vector from last point to conic end
459    /// - `w`: weight of added conic
460    ///
461    /// # Returns
462    /// reference to [`PathBuilder`]
463    pub fn r_conic_to(
464        &mut self,
465        pt1: impl Into<Vector>,
466        pt2: impl Into<Vector>,
467        w: scalar,
468    ) -> &mut Self {
469        unsafe {
470            self.native_mut()
471                .rConicTo(pt1.into().into_native(), pt2.into().into_native(), w);
472        }
473        self
474    }
475
476    /// Adds cubic from last point towards vector pt1, then towards
477    /// vector pt2, to vector pt3.
478    /// If [`PathBuilder`] is empty, or last [`PathVerb`]
479    /// is [`PathVerb::Close`], last point is set to (0, 0) before adding cubic.
480    ///
481    /// Appends [`PathVerb::Move`] to verb array and (0, 0) to [`Point`] array,
482    /// if needed; then appends [`PathVerb::Cubic`] to verb array; and appends cubic
483    /// control and cubic end to [`Point`] array.
484    /// Cubic control is last point plus vector (dx1, dy1).
485    /// Cubic end is last point plus vector (dx2, dy2).
486    /// Function name stands for "relative cubic to".
487    ///
488    /// - `pt1`: offset vector from last point to first cubic control
489    /// - `pt2`: offset vector from last point to second cubic control
490    /// - `pt3`: offset vector from last point to cubic end
491    ///
492    /// # Returns
493    /// reference to [`PathBuilder`]
494    pub fn r_cubic_to(
495        &mut self,
496        pt1: impl Into<Vector>,
497        pt2: impl Into<Vector>,
498        pt3: impl Into<Vector>,
499    ) -> &mut Self {
500        unsafe {
501            self.native_mut().rCubicTo(
502                pt1.into().into_native(),
503                pt2.into().into_native(),
504                pt3.into().into_native(),
505            );
506        }
507        self
508    }
509
510    /// Appends arc to [`PathBuilder`], relative to last [`Path`] [`Point`]. Arc is implemented by one or
511    /// more conic, weighted to describe part of oval with radii (rx, ry) rotated by
512    /// `x_axis_rotate` degrees. Arc curves from last [`PathBuilder`] [`Point`] to relative end [`Point`]:
513    /// (dx, dy), choosing one of four possible routes: clockwise or
514    /// counterclockwise, and smaller or larger. If [`PathBuilder`] is empty, the start arc [`Point`]
515    /// is (0, 0).
516    ///
517    /// Arc sweep is always less than 360 degrees. `arc_to()` appends line to end [`Point`]
518    /// if either radii are zero, or if last [`Path`] [`Point`] equals end [`Point`].
519    /// `arc_to()` scales radii (rx, ry) to fit last [`Path`] [`Point`] and end [`Point`] if both are
520    /// greater than zero but too small to describe an arc.
521    ///
522    /// `arc_to()` appends up to four conic curves.
523    /// `arc_to()` implements the functionality of svg arc, although SVG "sweep-flag" value is
524    /// opposite the integer value of sweep; SVG "sweep-flag" uses 1 for clockwise, while
525    /// [`PathDirection::CW`] cast to int is zero.
526    ///
527    /// - `r`: radii on axes before x-axis rotation
528    /// - `x_axis_rotate`: x-axis rotation in degrees; positive values are clockwise
529    /// - `large_arc`: chooses smaller or larger arc
530    /// - `sweep`: chooses clockwise or counterclockwise arc
531    /// - `dxdy`: offset end of arc from last [`Path`] point
532    ///
533    /// # Returns
534    /// reference to [`PathBuilder`]
535    pub fn r_arc_to(
536        &mut self,
537        r: impl Into<Vector>,
538        x_axis_rotate: scalar,
539        large_arc: ArcSize,
540        sweep: PathDirection,
541        dxdy: impl Into<Vector>,
542    ) -> &mut Self {
543        let r = r.into();
544        let d = dxdy.into();
545        unsafe {
546            self.native_mut().rArcTo(
547                r.into_native(),
548                x_axis_rotate,
549                large_arc,
550                sweep,
551                d.into_native(),
552            );
553        }
554        self
555    }
556
557    /// Appends arc to the builder. Arc added is part of ellipse
558    /// bounded by oval, from `start_angle` through `sweep_angle`. Both `start_angle` and
559    /// `sweep_angle` are measured in degrees, where zero degrees is aligned with the
560    /// positive x-axis, and positive sweeps extends arc clockwise.
561    ///
562    /// `arc_to()` adds line connecting the builder's last point to initial arc point if `force_move_to`
563    /// is false and the builder is not empty. Otherwise, added contour begins with first point
564    /// of arc. Angles greater than -360 and less than 360 are treated modulo 360.
565    ///
566    /// - `oval`: bounds of ellipse containing arc
567    /// - `start_angle_deg`: starting angle of arc in degrees
568    /// - `sweep_angle_deg`: sweep, in degrees. Positive is clockwise; treated modulo 360
569    /// - `force_move_to`: true to start a new contour with arc
570    ///
571    /// # Returns
572    /// reference to the builder
573    pub fn arc_to(
574        &mut self,
575        oval: impl AsRef<Rect>,
576        start_angle_deg: scalar,
577        sweep_angle_deg: scalar,
578        force_move_to: bool,
579    ) -> &mut Self {
580        unsafe {
581            self.native_mut().arcTo(
582                oval.as_ref().native(),
583                start_angle_deg,
584                sweep_angle_deg,
585                force_move_to,
586            );
587        }
588        self
589    }
590
591    /// Appends arc to [`Path`], after appending line if needed. Arc is implemented by conic
592    /// weighted to describe part of circle. Arc is contained by tangent from
593    /// last [`Path`] point to p1, and tangent from p1 to p2. Arc
594    /// is part of circle sized to radius, positioned so it touches both tangent lines.
595    ///
596    /// If last [`Path`] [`Point`] does not start arc, `arc_to()` appends connecting line to [`Path`].
597    /// The length of vector from p1 to p2 does not affect arc.
598    ///
599    /// Arc sweep is always less than 180 degrees. If radius is zero, or if
600    /// tangents are nearly parallel, `arc_to()` appends line from last [`Path`] [`Point`] to p1.
601    ///
602    /// `arc_to()` appends at most one line and one conic.
603    /// `arc_to()` implements the functionality of PostScript arct and HTML Canvas `arcTo`.
604    ///
605    /// - `p1`: [`Point`] common to pair of tangents
606    /// - `p2`: end of second tangent
607    /// - `radius`: distance from arc to circle center
608    ///
609    /// # Returns
610    /// reference to [`PathBuilder`]
611    pub fn arc_to_tangent(
612        &mut self,
613        p1: impl Into<Point>,
614        p2: impl Into<Point>,
615        radius: scalar,
616    ) -> &mut Self {
617        unsafe {
618            self.native_mut()
619                .arcTo1(p1.into().into_native(), p2.into().into_native(), radius);
620        }
621        self
622    }
623
624    /// Appends arc to [`Path`]. Arc is implemented by one or more conic weighted to describe
625    /// part of oval with radii (r.fX, r.fY) rotated by `x_axis_rotate` degrees. Arc curves
626    /// from last [`Path`] [`Point`] to (xy.fX, xy.fY), choosing one of four possible routes:
627    /// clockwise or counterclockwise,
628    /// and smaller or larger.
629    ///
630    /// Arc sweep is always less than 360 degrees. `arc_to_radius()` appends line to xy if either
631    /// radii are zero, or if last [`Path`] [`Point`] equals (xy.fX, xy.fY). `arc_to_radius()` scales radii r to
632    /// fit last [`Path`] [`Point`] and xy if both are greater than zero but too small to describe
633    /// an arc.
634    ///
635    /// `arc_to_radius()` appends up to four conic curves.
636    /// `arc_to_radius()` implements the functionality of SVG arc, although SVG sweep-flag value is
637    /// opposite the integer value of sweep; SVG sweep-flag uses 1 for clockwise, while
638    /// [`PathDirection::CW`] cast to int is zero.
639    ///
640    /// - `r`: radii on axes before x-axis rotation
641    /// - `x_axis_rotate`: x-axis rotation in degrees; positive values are clockwise
642    /// - `large_arc`: chooses smaller or larger arc
643    /// - `sweep`: chooses clockwise or counterclockwise arc
644    /// - `xy`: end of arc
645    ///
646    /// # Returns
647    /// reference to [`PathBuilder`]
648    pub fn arc_to_radius(
649        &mut self,
650        r: impl Into<Point>,
651        x_axis_rotate: scalar,
652        large_arc: ArcSize,
653        sweep: PathDirection,
654        xy: impl Into<Point>,
655    ) -> &mut Self {
656        unsafe {
657            self.native_mut().arcTo2(
658                r.into().into_native(),
659                x_axis_rotate,
660                large_arc,
661                sweep,
662                xy.into().into_native(),
663            );
664        }
665        self
666    }
667
668    /// Appends arc to the builder, as the start of new contour. Arc added is part of ellipse
669    /// bounded by oval, from `start_angle` through `sweep_angle`. Both `start_angle` and
670    /// `sweep_angle` are measured in degrees, where zero degrees is aligned with the
671    /// positive x-axis, and positive sweeps extends arc clockwise.
672    ///
673    /// If `sweep_angle` <= -360, or `sweep_angle` >= 360; and `start_angle` modulo 90 is nearly
674    /// zero, append oval instead of arc. Otherwise, `sweep_angle` values are treated
675    /// modulo 360, and arc may or may not draw depending on numeric rounding.
676    ///
677    /// - `oval`: bounds of ellipse containing arc
678    /// - `start_angle_deg`: starting angle of arc in degrees
679    /// - `sweep_angle_deg`: sweep, in degrees. Positive is clockwise; treated modulo 360
680    ///
681    /// # Returns
682    /// reference to this builder
683    pub fn add_arc(
684        &mut self,
685        oval: impl AsRef<Rect>,
686        start_angle_deg: scalar,
687        sweep_angle_deg: scalar,
688    ) -> &mut Self {
689        unsafe {
690            self.native_mut()
691                .addArc(oval.as_ref().native(), start_angle_deg, sweep_angle_deg);
692        }
693        self
694    }
695
696    pub fn add_line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> &mut Self {
697        self.move_to(a).line_to(b)
698    }
699
700    /// Adds a new contour to the [`PathBuilder`], defined by the rect, and wound in the
701    /// specified direction. The verbs added to the path will be:
702    ///
703    /// [`PathVerb::Move`], [`PathVerb::Line`], [`PathVerb::Line`], [`PathVerb::Line`], [`PathVerb::Close`]
704    ///
705    /// start specifies which corner to begin the contour:
706    ///     0: upper-left  corner
707    ///     1: upper-right corner
708    ///     2: lower-right corner
709    ///     3: lower-left  corner
710    ///
711    /// This start point also acts as the implied beginning of the subsequent,
712    /// contour, if it does not have an explicit `move_to()`. e.g.
713    ///
714    ///     path.add_rect(...)
715    ///     // if we don't say move_to() here, we will use the rect's start point
716    ///     path.line_to(...)
717    ///
718    /// - `rect`: [`Rect`] to add as a closed contour
719    /// - `dir`: [`PathDirection`] to orient the new contour
720    /// - `start_index`: initial corner of [`Rect`] to add
721    ///
722    /// # Returns
723    /// reference to [`PathBuilder`]
724    pub fn add_rect(
725        &mut self,
726        rect: impl AsRef<Rect>,
727        dir: impl Into<Option<PathDirection>>,
728        start_index: impl Into<Option<usize>>,
729    ) -> &mut Self {
730        let dir = dir.into().unwrap_or_default();
731        let start_index = start_index.into().unwrap_or(0);
732        unsafe {
733            self.native_mut()
734                .addRect(rect.as_ref().native(), dir, start_index.try_into().unwrap());
735        }
736        self
737    }
738
739    /// Adds oval to [`PathBuilder`], appending [`PathVerb::Move`], four [`PathVerb::Conic`], and [`PathVerb::Close`].
740    /// Oval is upright ellipse bounded by [`Rect`] oval with radii equal to half oval width
741    /// and half oval height. Oval begins at (oval.right, oval.center_y()) and continues
742    /// clockwise if dir is [`PathDirection::CW`], counterclockwise if dir is [`PathDirection::CCW`].
743    ///
744    /// - `rect`: bounds of ellipse added
745    /// - `dir`: [`PathDirection`] to wind ellipse
746    /// - `start_index`: index of initial point of ellipse
747    ///
748    /// # Returns
749    /// reference to [`PathBuilder`]
750    pub fn add_oval(
751        &mut self,
752        rect: impl AsRef<Rect>,
753        dir: impl Into<Option<PathDirection>>,
754        start_index: impl Into<Option<usize>>,
755    ) -> &mut Self {
756        let dir = dir.into().unwrap_or_default();
757        // m86: default start index changed from 0 to 1
758        let start_index = start_index.into().unwrap_or(1);
759        unsafe {
760            self.native_mut()
761                .addOval(rect.as_ref().native(), dir, start_index.try_into().unwrap());
762        }
763        self
764    }
765
766    /// Appends [`RRect`] to [`PathBuilder`], creating a new closed contour. If dir is [`PathDirection::CW`],
767    /// [`RRect`] winds clockwise. If dir is [`PathDirection::CCW`], [`RRect`] winds counterclockwise.
768    ///
769    /// After appending, [`PathBuilder`] may be empty, or may contain: [`Rect`], oval, or [`RRect`].
770    ///
771    /// - `rect`: [`RRect`] to add
772    /// - `dir`: [`PathDirection`] to wind [`RRect`]
773    /// - `start_index`: index of initial point of [`RRect`]
774    ///
775    /// # Returns
776    /// reference to [`PathBuilder`]
777    pub fn add_rrect(
778        &mut self,
779        rect: impl AsRef<RRect>,
780        dir: impl Into<Option<PathDirection>>,
781        start_index: impl Into<Option<usize>>,
782    ) -> &mut Self {
783        let dir = dir.into().unwrap_or_default();
784        // m86: default start index changed from 0 to 6 or 7 depending on the path's direction.
785        let start_index =
786            start_index
787                .into()
788                .unwrap_or(if dir == PathDirection::CW { 6 } else { 7 });
789        unsafe {
790            self.native_mut().addRRect(
791                rect.as_ref().native(),
792                dir,
793                start_index.try_into().unwrap(),
794            );
795        }
796        self
797    }
798
799    /// Adds circle centered at (x, y) of size radius to [`PathBuilder`], appending [`PathVerb::Move`],
800    /// four [`PathVerb::Conic`], and [`PathVerb::Close`]. Circle begins at: (x + radius, y), continuing
801    /// clockwise if dir is [`PathDirection::CW`], and counterclockwise if dir is [`PathDirection::CCW`].
802    ///
803    /// Has no effect if radius is zero or negative.
804    ///
805    /// - `center`: center of circle
806    /// - `radius`: distance from center to edge
807    /// - `dir`: [`PathDirection`] to wind circle
808    ///
809    /// # Returns
810    /// reference to [`PathBuilder`]
811    pub fn add_circle(
812        &mut self,
813        center: impl Into<Point>,
814        radius: scalar,
815        dir: impl Into<Option<PathDirection>>,
816    ) -> &mut Self {
817        let center = center.into();
818        let dir = dir.into().unwrap_or_default();
819        unsafe {
820            self.native_mut()
821                .addCircle(center.into_native(), radius, dir);
822        }
823        self
824    }
825
826    /// Adds contour created from line array, adding (pts.len() - 1) line segments.
827    /// Contour added starts at `pts[0]`, then adds a line for every additional [`Point`]
828    /// in pts array. If close is true, appends [`PathVerb::Close`] to [`Path`], connecting
829    /// `pts[count - 1]` and `pts[0]`.
830    ///
831    /// - `pts`: array of line sharing end and start [`Point`]
832    /// - `close`: true to add line connecting contour end and start
833    ///
834    /// # Returns
835    /// reference to [`PathBuilder`]
836    pub fn add_polygon(&mut self, pts: &[Point], close: bool) -> &mut Self {
837        unsafe {
838            sb::C_SkPathBuilder_addPolygon(
839                self.native_mut(),
840                pts.native().as_ptr(),
841                pts.len(),
842                close,
843            );
844        }
845        self
846    }
847
848    /// Appends src to [`PathBuilder`].
849    ///
850    /// If mode is [`path::AddPathMode::Append`], src verb array, [`Point`] array, and conic weights are
851    /// added unaltered. If mode is [`path::AddPathMode::Extend`], add line before appending
852    /// verbs, [`Point`], and conic weights.
853    ///
854    /// - `path`: [`Path`] verbs, [`Point`], and conic weights to add
855    /// - `mode`: [`path::AddPathMode::Append`] or [`path::AddPathMode::Extend`]
856    ///
857    /// # Returns
858    /// reference to [`PathBuilder`]
859    pub fn add_path(
860        &mut self,
861        path: &Path,
862        mode: impl Into<Option<path::AddPathMode>>,
863    ) -> &mut Self {
864        self.add_path_with_offset(path, (0.0, 0.0), mode)
865    }
866
867    /// Appends src to [`PathBuilder`], offset by (dx, dy).
868    ///
869    /// If mode is [`path::AddPathMode::Append`], src verb array, [`Point`] array, and conic weights are
870    /// added unaltered except for the applied offset. If mode is [`path::AddPathMode::Extend`], add line
871    /// before appending verbs, [`Point`], and conic weights.
872    ///
873    /// - `path`: [`Path`] verbs, [`Point`], and conic weights to add
874    /// - `offset`: offset applied to src before appending
875    /// - `mode`: [`path::AddPathMode::Append`] or [`path::AddPathMode::Extend`]
876    ///
877    /// # Returns
878    /// reference to [`PathBuilder`]
879    pub fn add_path_with_offset(
880        &mut self,
881        path: &Path,
882        offset: impl Into<Vector>,
883        mode: impl Into<Option<path::AddPathMode>>,
884    ) -> &mut Self {
885        let offset = offset.into();
886        unsafe {
887            self.native_mut().addPath(
888                path.native(),
889                offset.x,
890                offset.y,
891                mode.into().unwrap_or(path::AddPathMode::Append),
892            )
893        };
894        self
895    }
896
897    /// Appends src to [`PathBuilder`], transformed by matrix. Transformed curves may have different
898    /// verbs, [`Point`], and conic weights.
899    ///
900    /// If mode is [`path::AddPathMode::Append`], src verb array, [`Point`] array, and conic weights are
901    /// added unaltered. If mode is [`path::AddPathMode::Extend`], add line before appending
902    /// verbs, [`Point`], and conic weights.
903    ///
904    /// - `src`: [`Path`] verbs, [`Point`], and conic weights to add
905    /// - `matrix`: transform applied to src
906    /// - `mode`: [`path::AddPathMode::Append`] or [`path::AddPathMode::Extend`]
907    pub fn add_path_with_transform(
908        &mut self,
909        src: &Path,
910        matrix: &Matrix,
911        mode: impl Into<Option<path::AddPathMode>>,
912    ) {
913        unsafe {
914            self.native_mut().addPath1(
915                src.native(),
916                matrix.native(),
917                mode.into().unwrap_or(path::AddPathMode::Append),
918            )
919        };
920    }
921
922    /// Grows [`PathBuilder`] verb array and [`Point`] array to contain additional space.
923    /// May improve performance and use less memory by
924    /// reducing the number and size of allocations when creating [`PathBuilder`].
925    ///
926    /// - `extra_pt_count`: number of additional [`Point`] to allocate
927    /// - `extra_verb_count`: number of additional verbs
928    /// - `extra_conic_count`: number of additional conic weights
929    pub fn inc_reserve(
930        &mut self,
931        extra_pt_count: usize,
932        extra_verb_count: usize,
933        extra_conic_count: usize,
934    ) {
935        unsafe {
936            self.native_mut().incReserve(
937                extra_pt_count.try_into().unwrap(),
938                extra_verb_count.try_into().unwrap(),
939                extra_conic_count.try_into().unwrap(),
940            )
941        }
942    }
943
944    /// Offsets [`Point`] array by (dx, dy).
945    ///
946    /// - `d`: offset added to [`Point`] array coordinates
947    ///
948    /// # Returns
949    /// reference to [`PathBuilder`]
950    pub fn offset(&mut self, d: impl Into<Vector>) -> &mut Self {
951        let d = d.into();
952        unsafe {
953            self.native_mut().offset(d.x, d.y);
954        }
955        self
956    }
957
958    /// Transforms verb array, [`Point`] array, and weight by matrix.
959    /// transform may change verbs and increase their number.
960    ///
961    /// - `matrix`: [`Matrix`] to apply to [`Path`]
962    ///
963    /// # Returns
964    /// reference to [`PathBuilder`]
965    pub fn transform(&mut self, matrix: &Matrix) -> &mut Self {
966        unsafe {
967            self.native_mut().transform(matrix.native());
968        }
969        self
970    }
971
972    /// Returns true if the builder is empty, or all of its points are finite.
973    pub fn is_finite(&self) -> bool {
974        unsafe { self.native().isFinite() }
975    }
976
977    /// Replaces [`PathFillType`] with its inverse. The inverse of [`PathFillType`] describes the area
978    /// unmodified by the original [`PathFillType`].
979    ///
980    /// # Returns
981    /// reference to [`PathBuilder`]
982    pub fn toggle_inverse_fill_type(&mut self) -> &mut Self {
983        let n = self.native_mut();
984        n.fFillType = n.fFillType.toggle_inverse();
985        self
986    }
987
988    /// Returns if [`Path`] is empty.
989    /// Empty [`PathBuilder`] may have [`PathFillType`] but has no [`Point`], [`PathVerb`], or conic weight.
990    /// [`PathBuilder::new()`] constructs empty [`PathBuilder`]; `reset()` and `rewind()` make [`Path`] empty.
991    ///
992    /// # Returns
993    /// true if the path contains no [`PathVerb`] array
994    pub fn is_empty(&self) -> bool {
995        unsafe { sb::C_SkPathBuilder_isEmpty(self.native()) }
996    }
997
998    /// Returns last point on [`PathBuilder`]. Returns `None` if [`Point`] array is empty.
999    ///
1000    /// # Returns
1001    /// last [`Point`] if [`Point`] array contains one or more [`Point`], otherwise `None`
1002    pub fn get_last_pt(&self) -> Option<Point> {
1003        let mut p = Point::default();
1004        unsafe { sb::C_SkPathBuilder_getLastPt(self.native(), p.native_mut()) }.then_some(p)
1005    }
1006
1007    /// Change the point at the specified index (see `count_points()`).
1008    /// If index is out of range, the call does nothing.
1009    ///
1010    /// - `index`: which point to replace
1011    /// - `p`: the new point value
1012    pub fn set_point(&mut self, index: usize, p: impl Into<Point>) {
1013        let p = p.into();
1014        unsafe { sb::C_SkPathBuilder_setPoint(self.native_mut(), index, *p.native()) }
1015    }
1016
1017    /// Change the last point in the builder.
1018    /// If the builder is empty, the call does nothing.
1019    ///
1020    /// - `p`: the new point value
1021    pub fn set_last_point(&mut self, p: impl Into<Point>) {
1022        let len = self.points().len();
1023        if len != 0 {
1024            self.set_point(len - 1, p);
1025        };
1026    }
1027
1028    /// Sets the last point on the path. If [`Point`] array is empty, append [`PathVerb::Move`] to
1029    /// verb array and append p to [`Point`] array.
1030    ///
1031    /// - `p`: last point
1032    #[deprecated(since = "0.93.0", note = "Use set_last_point() or set_point()")]
1033    pub fn set_last_pt(&mut self, p: impl Into<Point>) {
1034        let p = p.into();
1035        unsafe { self.native_mut().setLastPt(p.into_native()) };
1036    }
1037
1038    /// Returns the number of points in [`PathBuilder`].
1039    /// [`Point`] count is initially zero.
1040    ///
1041    /// # Returns
1042    /// [`PathBuilder`] [`Point`] array length
1043    pub fn count_points(&self) -> usize {
1044        unsafe { sb::C_SkPathBuilder_countPoints(self.native()) }
1045    }
1046
1047    /// Returns if [`PathFillType`] describes area outside [`Path`] geometry. The inverse fill area
1048    /// extends indefinitely.
1049    ///
1050    /// # Returns
1051    /// true if [`PathFillType`] is [`PathFillType::InverseWinding`] or [`PathFillType::InverseEvenOdd`]
1052    pub fn is_inverse_fill_type(&self) -> bool {
1053        self.fill_type().is_inverse()
1054    }
1055
1056    pub fn points(&self) -> &[Point] {
1057        unsafe {
1058            let mut len = 0;
1059            let points = sb::C_SkPathBuilder_points(self.native(), &mut len);
1060            safer::from_raw_parts(Point::from_native_ptr(points), len)
1061        }
1062    }
1063
1064    pub fn verbs(&self) -> &[PathVerb] {
1065        unsafe {
1066            let mut len = 0;
1067            let verbs = sb::C_SkPathBuilder_verbs(self.native(), &mut len);
1068            safer::from_raw_parts(verbs, len)
1069        }
1070    }
1071
1072    pub fn conic_weights(&self) -> &[scalar] {
1073        unsafe {
1074            let mut len = 0;
1075            let weights = sb::C_SkPathBuilder_conicWeights(self.native(), &mut len);
1076            safer::from_raw_parts(weights, len)
1077        }
1078    }
1079}
1080
1081pub type DumpFormat = skia_bindings::SkPathBuilder_DumpFormat;
1082variant_name!(DumpFormat::Hex);
1083
1084impl PathBuilder {
1085    /// Dumps the path to a string using the specified format.
1086    ///
1087    /// # Arguments
1088    /// * `format` - The format to use for dumping (Decimal or Hex)
1089    ///
1090    /// # Returns
1091    /// A string representation of the path
1092    pub fn dump_to_string(&self, format: DumpFormat) -> String {
1093        let mut str = crate::interop::String::default();
1094        unsafe {
1095            sb::C_SkPathBuilder_dumpToString(self.native(), format, str.native_mut());
1096        }
1097        str.as_str().to_owned()
1098    }
1099
1100    /// Dumps the path to stdout using the specified format.
1101    ///
1102    /// # Arguments
1103    /// * `format` - The format to use for dumping (Decimal or Hex)
1104    pub fn dump(&self, format: DumpFormat) {
1105        unsafe {
1106            sb::C_SkPathBuilder_dump(self.native(), format);
1107        }
1108    }
1109}
1110
1111impl PathBuilder {
1112    pub fn contains(&self, point: impl Into<Point>) -> bool {
1113        unsafe { self.native().contains(point.into().into_native()) }
1114    }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use crate::{paint, surfaces, Paint};
1120
1121    use super::*;
1122
1123    #[test]
1124    fn test_creation_snapshot_and_detach() {
1125        let mut builder = PathBuilder::new();
1126        let _path = builder.snapshot();
1127        let _path = builder.detach();
1128    }
1129
1130    #[test]
1131    fn issue_1195() {
1132        let mut surface = surfaces::raster_n32_premul((1000, 1000)).unwrap();
1133        let canvas = surface.canvas();
1134        let mut paint = Paint::default();
1135        paint.set_style(paint::Style::Stroke);
1136        let mut path = PathBuilder::new();
1137        path.move_to((250., 250.));
1138        path.cubic_to((300., 300.), (700., 700.), (750., 750.));
1139        let path_sh = path.snapshot();
1140        canvas.draw_path(&path_sh, &paint);
1141    }
1142
1143    #[test]
1144    fn test_equality() {
1145        let mut a = PathBuilder::new();
1146        let mut b = PathBuilder::new();
1147
1148        // Empty builders should be equal
1149        assert_eq!(a, b);
1150
1151        // Different paths should not be equal
1152        a.move_to((0., 0.));
1153        assert_ne!(a, b);
1154
1155        // Same paths should be equal
1156        b.move_to((0., 0.));
1157        assert_eq!(a, b);
1158
1159        // Different fill types should not be equal
1160        b.set_fill_type(PathFillType::EvenOdd);
1161        assert_ne!(a, b);
1162
1163        a.set_fill_type(PathFillType::EvenOdd);
1164        assert_eq!(a, b);
1165    }
1166
1167    #[test]
1168    fn test_compute_bounds() {
1169        let mut builder = PathBuilder::new();
1170
1171        // Empty builder should return empty rect (0, 0, 0, 0), not None
1172        let empty_bounds = builder.compute_finite_bounds().unwrap();
1173        assert!(empty_bounds.is_empty());
1174        assert_eq!(empty_bounds, Rect::new(0., 0., 0., 0.));
1175
1176        let empty_tight = builder.compute_tight_bounds().unwrap();
1177        assert!(empty_tight.is_empty());
1178
1179        // Deprecated method should also return empty rect
1180        #[allow(deprecated)]
1181        let bounds = builder.compute_bounds();
1182        assert!(bounds.is_empty());
1183
1184        // Add a simple rectangle
1185        builder.move_to((10., 20.));
1186        builder.line_to((100., 20.));
1187        builder.line_to((100., 80.));
1188        builder.line_to((10., 80.));
1189        builder.close();
1190
1191        let finite_bounds = builder.compute_finite_bounds().unwrap();
1192        assert_eq!(finite_bounds.left, 10.);
1193        assert_eq!(finite_bounds.top, 20.);
1194        assert_eq!(finite_bounds.right, 100.);
1195        assert_eq!(finite_bounds.bottom, 80.);
1196
1197        // For a polygon, tight bounds should equal finite bounds
1198        let tight_bounds = builder.compute_tight_bounds().unwrap();
1199        assert_eq!(finite_bounds, tight_bounds);
1200
1201        // Test with curves - finite bounds includes control points
1202        let mut curve_builder = PathBuilder::new();
1203        curve_builder.move_to((0., 0.));
1204        curve_builder.cubic_to((50., 100.), (150., 100.), (200., 0.));
1205
1206        let finite = curve_builder.compute_finite_bounds().unwrap();
1207        let tight = curve_builder.compute_tight_bounds().unwrap();
1208
1209        // Finite bounds should include control points
1210        assert_eq!(finite.left, 0.);
1211        assert_eq!(finite.right, 200.);
1212        assert_eq!(finite.top, 0.);
1213        assert_eq!(finite.bottom, 100.);
1214
1215        // Tight bounds should be smaller (not including full extent of control points)
1216        assert!(tight.bottom < finite.bottom);
1217    }
1218
1219    #[test]
1220    fn test_dump_to_string() {
1221        let mut builder = PathBuilder::new();
1222        builder.move_to((10.5, 20.25));
1223        builder.line_to((100.0, 50.0));
1224        builder.cubic_to((30.0, 40.0), (70.0, 80.0), (90.0, 100.0));
1225        builder.close();
1226
1227        // Test decimal format
1228        let decimal = builder.dump_to_string(DumpFormat::Decimal);
1229        println!("Decimal format:\n{}", decimal);
1230        assert!(decimal.contains("10.5"));
1231        assert!(decimal.contains("20.25"));
1232
1233        // Test hex format
1234        let hex = builder.dump_to_string(DumpFormat::Hex);
1235        println!("Hex format:\n{}", hex);
1236        assert!(hex.contains("0x"));
1237
1238        // Both should contain verb information
1239        assert!(decimal.contains("path") || decimal.contains("move") || decimal.contains("line"));
1240    }
1241
1242    #[test]
1243    fn test_add_path_with_offset() {
1244        let mut src_builder = PathBuilder::new();
1245        src_builder.move_to((1.0, 2.0)).line_to((3.0, 4.0));
1246        let src = src_builder.detach();
1247
1248        let mut dst = PathBuilder::new();
1249        dst.add_path_with_offset(&src, (10.0, 20.0), None);
1250
1251        let points = dst.points();
1252        assert_eq!(points.len(), 2);
1253        assert_eq!(points[0].x, 11.0);
1254        assert_eq!(points[0].y, 22.0);
1255        assert_eq!(points[1].x, 13.0);
1256        assert_eq!(points[1].y, 24.0);
1257    }
1258
1259    #[test]
1260    fn test_contains() {
1261        let mut builder = PathBuilder::new();
1262        builder.add_rect(Rect::new(10., 10., 100., 100.), None, None);
1263
1264        assert!(builder.contains((50., 50.)));
1265        assert!(builder.contains((10., 10.)));
1266        assert!(!builder.contains((5., 5.)));
1267        assert!(!builder.contains((150., 150.)));
1268    }
1269
1270    #[test]
1271    fn test_add_circle() {
1272        let mut builder = PathBuilder::new();
1273        builder.add_circle((50., 50.), 25., None);
1274
1275        let bounds = builder.compute_finite_bounds().unwrap();
1276        assert_eq!(bounds, Rect::new(25., 25., 75., 75.));
1277
1278        assert!(builder.contains((50., 50.)));
1279        assert!(!builder.contains((10., 10.)));
1280    }
1281}