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).
17use facet::Facet;
18use fidget_core::context::Tree;
19
20pub mod types;
21use types::{Axis, Plane, Vec2, Vec3};
22
23////////////////////////////////////////////////////////////////////////////////
24// 2D shapes
25
26/// 2D circle
27#[derive(Clone, Facet)]
28pub struct Circle {
29    /// Center of the circle (in XY)
30    #[facet(default = Vec2::new(0.0, 0.0))]
31    pub center: Vec2,
32    /// Circle radius
33    #[facet(default = 1.0)]
34    pub radius: f64,
35}
36
37impl From<Circle> for Tree {
38    fn from(v: Circle) -> Self {
39        let (x, y, _) = Tree::axes();
40        ((x - v.center.x).square() + (y - v.center.y).square()).sqrt()
41            - v.radius
42    }
43}
44
45/// Rectangle defined by lower and upper corners
46#[derive(Clone, Facet)]
47pub struct Rectangle {
48    /// Lower corner of the rectangle
49    pub lower: Vec2,
50    /// Upper corner of the rectangle
51    pub upper: Vec2,
52}
53
54impl From<Rectangle> for Tree {
55    fn from(v: Rectangle) -> Self {
56        let (x, y, _) = Tree::axes();
57        (v.lower.x - x.clone())
58            .max(x - v.upper.x)
59            .max((v.lower.y - y.clone()).max(y - v.upper.y))
60    }
61}
62
63////////////////////////////////////////////////////////////////////////////////
64// 3D shapes
65
66/// 3D sphere
67#[derive(Clone, Facet)]
68pub struct Sphere {
69    /// Center of the circle (in XYZ)
70    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
71    pub center: Vec3,
72    /// Sphere radius
73    #[facet(default = 1.0)]
74    pub radius: f64,
75}
76
77impl From<Sphere> for Tree {
78    fn from(v: Sphere) -> Self {
79        let (x, y, z) = Tree::axes();
80        ((x - v.center.x).square()
81            + (y - v.center.y).square()
82            + (z - v.center.z).square())
83        .sqrt()
84            - v.radius
85    }
86}
87
88/// Box defined by lower and upper corners
89#[derive(Clone, Facet)]
90pub struct Box {
91    /// Lower corner of the rectangle
92    pub lower: Vec3,
93    /// Upper corner of the rectangle
94    pub upper: Vec3,
95}
96
97impl From<Box> for Tree {
98    fn from(v: Box) -> Self {
99        let (x, y, z) = Tree::axes();
100        (v.lower.x - x.clone())
101            .max(x - v.upper.x)
102            .max((v.lower.y - y.clone()).max(y - v.upper.y))
103            .max((v.lower.z - z.clone()).max(z - v.upper.z))
104    }
105}
106
107////////////////////////////////////////////////////////////////////////////////
108// CSG operations
109
110/// Take the union of a set of shapes
111///
112/// If the input is empty, returns an constant empty tree (at +∞)
113#[derive(Clone, Facet)]
114pub struct Union {
115    /// List of shapes to merge
116    pub input: Vec<Tree>,
117}
118
119impl From<Union> for Tree {
120    fn from(v: Union) -> Self {
121        if v.input.is_empty() {
122            // XXX should this be an error instead?
123            Tree::constant(f64::INFINITY)
124        } else {
125            fn recurse(s: &[Tree]) -> Tree {
126                match s.len() {
127                    1 => s[0].clone(),
128                    n => recurse(&s[..n / 2]).min(recurse(&s[n / 2..])),
129                }
130            }
131            recurse(&v.input)
132        }
133    }
134}
135
136/// Smooth quadratic blend of two shapes
137///
138/// This formula is taken from "Lipschitz Pruning: Hierarchical Simplification
139/// of Primitive-Based SDFs" (Barbier _et al_ '25), which in turn cites
140/// [Quilez '20](https://iquilezles.org/articles/smin/)
141#[derive(Clone, Facet)]
142pub struct Blend {
143    /// First shape input
144    pub a: Tree,
145    /// Second shape input
146    pub b: Tree,
147    /// Blending radius
148    pub radius: f64,
149}
150
151impl From<Blend> for Tree {
152    fn from(v: Blend) -> Self {
153        if v.radius > 0.0 {
154            v.a.clone().min(v.b.clone())
155                - 1.0 / (4.0 * v.radius)
156                    * (v.radius - (v.a - v.b).abs()).max(0.0).square()
157        } else {
158            v.a.min(v.b)
159        }
160    }
161}
162
163/// Take the intersection of a set of shapes
164///
165/// If the input is empty, returns a constant full tree (at -∞)
166#[derive(Clone, Facet)]
167pub struct Intersection {
168    /// List of shapes to intersect
169    pub input: Vec<Tree>,
170}
171
172impl From<Intersection> for Tree {
173    fn from(v: Intersection) -> Self {
174        if v.input.is_empty() {
175            // XXX should this be an error instead?
176            Tree::constant(-f64::INFINITY)
177        } else {
178            fn recurse(s: &[Tree]) -> Tree {
179                match s.len() {
180                    1 => s[0].clone(),
181                    n => recurse(&s[..n / 2]).max(recurse(&s[n / 2..])),
182                }
183            }
184            recurse(&v.input)
185        }
186    }
187}
188
189/// Computes the inverse of a shape
190#[derive(Clone, Facet)]
191pub struct Inverse {
192    /// Shape to invert
193    pub shape: Tree,
194}
195
196impl From<Inverse> for Tree {
197    fn from(v: Inverse) -> Self {
198        -v.shape
199    }
200}
201
202/// Take the difference of two shapes
203#[derive(Clone, Facet)]
204pub struct Difference {
205    /// Original shape
206    pub shape: Tree,
207    /// Shape to be subtracted from the original
208    pub cutout: Tree,
209}
210
211impl From<Difference> for Tree {
212    fn from(v: Difference) -> Self {
213        v.shape.max(-v.cutout)
214    }
215}
216
217////////////////////////////////////////////////////////////////////////////////
218// Transforms
219
220/// Move a shape
221#[derive(Clone, Facet)]
222pub struct Move {
223    /// Shape to move
224    pub shape: Tree,
225    /// Position offset
226    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
227    pub offset: Vec3,
228}
229
230impl From<Move> for Tree {
231    fn from(v: Move) -> Self {
232        v.shape.remap_affine(nalgebra::convert(
233            nalgebra::Translation3::<f64>::new(
234                -v.offset.x,
235                -v.offset.y,
236                -v.offset.z,
237            ),
238        ))
239    }
240}
241
242/// Non-uniform scaling
243#[derive(Clone, Facet)]
244pub struct Scale {
245    /// Shape to scale
246    pub shape: Tree,
247    /// Scale to apply on each axis
248    #[facet(default = Vec3::new(1.0, 1.0, 1.0))]
249    pub scale: Vec3,
250}
251
252impl From<Scale> for Tree {
253    fn from(v: Scale) -> Self {
254        v.shape
255            .remap_affine(nalgebra::convert(nalgebra::Scale3::<f64>::new(
256                1.0 / v.scale.x,
257                1.0 / v.scale.y,
258                1.0 / v.scale.z,
259            )))
260    }
261}
262
263/// Uniform scaling
264#[derive(Clone, Facet)]
265pub struct ScaleUniform {
266    /// Shape to scale
267    pub shape: Tree,
268    /// Scale to apply
269    #[facet(default = 1.0)]
270    pub scale: f64,
271}
272
273impl From<ScaleUniform> for Tree {
274    fn from(v: ScaleUniform) -> Self {
275        let s = 1.0 / v.scale;
276        v.shape
277            .remap_affine(nalgebra::convert(nalgebra::Scale3::<f64>::new(
278                s, s, s,
279            )))
280    }
281}
282
283/// Reflection
284#[derive(Clone, Facet)]
285pub struct Reflect {
286    /// Shape to reflect
287    pub shape: Tree,
288
289    /// Plane about which to reflect the shape
290    #[facet(default = Plane::YZ)]
291    pub plane: Plane,
292}
293
294impl From<Reflect> for Tree {
295    fn from(v: Reflect) -> Self {
296        let a = v.plane.axis.vec();
297        let (x, y, z) = Tree::axes();
298        let d = a.x * x.clone() + a.y * y.clone() + a.z * z.clone()
299            - v.plane.offset;
300        let scale: Tree = 2.0 * d;
301        // TODO could we use nalgebra::Reflection3 here to make the transform
302        // affine?  For some reason, it doesn't implement the right SubSet
303        // https://github.com/dimforge/nalgebra/issues/1527
304        v.shape.remap_xyz(
305            x - scale.clone() * a.x,
306            y - scale.clone() * a.y,
307            z - scale * a.z,
308        )
309    }
310}
311
312/// Reflection about the X axis
313#[derive(Clone, Facet)]
314pub struct ReflectX {
315    /// Shape to reflect
316    pub shape: Tree,
317
318    /// Plane about which to reflect the shape
319    #[facet(default = 0.0)]
320    pub offset: f64,
321}
322
323impl From<ReflectX> for Tree {
324    fn from(v: ReflectX) -> Self {
325        Reflect {
326            shape: v.shape,
327            plane: Plane {
328                axis: Axis::X,
329                offset: v.offset,
330            },
331        }
332        .into()
333    }
334}
335
336/// Reflection about the Y axis
337#[derive(Clone, Facet)]
338pub struct ReflectY {
339    /// Shape to reflect
340    pub shape: Tree,
341
342    /// Plane about which to reflect the shape
343    #[facet(default = 0.0)]
344    pub offset: f64,
345}
346
347impl From<ReflectY> for Tree {
348    fn from(v: ReflectY) -> Self {
349        Reflect {
350            shape: v.shape,
351            plane: Plane {
352                axis: Axis::Y,
353                offset: v.offset,
354            },
355        }
356        .into()
357    }
358}
359
360/// Reflection about the Z axis
361#[derive(Clone, Facet)]
362pub struct ReflectZ {
363    /// Shape to reflect
364    pub shape: Tree,
365
366    /// Plane about which to reflect the shape
367    #[facet(default = 0.0)]
368    pub offset: f64,
369}
370
371impl From<ReflectZ> for Tree {
372    fn from(v: ReflectZ) -> Self {
373        Reflect {
374            shape: v.shape,
375            plane: Plane {
376                axis: Axis::Z,
377                offset: v.offset,
378            },
379        }
380        .into()
381    }
382}
383
384/// Rotates an object about an arbitrary axis and rotation center
385#[derive(Clone, Facet)]
386pub struct Rotate {
387    /// Shape to rotate
388    pub shape: Tree,
389
390    /// Axis about which to rotate
391    #[facet(default = Axis::Z)]
392    pub axis: Axis,
393
394    /// Angle to rotate (in degrees)
395    #[facet(default = 0.0)]
396    pub angle: f64,
397
398    /// Center of rotation
399    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
400    pub center: Vec3,
401}
402
403impl From<Rotate> for Tree {
404    fn from(v: Rotate) -> Self {
405        let shape = Tree::from(Move {
406            shape: v.shape,
407            offset: -v.center,
408        });
409        let d = -v.angle.to_radians();
410        let axis = v.axis.vec();
411        let shape = shape.remap_affine(nalgebra::convert(
412            nalgebra::Rotation3::<f64>::new(nalgebra::Vector3::from(d * *axis)),
413        ));
414        Move {
415            shape,
416            offset: v.center,
417        }
418        .into()
419    }
420}
421
422/// Rotates an object about the X axis
423#[derive(Clone, Facet)]
424pub struct RotateX {
425    /// Shape to rotate
426    pub shape: Tree,
427
428    /// Angle to rotate (in degrees)
429    #[facet(default = 0.0)]
430    pub angle: f64,
431
432    /// Center of rotation
433    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
434    pub center: Vec3,
435}
436
437impl From<RotateX> for Tree {
438    fn from(v: RotateX) -> Self {
439        Rotate {
440            shape: v.shape,
441            angle: v.angle,
442            center: v.center,
443            axis: Axis::X,
444        }
445        .into()
446    }
447}
448
449/// Rotates an object about the Y axis
450#[derive(Clone, Facet)]
451pub struct RotateY {
452    /// Shape to rotate
453    pub shape: Tree,
454
455    /// Angle to rotate (in degrees)
456    #[facet(default = 0.0)]
457    pub angle: f64,
458
459    /// Center of rotation
460    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
461    pub center: Vec3,
462}
463
464impl From<RotateY> for Tree {
465    fn from(v: RotateY) -> Self {
466        Rotate {
467            shape: v.shape,
468            angle: v.angle,
469            center: v.center,
470            axis: Axis::Y,
471        }
472        .into()
473    }
474}
475
476/// Rotates an object about the Z axis
477#[derive(Clone, Facet)]
478pub struct RotateZ {
479    /// Shape to rotate
480    pub shape: Tree,
481
482    /// Angle to rotate (in degrees)
483    #[facet(default = 0.0)]
484    pub angle: f64,
485
486    /// Center of rotation
487    #[facet(default = Vec3::new(0.0, 0.0, 0.0))]
488    pub center: Vec3,
489}
490
491impl From<RotateZ> for Tree {
492    fn from(v: RotateZ) -> Self {
493        Rotate {
494            shape: v.shape,
495            angle: v.angle,
496            center: v.center,
497            axis: Axis::Z,
498        }
499        .into()
500    }
501}
502
503// TODO figure out a generic Revolve?  The matrix math is a bit tricky!
504
505/// Revolve a shape about the Y axis, creating a 3D volume
506#[derive(Clone, Facet)]
507pub struct RevolveY {
508    /// Shape to revolve
509    pub shape: Tree,
510    /// X offset about which to revolve
511    #[facet(default = 0.0)]
512    pub offset: f64,
513}
514
515impl From<RevolveY> for Tree {
516    fn from(v: RevolveY) -> Self {
517        let offset = Vec3::new(-v.offset, 0.0, 0.0);
518        let shape = Tree::from(Move {
519            shape: v.shape.clone(),
520            offset: -offset,
521        });
522        let (x, y, z) = Tree::axes();
523        let r = (x.square() + y.square()).sqrt();
524        let shape = shape.remap_xyz(r, y, z);
525        Move { shape, offset }.into()
526    }
527}
528
529/// Extrude an XY shape in the Z direction
530#[derive(Clone, Facet)]
531pub struct ExtrudeZ {
532    /// Shape to extrude
533    pub shape: Tree,
534    /// Lower bounds of the extrusion
535    #[facet(default = 0.0)]
536    pub lower: f64,
537    /// Upper bounds of the extrusion
538    #[facet(default = 1.0)]
539    pub upper: f64,
540}
541
542impl From<ExtrudeZ> for Tree {
543    fn from(v: ExtrudeZ) -> Self {
544        let (x, y, z) = Tree::axes();
545        let t = v.shape.remap_xyz(x, y, Tree::constant(0.0));
546        t.max((v.lower - z.clone()).max(z - v.upper))
547    }
548}
549
550/// Loft between two XY shape in the Z direction
551#[derive(Clone, Facet)]
552pub struct LoftZ {
553    /// Lower shape
554    pub a: Tree,
555    /// Upper shape
556    pub b: Tree,
557    /// Lower bounds of the loft
558    #[facet(default = 0.0)]
559    pub lower: f64,
560    /// Upper bounds of the loft
561    #[facet(default = 1.0)]
562    pub upper: f64,
563}
564
565impl From<LoftZ> for Tree {
566    fn from(v: LoftZ) -> Self {
567        let (x, y, z) = Tree::axes();
568        let ta = v.a.remap_xyz(x.clone(), y.clone(), Tree::constant(0.0));
569        let tb = v.b.remap_xyz(x, y, Tree::constant(0.0));
570        let t = ((z.clone() - v.lower) * tb + (v.upper - z.clone()) * ta)
571            / (v.upper - v.lower);
572        t.max((v.lower - z.clone()).max(z - v.upper))
573    }
574}
575
576////////////////////////////////////////////////////////////////////////////////
577
578/// Trait for a type which can visit each of the shapes in our library
579pub trait ShapeVisitor {
580    /// Process the given type
581    fn visit<T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static>(
582        &mut self,
583    );
584}
585
586/// Maps a shape visitor across all shape definitions
587pub fn visit_shapes<V: ShapeVisitor>(visitor: &mut V) {
588    visitor.visit::<Sphere>();
589    visitor.visit::<Box>();
590    visitor.visit::<Plane>();
591
592    visitor.visit::<Circle>();
593    visitor.visit::<Rectangle>();
594
595    visitor.visit::<Move>();
596    visitor.visit::<Scale>();
597    visitor.visit::<ScaleUniform>();
598    visitor.visit::<Reflect>();
599    visitor.visit::<ReflectX>();
600    visitor.visit::<ReflectY>();
601    visitor.visit::<ReflectZ>();
602    visitor.visit::<Rotate>();
603    visitor.visit::<RotateX>();
604    visitor.visit::<RotateY>();
605    visitor.visit::<RotateZ>();
606    visitor.visit::<RevolveY>();
607    visitor.visit::<ExtrudeZ>();
608    visitor.visit::<LoftZ>();
609
610    visitor.visit::<Union>();
611    visitor.visit::<Blend>();
612    visitor.visit::<Intersection>();
613    visitor.visit::<Difference>();
614    visitor.visit::<Inverse>();
615}
616
617////////////////////////////////////////////////////////////////////////////////
618
619#[cfg(test)]
620mod test {
621    use super::*;
622    use fidget_core::Context;
623
624    #[test]
625    fn circle_docstring() {
626        assert_eq!(Circle::SHAPE.doc, &[" 2D circle"]);
627    }
628
629    #[test]
630    fn transform_order() {
631        let x = Tree::x();
632        let moved: Tree = Move {
633            shape: x,
634            offset: Vec3::new(-1.0, 0.0, 0.0),
635        }
636        .into();
637        let mut ctx = Context::new();
638        let cm = ctx.import(&moved);
639        assert_eq!(ctx.eval_xyz(cm, 0.0, 0.0, 0.0).unwrap(), 1.0);
640        assert_eq!(ctx.eval_xyz(cm, 0.0, 1.0, 0.0).unwrap(), 1.0);
641        assert_eq!(ctx.eval_xyz(cm, -1.0, 0.0, 0.0).unwrap(), 0.0);
642
643        let rotated: Tree = RotateZ {
644            shape: moved,
645            angle: 90.0,
646            center: Vec3::new(0.0, 0.0, 0.0),
647        }
648        .into();
649        let cr = ctx.import(&rotated);
650        assert_eq!(ctx.eval_xyz(cr, 0.0, 0.0, 0.0).unwrap(), 1.0);
651        assert_eq!(ctx.eval_xyz(cr, 0.0, -1.0, 0.0).unwrap(), 0.0);
652        assert_eq!(ctx.eval_xyz(cr, 0.0, 1.0, 0.0).unwrap(), 2.0);
653    }
654
655    #[test]
656    fn scale_default_fn() {
657        let facet::Type::User(facet::UserType::Struct(s)) = Scale::SHAPE.ty
658        else {
659            panic!();
660        };
661        for f in s.fields {
662            if f.name == "scale" {
663                let Some(f) = f.vtable.default_fn else {
664                    panic!()
665                };
666                let mut v = std::mem::MaybeUninit::<Vec3>::uninit();
667                let ptr = facet::PtrUninit::new(&mut v);
668                let v: Vec3 = unsafe { *f(ptr).as_ptr() };
669                assert_eq!(v.x, 1.0);
670                assert_eq!(v.y, 1.0);
671                assert_eq!(v.z, 1.0);
672            } else {
673                assert!(f.vtable.default_fn.is_none());
674            }
675        }
676    }
677
678    #[test]
679    fn validate_shapes() {
680        struct Visitor;
681        impl ShapeVisitor for Visitor {
682            fn visit<
683                T: Facet<'static> + Clone + Send + Sync + Into<Tree> + 'static,
684            >(
685                &mut self,
686            ) {
687                let facet::Type::User(facet::UserType::Struct(s)) = T::SHAPE.ty
688                else {
689                    panic!("must be a struct-shaped type");
690                };
691                for f in s.fields {
692                    if types::Type::try_from(f.shape().id).is_err() {
693                        panic!("unknown type: {}", f.shape());
694                    }
695                }
696            }
697        }
698        let mut v = Visitor;
699        visit_shapes(&mut v);
700    }
701}