1use glam::DVec3;
5use manifold_csg::{CrossSection, JoinType, Manifold};
6use std::collections::HashMap;
7
8use rscad_core::Object;
9use rscad_csg_traits::{CsgBackend, DynamicBackend, ToCsg, ToSketch};
10
11pub struct ManifoldBackend;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub struct TriangleSourceId {
21 pub original_id: u32,
22 pub face_id: Option<u32>,
23}
24
25#[derive(Clone, Debug, Default, PartialEq)]
27pub struct SurfaceMetadata {
28 pub color: Option<[f32; 4]>,
29}
30
31#[derive(Clone)]
33pub struct ManifoldSolid {
34 manifold: Manifold,
35 metadata: HashMap<u32, SurfaceMetadata>,
36}
37
38impl ManifoldSolid {
39 fn tracked(manifold: Manifold) -> Self {
40 let mut metadata = HashMap::new();
41 if let Ok(id) = u32::try_from(manifold.original_id()) {
42 metadata.insert(id, SurfaceMetadata::default());
43 }
44 Self { manifold, metadata }
45 }
46
47 fn product(
48 manifold: Manifold,
49 mut left: HashMap<u32, SurfaceMetadata>,
50 right: HashMap<u32, SurfaceMetadata>,
51 ) -> Self {
52 for (id, meta) in right {
53 match left.entry(id) {
54 std::collections::hash_map::Entry::Vacant(entry) => {
55 entry.insert(meta);
56 }
57 std::collections::hash_map::Entry::Occupied(entry) => {
58 debug_assert_eq!(
59 entry.get(),
60 &meta,
61 "one Manifold OriginalID carried conflicting surface metadata"
62 );
63 }
64 }
65 }
66 Self {
67 manifold,
68 metadata: left,
69 }
70 }
71
72 pub fn manifold(&self) -> &Manifold {
73 &self.manifold
74 }
75
76 pub fn into_manifold(self) -> Manifold {
77 self.manifold
78 }
79
80 pub fn metadata(&self) -> &HashMap<u32, SurfaceMetadata> {
81 &self.metadata
82 }
83
84 pub fn surface_metadata(&self, id: TriangleSourceId) -> Option<&SurfaceMetadata> {
85 self.metadata.get(&id.original_id)
86 }
87
88 pub fn triangle_source_ids(&self) -> Result<Vec<TriangleSourceId>, ProvenanceError> {
90 let mesh = self.manifold.to_meshgl();
91 triangle_source_ids_from_parts(
92 mesh.tri_verts().len(),
93 &mesh.face_id(),
94 &mesh.run_index(),
95 &mesh.run_original_id(),
96 self.manifold.original_id(),
97 )
98 }
99}
100
101fn triangle_source_ids_from_parts(
102 triangle_index_count: usize,
103 face_ids: &[u32],
104 run_starts: &[u32],
105 original_ids: &[u32],
106 fallback_original_id: i32,
107) -> Result<Vec<TriangleSourceId>, ProvenanceError> {
108 let triangle_count = triangle_index_count / 3;
109 if !triangle_index_count.is_multiple_of(3) {
110 return Err(ProvenanceError::TriangleIndexCount);
111 }
112 if triangle_count == 0 {
113 return Ok(Vec::new());
114 }
115 if !face_ids.is_empty() && face_ids.len() != triangle_count {
116 return Err(ProvenanceError::FaceIdCount {
117 expected: triangle_count,
118 actual: face_ids.len(),
119 });
120 }
121 if run_starts.is_empty() && original_ids.is_empty() {
122 let original_id =
123 u32::try_from(fallback_original_id).map_err(|_| ProvenanceError::MissingRuns)?;
124 return Ok((0..triangle_count)
125 .map(|triangle| TriangleSourceId {
126 original_id,
127 face_id: face_ids.get(triangle).copied(),
128 })
129 .collect());
130 }
131 if original_ids.is_empty() || run_starts.len() < original_ids.len() {
132 return Err(ProvenanceError::RunCount {
133 starts: run_starts.len(),
134 originals: original_ids.len(),
135 });
136 }
137
138 let mut result = vec![None; triangle_count];
139 for (run, &original_id) in original_ids.iter().enumerate() {
140 let start = run_starts[run] as usize;
141 let end = run_starts
142 .get(run + 1)
143 .map_or(triangle_index_count, |&end| end as usize);
144 if !start.is_multiple_of(3)
145 || !end.is_multiple_of(3)
146 || start > end
147 || end > triangle_index_count
148 {
149 return Err(ProvenanceError::RunBoundary { run, start, end });
150 }
151 for (triangle, slot) in result.iter_mut().enumerate().take(end / 3).skip(start / 3) {
152 if slot.is_some() {
153 return Err(ProvenanceError::OverlappingRun { triangle });
154 }
155 *slot = Some(TriangleSourceId {
156 original_id,
157 face_id: face_ids.get(triangle).copied(),
158 });
159 }
160 }
161
162 result
163 .into_iter()
164 .enumerate()
165 .map(|(triangle, id)| id.ok_or(ProvenanceError::UncoveredTriangle { triangle }))
166 .collect()
167}
168
169impl std::ops::Deref for ManifoldSolid {
170 type Target = Manifold;
171
172 fn deref(&self) -> &Self::Target {
173 &self.manifold
174 }
175}
176
177impl AsRef<Manifold> for ManifoldSolid {
178 fn as_ref(&self) -> &Manifold {
179 &self.manifold
180 }
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
185pub enum ProvenanceError {
186 TriangleIndexCount,
187 FaceIdCount {
188 expected: usize,
189 actual: usize,
190 },
191 MissingRuns,
192 RunCount {
193 starts: usize,
194 originals: usize,
195 },
196 RunBoundary {
197 run: usize,
198 start: usize,
199 end: usize,
200 },
201 OverlappingRun {
202 triangle: usize,
203 },
204 UncoveredTriangle {
205 triangle: usize,
206 },
207}
208
209impl std::fmt::Display for ProvenanceError {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 write!(f, "invalid Manifold triangle provenance: {self:?}")
212 }
213}
214
215impl std::error::Error for ProvenanceError {}
216
217impl CsgBackend for ManifoldBackend {
218 type CsgType = ManifoldSolid;
219 type SketchType = CrossSection;
220
221 fn empty_csg() -> ManifoldSolid {
222 ManifoldSolid::tracked(Manifold::batch_union(&[]))
223 }
224 fn empty_sketch() -> CrossSection {
225 CrossSection::batch_union(&[])
226 }
227
228 fn union(a: ManifoldSolid, b: ManifoldSolid) -> ManifoldSolid {
229 let manifold = a.manifold.union(&b.manifold);
230 ManifoldSolid::product(manifold, a.metadata, b.metadata)
231 }
232 fn difference(a: ManifoldSolid, b: ManifoldSolid) -> ManifoldSolid {
233 let manifold = a.manifold.difference(&b.manifold);
234 ManifoldSolid::product(manifold, a.metadata, b.metadata)
235 }
236 fn intersection(a: ManifoldSolid, b: ManifoldSolid) -> ManifoldSolid {
237 let manifold = a.manifold.intersection(&b.manifold);
238 ManifoldSolid::product(manifold, a.metadata, b.metadata)
239 }
240 fn xor(a: ManifoldSolid, b: ManifoldSolid) -> ManifoldSolid {
241 let ab = a.manifold.union(&b.manifold);
242 let inter = a.manifold.intersection(&b.manifold);
243 ManifoldSolid::product(ab.difference(&inter), a.metadata, b.metadata)
244 }
245
246 fn union_2d(a: CrossSection, b: CrossSection) -> CrossSection {
247 a.union(&b)
248 }
249 fn difference_2d(a: CrossSection, b: CrossSection) -> CrossSection {
250 a.difference(&b)
251 }
252 fn intersection_2d(a: CrossSection, b: CrossSection) -> CrossSection {
253 a.intersection(&b)
254 }
255 fn xor_2d(a: CrossSection, b: CrossSection) -> CrossSection {
256 let ab = a.union(&b);
257 let inter = a.intersection(&b);
258 ab.difference(&inter)
259 }
260
261 fn translate(csg: ManifoldSolid, offset: DVec3) -> ManifoldSolid {
262 ManifoldSolid {
263 manifold: csg.manifold.translate(offset.x, offset.y, offset.z),
264 metadata: csg.metadata,
265 }
266 }
267 fn rotate(csg: ManifoldSolid, angles: DVec3) -> ManifoldSolid {
268 ManifoldSolid {
269 manifold: csg.manifold.rotate(angles.x, angles.y, angles.z),
270 metadata: csg.metadata,
271 }
272 }
273 fn scale(csg: ManifoldSolid, factors: DVec3) -> ManifoldSolid {
274 ManifoldSolid {
275 manifold: csg.manifold.scale(factors.x, factors.y, factors.z),
276 metadata: csg.metadata,
277 }
278 }
279 fn mirror(csg: ManifoldSolid, axes: DVec3) -> ManifoldSolid {
280 ManifoldSolid {
281 manifold: csg.manifold.mirror([axes.x, axes.y, axes.z]),
282 metadata: csg.metadata,
283 }
284 }
285
286 fn translate_2d(sketch: CrossSection, offset: DVec3) -> CrossSection {
287 sketch.translate(offset.x, offset.y)
288 }
289 fn rotate_2d(sketch: CrossSection, angles: DVec3) -> CrossSection {
290 sketch.rotate(angles.z)
291 }
292 fn scale_2d(sketch: CrossSection, factors: DVec3) -> CrossSection {
293 sketch.scale(factors.x, factors.y)
294 }
295 fn mirror_2d(sketch: CrossSection, axes: DVec3) -> CrossSection {
296 sketch.mirror(axes.x, axes.y)
297 }
298
299 fn color(mut csg: ManifoldSolid, color: [f32; 4]) -> ManifoldSolid {
300 if csg.metadata.is_empty()
301 && let Ok(id) = u32::try_from(csg.manifold.original_id())
302 {
303 csg.metadata.insert(id, SurfaceMetadata::default());
304 }
305 for metadata in csg.metadata.values_mut() {
306 metadata.color.get_or_insert(color);
307 }
308 csg
309 }
310 fn color_2d(sketch: CrossSection, _color: [f32; 4]) -> CrossSection {
311 sketch
312 }
313
314 fn extrude(
315 sketch: CrossSection,
316 height: f64,
317 twist: f64,
318 scale: f64,
319 slices: usize,
320 ) -> ManifoldSolid {
321 ManifoldSolid::tracked(z_to_y_up(Manifold::extrude_with_options(
322 &sketch,
323 height,
324 slices as i32,
325 twist,
326 scale,
327 scale,
328 )))
329 }
330
331 fn revolve(sketch: CrossSection, angle_degrees: f64, segments: usize) -> ManifoldSolid {
332 ManifoldSolid::tracked(z_to_y_up(Manifold::revolve(
333 &sketch,
334 segments as i32,
335 angle_degrees,
336 )))
337 }
338}
339
340fn z_to_y_up(m: Manifold) -> Manifold {
342 m.rotate(-90.0, 0.0, 0.0).mirror([0.0, 0.0, 1.0])
343}
344
345impl ToCsg<ManifoldBackend> for rscad_core::Cube {
348 fn to_csg(&self) -> ManifoldSolid {
349 ManifoldSolid::tracked(Manifold::cube(self.x, self.y, self.z, false))
350 }
351}
352
353impl ToCsg<ManifoldBackend> for rscad_core::Sphere {
354 fn to_csg(&self) -> ManifoldSolid {
355 ManifoldSolid::tracked(Manifold::sphere(self.radius, self.fn_ as i32))
356 }
357}
358
359impl ToCsg<ManifoldBackend> for rscad_core::Cylinder {
360 fn to_csg(&self) -> ManifoldSolid {
361 ManifoldSolid::tracked(z_to_y_up(Manifold::cylinder(
362 self.height,
363 self.radius,
364 self.radius,
365 self.fn_ as i32,
366 false,
367 )))
368 }
369}
370
371impl ToSketch<ManifoldBackend> for rscad_core::Circle {
374 fn to_sketch(&self) -> CrossSection {
375 CrossSection::circle(self.radius, self.fn_ as i32)
376 }
377}
378
379impl ToSketch<ManifoldBackend> for rscad_core::Square {
380 fn to_sketch(&self) -> CrossSection {
381 CrossSection::square(self.x, self.y, false)
382 }
383}
384
385impl<const N: usize> ToSketch<ManifoldBackend> for rscad_core::Polygon<N> {
386 fn to_sketch(&self) -> CrossSection {
387 let points = self.points_array().map(|p| p.to_array());
388 CrossSection::from_simple_polygon(&points)
389 }
390}
391
392impl ToSketch<ManifoldBackend> for rscad_core::PolygonDynamic {
393 fn to_sketch(&self) -> CrossSection {
394 let points: Vec<[f64; 2]> = self.points().map(|p| p.to_array()).collect();
395 CrossSection::from_simple_polygon(&points)
396 }
397}
398
399impl<T: Object + ToSketch<ManifoldBackend>> ToSketch<ManifoldBackend> for rscad_core::Offset<T> {
402 fn to_sketch(&self) -> CrossSection {
403 use rscad_core::transformations::OffsetType;
404 let join_type = match self.offset_type {
405 OffsetType::Normal => JoinType::Square,
406 OffsetType::Rounded => JoinType::Round,
407 };
408 self.object
409 .to_sketch()
410 .offset(self.offset, join_type, 2.0, 0)
411 }
412}
413
414impl DynamicBackend for ManifoldBackend {
417 fn cube(size: [f64; 3]) -> ManifoldSolid {
421 ManifoldSolid::tracked(Manifold::cube(size[0], size[1], size[2], false))
422 }
423 fn sphere(radius: f64, segments: usize, _stacks: usize) -> ManifoldSolid {
424 ManifoldSolid::tracked(Manifold::sphere(radius, segments as i32))
425 }
426 fn cylinder(radius: f64, height: f64, segments: usize) -> ManifoldSolid {
427 ManifoldSolid::tracked(z_to_y_up(Manifold::cylinder(
428 height,
429 radius,
430 radius,
431 segments as i32,
432 false,
433 )))
434 }
435 fn cone(radius: f64, height: f64, segments: usize) -> ManifoldSolid {
436 ManifoldSolid::tracked(z_to_y_up(Manifold::cylinder(
437 height,
438 radius,
439 0.0,
440 segments as i32,
441 false,
442 )))
443 }
444
445 fn circle(radius: f64, segments: usize) -> CrossSection {
446 CrossSection::circle(radius, segments as i32)
447 }
448 fn square(size: [f64; 2]) -> CrossSection {
449 CrossSection::square(size[0], size[1], false)
450 }
451 fn polygon(points: &[[f64; 2]]) -> CrossSection {
452 CrossSection::from_simple_polygon(points)
453 }
454 fn sketch(loops: &[Vec<glam::DVec2>]) -> CrossSection {
455 let loops: Vec<Vec<[f64; 2]>> = loops
456 .iter()
457 .map(|l| l.iter().map(|p| p.to_array()).collect())
458 .collect();
459 CrossSection::from_polygons(&loops)
461 }
462
463 fn offset(
464 sketch: CrossSection,
465 delta: f64,
466 round: bool,
467 chamfer: bool,
468 segments: usize,
469 ) -> CrossSection {
470 let join = match (round, chamfer) {
471 (true, _) => JoinType::Round,
472 (false, true) => JoinType::Square,
473 (false, false) => JoinType::Miter,
474 };
475 sketch.offset(delta, join, 2.0, segments as i32)
476 }
477
478 fn union_all(items: Vec<ManifoldSolid>) -> ManifoldSolid {
479 let manifolds: Vec<Manifold> = items.iter().map(|item| item.manifold.clone()).collect();
480 let mut metadata = HashMap::new();
481 for item in items {
482 for (id, meta) in item.metadata {
483 match metadata.entry(id) {
484 std::collections::hash_map::Entry::Vacant(entry) => {
485 entry.insert(meta);
486 }
487 std::collections::hash_map::Entry::Occupied(entry) => {
488 debug_assert_eq!(entry.get(), &meta);
489 }
490 }
491 }
492 }
493 ManifoldSolid {
494 manifold: Manifold::batch_union(&manifolds),
495 metadata,
496 }
497 }
498 fn union_all_2d(items: Vec<CrossSection>) -> CrossSection {
499 CrossSection::batch_union(&items)
500 }
501}
502
503#[cfg(feature = "bevy")]
509pub const ATTRIBUTE_SURFACE_ID: bevy_mesh::MeshVertexAttribute =
510 bevy_mesh::MeshVertexAttribute::new(
511 "Rscad_SurfaceId",
512 0x5253_0001,
513 wgpu::VertexFormat::Uint32x2,
514 );
515
516#[cfg(feature = "bevy")]
517pub const UNKNOWN_FACE_ID: u32 = u32::MAX;
518
519#[cfg(feature = "bevy")]
520impl rscad_csg_traits::CsgToMesh for ManifoldBackend {
521 fn to_bevy_mesh(solid: &ManifoldSolid) -> bevy_mesh::Mesh {
522 use bevy_asset::RenderAssetUsages;
523 use bevy_mesh::{Indices, Mesh as BevyMesh, PrimitiveTopology};
524
525 let manifold_mesh = solid.manifold.to_meshgl();
526 let props = manifold_mesh.vert_properties();
527 let n_props = manifold_mesh.num_prop();
528 let indices = manifold_mesh.tri_verts();
529
530 if n_props < 3 {
531 return BevyMesh::new(
532 PrimitiveTopology::TriangleList,
533 RenderAssetUsages::default(),
534 );
535 }
536
537 let n_verts = props.len().checked_div(n_props).unwrap_or(0);
538
539 let positions: Vec<[f32; 3]> = (0..n_verts)
540 .map(|i| {
541 let b = i * n_props;
542 [props[b], props[b + 1], props[b + 2]]
543 })
544 .collect();
545
546 let mut mesh = BevyMesh::new(
547 PrimitiveTopology::TriangleList,
548 RenderAssetUsages::default(),
549 );
550 mesh.insert_attribute(BevyMesh::ATTRIBUTE_POSITION, positions);
551 mesh.insert_indices(Indices::U32(indices));
552 mesh.duplicate_vertices();
553 mesh.compute_flat_normals();
554
555 match triangle_source_ids_from_parts(
556 manifold_mesh.tri_verts().len(),
557 &manifold_mesh.face_id(),
558 &manifold_mesh.run_index(),
559 &manifold_mesh.run_original_id(),
560 solid.manifold.original_id(),
561 ) {
562 Ok(source_ids) => {
563 let surface_ids: Vec<[u32; 2]> = source_ids
564 .iter()
565 .flat_map(|id| {
566 std::iter::repeat_n(
567 [id.original_id, id.face_id.unwrap_or(UNKNOWN_FACE_ID)],
568 3,
569 )
570 })
571 .collect();
572 mesh.insert_attribute(ATTRIBUTE_SURFACE_ID, surface_ids);
573
574 let has_colors = source_ids.iter().any(|id| {
575 solid
576 .surface_metadata(*id)
577 .and_then(|metadata| metadata.color)
578 .is_some()
579 });
580 if has_colors {
581 let colors: Vec<[f32; 4]> = source_ids
582 .iter()
583 .flat_map(|id| {
584 let color = solid
585 .surface_metadata(*id)
586 .and_then(|metadata| metadata.color)
587 .unwrap_or([1.0; 4]);
588 std::iter::repeat_n(color, 3)
589 })
590 .collect();
591 mesh.insert_attribute(BevyMesh::ATTRIBUTE_COLOR, colors);
592 }
593 }
594 Err(error) => {
595 tracing::warn!(%error, "Manifold mesh omitted invalid surface provenance");
596 }
597 }
598 mesh
599 }
600}
601
602impl rscad_csg_traits::CsgToStl for ManifoldBackend {
605 fn write_stl_binary(
606 solid: &ManifoldSolid,
607 writer: &mut dyn std::io::Write,
608 ) -> std::io::Result<()> {
609 rscad_csg_traits::stl::write_stl_binary(writer, stl_triangles(&solid.manifold).into_iter())
610 }
611
612 fn write_stl_ascii(
613 solid: &ManifoldSolid,
614 writer: &mut dyn std::io::Write,
615 ) -> std::io::Result<()> {
616 rscad_csg_traits::stl::write_stl_ascii(writer, stl_triangles(&solid.manifold).into_iter())
617 }
618}
619
620fn stl_triangles(solid: &Manifold) -> Vec<rscad_csg_traits::stl::StlTriangle> {
621 let (props, n_props, indices) = solid.to_mesh_f32();
622 if n_props < 3 {
623 return vec![];
624 }
625 let get_pos = |i: usize| -> [f32; 3] {
626 let b = i * n_props;
627 [props[b], props[b + 1], props[b + 2]]
628 };
629 indices
630 .chunks_exact(3)
631 .map(|tri| {
632 let v0 = get_pos(tri[0] as usize);
633 let v1 = get_pos(tri[1] as usize);
634 let v2 = get_pos(tri[2] as usize);
635 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
637 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
638 let n = [
639 e1[1] * e2[2] - e1[2] * e2[1],
640 e1[2] * e2[0] - e1[0] * e2[2],
641 e1[0] * e2[1] - e1[1] * e2[0],
642 ];
643 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
644 let normal = if len > 0.0 {
645 [n[0] / len, n[1] / len, n[2] / len]
646 } else {
647 [0.0, 0.0, 0.0]
648 };
649 (normal, [v0, v1, v2])
650 })
651 .collect()
652}
653
654#[cfg(test)]
657mod tests {
658 use super::*;
659 use rscad_core::{Cube, DynamicObject};
660
661 fn colored(color: [f64; 4], child: DynamicObject) -> DynamicObject {
662 DynamicObject::Color {
663 color,
664 child: Box::new(child),
665 }
666 }
667
668 #[test]
669 fn run_boundaries_expand_to_triangle_sources() {
670 let sources = triangle_source_ids_from_parts(
671 18,
672 &[100, 101, 102, 103, 104, 105],
673 &[0, 6, 18],
674 &[10, 20],
675 -1,
676 )
677 .unwrap();
678 assert_eq!(sources.len(), 6);
679 assert!(sources[..2].iter().all(|source| source.original_id == 10));
680 assert!(sources[2..].iter().all(|source| source.original_id == 20));
681 assert_eq!(sources[3].face_id, Some(103));
682 }
683
684 #[test]
685 fn malformed_run_boundaries_are_rejected() {
686 assert!(matches!(
687 triangle_source_ids_from_parts(6, &[], &[0, 4], &[10], -1),
688 Err(ProvenanceError::RunBoundary { .. })
689 ));
690 assert!(matches!(
691 triangle_source_ids_from_parts(6, &[], &[3], &[10], -1),
692 Err(ProvenanceError::UncoveredTriangle { triangle: 0 })
693 ));
694 }
695
696 #[test]
697 fn cube_to_manifold_produces_mesh() {
698 let cube = Cube::new([10]);
699 let m = ToCsg::<ManifoldBackend>::to_csg(&cube);
700 let (props, n_props, indices) = m.to_mesh_f32_with_normals(3);
701 assert!(n_props > 0, "should have vertex properties");
702 assert!(!props.is_empty(), "should have vertex data");
703 assert!(!indices.is_empty(), "should have triangle indices");
704 }
705
706 #[test]
707 fn cube_has_triangle_provenance_for_six_faces() {
708 let solid = ToCsg::<ManifoldBackend>::to_csg(&Cube::new([10]));
709 let (_, _, indices) = solid.to_mesh_f32();
710 let sources = solid.triangle_source_ids().unwrap();
711 assert_eq!(sources.len(), indices.len() / 3);
712 assert!(
713 sources
714 .iter()
715 .all(|id| id.original_id == sources[0].original_id)
716 );
717 let faces: std::collections::HashSet<_> =
718 sources.iter().filter_map(|id| id.face_id).collect();
719 assert_eq!(faces.len(), 6);
720 }
721
722 #[test]
723 fn translate_preserves_mesh() {
724 let shape = rscad_core::Translate::new(Cube::new([5]), 1.0, 2.0, 3.0);
725 let m = ToCsg::<ManifoldBackend>::to_csg(&shape);
726 let (_, _, indices) = m.to_mesh_f32_with_normals(3);
727 assert!(!indices.is_empty());
728 }
729
730 #[test]
731 fn union_combines_two_cubes() {
732 let a = Cube::new([5]);
733 let b = rscad_core::Translate::new(Cube::new([5]), 3.0, 0.0, 0.0);
734 let shape = rscad_core::Union { this: a, other: b };
735 let m = ToCsg::<ManifoldBackend>::to_csg(&shape);
736 let (_, _, indices) = m.to_mesh_f32_with_normals(3);
737 assert!(!indices.is_empty());
738 }
739
740 #[test]
741 fn difference_subtracts() {
742 let a = Cube::new([10]);
743 let b = rscad_core::Translate::new(Cube::new([5]), 2.5, 2.5, 2.5);
744 let shape = rscad_core::Difference { this: a, other: b };
745 let m = ToCsg::<ManifoldBackend>::to_csg(&shape);
746 let (_, _, indices) = m.to_mesh_f32_with_normals(3);
747 assert!(!indices.is_empty());
748 }
749
750 #[test]
751 fn difference_retains_both_operand_colors() {
752 let scene = DynamicObject::Difference {
753 children: vec![
754 colored(
755 [1.0, 0.0, 0.0, 1.0],
756 DynamicObject::Cube {
757 size: [10.0, 10.0, 10.0],
758 },
759 ),
760 colored(
761 [0.0, 0.0, 1.0, 1.0],
762 DynamicObject::Translate {
763 offset: [2.5, 2.5, 2.5],
764 child: Box::new(DynamicObject::Cube {
765 size: [5.0, 5.0, 5.0],
766 }),
767 },
768 ),
769 ],
770 };
771 let solid = ToCsg::<ManifoldBackend>::to_csg(&scene);
772 let sources = solid.triangle_source_ids().unwrap();
773 let colors: std::collections::HashSet<[u32; 4]> = sources
774 .iter()
775 .filter_map(|id| solid.surface_metadata(*id)?.color)
776 .map(|color| color.map(f32::to_bits))
777 .collect();
778 assert_eq!(colors.len(), 2);
779 assert!(colors.contains(&[1.0f32.to_bits(), 0, 0, 1.0f32.to_bits()]));
780 assert!(colors.contains(&[0, 0, 1.0f32.to_bits(), 1.0f32.to_bits()]));
781 }
782
783 #[test]
784 fn transforms_preserve_original_id_and_color() {
785 let scene = DynamicObject::Translate {
786 offset: [10.0, 20.0, 30.0],
787 child: Box::new(colored(
788 [0.25, 0.5, 0.75, 1.0],
789 DynamicObject::Cube {
790 size: [1.0, 1.0, 1.0],
791 },
792 )),
793 };
794 let solid = ToCsg::<ManifoldBackend>::to_csg(&scene);
795 let sources = solid.triangle_source_ids().unwrap();
796 assert!(sources.iter().all(|id| {
797 solid.surface_metadata(*id).and_then(|meta| meta.color) == Some([0.25, 0.5, 0.75, 1.0])
798 }));
799 }
800
801 #[test]
802 fn inner_color_wins_like_bsp_metadata() {
803 let scene = colored(
804 [0.0, 0.0, 1.0, 1.0],
805 colored(
806 [1.0, 0.0, 0.0, 1.0],
807 DynamicObject::Cube {
808 size: [1.0, 1.0, 1.0],
809 },
810 ),
811 );
812 let solid = ToCsg::<ManifoldBackend>::to_csg(&scene);
813 assert!(
814 solid
815 .metadata()
816 .values()
817 .all(|metadata| { metadata.color == Some([1.0, 0.0, 0.0, 1.0]) })
818 );
819 }
820
821 #[test]
822 fn empty_produces_no_mesh() {
823 let m = ToCsg::<ManifoldBackend>::to_csg(&rscad_core::Empty);
824 let (props, _, indices) = m.to_mesh_f32_with_normals(3);
825 assert!(props.is_empty());
826 assert!(indices.is_empty());
827 }
828
829 #[test]
830 fn linear_extrude_circle() {
831 let circle = rscad_core::Circle::new(5.0, 0.0, 0.0, 32);
832 let extruded = rscad_core::extrude::LinearExtrude::new(circle).with_height(10.0);
833 let m = ToCsg::<ManifoldBackend>::to_csg(&extruded);
834 let (_, _, indices) = m.to_mesh_f32_with_normals(3);
835 assert!(!indices.is_empty());
836 }
837
838 #[test]
839 fn cross_section_square() {
840 let sq = rscad_core::Square::new(5.0, 3.0);
841 let cs = ToSketch::<ManifoldBackend>::to_sketch(&sq);
842 let area = cs.area();
843 assert!((area - 15.0).abs() < 0.01, "square area should be ~15.0");
844 }
845
846 #[test]
847 fn sketch_profile_with_hole() {
848 use rscad_core::sketch::SketchData;
849 let mut data = SketchData::default();
850 data.add_rect(glam::DVec2::ZERO, glam::DVec2::new(2.0, 1.0));
851 data.add_circle(glam::DVec2::new(1.0, 0.5), 0.25);
852 let cs = ToSketch::<ManifoldBackend>::to_sketch(&DynamicObject::Sketch { data });
853 let area = cs.area();
854 let expected = 2.0 - core::f64::consts::PI * 0.25 * 0.25;
855 assert!((area - expected).abs() < 0.02, "area {area} vs {expected}");
857 }
858
859 #[test]
862 fn sketch_crossing_outers_union_area() {
863 use rscad_core::sketch::SketchData;
864 let mut data = SketchData::default();
865 data.add_rect(glam::DVec2::ZERO, glam::DVec2::new(2.0, 2.0));
866 data.add_rect(glam::DVec2::new(1.0, 1.0), glam::DVec2::new(3.0, 3.0));
867 let cs = ToSketch::<ManifoldBackend>::to_sketch(&DynamicObject::Sketch { data });
868 let area = cs.area();
869 let expected = 4.0 + 4.0 - 1.0;
870 assert!((area - expected).abs() < 0.01, "area {area} vs {expected}");
871 }
872
873 #[cfg(feature = "bevy")]
874 #[test]
875 fn bevy_mesh_carries_surface_ids_and_mixed_colors() {
876 use bevy_mesh::{Mesh as BevyMesh, VertexAttributeValues};
877 use rscad_csg_traits::CsgToMesh;
878
879 let scene = DynamicObject::Union {
880 children: vec![
881 colored(
882 [1.0, 0.0, 0.0, 1.0],
883 DynamicObject::Cube {
884 size: [1.0, 1.0, 1.0],
885 },
886 ),
887 DynamicObject::Translate {
888 offset: [2.0, 0.0, 0.0],
889 child: Box::new(DynamicObject::Cube {
890 size: [1.0, 1.0, 1.0],
891 }),
892 },
893 ],
894 };
895 let solid = ToCsg::<ManifoldBackend>::to_csg(&scene);
896 let mesh = ManifoldBackend::to_bevy_mesh(&solid);
897 assert!(
898 mesh.indices().is_none(),
899 "triangle vertices must be separated"
900 );
901
902 let surface_ids = match mesh.attribute(ATTRIBUTE_SURFACE_ID).unwrap() {
903 VertexAttributeValues::Uint32x2(values) => values,
904 other => panic!("unexpected surface ID format: {other:?}"),
905 };
906 let colors = match mesh.attribute(BevyMesh::ATTRIBUTE_COLOR).unwrap() {
907 VertexAttributeValues::Float32x4(values) => values,
908 other => panic!("unexpected color format: {other:?}"),
909 };
910 assert_eq!(surface_ids.len(), mesh.count_vertices());
911 assert_eq!(colors.len(), mesh.count_vertices());
912 assert!(
913 surface_ids
914 .chunks_exact(3)
915 .all(|tri| tri[0] == tri[1] && tri[1] == tri[2])
916 );
917 assert!(colors.contains(&[1.0, 0.0, 0.0, 1.0]));
918 assert!(colors.contains(&[1.0; 4]));
919 }
920
921 #[cfg(feature = "bevy")]
922 #[test]
923 fn uncolored_bevy_mesh_omits_color_attribute() {
924 use bevy_mesh::Mesh as BevyMesh;
925 use rscad_csg_traits::CsgToMesh;
926
927 let solid = ToCsg::<ManifoldBackend>::to_csg(&Cube::new([1]));
928 let mesh = ManifoldBackend::to_bevy_mesh(&solid);
929 assert!(mesh.attribute(ATTRIBUTE_SURFACE_ID).is_some());
930 assert!(mesh.attribute(BevyMesh::ATTRIBUTE_COLOR).is_none());
931 }
932}