Skip to main content

motion_core/shape/
primitives.rs

1//! Built-in geometric primitives.
2//!
3//! Constructors validate their inputs and return a [`ValidationError`], so a
4//! stored primitive always holds valid geometry. Shapes configured through
5//! chained setters, such as `Ring`, `Arrow` and `Spline`, report invalid
6//! settings from [`Shape::path`].
7//!
8//! ```
9//! use motion_core::prelude::*;
10//! use motion_core::{
11//!     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
12//!     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
13//! };
14//! let curve = Spline::through([
15//!     Point::new(1.0, 6.0),
16//!     Point::new(4.0, 1.0),
17//!     Point::new(7.0, 6.0),
18//! ])?
19//! .stroke(0.5, Color::WHITE)
20//! .trim(0.0, 0.8)
21//! .dash(&[1.0, 0.5], -0.2);
22//! Node::from(curve).compile(
23//!     Size::new(8.0, 8.0),
24//!     PixelSize::new(8, 8),
25//!     ValidationLimits::default(),
26//! )?;
27//! # Ok::<(), motion_core::Error>(())
28//! ```
29
30use crate::{MAX_PATH_VERBS, Path2D, Point, Rect, Shape, ValidationError, Vector2};
31use std::{ops::Range, sync::Arc as Shared};
32
33fn invalid(message: &str) -> ValidationError {
34    ValidationError::invalid("shape", "geometry", message)
35}
36
37fn radius(value: f64) -> Result<(), ValidationError> {
38    if value.is_finite() && value >= 0.0 {
39        Ok(())
40    } else {
41        Err(invalid(
42            "Radii and dimensions must be finite and nonnegative",
43        ))
44    }
45}
46
47fn points(values: impl IntoIterator<Item = Point>) -> Result<Shared<[Point]>, ValidationError> {
48    let mut result = Vec::new();
49    for value in values {
50        if result.len() >= MAX_PATH_VERBS - 1 {
51            return Err(ValidationError::limit(
52                "shape",
53                "points",
54                "Shape point allowance exceeded",
55            ));
56        }
57        if !value.x.is_finite() || !value.y.is_finite() {
58            return Err(invalid("Shape points must be finite"));
59        }
60        result.push(value);
61    }
62    Ok(result.into())
63}
64
65fn polyline(values: &[Point], closed: bool) -> Result<Path2D, ValidationError> {
66    let mut path = Path2D::new();
67    if let Some(first) = values.first() {
68        path.try_move_to(*first)?;
69        for value in &values[1..] {
70            path.try_line_to(*value)?;
71        }
72        if closed {
73            path.try_close()?;
74        }
75    }
76    Ok(path)
77}
78
79macro_rules! computed_shape {
80    ($name:ident) => {
81        impl Shape for $name {
82            fn path(&self) -> Result<Path2D, ValidationError> {
83                self.checked_path()
84            }
85        }
86    };
87}
88
89macro_rules! stored_shape {
90    ($name:ident, $description:literal) => {
91        #[doc = $description]
92        #[derive(Clone, Debug)]
93        pub struct $name {
94            path: Path2D,
95        }
96        impl Shape for $name {
97            fn path(&self) -> Result<Path2D, ValidationError> {
98                Ok(self.path.clone())
99            }
100
101            fn bounding_box(&self) -> Result<Rect, ValidationError> {
102                Ok(self.path.bounding_box())
103            }
104        }
105    };
106}
107
108stored_shape!(
109    Circle,
110    "A circular contour, using bounded cubic arc approximation."
111);
112
113impl Circle {
114    /// Builds a clockwise closed contour; zero radius is degenerate geometry.
115    pub fn new(center: Point, radius: f64) -> Result<Self, ValidationError> {
116        Ellipse::new(center, Vector2::new(radius, radius))
117            .map(|ellipse| Self { path: ellipse.path })
118    }
119}
120
121stored_shape!(Ellipse, "An axis-aligned elliptical contour.");
122
123impl Ellipse {
124    /// Builds a clockwise closed ellipse from nonnegative horizontal/vertical radii.
125    pub fn new(center: Point, radii: Vector2) -> Result<Self, ValidationError> {
126        let mut path = Path2D::new();
127        path.try_arc_to(center, radii, 0.0, std::f64::consts::TAU)?;
128        path.try_close()?;
129        Ok(Self { path })
130    }
131}
132
133stored_shape!(
134    Arc,
135    "An open circular arc; fill closes its chord, stroke keeps open ends."
136);
137
138impl Arc {
139    /// Builds a clockwise-positive arc in radians, connecting no implicit center.
140    pub fn new(
141        center: Point,
142        radius: f64,
143        start_angle: f64,
144        sweep_angle: f64,
145    ) -> Result<Self, ValidationError> {
146        let mut path = Path2D::new();
147        path.try_arc_to(
148            center,
149            Vector2::new(radius, radius),
150            start_angle,
151            sweep_angle,
152        )?;
153        Ok(Self { path })
154    }
155}
156
157stored_shape!(
158    RoundedRect,
159    "A rectangle with uniformly rounded, clamped corner radii."
160);
161
162impl RoundedRect {
163    /// Clamps the radius to half the smaller valid rectangle dimension.
164    pub fn new(rect: Rect, corner_radius: f64) -> Result<Self, ValidationError> {
165        crate::drawing::validate_rect(rect, "shape", "bounds")?;
166        radius(corner_radius)?;
167        let corner_radius = corner_radius
168            .min(rect.width() * 0.5)
169            .min(rect.height() * 0.5);
170        if corner_radius == 0.0 {
171            return Ok(Self {
172                path: Path2D::from_rect(rect)?,
173            });
174        }
175        let mut path = Path2D::new();
176        path.try_move_to(Point::new(rect.left + corner_radius, rect.top))?;
177        let radii = Vector2::new(corner_radius, corner_radius);
178        for (center, start) in [
179            (
180                Point::new(rect.right - corner_radius, rect.top + corner_radius),
181                -std::f64::consts::FRAC_PI_2,
182            ),
183            (
184                Point::new(rect.right - corner_radius, rect.bottom - corner_radius),
185                0.0,
186            ),
187            (
188                Point::new(rect.left + corner_radius, rect.bottom - corner_radius),
189                std::f64::consts::FRAC_PI_2,
190            ),
191            (
192                Point::new(rect.left + corner_radius, rect.top + corner_radius),
193                std::f64::consts::PI,
194            ),
195        ] {
196            path.try_arc_to(center, radii, start, std::f64::consts::FRAC_PI_2)?;
197        }
198        path.try_close()?;
199        Ok(Self { path })
200    }
201}
202
203stored_shape!(Line, "An open line segment with f64 endpoints.");
204
205impl Line {
206    /// Builds a segment without an implicit closed interior.
207    pub fn new(from: Point, to: Point) -> Result<Self, ValidationError> {
208        Ok(Self {
209            path: polyline(&[from, to], false)?,
210        })
211    }
212}
213
214stored_shape!(Polyline, "An open bounded sequence of line segments.");
215
216impl Polyline {
217    /// Captures at most MAX_PATH_VERBS-1 finite points; empty input is empty.
218    pub fn new(values: impl IntoIterator<Item = Point>) -> Result<Self, ValidationError> {
219        Ok(Self {
220            path: polyline(&points(values)?, false)?,
221        })
222    }
223}
224
225stored_shape!(
226    Polygon,
227    "A closed bounded polygon; self-intersections use the fill rule."
228);
229
230impl Polygon {
231    /// Captures points and closes the last edge to the first point.
232    pub fn new(values: impl IntoIterator<Item = Point>) -> Result<Self, ValidationError> {
233        Ok(Self {
234            path: polyline(&points(values)?, true)?,
235        })
236    }
237}
238
239fn radial(
240    center: Point,
241    outer: f64,
242    inner: Option<f64>,
243    count: usize,
244) -> Result<Path2D, ValidationError> {
245    radius(outer)?;
246    if let Some(inner) = inner {
247        radius(inner)?;
248        if inner > outer {
249            return Err(invalid(
250                "Star inner radius must not exceed its outer radius",
251            ));
252        }
253    }
254    let minimum = if inner.is_some() { 2 } else { 3 };
255    let vertices = count
256        .checked_mul(if inner.is_some() { 2 } else { 1 })
257        .filter(|count| *count < MAX_PATH_VERBS - 1);
258    let vertices = vertices
259        .filter(|_| count >= minimum)
260        .ok_or_else(|| invalid("Radial shape point count is outside its supported range"))?;
261    let values = (0..vertices)
262        .map(|index| {
263            let length = if index % 2 == 1 {
264                inner.unwrap_or(outer)
265            } else {
266                outer
267            };
268            center
269                + Vector2::from_angle(
270                    -std::f64::consts::FRAC_PI_2
271                        + std::f64::consts::TAU * index as f64 / vertices as f64,
272                ) * length
273        })
274        .collect::<Vec<_>>();
275    polyline(&values, true)
276}
277
278stored_shape!(
279    RegularPolygon,
280    "A regular polygon with its first vertex above the center."
281);
282
283impl RegularPolygon {
284    /// Builds at least three equally spaced vertices on a radius.
285    pub fn new(center: Point, radius: f64, sides: usize) -> Result<Self, ValidationError> {
286        Ok(Self {
287            path: radial(center, radius, None, sides)?,
288        })
289    }
290}
291
292stored_shape!(Star, "A closed star alternating outer and inner radii.");
293
294impl Star {
295    /// Builds at least two tips, alternating finite outer and inner radii.
296    pub fn new(
297        center: Point,
298        count: usize,
299        outer_radius: f64,
300        inner_radius: f64,
301    ) -> Result<Self, ValidationError> {
302        Ok(Self {
303            path: radial(center, outer_radius, Some(inner_radius), count)?,
304        })
305    }
306}
307
308/// A annulus or closed annular sector with opposite-winding inner contour.
309#[derive(Clone, Debug)]
310pub struct Ring {
311    center: Point,
312    inner: f64,
313    outer: f64,
314    start: f64,
315    sweep: f64,
316}
317
318impl Ring {
319    /// Builds a complete ring with 0 <= inner <= outer.
320    pub fn new(center: Point, inner_radius: f64, outer_radius: f64) -> Self {
321        Self {
322            center,
323            inner: inner_radius,
324            outer: outer_radius,
325            start: 0.0,
326            sweep: std::f64::consts::TAU,
327        }
328    }
329
330    /// Selects a clockwise-positive annular sector in radians; clamps to one turn.
331    pub fn sweep(mut self, start_angle: f64, sweep_angle: f64) -> Self {
332        self.start = start_angle;
333        self.sweep = sweep_angle;
334        self
335    }
336
337    /// Returns checked annular geometry; full rings retain a real winding hole.
338    fn checked_path(&self) -> Result<Path2D, ValidationError> {
339        radius(self.inner)?;
340        radius(self.outer)?;
341        if self.inner > self.outer || !self.sweep.is_finite() {
342            return Err(invalid("Ring requires inner <= outer and a finite sweep"));
343        }
344        let sweep = self
345            .sweep
346            .clamp(-std::f64::consts::TAU, std::f64::consts::TAU);
347        let mut path = Path2D::new();
348        path.try_arc_to(
349            self.center,
350            Vector2::new(self.outer, self.outer),
351            self.start,
352            sweep,
353        )?;
354        if sweep.abs() == std::f64::consts::TAU {
355            path.try_close()?;
356            if self.inner > 0.0 {
357                let inner = Arc::new(self.center, self.inner, self.start, -sweep)?.path;
358                for verb in inner.verbs() {
359                    append(&mut path, *verb)?;
360                }
361                path.try_close()?;
362            }
363        } else {
364            path.try_arc_to(
365                self.center,
366                Vector2::new(self.inner, self.inner),
367                self.start + sweep,
368                -sweep,
369            )?;
370            path.try_close()?;
371        }
372        Ok(path)
373    }
374}
375computed_shape!(Ring);
376
377fn append(path: &mut Path2D, verb: crate::PathVerb) -> Result<(), ValidationError> {
378    match verb {
379        crate::PathVerb::MoveTo(value) => path.try_move_to(value),
380        crate::PathVerb::LineTo(value) => path.try_line_to(value),
381        crate::PathVerb::QuadraticTo(control, end) => path.try_quadratic_to(control, end),
382        crate::PathVerb::CubicTo(first, second, end) => path.try_cubic_to(first, second, end),
383        crate::PathVerb::Close => path.try_close(),
384    }
385}
386
387/// arrow-head geometry; Vee is an open strokable head.
388#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
389#[cfg_attr(
390    feature = "serde",
391    derive(serde::Serialize, serde::Deserialize),
392    serde(rename_all = "snake_case")
393)]
394#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
395pub enum ArrowHead {
396    /// Filled triangular head integrated with the shaft.
397    #[default]
398    Triangle,
399    /// Two open sides, intended for stroke paint.
400    Vee,
401    /// Shaft segment only.
402    None,
403}
404
405/// A single-outline arrow, with independent shaft and head geometry.
406#[derive(Clone, Debug)]
407pub struct Arrow {
408    from: Point,
409    to: Point,
410    head: ArrowHead,
411    size: f64,
412    head_width: Option<f64>,
413    width: f64,
414}
415
416impl Arrow {
417    /// Creates a width-one shaft and a ten-unit triangular head.
418    pub fn new(from: Point, to: Point) -> Self {
419        Self {
420            from,
421            to,
422            head: ArrowHead::Triangle,
423            size: 10.0,
424            head_width: None,
425            width: 1.0,
426        }
427    }
428
429    /// Replaces head geometry.
430    pub fn head(mut self, head: ArrowHead) -> Self {
431        self.head = head;
432        self
433    }
434
435    /// Sets head length and width; head length clamps to endpoint distance.
436    pub fn head_size(mut self, size: f64) -> Self {
437        self.size = size;
438        self.head_width = None;
439        self
440    }
441
442    /// Sets independent positive head length and full width. Length clamps to
443    /// endpoint distance; explicit width stays unchanged for a short arrow.
444    pub fn head_dimensions(mut self, length: f64, width: f64) -> Self {
445        self.size = length;
446        self.head_width = Some(width);
447        self
448    }
449
450    /// Sets the full shaft width in logical coordinates.
451    pub fn width(mut self, width: f64) -> Self {
452        self.width = width;
453        self
454    }
455
456    /// Returns a single filled arrow, an open Vee or shaft-only segment.
457    fn checked_path(&self) -> Result<Path2D, ValidationError> {
458        radius(self.size)?;
459        radius(self.width)?;
460        if let Some(width) = self.head_width {
461            radius(width)?;
462            if self.size <= 0.0 || width <= 0.0 {
463                return Err(invalid("Explicit arrow head dimensions must be positive"));
464            }
465        }
466        let line = Line::new(self.from, self.to)?.path;
467        let displacement = self.to - self.from;
468        let length = displacement.length();
469        if !length.is_finite() {
470            return Err(invalid("Arrow length must remain finite"));
471        }
472        if length == 0.0 {
473            return Ok(Path2D::new());
474        }
475        if matches!(self.head, ArrowHead::None) {
476            return Ok(line);
477        }
478        let direction = displacement.normalize();
479        let normal = Vector2::new(-direction.y, direction.x);
480        let head = self.size.min(length);
481        let half_head_width = self.head_width.unwrap_or(head) * 0.5;
482        let neck = self.to - direction * head;
483        if matches!(self.head, ArrowHead::Vee) {
484            let mut path = line;
485            path.try_move_to(neck + normal * half_head_width)?;
486            path.try_line_to(self.to)?;
487            path.try_line_to(neck - normal * half_head_width)?;
488            return Ok(path);
489        }
490        polyline(
491            &[
492                self.from + normal * (self.width * 0.5),
493                neck + normal * (self.width * 0.5),
494                neck + normal * half_head_width,
495                self.to,
496                neck - normal * half_head_width,
497                neck - normal * (self.width * 0.5),
498                self.from - normal * (self.width * 0.5),
499            ],
500            true,
501        )
502    }
503}
504computed_shape!(Arrow);
505
506/// A cardinal Catmull-Rom interpolating spline with clamped endpoint neighbors.
507#[derive(Clone, Debug)]
508pub struct Spline {
509    points: Shared<[Point]>,
510    tension: f64,
511}
512
513impl Spline {
514    /// Captures a bounded point list, including empty/single-point degeneracies.
515    pub fn through(values: impl IntoIterator<Item = Point>) -> Result<Self, ValidationError> {
516        Ok(Self {
517            points: points(values)?,
518            tension: 0.0,
519        })
520    }
521
522    /// Sets tension in 0..=1; zero is Catmull-Rom, one has zero endpoint tangents.
523    pub fn tension(mut self, tension: f64) -> Self {
524        self.tension = tension;
525        self
526    }
527
528    /// Produces cubic spans passing through every supplied point.
529    fn checked_path(&self) -> Result<Path2D, ValidationError> {
530        if !self.tension.is_finite() || !(0.0..=1.0).contains(&self.tension) {
531            return Err(invalid("Spline tension must be in 0..=1"));
532        }
533        let values = &self.points;
534        let mut path = Path2D::new();
535        if let Some(first) = values.first() {
536            path.try_move_to(*first)?;
537        }
538        let factor = (1.0 - self.tension) / 6.0;
539        for index in 0..values.len().saturating_sub(1) {
540            let before = values[index.saturating_sub(1)];
541            let after = values[(index + 2).min(values.len() - 1)];
542            path.try_cubic_to(
543                values[index] + (values[index + 1] - before) * factor,
544                values[index + 1] - (after - values[index]) * factor,
545                values[index + 1],
546            )?;
547        }
548        Ok(path)
549    }
550}
551computed_shape!(Spline);
552
553/// A deterministically sampled function/parametric curve with finite output.
554/// Callbacks must be pure; their panics propagate like other user callbacks.
555#[derive(Clone)]
556pub struct FunctionGraph {
557    function: Shared<dyn Fn(f64) -> Point + Send + Sync>,
558    domain: Range<f64>,
559    samples: usize,
560}
561
562impl std::fmt::Debug for FunctionGraph {
563    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564        formatter
565            .debug_struct("FunctionGraph")
566            .field("domain", &self.domain)
567            .field("samples", &self.samples)
568            .finish_non_exhaustive()
569    }
570}
571
572impl FunctionGraph {
573    /// Samples y=f(x), including both domain endpoints, with 128 default points.
574    pub fn new(function: impl Fn(f64) -> f64 + Send + Sync + 'static, domain: Range<f64>) -> Self {
575        Self::parametric(
576            move |parameter| Point::new(parameter, function(parameter)),
577            domain,
578        )
579    }
580
581    /// Samples an explicit two-dimensional curve over the given parameter domain.
582    pub fn parametric(
583        function: impl Fn(f64) -> Point + Send + Sync + 'static,
584        domain: Range<f64>,
585    ) -> Self {
586        Self {
587            function: Shared::new(function),
588            domain,
589            samples: 128,
590        }
591    }
592
593    /// Sets 2..MAX_PATH_VERBS sample points; excessive requests fail before callbacks.
594    pub fn samples(mut self, samples: usize) -> Self {
595        self.samples = samples;
596        self
597    }
598
599    /// Evaluates a bounded sample list and rejects nonfinite output, never bridges gaps.
600    fn checked_path(&self) -> Result<Path2D, ValidationError> {
601        if !self.domain.start.is_finite()
602            || !self.domain.end.is_finite()
603            || self.domain.start >= self.domain.end
604            || !(2..MAX_PATH_VERBS).contains(&self.samples)
605        {
606            return Err(invalid(
607                "Function graph requires a finite increasing domain and bounded sample count",
608            ));
609        }
610        let mut path = Path2D::new();
611        for index in 0..self.samples {
612            let progress = index as f64 / (self.samples - 1) as f64;
613            let parameter = if index == 0 {
614                self.domain.start
615            } else if index + 1 == self.samples {
616                self.domain.end
617            } else {
618                self.domain.start * (1.0 - progress) + self.domain.end * progress
619            };
620            let value = (self.function)(parameter);
621            if index == 0 {
622                path.try_move_to(value)?;
623            } else {
624                path.try_line_to(value)?;
625            }
626        }
627        Ok(path)
628    }
629}
630computed_shape!(FunctionGraph);
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use crate::{Color, Node, PixelSize, Point, Rect, Size, ValidationLimits};
636
637    #[test]
638    fn arrows_keep_independent_head_dimensions_single_fill_and_empty_endpoints() {
639        for length in [40.0, 3.0] {
640            let arrow = Arrow::new(Point::ORIGIN, Point::new(length, 0.0))
641                .width(2.0)
642                .head_dimensions(12.0, 10.0);
643            let path = arrow.path().unwrap();
644            assert_eq!(path.bounding_box(), Rect::new(0.0, -5.0, length, 5.0));
645            assert!(path.verbs().iter().any(|verb| matches!(verb, crate::PathVerb::LineTo(point) if *point == Point::new(length, 0.0))));
646            let commands = crate::DrawContext::default()
647                .record(
648                    &arrow.fill(Color::from_hex(0x118677)),
649                    ValidationLimits::default(),
650                )
651                .unwrap();
652            assert_eq!(commands.len(), 1);
653            assert!(matches!(
654                commands[0],
655                crate::DrawCommand::Fill {
656                    rule: crate::FillRule::NonZero,
657                    ..
658                }
659            ));
660        }
661        let empty = Arrow::new(Point::new(2.0, 3.0), Point::new(2.0, 3.0))
662            .head_dimensions(12.0, 10.0)
663            .path()
664            .unwrap();
665        assert!(empty.verbs().is_empty());
666        for invalid in [
667            Arrow::new(Point::ORIGIN, Point::ORIGIN).head_dimensions(0.0, 10.0),
668            Arrow::new(Point::ORIGIN, Point::ORIGIN).head_dimensions(12.0, f64::NAN),
669        ] {
670            assert!(invalid.path().is_err());
671        }
672    }
673
674    #[test]
675    fn shapes_validate_bounds_degeneracies_splines_and_function_samples() {
676        assert_eq!(
677            Circle::new(Point::ORIGIN, 3.0)
678                .unwrap()
679                .bounding_box()
680                .unwrap(),
681            Rect::new(-3.0, -3.0, 3.0, 3.0)
682        );
683        assert_eq!(
684            RoundedRect::new(Rect::new(0.0, 0.0, 6.0, 4.0), 100.0)
685                .unwrap()
686                .bounding_box()
687                .unwrap(),
688            Rect::new(0.0, 0.0, 6.0, 4.0)
689        );
690        assert!(Circle::new(Point::ORIGIN, -1.0).is_err());
691        assert!(Star::new(Point::ORIGIN, usize::MAX, 2.0, 1.0).is_err());
692        assert!(RegularPolygon::new(Point::ORIGIN, 1.0, 2).is_err());
693        assert!(Ring::new(Point::ORIGIN, 3.0, 2.0).path().is_err());
694        assert!(
695            Arrow::new(Point::ORIGIN, Point::ORIGIN)
696                .path()
697                .unwrap()
698                .verbs()
699                .is_empty()
700        );
701        assert!(
702            Circle::new(Point::ORIGIN, 0.0)
703                .unwrap()
704                .path()
705                .unwrap()
706                .length()
707                == 0.0
708        );
709        let knots = [Point::ORIGIN, Point::new(2.0, 3.0), Point::new(4.0, 1.0)];
710        let spline = Spline::through(knots).unwrap().path().unwrap();
711        assert!(
712            matches!(spline.verbs()[1], crate::PathVerb::CubicTo(_, _, end) if end == knots[1])
713        );
714        assert!(
715            Spline::through(knots)
716                .unwrap()
717                .tension(f64::NAN)
718                .path()
719                .is_err()
720        );
721        assert!(Spline::through([Point::new(f64::NAN, 0.0)]).is_err());
722        let graph = FunctionGraph::new(|value| value * value, -2.0..2.0)
723            .samples(5)
724            .path()
725            .unwrap();
726        assert_eq!(
727            graph.verbs()[0],
728            crate::PathVerb::MoveTo(Point::new(-2.0, 4.0))
729        );
730        assert_eq!(
731            graph.verbs().last(),
732            Some(&crate::PathVerb::LineTo(Point::new(2.0, 4.0)))
733        );
734        assert!(FunctionGraph::new(|_| f64::NAN, 0.0..1.0).path().is_err());
735        assert!(
736            FunctionGraph::new(|_| panic!("Invalid count must precede callback"), 0.0..1.0)
737                .samples(usize::MAX)
738                .path()
739                .is_err()
740        );
741        assert!(
742            Node::from(Rect::new(f64::NAN, 0.0, 1.0, 1.0).fill(Color::WHITE))
743                .compile(
744                    Size::new(8.0, 8.0),
745                    PixelSize::new(8, 8),
746                    ValidationLimits::default()
747                )
748                .is_err()
749        );
750    }
751}