1use glam::DVec3;
4
5use crate::Csg;
6use rscad_csg_traits::{CsgBackend, CsgToStl, DynamicBackend, ToCsg, ToSketch};
7
8pub struct BspBackend;
10
11impl CsgBackend for BspBackend {
12 type CsgType = Csg<f64>;
13 type SketchType = Csg<f64>;
14
15 fn empty_csg() -> Csg<f64> {
16 Csg::new(vec![])
17 }
18 fn empty_sketch() -> Csg<f64> {
19 Csg::new(vec![])
20 }
21
22 fn union(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
23 a.union(b)
24 }
25 fn difference(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
26 a.difference(b)
27 }
28 fn intersection(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
29 a.intersection(b)
30 }
31 fn xor(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
32 a.xor(b)
33 }
34
35 fn union_2d(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
36 a.union(b)
37 }
38 fn difference_2d(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
39 a.difference(b)
40 }
41 fn intersection_2d(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
42 a.intersection(b)
43 }
44 fn xor_2d(a: Csg<f64>, b: Csg<f64>) -> Csg<f64> {
45 a.xor(b)
46 }
47
48 fn translate(csg: Csg<f64>, offset: DVec3) -> Csg<f64> {
49 csg.translate(offset.into())
50 }
51 fn rotate(csg: Csg<f64>, angles: DVec3) -> Csg<f64> {
52 csg.rotate_euler(angles.into())
53 }
54 fn scale(csg: Csg<f64>, factors: DVec3) -> Csg<f64> {
55 csg.scale(factors.into())
56 }
57 fn mirror(csg: Csg<f64>, axes: DVec3) -> Csg<f64> {
58 csg.mirror(axes.into())
59 }
60
61 fn translate_2d(sketch: Csg<f64>, offset: DVec3) -> Csg<f64> {
62 sketch.translate(offset.into())
63 }
64 fn rotate_2d(sketch: Csg<f64>, angles: DVec3) -> Csg<f64> {
65 sketch.rotate_euler(angles.into())
66 }
67 fn scale_2d(sketch: Csg<f64>, factors: DVec3) -> Csg<f64> {
68 sketch.scale(factors.into())
69 }
70 fn mirror_2d(sketch: Csg<f64>, axes: DVec3) -> Csg<f64> {
71 sketch.mirror(axes.into())
72 }
73
74 fn color(mut csg: Csg<f64>, color: [f32; 4]) -> Csg<f64> {
75 apply_color(&mut csg, color);
76 csg
77 }
78 fn color_2d(mut sketch: Csg<f64>, color: [f32; 4]) -> Csg<f64> {
79 apply_color(&mut sketch, color);
80 sketch
81 }
82
83 fn extrude(sketch: Csg<f64>, height: f64, twist: f64, scale: f64, slices: usize) -> Csg<f64> {
84 use crate::Affine3Ext;
85 let along = nalgebra::Vector3::new(0.0, height, 0.0);
86 let transform = nalgebra::Affine3::identity().scale(scale).rotate_y(twist);
87 sketch
88 .extrude(along, transform, slices)
89 .unwrap_or_else(|_| Csg::new(vec![]))
90 }
91
92 fn revolve(sketch: Csg<f64>, angle_degrees: f64, segments: usize) -> Csg<f64> {
93 sketch
94 .lathe(angle_degrees, segments)
95 .unwrap_or_else(|_| Csg::new(vec![]))
96 }
97}
98
99impl ToCsg<BspBackend> for rscad_core::Cube {
102 fn to_csg(&self) -> Csg<f64> {
103 crate::cuboid(self.x, self.y, self.z)
104 }
105}
106
107impl ToCsg<BspBackend> for rscad_core::Sphere {
108 fn to_csg(&self) -> Csg<f64> {
109 crate::sphere(self.radius, self.fn_, self.stacks)
110 }
111}
112
113impl ToCsg<BspBackend> for rscad_core::Cylinder {
114 fn to_csg(&self) -> Csg<f64> {
115 crate::cylinder(self.radius, self.height, self.fn_)
116 }
117}
118
119impl ToSketch<BspBackend> for rscad_core::Circle {
122 fn to_sketch(&self) -> Csg<f64> {
123 crate::circle(self.radius, self.fn_)
124 }
125}
126
127impl ToSketch<BspBackend> for rscad_core::Square {
128 fn to_sketch(&self) -> Csg<f64> {
129 crate::rect(self.x, self.y).translate(nalgebra::Vector3::new(
131 self.x / 2.0,
132 0.0,
133 self.y / 2.0,
134 ))
135 }
136}
137
138impl<const N: usize> ToSketch<BspBackend> for rscad_core::Polygon<N> {
139 fn to_sketch(&self) -> Csg<f64> {
140 crate::regular_polygon(self.radius, N)
141 }
142}
143
144impl ToSketch<BspBackend> for rscad_core::PolygonDynamic {
145 fn to_sketch(&self) -> Csg<f64> {
146 crate::regular_polygon(self.radius, self.points)
147 }
148}
149
150impl DynamicBackend for BspBackend {
153 const SKETCH_IS_CSG: bool = true;
156
157 fn cube(size: [f64; 3]) -> Csg<f64> {
158 let [x, y, z] = size;
159 factory_prim(|f| f.cuboid(x, y, z)).translate(nalgebra::Vector3::new(
161 x * 0.5,
162 y * 0.5,
163 z * 0.5,
164 ))
165 }
166 fn sphere(radius: f64, segments: usize, stacks: usize) -> Csg<f64> {
167 factory_prim(|f| f.sphere(radius, segments, stacks))
168 }
169 fn cylinder(radius: f64, height: f64, segments: usize) -> Csg<f64> {
170 factory_prim(|f| f.cylinder(radius, height, segments)).translate(nalgebra::Vector3::new(
172 0.0,
173 height * 0.5,
174 0.0,
175 ))
176 }
177 fn cone(radius: f64, height: f64, segments: usize) -> Csg<f64> {
178 factory_prim(|f| f.cone(radius, height, segments)).translate(nalgebra::Vector3::new(
179 0.0,
180 height * 0.5,
181 0.0,
182 ))
183 }
184
185 fn circle(radius: f64, segments: usize) -> Csg<f64> {
186 crate::circle(radius, segments)
187 }
188 fn square(size: [f64; 2]) -> Csg<f64> {
189 crate::rect(size[0], size[1]).translate(nalgebra::Vector3::new(
191 size[0] / 2.0,
192 0.0,
193 size[1] / 2.0,
194 ))
195 }
196 fn polygon(points: &[[f64; 2]]) -> Csg<f64> {
197 let pts = points
198 .iter()
199 .map(|[x, z]| nalgebra::Point::from([*x, 0.0, *z]))
200 .collect::<Vec<_>>();
201 Csg::from_polygons([crate::Polygon::from_points(pts)
202 .expect("BUG: DynamicObject::Polygon points must form a valid polygon")])
203 }
204 fn sketch(loops: &[Vec<glam::DVec2>]) -> Csg<f64> {
205 let flat = |l: &[glam::DVec2], reverse: bool| {
208 let pts: Vec<nalgebra::Point3<f64>> = if reverse {
209 l.iter()
210 .rev()
211 .map(|p| nalgebra::Point::from([p.x, 0.0, p.y]))
212 .collect()
213 } else {
214 l.iter()
215 .map(|p| nalgebra::Point::from([p.x, 0.0, p.y]))
216 .collect()
217 };
218 crate::Polygon::from_points(pts)
219 .ok()
220 .map(|p| Csg::from_polygons([p]))
221 };
222 let (outers, holes): (Vec<&Vec<glam::DVec2>>, _) = loops
223 .iter()
224 .partition(|l| rscad_core::sketch::signed_area(l) >= 0.0);
225 let base = outers
226 .into_iter()
227 .filter_map(|l| flat(l, false))
228 .fold(Csg::new(vec![]), |acc, c| acc.union(c));
229 holes
230 .into_iter()
231 .filter_map(|l| flat(l, true))
232 .fold(base, |acc, c| acc.difference(c))
233 }
234
235 fn sketch_as_csg(sketch: Csg<f64>) -> Csg<f64> {
236 sketch
237 }
238 fn csg_as_sketch(csg: Csg<f64>) -> Csg<f64> {
239 csg
240 }
241}
242
243#[cfg(feature = "bridge-bevy")]
246impl rscad_csg_traits::CsgToMesh for BspBackend {
247 fn to_bevy_mesh(solid: &Csg<f64>) -> bevy_mesh::Mesh {
248 solid.clone().cleanup().cast::<f32>().to_bevy_mesh()
249 }
250}
251
252impl CsgToStl for BspBackend {
255 fn write_stl_binary(solid: &Csg<f64>, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
256 rscad_csg_traits::stl::write_stl_binary(writer, stl_triangles(solid).into_iter())
257 }
258
259 fn write_stl_ascii(solid: &Csg<f64>, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
260 rscad_csg_traits::stl::write_stl_ascii(writer, stl_triangles(solid).into_iter())
261 }
262}
263
264fn stl_triangles(solid: &Csg<f64>) -> Vec<rscad_csg_traits::stl::StlTriangle> {
265 let cleaned = solid.clone().cleanup();
266 cleaned
267 .polygons()
268 .iter()
269 .flat_map(|polygon| {
270 let tessellated = polygon.tessellate_earcut::<u32>();
271 let normal = polygon.plane().normal();
272 let n = [normal.x as f32, normal.y as f32, normal.z as f32];
273 let verts = tessellated.polygon().vertices();
274 let tris: Vec<u32> = tessellated.triangles().iter().copied().collect();
275 tris.chunks(3)
276 .map(|tri| {
277 let v = |i: u32| {
278 let p = verts[i as usize];
279 [p.x as f32, p.y as f32, p.z as f32]
280 };
281 (n, [v(tri[0]), v(tri[1]), v(tri[2])])
282 })
283 .collect::<Vec<_>>()
284 })
285 .collect()
286}
287
288fn apply_color(csg: &mut Csg<f64>, color: [f32; 4]) {
291 for (_, pm) in &mut csg.factory_mut().primitives {
292 if pm.color.is_none() {
293 pm.color = Some(color);
294 }
295 }
296}
297
298fn factory_prim(f: impl FnOnce(&mut crate::CsgFactory<f64>) -> Csg<f64>) -> Csg<f64> {
299 let mut factory = crate::CsgFactory::new();
300 let csg = f(&mut factory);
301 csg.with_factory(factory)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use rscad_core::Cube;
308
309 #[test]
310 fn cube_to_csg_produces_polygons() {
311 let cube = Cube::new([10]);
312 let csg = ToCsg::<BspBackend>::to_csg(&cube);
313 assert!(!csg.polygons().is_empty(), "cube should produce polygons");
314 assert_eq!(csg.polygons().len(), 6);
315 }
316
317 #[test]
318 fn translate_preserves_polygon_count() {
319 let shape = rscad_core::Translate::new(Cube::new([5]), 1.0, 2.0, 3.0);
320 let csg = ToCsg::<BspBackend>::to_csg(&shape);
321 assert_eq!(csg.polygons().len(), 6);
322 }
323
324 #[test]
325 fn union_combines_two_cubes() {
326 let a = Cube::new([5]);
327 let b = rscad_core::Translate::new(Cube::new([5]), 3.0, 0.0, 0.0);
328 let shape = rscad_core::Union { this: a, other: b };
329 let csg = ToCsg::<BspBackend>::to_csg(&shape);
330 assert!(!csg.polygons().is_empty());
331 }
332
333 #[test]
334 fn empty_produces_no_polygons() {
335 assert!(
336 ToCsg::<BspBackend>::to_csg(&rscad_core::Empty)
337 .polygons()
338 .is_empty()
339 );
340 assert!(ToCsg::<BspBackend>::to_csg(&()).polygons().is_empty());
341 }
342
343 fn signed_volume(csg: &Csg<f64>) -> f64 {
348 csg.polygons()
349 .iter()
350 .map(|poly| {
351 let vs = poly.vertices();
352 let v0 = vs[0].coords;
353 (1..vs.len() - 1)
354 .map(|i| v0.dot(&vs[i].coords.cross(&vs[i + 1].coords)) / 6.0)
355 .sum::<f64>()
356 })
357 .sum()
358 }
359
360 fn washer_profile() -> Csg<f64> {
362 crate::rect(1.0, 1.0).translate(nalgebra::Vector3::new(1.5, 0.0, 0.5))
363 }
364
365 #[test]
366 fn lathe_full_circle_volume_and_bounds() {
367 let solid = washer_profile().lathe(360.0, 64).expect("lathe");
368 let vol = signed_volume(&solid);
371 assert!(vol > 8.8 && vol < 9.5, "volume {vol}");
372
373 let (mut max_r, mut min_y, mut max_y) = (0.0f64, f64::MAX, f64::MIN);
374 for p in solid.polygons() {
375 for v in p.vertices() {
376 max_r = max_r.max((v.x * v.x + v.z * v.z).sqrt());
377 min_y = min_y.min(v.y);
378 max_y = max_y.max(v.y);
379 }
380 }
381 assert!((max_r - 2.0).abs() < 1e-9, "radius {max_r}");
382 assert!(min_y.abs() < 1e-9 && (max_y - 1.0).abs() < 1e-9);
383 }
384
385 #[test]
386 fn lathe_partial_angle_is_capped_quarter() {
387 let solid = washer_profile().lathe(90.0, 16).expect("lathe");
388 let vol = signed_volume(&solid);
389 assert!((vol - 9.42 / 4.0).abs() < 0.3, "volume {vol}");
392 }
393
394 #[test]
395 fn lathe_on_axis_profile_drops_degenerates() {
396 let profile = crate::rect(1.0, 1.0).translate(nalgebra::Vector3::new(0.5, 0.0, 0.5));
398 let solid = profile.lathe(360.0, 32).expect("lathe");
399 let vol = signed_volume(&solid);
400 assert!(vol > 2.9 && vol < 3.2, "volume {vol}");
402 }
403}