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 about the X axis
314#[derive(Clone, Facet)]
315pub struct ReflectX {
316    /// Shape to reflect
317    pub shape: Tree,
318
319    /// Plane about which to reflect the shape
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 Y axis
338#[derive(Clone, Facet)]
339pub struct ReflectY {
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<ReflectY> for Tree {
349    fn from(v: ReflectY) -> Self {
350        Reflect {
351            shape: v.shape,
352            plane: Plane {
353                axis: Axis::Y,
354                offset: v.offset,
355            },
356        }
357        .into()
358    }
359}
360
361/// Reflection about the Z axis
362#[derive(Clone, Facet)]
363pub struct ReflectZ {
364    /// Shape to reflect
365    pub shape: Tree,
366
367    /// Plane about which to reflect the shape
368    #[facet(default = 0.0)]
369    pub offset: f64,
370}
371
372impl From<ReflectZ> for Tree {
373    fn from(v: ReflectZ) -> Self {
374        Reflect {
375            shape: v.shape,
376            plane: Plane {
377                axis: Axis::Z,
378                offset: v.offset,
379            },
380        }
381        .into()
382    }
383}
384
385/// Rotates an object about an arbitrary axis and rotation center
386#[derive(Clone, Facet)]
387pub struct Rotate {
388    /// Shape to rotate
389    pub shape: Tree,
390
391    /// Axis about which to rotate
392    #[facet(default = Axis::Z)]
393    pub axis: Axis,
394
395    /// Angle to rotate (in degrees)
396    #[facet(default = 0.0)]
397    pub angle: f64,
398
399    /// Center of rotation
400    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
401    pub center: Vec3,
402}
403
404impl From<Rotate> for Tree {
405    fn from(v: Rotate) -> Self {
406        let shape = Tree::from(Move {
407            shape: v.shape,
408            offset: -v.center,
409        });
410        let d = -v.angle.to_radians();
411        let axis = v.axis.vec();
412        let shape = shape.remap_affine(nalgebra::convert(
413            nalgebra::Rotation3::<f64>::new(nalgebra::Vector3::from(d * *axis)),
414        ));
415        Move {
416            shape,
417            offset: v.center,
418        }
419        .into()
420    }
421}
422
423/// Rotates an object about the X axis
424#[derive(Clone, Facet)]
425pub struct RotateX {
426    /// Shape to rotate
427    pub shape: Tree,
428
429    /// Angle to rotate (in degrees)
430    #[facet(default = 0.0)]
431    pub angle: f64,
432
433    /// Center of rotation
434    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
435    pub center: Vec3,
436}
437
438impl From<RotateX> for Tree {
439    fn from(v: RotateX) -> Self {
440        Rotate {
441            shape: v.shape,
442            angle: v.angle,
443            center: v.center,
444            axis: Axis::X,
445        }
446        .into()
447    }
448}
449
450/// Rotates an object about the Y axis
451#[derive(Clone, Facet)]
452pub struct RotateY {
453    /// Shape to rotate
454    pub shape: Tree,
455
456    /// Angle to rotate (in degrees)
457    #[facet(default = 0.0)]
458    pub angle: f64,
459
460    /// Center of rotation
461    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
462    pub center: Vec3,
463}
464
465impl From<RotateY> for Tree {
466    fn from(v: RotateY) -> Self {
467        Rotate {
468            shape: v.shape,
469            angle: v.angle,
470            center: v.center,
471            axis: Axis::Y,
472        }
473        .into()
474    }
475}
476
477/// Rotates an object about the Z axis
478#[derive(Clone, Facet)]
479pub struct RotateZ {
480    /// Shape to rotate
481    pub shape: Tree,
482
483    /// Angle to rotate (in degrees)
484    #[facet(default = 0.0)]
485    pub angle: f64,
486
487    /// Center of rotation
488    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
489    pub center: Vec3,
490}
491
492impl From<RotateZ> for Tree {
493    fn from(v: RotateZ) -> Self {
494        Rotate {
495            shape: v.shape,
496            angle: v.angle,
497            center: v.center,
498            axis: Axis::Z,
499        }
500        .into()
501    }
502}
503
504// TODO figure out a generic Revolve?  The matrix math is a bit tricky!
505
506/// Revolve a shape about the Y axis, creating a 3D volume
507#[derive(Clone, Facet)]
508pub struct RevolveY {
509    /// Shape to revolve
510    pub shape: Tree,
511    /// X offset about which to revolve
512    #[facet(default = 0.0)]
513    pub offset: f64,
514}
515
516impl From<RevolveY> for Tree {
517    fn from(v: RevolveY) -> Self {
518        let offset = Vec3::new(-v.offset, 0.0, 0.0);
519        let shape = Tree::from(Move {
520            shape: v.shape.clone(),
521            offset: -offset,
522        });
523        let (x, y, z) = Tree::axes();
524        let r = (x.square() + y.square()).sqrt();
525        let shape = shape.remap_xyz(r, y, z);
526        Move { shape, offset }.into()
527    }
528}
529
530/// Extrude an XY shape in the Z direction
531#[derive(Clone, Facet)]
532pub struct ExtrudeZ {
533    /// Shape to extrude
534    pub shape: Tree,
535    /// Lower bounds of the extrusion
536    #[facet(default = 0.0)]
537    pub lower: f64,
538    /// Upper bounds of the extrusion
539    #[facet(default = 1.0)]
540    pub upper: f64,
541}
542
543impl From<ExtrudeZ> for Tree {
544    fn from(v: ExtrudeZ) -> Self {
545        let (x, y, z) = Tree::axes();
546        let t = v.shape.remap_xyz(x, y, Tree::constant(0.0));
547        t.max((v.lower - z.clone()).max(z - v.upper))
548    }
549}
550
551/// Loft between two XY shape in the Z direction
552#[derive(Clone, Facet)]
553pub struct LoftZ {
554    /// Lower shape
555    pub a: Tree,
556    /// Upper shape
557    pub b: Tree,
558    /// Lower bounds of the loft
559    #[facet(default = 0.0)]
560    pub lower: f64,
561    /// Upper bounds of the loft
562    #[facet(default = 1.0)]
563    pub upper: f64,
564}
565
566impl From<LoftZ> for Tree {
567    fn from(v: LoftZ) -> Self {
568        let (x, y, z) = Tree::axes();
569        let ta = v.a.remap_xyz(x.clone(), y.clone(), Tree::constant(0.0));
570        let tb = v.b.remap_xyz(x, y, Tree::constant(0.0));
571        let t = ((z.clone() - v.lower) * tb + (v.upper - z.clone()) * ta)
572            / (v.upper - v.lower);
573        t.max((v.lower - z.clone()).max(z - v.upper))
574    }
575}
576
577////////////////////////////////////////////////////////////////////////////////
578
579/// Trait for a type which can visit each of the shapes in our library
580pub trait ShapeVisitor {
581    /// Process the given type
582    fn visit<T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static>(
583        &mut self,
584    );
585}
586
587/// Maps a shape visitor across all shape definitions
588pub fn visit_shapes<V: ShapeVisitor>(visitor: &mut V) {
589    visitor.visit::<Sphere>();
590    visitor.visit::<Box>();
591    visitor.visit::<Plane>();
592
593    visitor.visit::<Circle>();
594    visitor.visit::<Rectangle>();
595
596    visitor.visit::<Move>();
597    visitor.visit::<Scale>();
598    visitor.visit::<ScaleUniform>();
599    visitor.visit::<Reflect>();
600    visitor.visit::<ReflectX>();
601    visitor.visit::<ReflectY>();
602    visitor.visit::<ReflectZ>();
603    visitor.visit::<Rotate>();
604    visitor.visit::<RotateX>();
605    visitor.visit::<RotateY>();
606    visitor.visit::<RotateZ>();
607    visitor.visit::<RevolveY>();
608    visitor.visit::<ExtrudeZ>();
609    visitor.visit::<LoftZ>();
610
611    visitor.visit::<Union>();
612    visitor.visit::<Blend>();
613    visitor.visit::<Intersection>();
614    visitor.visit::<Difference>();
615    visitor.visit::<Inverse>();
616}
617
618////////////////////////////////////////////////////////////////////////////////
619
620#[cfg(test)]
621mod test {
622    use super::*;
623    use fidget_core::Context;
624
625    #[test]
626    fn circle_docstring() {
627        assert_eq!(Circle::SHAPE.doc, &[" 2D circle"]);
628    }
629
630    #[test]
631    fn transform_order() {
632        let x = Tree::x();
633        let moved: Tree = Move {
634            shape: x,
635            offset: Vec3::new(-1.0, 0.0, 0.0),
636        }
637        .into();
638        let mut ctx = Context::new();
639        let cm = ctx.import(&moved);
640        assert_eq!(ctx.eval_xyz(cm, 0.0, 0.0, 0.0).unwrap(), 1.0);
641        assert_eq!(ctx.eval_xyz(cm, 0.0, 1.0, 0.0).unwrap(), 1.0);
642        assert_eq!(ctx.eval_xyz(cm, -1.0, 0.0, 0.0).unwrap(), 0.0);
643
644        let rotated: Tree = RotateZ {
645            shape: moved,
646            angle: 90.0,
647            center: Vec3::new(0.0, 0.0, 0.0),
648        }
649        .into();
650        let cr = ctx.import(&rotated);
651        assert_eq!(ctx.eval_xyz(cr, 0.0, 0.0, 0.0).unwrap(), 1.0);
652        assert_eq!(ctx.eval_xyz(cr, 0.0, -1.0, 0.0).unwrap(), 0.0);
653        assert_eq!(ctx.eval_xyz(cr, 0.0, 1.0, 0.0).unwrap(), 2.0);
654    }
655
656    #[test]
657    fn scale_default_fn() {
658        let facet::Type::User(facet::UserType::Struct(s)) = Scale::SHAPE.ty
659        else {
660            panic!();
661        };
662        for f in s.fields {
663            if f.name == "scale" {
664                let Some(f) = f.vtable.default_fn else {
665                    panic!()
666                };
667                let mut v = std::mem::MaybeUninit::<Vec3>::uninit();
668                let ptr = facet::PtrUninit::new(&mut v);
669                let v: Vec3 = unsafe { *f(ptr).as_ptr() };
670                assert_eq!(v.x, 1.0);
671                assert_eq!(v.y, 1.0);
672                assert_eq!(v.z, 1.0);
673            } else {
674                assert!(f.vtable.default_fn.is_none());
675            }
676        }
677    }
678
679    #[test]
680    fn validate_shapes() {
681        struct Visitor;
682        impl ShapeVisitor for Visitor {
683            fn visit<
684                T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static,
685            >(
686                &mut self,
687            ) {
688                let facet::Type::User(facet::UserType::Struct(s)) = T::SHAPE.ty
689                else {
690                    panic!("must be a struct-shaped type");
691                };
692                for f in s.fields {
693                    if types::Type::try_from(f.shape().id).is_err() {
694                        panic!("unknown type: {}", f.shape());
695                    }
696                }
697            }
698        }
699        let mut v = Visitor;
700        visit_shapes(&mut v);
701    }
702}