Skip to main content

rscad_csg_traits/
dynamic.rs

1//! Generic [`DynamicObject`] dispatch shared by every CSG backend.
2//!
3//! Backends implement [`DynamicBackend`] (parameterized primitive
4//! constructors plus two 2D/3D bridge hooks); the tree walk itself —
5//! booleans, transforms, extrusions, sketch loop handling — lives once in
6//! [`dynamic_to_csg`]/[`dynamic_to_sketch`]. Blanket [`ToCsg`]/[`ToSketch`]
7//! impls for `DynamicObject` route through the walkers, so backends must NOT
8//! provide their own (the compiler rejects the overlap).
9//!
10//! Conventions the constructors must honor (pinned by
11//! `tests/dynamic_dispatch.rs`): `cube`/`square` corner-anchored at the
12//! origin, `cylinder`/`cone` base on y=0 with +Y axis, `sphere` centered.
13//! Backend-specific fixes (BSP's centered-factory offsets, Manifold's
14//! `z_to_y_up`) belong inside the backend impls, never in the walkers.
15
16use glam::{DVec2, DVec3};
17use rscad_core::DynamicObject;
18
19use crate::CsgBackend;
20
21/// A [`CsgBackend`] that can build every `DynamicObject` primitive.
22pub trait DynamicBackend: CsgBackend {
23    /// `true` when the backend uses one type for 2D and 3D (BSP): bare 2D
24    /// nodes keep their geometry in 3D context and vice versa, via the two
25    /// bridge hooks. When `false` (Manifold), out-of-context nodes are empty
26    /// and the hooks are never called.
27    const SKETCH_IS_CSG: bool = false;
28
29    fn cube(size: [f64; 3]) -> Self::CsgType;
30    /// `stacks` is advisory; backends with a single tessellation knob ignore it.
31    fn sphere(radius: f64, segments: usize, stacks: usize) -> Self::CsgType;
32    fn cylinder(radius: f64, height: f64, segments: usize) -> Self::CsgType;
33    fn cone(radius: f64, height: f64, segments: usize) -> Self::CsgType;
34
35    fn circle(radius: f64, segments: usize) -> Self::SketchType;
36    fn square(size: [f64; 2]) -> Self::SketchType;
37    fn polygon(points: &[[f64; 2]]) -> Self::SketchType;
38    /// Build a profile from winding-normalized loops (CCW outers, CW holes)
39    /// as produced by `rscad_core::sketch::profile_loops`.
40    fn sketch(loops: &[Vec<DVec2>]) -> Self::SketchType;
41
42    /// 2D polygon offset (`offset(delta=…)` / `offset(r=…)`). Default passes
43    /// the profile through unchanged — for backends without polygon offsetting
44    /// (BSP, the documented 2D outlier). `round` wins over `chamfer`.
45    fn offset(
46        sketch: Self::SketchType,
47        _delta: f64,
48        _round: bool,
49        _chamfer: bool,
50        _segments: usize,
51    ) -> Self::SketchType {
52        sketch
53    }
54
55    /// Bare 2D geometry appearing in 3D context. Only called when
56    /// [`SKETCH_IS_CSG`](Self::SKETCH_IS_CSG) is `true`.
57    fn sketch_as_csg(_sketch: Self::SketchType) -> Self::CsgType {
58        Self::empty_csg()
59    }
60    /// 3D geometry appearing in 2D context. Only called when
61    /// [`SKETCH_IS_CSG`](Self::SKETCH_IS_CSG) is `true`.
62    fn csg_as_sketch(_csg: Self::CsgType) -> Self::SketchType {
63        Self::empty_sketch()
64    }
65
66    /// N-ary union; override when the backend has a batch operation.
67    fn union_all(items: Vec<Self::CsgType>) -> Self::CsgType {
68        items.into_iter().fold(Self::empty_csg(), Self::union)
69    }
70    fn union_all_2d(items: Vec<Self::SketchType>) -> Self::SketchType {
71        items.into_iter().fold(Self::empty_sketch(), Self::union_2d)
72    }
73}
74
75impl<B: DynamicBackend> crate::ToCsg<B> for DynamicObject {
76    fn to_csg(&self) -> B::CsgType {
77        dynamic_to_csg::<B>(self)
78    }
79}
80
81impl<B: DynamicBackend> crate::ToSketch<B> for DynamicObject {
82    fn to_sketch(&self) -> B::SketchType {
83        dynamic_to_sketch::<B>(self)
84    }
85}
86
87/// Evaluate a `DynamicObject` tree as a 3D solid.
88pub fn dynamic_to_csg<B: DynamicBackend>(obj: &DynamicObject) -> B::CsgType {
89    match obj {
90        DynamicObject::Cube { size } => B::cube(*size),
91        DynamicObject::Sphere {
92            radius,
93            segments,
94            stacks,
95        } => B::sphere(*radius, *segments, *stacks),
96        DynamicObject::Cylinder {
97            radius,
98            height,
99            segments,
100        } => B::cylinder(*radius, *height, *segments),
101        DynamicObject::Cone {
102            radius,
103            height,
104            segments,
105        } => B::cone(*radius, *height, *segments),
106        DynamicObject::Circle { .. }
107        | DynamicObject::Square { .. }
108        | DynamicObject::Polygon { .. }
109        | DynamicObject::Sketch { .. }
110        | DynamicObject::Offset { .. } => {
111            if B::SKETCH_IS_CSG {
112                B::sketch_as_csg(dynamic_to_sketch::<B>(obj))
113            } else {
114                B::empty_csg()
115            }
116        }
117        DynamicObject::Union { children } | DynamicObject::Group { children } => {
118            B::union_all(children.iter().map(dynamic_to_csg::<B>).collect())
119        }
120        DynamicObject::Difference { children } => match children.as_slice() {
121            [] => B::empty_csg(),
122            [first, rest @ ..] => rest.iter().fold(dynamic_to_csg::<B>(first), |acc, c| {
123                B::difference(acc, dynamic_to_csg::<B>(c))
124            }),
125        },
126        DynamicObject::Intersection { children } => match children.as_slice() {
127            [] => B::empty_csg(),
128            [first, rest @ ..] => rest.iter().fold(dynamic_to_csg::<B>(first), |acc, c| {
129                B::intersection(acc, dynamic_to_csg::<B>(c))
130            }),
131        },
132        DynamicObject::Translate { offset, child } => {
133            B::translate(dynamic_to_csg::<B>(child), DVec3::from_array(*offset))
134        }
135        DynamicObject::Rotate { angles, child } => {
136            B::rotate(dynamic_to_csg::<B>(child), DVec3::from_array(*angles))
137        }
138        DynamicObject::Scale { factors, child } => {
139            B::scale(dynamic_to_csg::<B>(child), DVec3::from_array(*factors))
140        }
141        DynamicObject::Mirror { axis, child } => {
142            B::mirror(dynamic_to_csg::<B>(child), DVec3::from_array(*axis))
143        }
144        DynamicObject::Color { color, child } => {
145            B::color(dynamic_to_csg::<B>(child), color.map(|c| c as f32))
146        }
147        // Surface finish is a render-time concern; geometry passes through.
148        DynamicObject::Material { child, .. } => dynamic_to_csg::<B>(child),
149        DynamicObject::LinearExtrude {
150            height,
151            twist,
152            scale,
153            slices,
154            child,
155        } => B::extrude(
156            dynamic_to_sketch::<B>(child),
157            *height,
158            *twist,
159            *scale,
160            *slices,
161        ),
162        DynamicObject::Revolve {
163            angle_degrees,
164            segments,
165            child,
166        } => B::revolve(dynamic_to_sketch::<B>(child), *angle_degrees, *segments),
167        // Edge features lower to plain CSG masks (re-resolved against the
168        // current child), so backends only ever see supported variants.
169        DynamicObject::EdgeFeature { data, child } => {
170            dynamic_to_csg::<B>(&rscad_core::feature::lower(data, child).0)
171        }
172        DynamicObject::Empty => B::empty_csg(),
173    }
174}
175
176/// Evaluate a `DynamicObject` tree as a 2D sketch/cross-section.
177pub fn dynamic_to_sketch<B: DynamicBackend>(obj: &DynamicObject) -> B::SketchType {
178    match obj {
179        DynamicObject::Circle { radius, segments } => B::circle(*radius, *segments),
180        DynamicObject::Square { size } => B::square(*size),
181        DynamicObject::Polygon { points } => B::polygon(points),
182        DynamicObject::Sketch { data } => {
183            let profile =
184                rscad_core::sketch::profile_loops(data, rscad_core::sketch::DEFAULT_ARC_SEGMENTS);
185            B::sketch(&profile.loops)
186        }
187        DynamicObject::Offset {
188            delta,
189            round,
190            chamfer,
191            segments,
192            child,
193        } => B::offset(
194            dynamic_to_sketch::<B>(child),
195            *delta,
196            *round,
197            *chamfer,
198            *segments,
199        ),
200        DynamicObject::Cube { .. }
201        | DynamicObject::Sphere { .. }
202        | DynamicObject::Cylinder { .. }
203        | DynamicObject::Cone { .. }
204        | DynamicObject::LinearExtrude { .. }
205        | DynamicObject::Revolve { .. }
206        | DynamicObject::EdgeFeature { .. } => {
207            if B::SKETCH_IS_CSG {
208                B::csg_as_sketch(dynamic_to_csg::<B>(obj))
209            } else {
210                B::empty_sketch()
211            }
212        }
213        DynamicObject::Union { children } | DynamicObject::Group { children } => {
214            B::union_all_2d(children.iter().map(dynamic_to_sketch::<B>).collect())
215        }
216        DynamicObject::Difference { children } => match children.as_slice() {
217            [] => B::empty_sketch(),
218            [first, rest @ ..] => rest.iter().fold(dynamic_to_sketch::<B>(first), |acc, c| {
219                B::difference_2d(acc, dynamic_to_sketch::<B>(c))
220            }),
221        },
222        DynamicObject::Intersection { children } => match children.as_slice() {
223            [] => B::empty_sketch(),
224            [first, rest @ ..] => rest.iter().fold(dynamic_to_sketch::<B>(first), |acc, c| {
225                B::intersection_2d(acc, dynamic_to_sketch::<B>(c))
226            }),
227        },
228        DynamicObject::Translate { offset, child } => {
229            B::translate_2d(dynamic_to_sketch::<B>(child), DVec3::from_array(*offset))
230        }
231        DynamicObject::Rotate { angles, child } => {
232            B::rotate_2d(dynamic_to_sketch::<B>(child), DVec3::from_array(*angles))
233        }
234        DynamicObject::Scale { factors, child } => {
235            B::scale_2d(dynamic_to_sketch::<B>(child), DVec3::from_array(*factors))
236        }
237        DynamicObject::Mirror { axis, child } => {
238            B::mirror_2d(dynamic_to_sketch::<B>(child), DVec3::from_array(*axis))
239        }
240        DynamicObject::Color { color, child } => {
241            B::color_2d(dynamic_to_sketch::<B>(child), color.map(|c| c as f32))
242        }
243        DynamicObject::Material { child, .. } => dynamic_to_sketch::<B>(child),
244        DynamicObject::Empty => B::empty_sketch(),
245    }
246}