Skip to main content

fidget_shapes/
lib.rs

1//! Standard library of shapes and transforms
2//!
3//! These shapes are designed for use in higher-level languages and tools.
4//! Each shape is a single `struct` with named member variables.  The "output"
5//! of the shape is always a [`Tree`], and can be generated by calling
6//! `Tree::from(..)`.  Shape member variables can always be represented by
7//! variants of [`Value`](types::Value) (checked by a unit test).
8//!
9//! Every shape implements `Facet + Clone + Send + Sync + Into<Tree> + 'static`.
10//! When generating bindings, users are expected to inspect the shape's type
11//! using annotations provided by the [`facet`] crate; to iterate over the
12//! entire shape library, use [`ShapeVisitor`] and [`visit_shapes`].
13//!
14//! For an example of binding shapes into a dynamic language, look at the
15//! implementation of `fidget_rhai::shapes` (specifically the internal
16//! `register_shape` function).
17#![warn(missing_docs)]
18use facet::Facet;
19use fidget_core::context::Tree;
20
21pub mod types;
22use types::{Axis, Plane, Vec2, Vec3};
23
24////////////////////////////////////////////////////////////////////////////////
25// 2D shapes
26
27/// 2D circle
28#[derive(Clone, Facet)]
29pub struct Circle {
30    /// Center of the circle (in XY)
31    #[facet(default = Vec2::new(0.0, 0.0))]
32    pub center: Vec2,
33    /// Circle radius
34    #[facet(default = 1.0)]
35    pub radius: f64,
36}
37
38impl From<Circle> for Tree {
39    fn from(v: Circle) -> Self {
40        let (x, y, _) = Tree::axes();
41        ((x - v.center.x).square() + (y - v.center.y).square()).sqrt()
42            - v.radius
43    }
44}
45
46/// Rectangle defined by lower and upper corners
47#[derive(Clone, Facet)]
48pub struct Rectangle {
49    /// Lower corner of the rectangle
50    pub lower: Vec2,
51    /// Upper corner of the rectangle
52    pub upper: Vec2,
53}
54
55impl From<Rectangle> for Tree {
56    fn from(v: Rectangle) -> Self {
57        let (x, y, _) = Tree::axes();
58        (v.lower.x - x.clone())
59            .max(x - v.upper.x)
60            .max((v.lower.y - y.clone()).max(y - v.upper.y))
61    }
62}
63
64////////////////////////////////////////////////////////////////////////////////
65// 3D shapes
66
67/// 3D sphere
68#[derive(Clone, Facet)]
69pub struct Sphere {
70    /// Center of the circle (in XYZ)
71    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
72    pub center: Vec3,
73    /// Sphere radius
74    #[facet(default = 1.0)]
75    pub radius: f64,
76}
77
78impl From<Sphere> for Tree {
79    fn from(v: Sphere) -> Self {
80        let (x, y, z) = Tree::axes();
81        ((x - v.center.x).square()
82            + (y - v.center.y).square()
83            + (z - v.center.z).square())
84        .sqrt()
85            - v.radius
86    }
87}
88
89/// Box defined by lower and upper corners
90#[derive(Clone, Facet)]
91pub struct Box {
92    /// Lower corner of the rectangle
93    pub lower: Vec3,
94    /// Upper corner of the rectangle
95    pub upper: Vec3,
96}
97
98impl From<Box> for Tree {
99    fn from(v: Box) -> Self {
100        let (x, y, z) = Tree::axes();
101        (v.lower.x - x.clone())
102            .max(x - v.upper.x)
103            .max((v.lower.y - y.clone()).max(y - v.upper.y))
104            .max((v.lower.z - z.clone()).max(z - v.upper.z))
105    }
106}
107
108////////////////////////////////////////////////////////////////////////////////
109// CSG operations
110
111/// Take the union of a set of shapes
112///
113/// If the input is empty, returns an constant empty tree (at +∞)
114#[derive(Clone, Facet)]
115pub struct Union {
116    /// List of shapes to merge
117    pub input: Vec<Tree>,
118}
119
120impl From<Union> for Tree {
121    fn from(v: Union) -> Self {
122        if v.input.is_empty() {
123            // XXX should this be an error instead?
124            Tree::constant(f64::INFINITY)
125        } else {
126            fn recurse(s: &[Tree]) -> Tree {
127                match s.len() {
128                    1 => s[0].clone(),
129                    n => recurse(&s[..n / 2]).min(recurse(&s[n / 2..])),
130                }
131            }
132            recurse(&v.input)
133        }
134    }
135}
136
137/// Smooth quadratic blend of two shapes
138///
139/// This formula is taken from "Lipschitz Pruning: Hierarchical Simplification
140/// of Primitive-Based SDFs" (Barbier _et al_ '25), which in turn cites
141/// [Quilez '20](https://iquilezles.org/articles/smin/)
142#[derive(Clone, Facet)]
143pub struct Blend {
144    /// First shape input
145    pub a: Tree,
146    /// Second shape input
147    pub b: Tree,
148    /// Blending radius
149    pub radius: f64,
150}
151
152impl From<Blend> for Tree {
153    fn from(v: Blend) -> Self {
154        if v.radius > 0.0 {
155            v.a.clone().min(v.b.clone())
156                - 1.0 / (4.0 * v.radius)
157                    * (v.radius - (v.a - v.b).abs()).max(0.0).square()
158        } else {
159            v.a.min(v.b)
160        }
161    }
162}
163
164/// Take the intersection of a set of shapes
165///
166/// If the input is empty, returns a constant full tree (at -∞)
167#[derive(Clone, Facet)]
168pub struct Intersection {
169    /// List of shapes to intersect
170    pub input: Vec<Tree>,
171}
172
173impl From<Intersection> for Tree {
174    fn from(v: Intersection) -> Self {
175        if v.input.is_empty() {
176            // XXX should this be an error instead?
177            Tree::constant(-f64::INFINITY)
178        } else {
179            fn recurse(s: &[Tree]) -> Tree {
180                match s.len() {
181                    1 => s[0].clone(),
182                    n => recurse(&s[..n / 2]).max(recurse(&s[n / 2..])),
183                }
184            }
185            recurse(&v.input)
186        }
187    }
188}
189
190/// Computes the inverse of a shape
191#[derive(Clone, Facet)]
192pub struct Inverse {
193    /// Shape to invert
194    pub shape: Tree,
195}
196
197impl From<Inverse> for Tree {
198    fn from(v: Inverse) -> Self {
199        -v.shape
200    }
201}
202
203/// Take the difference of two shapes
204#[derive(Clone, Facet)]
205pub struct Difference {
206    /// Original shape
207    pub shape: Tree,
208    /// Shape to be subtracted from the original
209    pub cutout: Tree,
210}
211
212impl From<Difference> for Tree {
213    fn from(v: Difference) -> Self {
214        v.shape.max(-v.cutout)
215    }
216}
217
218////////////////////////////////////////////////////////////////////////////////
219// Transforms
220
221/// Move a shape
222#[derive(Clone, Facet)]
223pub struct Move {
224    /// Shape to move
225    pub shape: Tree,
226    /// Position offset
227    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
228    pub offset: Vec3,
229}
230
231impl From<Move> for Tree {
232    fn from(v: Move) -> Self {
233        v.shape.remap_affine(nalgebra::convert(
234            nalgebra::Translation3::<f64>::new(
235                -v.offset.x,
236                -v.offset.y,
237                -v.offset.z,
238            ),
239        ))
240    }
241}
242
243/// Non-uniform scaling
244#[derive(Clone, Facet)]
245pub struct Scale {
246    /// Shape to scale
247    pub shape: Tree,
248    /// Scale to apply on each axis
249    #[facet(default = Vec3::new(1.0, 1.0, 1.0))]
250    pub scale: Vec3,
251}
252
253impl From<Scale> for Tree {
254    fn from(v: Scale) -> Self {
255        v.shape
256            .remap_affine(nalgebra::convert(nalgebra::Scale3::<f64>::new(
257                1.0 / v.scale.x,
258                1.0 / v.scale.y,
259                1.0 / v.scale.z,
260            )))
261    }
262}
263
264/// Uniform scaling
265#[derive(Clone, Facet)]
266pub struct ScaleUniform {
267    /// Shape to scale
268    pub shape: Tree,
269    /// Scale to apply
270    #[facet(default = 1.0)]
271    pub scale: f64,
272}
273
274impl From<ScaleUniform> for Tree {
275    fn from(v: ScaleUniform) -> Self {
276        let s = 1.0 / v.scale;
277        v.shape
278            .remap_affine(nalgebra::convert(nalgebra::Scale3::<f64>::new(
279                s, s, s,
280            )))
281    }
282}
283
284/// Reflection
285#[derive(Clone, Facet)]
286pub struct Reflect {
287    /// Shape to reflect
288    pub shape: Tree,
289
290    /// Plane about which to reflect the shape
291    #[facet(default = Plane::YZ)]
292    pub plane: Plane,
293}
294
295impl From<Reflect> for Tree {
296    fn from(v: Reflect) -> Self {
297        let a = v.plane.axis.vec();
298        let (x, y, z) = Tree::axes();
299        let d = a.x * x.clone() + a.y * y.clone() + a.z * z.clone()
300            - v.plane.offset;
301        let scale: Tree = 2.0 * d;
302        // TODO could we use nalgebra::Reflection3 here to make the transform
303        // affine?  For some reason, it doesn't implement the right SubSet
304        // https://github.com/dimforge/nalgebra/issues/1527
305        v.shape.remap_xyz(
306            x - scale.clone() * a.x,
307            y - scale.clone() * a.y,
308            z - scale * a.z,
309        )
310    }
311}
312
313/// Reflection on the X axis
314#[derive(Clone, Facet)]
315pub struct ReflectX {
316    /// Shape to reflect
317    pub shape: Tree,
318
319    /// X offset
320    #[facet(default = 0.0)]
321    pub offset: f64,
322}
323
324impl From<ReflectX> for Tree {
325    fn from(v: ReflectX) -> Self {
326        Reflect {
327            shape: v.shape,
328            plane: Plane {
329                axis: Axis::X,
330                offset: v.offset,
331            },
332        }
333        .into()
334    }
335}
336
337/// Reflection about the `X = Y` line
338#[derive(Clone, Facet)]
339pub struct ReflectXY {
340    /// Shape to reflect
341    pub shape: Tree,
342
343    /// Plane about which to reflect the shape
344    #[facet(default = 0.0)]
345    pub offset: f64,
346}
347
348impl From<ReflectXY> for Tree {
349    fn from(v: ReflectXY) -> Self {
350        Reflect {
351            shape: v.shape,
352            plane: Plane {
353                axis: Axis::try_from(Vec3::new(-1.0, 1.0, 0.0)).unwrap(),
354                offset: v.offset,
355            },
356        }
357        .into()
358    }
359}
360
361/// Reflection on the Y axis
362#[derive(Clone, Facet)]
363pub struct ReflectY {
364    /// Shape to reflect
365    pub shape: Tree,
366
367    /// Y offset
368    #[facet(default = 0.0)]
369    pub offset: f64,
370}
371
372impl From<ReflectY> for Tree {
373    fn from(v: ReflectY) -> Self {
374        Reflect {
375            shape: v.shape,
376            plane: Plane {
377                axis: Axis::Y,
378                offset: v.offset,
379            },
380        }
381        .into()
382    }
383}
384
385/// Reflection on the Z axis
386#[derive(Clone, Facet)]
387pub struct ReflectZ {
388    /// Shape to reflect
389    pub shape: Tree,
390
391    /// Z offset
392    #[facet(default = 0.0)]
393    pub offset: f64,
394}
395
396impl From<ReflectZ> for Tree {
397    fn from(v: ReflectZ) -> Self {
398        Reflect {
399            shape: v.shape,
400            plane: Plane {
401                axis: Axis::Z,
402                offset: v.offset,
403            },
404        }
405        .into()
406    }
407}
408
409/// Rotates an object about an arbitrary axis and rotation center
410#[derive(Clone, Facet)]
411pub struct Rotate {
412    /// Shape to rotate
413    pub shape: Tree,
414
415    /// Axis about which to rotate
416    #[facet(default = Axis::Z)]
417    pub axis: Axis,
418
419    /// Angle to rotate (in degrees)
420    #[facet(default = 0.0)]
421    pub angle: f64,
422
423    /// Center of rotation
424    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
425    pub center: Vec3,
426}
427
428impl From<Rotate> for Tree {
429    fn from(v: Rotate) -> Self {
430        let shape = Tree::from(Move {
431            shape: v.shape,
432            offset: -v.center,
433        });
434        let d = -v.angle.to_radians();
435        let axis = v.axis.vec();
436        let shape = shape.remap_affine(nalgebra::convert(
437            nalgebra::Rotation3::<f64>::new(nalgebra::Vector3::from(d * *axis)),
438        ));
439        Move {
440            shape,
441            offset: v.center,
442        }
443        .into()
444    }
445}
446
447/// Rotates an object about the X axis
448#[derive(Clone, Facet)]
449pub struct RotateX {
450    /// Shape to rotate
451    pub shape: Tree,
452
453    /// Angle to rotate (in degrees)
454    #[facet(default = 0.0)]
455    pub angle: f64,
456
457    /// Center of rotation
458    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
459    pub center: Vec3,
460}
461
462impl From<RotateX> for Tree {
463    fn from(v: RotateX) -> Self {
464        Rotate {
465            shape: v.shape,
466            angle: v.angle,
467            center: v.center,
468            axis: Axis::X,
469        }
470        .into()
471    }
472}
473
474/// Rotates an object about the Y axis
475#[derive(Clone, Facet)]
476pub struct RotateY {
477    /// Shape to rotate
478    pub shape: Tree,
479
480    /// Angle to rotate (in degrees)
481    #[facet(default = 0.0)]
482    pub angle: f64,
483
484    /// Center of rotation
485    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
486    pub center: Vec3,
487}
488
489impl From<RotateY> for Tree {
490    fn from(v: RotateY) -> Self {
491        Rotate {
492            shape: v.shape,
493            angle: v.angle,
494            center: v.center,
495            axis: Axis::Y,
496        }
497        .into()
498    }
499}
500
501/// Rotates an object about the Z axis
502#[derive(Clone, Facet)]
503pub struct RotateZ {
504    /// Shape to rotate
505    pub shape: Tree,
506
507    /// Angle to rotate (in degrees)
508    #[facet(default = 0.0)]
509    pub angle: f64,
510
511    /// Center of rotation
512    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
513    pub center: Vec3,
514}
515
516impl From<RotateZ> for Tree {
517    fn from(v: RotateZ) -> Self {
518        Rotate {
519            shape: v.shape,
520            angle: v.angle,
521            center: v.center,
522            axis: Axis::Z,
523        }
524        .into()
525    }
526}
527
528// TODO figure out a generic Revolve?  The matrix math is a bit tricky!
529
530/// Revolve a shape about the Y axis, creating a 3D volume
531#[derive(Clone, Facet)]
532pub struct RevolveY {
533    /// Shape to revolve
534    pub shape: Tree,
535    /// X offset about which to revolve
536    #[facet(default = 0.0)]
537    pub offset: f64,
538}
539
540impl From<RevolveY> for Tree {
541    fn from(v: RevolveY) -> Self {
542        let offset = Vec3::new(-v.offset, 0.0, 0.0);
543        let shape = Tree::from(Move {
544            shape: v.shape.clone(),
545            offset: -offset,
546        });
547        let (x, y, z) = Tree::axes();
548        let r = (x.square() + y.square()).sqrt();
549        let shape = shape.remap_xyz(r, y, z);
550        Move { shape, offset }.into()
551    }
552}
553
554/// Extrude an XY shape in the Z direction
555#[derive(Clone, Facet)]
556pub struct ExtrudeZ {
557    /// Shape to extrude
558    pub shape: Tree,
559    /// Lower bounds of the extrusion
560    #[facet(default = 0.0)]
561    pub lower: f64,
562    /// Upper bounds of the extrusion
563    #[facet(default = 1.0)]
564    pub upper: f64,
565}
566
567impl From<ExtrudeZ> for Tree {
568    fn from(v: ExtrudeZ) -> Self {
569        let (x, y, z) = Tree::axes();
570        let t = v.shape.remap_xyz(x, y, Tree::constant(0.0));
571        t.max((v.lower - z.clone()).max(z - v.upper))
572    }
573}
574
575/// Loft between two XY shape in the Z direction
576#[derive(Clone, Facet)]
577pub struct LoftZ {
578    /// Lower shape
579    pub a: Tree,
580    /// Upper shape
581    pub b: Tree,
582    /// Lower bounds of the loft
583    #[facet(default = 0.0)]
584    pub lower: f64,
585    /// Upper bounds of the loft
586    #[facet(default = 1.0)]
587    pub upper: f64,
588}
589
590impl From<LoftZ> for Tree {
591    fn from(v: LoftZ) -> Self {
592        let (x, y, z) = Tree::axes();
593        let ta = v.a.remap_xyz(x.clone(), y.clone(), Tree::constant(0.0));
594        let tb = v.b.remap_xyz(x, y, Tree::constant(0.0));
595        let t = ((z.clone() - v.lower) * tb + (v.upper - z.clone()) * ta)
596            / (v.upper - v.lower);
597        t.max((v.lower - z.clone()).max(z - v.upper))
598    }
599}
600
601/// Repeat a shape in the X axis
602///
603/// This uses the modulo operator, which may introduce discontinuities; shapes
604/// should be designed to have the same value at `x = ±radius`.
605#[derive(Clone, Facet)]
606pub struct RepeatX {
607    /// Shape to repeat
608    pub shape: Tree,
609    /// Radius of the region to repeat
610    #[facet(default = 1.0)]
611    pub radius: f64,
612    /// X position about which to repeat
613    #[facet(default = 0.0)]
614    pub offset: f64,
615}
616
617impl From<RepeatX> for Tree {
618    fn from(value: RepeatX) -> Self {
619        let (x, y, z) = Tree::axes();
620        let r = value.radius - value.offset;
621        value
622            .shape
623            .remap_xyz(((x + r).modulo(value.radius * 2.0)) - r, y, z)
624    }
625}
626
627////////////////////////////////////////////////////////////////////////////////
628
629/// Trait for a type which can visit each of the shapes in our library
630pub trait ShapeVisitor {
631    /// Process the given type
632    fn visit<T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static>(
633        &mut self,
634    );
635}
636
637/// Maps a shape visitor across all shape definitions
638pub fn visit_shapes<V: ShapeVisitor>(visitor: &mut V) {
639    visitor.visit::<Sphere>();
640    visitor.visit::<Box>();
641    visitor.visit::<Plane>();
642
643    visitor.visit::<Circle>();
644    visitor.visit::<Rectangle>();
645
646    visitor.visit::<Move>();
647    visitor.visit::<Scale>();
648    visitor.visit::<ScaleUniform>();
649    visitor.visit::<Reflect>();
650    visitor.visit::<ReflectX>();
651    visitor.visit::<ReflectY>();
652    visitor.visit::<ReflectZ>();
653    visitor.visit::<ReflectXY>();
654    visitor.visit::<RepeatX>();
655    visitor.visit::<Rotate>();
656    visitor.visit::<RotateX>();
657    visitor.visit::<RotateY>();
658    visitor.visit::<RotateZ>();
659    visitor.visit::<RevolveY>();
660    visitor.visit::<ExtrudeZ>();
661    visitor.visit::<LoftZ>();
662
663    visitor.visit::<Union>();
664    visitor.visit::<Blend>();
665    visitor.visit::<Intersection>();
666    visitor.visit::<Difference>();
667    visitor.visit::<Inverse>();
668}
669
670////////////////////////////////////////////////////////////////////////////////
671
672#[cfg(test)]
673mod test {
674    use super::*;
675    use crate::types::eval_default_fn;
676    use fidget_core::Context;
677
678    #[test]
679    fn circle_docstring() {
680        assert_eq!(Circle::SHAPE.doc, &[" 2D circle"]);
681    }
682
683    #[test]
684    fn transform_order() {
685        let x = Tree::x();
686        let moved: Tree = Move {
687            shape: x,
688            offset: Vec3::new(-1.0, 0.0, 0.0),
689        }
690        .into();
691        let mut ctx = Context::new();
692        let cm = ctx.import(&moved);
693        assert_eq!(ctx.eval_xyz(cm, 0.0, 0.0, 0.0).unwrap(), 1.0);
694        assert_eq!(ctx.eval_xyz(cm, 0.0, 1.0, 0.0).unwrap(), 1.0);
695        assert_eq!(ctx.eval_xyz(cm, -1.0, 0.0, 0.0).unwrap(), 0.0);
696
697        let rotated: Tree = RotateZ {
698            shape: moved,
699            angle: 90.0,
700            center: Vec3::new(0.0, 0.0, 0.0),
701        }
702        .into();
703        let cr = ctx.import(&rotated);
704        assert_eq!(ctx.eval_xyz(cr, 0.0, 0.0, 0.0).unwrap(), 1.0);
705        assert_eq!(ctx.eval_xyz(cr, 0.0, -1.0, 0.0).unwrap(), 0.0);
706        assert_eq!(ctx.eval_xyz(cr, 0.0, 1.0, 0.0).unwrap(), 2.0);
707    }
708
709    #[test]
710    fn scale_default_fn() {
711        let facet::Type::User(facet::UserType::Struct(s)) = Scale::SHAPE.ty
712        else {
713            panic!();
714        };
715        for f in s.fields {
716            if f.name == "scale" {
717                let Some(facet::DefaultSource::Custom(f)) = f.default else {
718                    panic!()
719                };
720                let v: Vec3 = unsafe { eval_default_fn(f) };
721                assert_eq!(v.x, 1.0);
722                assert_eq!(v.y, 1.0);
723                assert_eq!(v.z, 1.0);
724            } else {
725                assert!(f.default.is_none());
726            }
727        }
728    }
729
730    struct ValidateVisitor;
731    impl ShapeVisitor for ValidateVisitor {
732        fn visit<
733            T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static,
734        >(
735            &mut self,
736        ) {
737            let facet::Type::User(facet::UserType::Struct(s)) = T::SHAPE.ty
738            else {
739                panic!("shape `{}` must be a struct", T::SHAPE.type_name());
740            };
741            for f in s.fields {
742                if types::Type::try_from(f.shape().id).is_err() {
743                    panic!(
744                        "field `{}` in `{}` has unhandled type: {}",
745                        f.name,
746                        T::SHAPE.type_name(),
747                        f.shape()
748                    );
749                }
750                if let Some(d) = f.default {
751                    assert!(
752                        matches!(d, facet::DefaultSource::Custom(..)),
753                        "default on field `{}` in `{}` must include value",
754                        f.name,
755                        T::SHAPE.type_name()
756                    );
757                }
758            }
759        }
760    }
761
762    #[test]
763    fn validate_shapes() {
764        let mut v = ValidateVisitor;
765        visit_shapes(&mut v);
766    }
767
768    #[test]
769    #[should_panic(
770        expected = "field `uhoh` in `BadShape` has unhandled type: String"
771    )]
772    fn bad_shape_type() {
773        #[derive(Clone, facet::Facet)]
774        struct BadShape {
775            uhoh: String,
776        }
777        impl From<BadShape> for Tree {
778            fn from(_: BadShape) -> Tree {
779                unimplemented!()
780            }
781        }
782        let mut v = ValidateVisitor;
783        v.visit::<BadShape>();
784    }
785
786    #[test]
787    #[should_panic(
788        expected = "default on field `center` in `BadShape` must include value"
789    )]
790    fn bad_shape_default() {
791        #[derive(Clone, facet::Facet)]
792        struct BadShape {
793            #[facet(default)]
794            center: f64,
795        }
796        impl From<BadShape> for Tree {
797            fn from(_: BadShape) -> Tree {
798                unimplemented!()
799            }
800        }
801        let mut v = ValidateVisitor;
802        v.visit::<BadShape>();
803    }
804
805    #[test]
806    #[should_panic(expected = "shape `BadShapeEnum` must be a struct")]
807    fn bad_shape_shape() {
808        #[derive(Clone, facet::Facet)]
809        #[repr(C)]
810        enum BadShapeEnum {
811            UhOh,
812        }
813        impl From<BadShapeEnum> for Tree {
814            fn from(_: BadShapeEnum) -> Tree {
815                unimplemented!()
816            }
817        }
818        let mut v = ValidateVisitor;
819        v.visit::<BadShapeEnum>();
820    }
821
822    #[test]
823    fn repeat_x() {
824        let c = Circle {
825            center: Vec2::new(0.0, 0.0),
826            radius: 0.5,
827        };
828        let r = RepeatX {
829            shape: c.clone().into(),
830            radius: 1.0,
831            offset: 0.0,
832        };
833        let mut ctx = Context::new();
834        let c = ctx.import(&c.into());
835        let r = ctx.import(&r.into());
836
837        // Check the three repetitions closest to the center
838        for i in 0..=1000 {
839            let x = (i as f64 / 1000.0) - (1000.0 - i as f64) / 1000.0;
840            let vc = ctx.eval_xyz(c, x, 0.0, 0.0).unwrap();
841            let err = (vc - ctx.eval_xyz(r, x, 0.0, 0.0).unwrap()).abs();
842            assert!(err < 1e-6, "bad err {err} at {x}");
843            let err = (vc - ctx.eval_xyz(r, x + 2.0, 0.0, 0.0).unwrap()).abs();
844            assert!(err < 1e-6, "bad err {err} at {x} (+1)");
845            let err = (vc - ctx.eval_xyz(r, x - 2.0, 0.0, 0.0).unwrap()).abs();
846            assert!(err < 1e-6, "bad err {err} at {x} (-1)");
847        }
848
849        // Test with a circle centered at x = 1, with a smaller repeat radius
850        let c = Circle {
851            center: Vec2::new(1.0, 0.0),
852            radius: 0.5,
853        };
854        let r = RepeatX {
855            shape: c.clone().into(),
856            radius: 0.75,
857            offset: 1.0,
858        };
859        let mut ctx = Context::new();
860        let c = ctx.import(&c.into());
861        let r = ctx.import(&r.into());
862
863        // Check the three repetitions closest to the center
864        for i in 0..=1000 {
865            let x = 0.75 * ((i as f64 / 1000.0) - (1000.0 - i as f64) / 1000.0)
866                + 1.0;
867            let vc = ctx.eval_xyz(c, x, 0.0, 0.0).unwrap();
868            let err = (vc - ctx.eval_xyz(r, x, 0.0, 0.0).unwrap()).abs();
869            assert!(err < 1e-6, "bad err {err} at {x}");
870            let err = (vc - ctx.eval_xyz(r, x + 1.5, 0.0, 0.0).unwrap()).abs();
871            assert!(err < 1e-6, "bad err {err} at {x} (+1)");
872            let err = (vc - ctx.eval_xyz(r, x - 1.5, 0.0, 0.0).unwrap()).abs();
873            assert!(err < 1e-6, "bad err {err} at {x} (-1)");
874        }
875    }
876}