Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 30 kB View raw
1use std::collections::HashSet; 2 3use bone_types::{ 4 Aabb3, AngleTolerance, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, 5 ChordHeightTolerance, CreaseAngle, EdgeLabel, EdgeRole, FaceFingerprint, FaceLabel, Point3, 6 SideKind, SketchPlaneBasis, StepEntityKind, Tolerance, VertexLabel, 7}; 8use slotmap::SlotMap; 9use truck_modeling::ShellCondition; 10 11mod build; 12pub(crate) mod convert; 13mod edges; 14pub mod eval; 15pub mod persist; 16pub mod profile; 17pub mod step; 18pub mod tessellate; 19use crate::curve3::Curve3; 20use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length, edge_points}; 21use convert::point_from_truck; 22pub use edges::{EdgeCurve3, EdgePolyline, EdgePolylines}; 23pub use persist::{BrepReattach, EdgeReattach}; 24pub use tessellate::{FaceMesh, MeshError, SolidMesh}; 25 26#[derive(Debug, Clone, Copy, PartialEq, Eq)] 27pub enum TruckGap { 28 ReverseNormal, 29 AxisDirection, 30 ReferenceDirection, 31 ThroughAll, 32 UpToNext, 33 UpToVertex, 34 UpToSurface, 35 OffsetFromSurface, 36 UpToBody, 37 Draft, 38 ThinWall, 39} 40 41impl core::fmt::Display for TruckGap { 42 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 43 f.write_str(match self { 44 Self::ReverseNormal => "reverse-normal extrude direction", 45 Self::AxisDirection => "along-axis extrude direction", 46 Self::ReferenceDirection => "between-references extrude direction", 47 Self::ThroughAll => "through-all end condition", 48 Self::UpToNext => "up-to-next end condition", 49 Self::UpToVertex => "up-to-vertex end condition", 50 Self::UpToSurface => "up-to-surface end condition", 51 Self::OffsetFromSurface => "offset-from-surface end condition", 52 Self::UpToBody => "up-to-body end condition", 53 Self::Draft => "draft taper", 54 Self::ThinWall => "thin-wall shelling", 55 }) 56 } 57} 58 59#[derive(Debug, Clone, Copy, PartialEq, Eq)] 60pub enum LabelKind { 61 Face, 62 Edge, 63 Vertex, 64} 65 66impl core::fmt::Display for LabelKind { 67 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 68 f.write_str(match self { 69 Self::Face => "face", 70 Self::Edge => "edge", 71 Self::Vertex => "vertex", 72 }) 73 } 74} 75 76#[derive(Debug, Clone, Copy, PartialEq, Eq)] 77pub enum ProfileDefect { 78 OpenLoop, 79 BranchingVertex, 80 SelfIntersectingLoop, 81 ZeroArea, 82 UncontainedLoop, 83 OverlappingLoops, 84} 85 86impl core::fmt::Display for ProfileDefect { 87 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 88 f.write_str(match self { 89 Self::OpenLoop => "an open loop", 90 Self::BranchingVertex => "a vertex shared by more than two edges", 91 Self::SelfIntersectingLoop => "a self-intersecting loop", 92 Self::ZeroArea => "zero area", 93 Self::UncontainedLoop => "a loop outside the outer boundary", 94 Self::OverlappingLoops => "overlapping inner loops", 95 }) 96 } 97} 98 99#[derive(Debug, Clone, thiserror::Error)] 100pub enum BrepError { 101 #[error("extrude profile has {reason}")] 102 InvalidProfile { reason: ProfileDefect }, 103 #[error("extrude depth is zero")] 104 EmptyExtrudeDepth, 105 #[error("boundary shell is not a closed manifold")] 106 ShellNotClosed, 107 #[error("edge {edge:?} collapses below the validation tolerance")] 108 DegenerateEdge { edge: BrepEdgeId }, 109 #[error("edge {edge:?} belongs to no loop")] 110 DanglingEdge { edge: BrepEdgeId }, 111 #[error("vertex {vertex:?} belongs to no loop")] 112 DanglingVertex { vertex: BrepVertexId }, 113 #[error("kernel facade does not yet wrap this path: {detail}")] 114 TruckUnsupported { detail: TruckGap }, 115 #[error("{kind} carries no extrude label")] 116 MissingLabel { kind: LabelKind }, 117 #[error("solid has {found} {kind} entries, reattach payload carries {expected}")] 118 ReattachMismatch { 119 kind: LabelKind, 120 found: usize, 121 expected: usize, 122 }, 123 #[error("reattach payload geometry order does not match the solid")] 124 ReattachOrder, 125 #[error("solid geometry blob could not be serialized")] 126 BlobSerialize, 127 #[error("solid geometry blob could not be parsed")] 128 BlobParse, 129 #[error("STEP text is not valid ISO-10303-21")] 130 StepSyntax, 131 #[error("STEP file has no data section")] 132 StepNoData, 133 #[error("STEP shell could not be reconstructed")] 134 StepShellMalformed, 135 #[error("STEP file carries no solid shell")] 136 StepEmpty, 137 #[error("STEP file carries {count} solids; assemblies are not yet imported")] 138 StepMultipleSolids { count: usize }, 139 #[error("STEP geometry uses {kind}, which the facade does not yet bridge")] 140 StepUnsupported { kind: StepEntityKind }, 141 #[error("STEP parse canceled before it completed")] 142 Canceled, 143} 144 145#[derive(Clone, Debug)] 146pub struct BrepShell { 147 id: BrepShellId, 148 boundary_index: BoundaryIndex, 149 faces: Vec<BrepFaceId>, 150} 151 152impl BrepShell { 153 #[must_use] 154 pub fn id(&self) -> BrepShellId { 155 self.id 156 } 157 158 #[must_use] 159 pub fn faces(&self) -> &[BrepFaceId] { 160 &self.faces 161 } 162} 163 164#[derive(Clone, Debug)] 165pub struct BrepFace { 166 id: BrepFaceId, 167 label: FaceLabel, 168 loops: Vec<BrepLoopId>, 169} 170 171impl BrepFace { 172 #[must_use] 173 pub fn id(&self) -> BrepFaceId { 174 self.id 175 } 176 177 #[must_use] 178 pub fn label(&self) -> FaceLabel { 179 self.label 180 } 181 182 #[must_use] 183 pub fn loops(&self) -> &[BrepLoopId] { 184 &self.loops 185 } 186} 187 188#[derive(Clone, Debug)] 189pub struct BrepLoop { 190 id: BrepLoopId, 191 edges: Vec<BrepEdgeId>, 192} 193 194impl BrepLoop { 195 #[must_use] 196 pub fn id(&self) -> BrepLoopId { 197 self.id 198 } 199 200 #[must_use] 201 pub fn edges(&self) -> &[BrepEdgeId] { 202 &self.edges 203 } 204} 205 206#[derive(Clone, Debug)] 207pub struct BrepEdge { 208 id: BrepEdgeId, 209 label: EdgeLabel, 210 handles: Vec<EdgeArenaHandle>, 211 vertices: [BrepVertexId; 2], 212 curve: EdgeCurve3, 213 crease: CreaseAngle, 214} 215 216impl BrepEdge { 217 #[must_use] 218 pub fn id(&self) -> BrepEdgeId { 219 self.id 220 } 221 222 #[must_use] 223 pub fn label(&self) -> EdgeLabel { 224 self.label 225 } 226 227 #[must_use] 228 pub fn vertices(&self) -> [BrepVertexId; 2] { 229 self.vertices 230 } 231 232 #[must_use] 233 pub fn curve(&self) -> &EdgeCurve3 { 234 &self.curve 235 } 236 237 #[must_use] 238 pub fn crease(&self) -> CreaseAngle { 239 self.crease 240 } 241} 242 243#[derive(Clone, Debug)] 244pub struct BrepVertex { 245 id: BrepVertexId, 246 label: VertexLabel, 247 position: Point3, 248} 249 250impl BrepVertex { 251 #[must_use] 252 pub fn id(&self) -> BrepVertexId { 253 self.id 254 } 255 256 #[must_use] 257 pub fn label(&self) -> VertexLabel { 258 self.label 259 } 260 261 #[must_use] 262 pub fn position(&self) -> Point3 { 263 self.position 264 } 265} 266 267#[derive(Clone)] 268pub struct BrepSolid { 269 arena: Arena, 270 shells: SlotMap<BrepShellId, BrepShell>, 271 faces: SlotMap<BrepFaceId, BrepFace>, 272 loops: SlotMap<BrepLoopId, BrepLoop>, 273 edges: SlotMap<BrepEdgeId, BrepEdge>, 274 vertices: SlotMap<BrepVertexId, BrepVertex>, 275 shell_order: Vec<BrepShellId>, 276 face_order: Vec<BrepFaceId>, 277 loop_order: Vec<BrepLoopId>, 278 edge_order: Vec<BrepEdgeId>, 279 vertex_order: Vec<BrepVertexId>, 280 reattach: persist::BrepReattach, 281} 282 283impl BrepSolid { 284 pub fn iter_shells(&self) -> impl Iterator<Item = &BrepShell> { 285 self.shell_order.iter().map(|id| &self.shells[*id]) 286 } 287 288 pub fn iter_faces(&self) -> impl Iterator<Item = &BrepFace> { 289 self.face_order.iter().map(|id| &self.faces[*id]) 290 } 291 292 #[must_use] 293 pub fn face_plane_basis(&self, face: BrepFaceId) -> Option<SketchPlaneBasis> { 294 self.arena 295 .truck_face(face) 296 .and_then(build::face_plane_basis) 297 } 298 299 #[must_use] 300 pub fn face_fingerprint(&self, face: BrepFaceId) -> Option<FaceFingerprint> { 301 self.arena 302 .truck_face(face) 303 .and_then(build::face_fingerprint) 304 } 305 306 pub fn iter_loops(&self) -> impl Iterator<Item = &BrepLoop> { 307 self.loop_order.iter().map(|id| &self.loops[*id]) 308 } 309 310 pub fn iter_edges(&self) -> impl Iterator<Item = &BrepEdge> { 311 self.edge_order.iter().map(|id| &self.edges[*id]) 312 } 313 314 pub fn iter_vertices(&self) -> impl Iterator<Item = &BrepVertex> { 315 self.vertex_order.iter().map(|id| &self.vertices[*id]) 316 } 317 318 pub fn validate(&self, tolerance: Tolerance) -> Result<(), BrepError> { 319 if self.shells.is_empty() { 320 return Err(BrepError::ShellNotClosed); 321 } 322 self.iter_shells().try_for_each(|shell| { 323 let truck_shell = self.arena.boundary(shell.boundary_index); 324 let closed = truck_shell.shell_condition() == ShellCondition::Closed; 325 if closed && truck_shell.singular_vertices().is_empty() { 326 Ok(()) 327 } else { 328 Err(BrepError::ShellNotClosed) 329 } 330 })?; 331 self.iter_edges().try_for_each(|edge| { 332 let length: f64 = edge 333 .handles 334 .iter() 335 .map(|handle| edge_length(self.arena.edge(*handle))) 336 .sum(); 337 if length <= tolerance.value() { 338 Err(BrepError::DegenerateEdge { edge: edge.id }) 339 } else { 340 Ok(()) 341 } 342 })?; 343 let loop_edges: HashSet<BrepEdgeId> = self 344 .iter_loops() 345 .flat_map(|brep_loop| brep_loop.edges().iter().copied()) 346 .collect(); 347 self.iter_edges().try_for_each(|edge| { 348 if loop_edges.contains(&edge.id) 349 || matches!( 350 edge.label.role, 351 EdgeRole::SideEdge { 352 side: SideKind::Seam, 353 .. 354 } 355 ) 356 { 357 Ok(()) 358 } else { 359 Err(BrepError::DanglingEdge { edge: edge.id }) 360 } 361 })?; 362 let loop_vertices: HashSet<BrepVertexId> = loop_edges 363 .iter() 364 .flat_map(|edge| self.edges[*edge].vertices()) 365 .collect(); 366 self.iter_vertices().try_for_each(|vertex| { 367 if loop_vertices.contains(&vertex.id) { 368 Ok(()) 369 } else { 370 Err(BrepError::DanglingVertex { vertex: vertex.id }) 371 } 372 }) 373 } 374 375 #[must_use] 376 pub fn bounding_box(&self) -> Option<Aabb3> { 377 let points = self.iter_edges().flat_map(|edge| { 378 edge.handles.iter().flat_map(|handle| { 379 edge_points(self.arena.edge(*handle)) 380 .into_iter() 381 .map(point_from_truck) 382 }) 383 }); 384 Aabb3::from_points(points) 385 } 386 387 pub fn tessellate( 388 &self, 389 chord: ChordHeightTolerance, 390 angle: AngleTolerance, 391 ) -> Result<SolidMesh, MeshError> { 392 tessellate::tessellate_solid(self, chord, angle) 393 } 394 395 #[must_use] 396 pub fn edges_for_render(&self, chord: ChordHeightTolerance) -> EdgePolylines { 397 EdgePolylines::new( 398 self.iter_edges() 399 .filter(|edge| edges::is_render_visible(edge.label)) 400 .map(|edge| { 401 let points = edge.curve.tessellate(chord); 402 EdgePolyline::new(edge.id, edge.label, points, edge.curve.clone(), edge.crease) 403 }) 404 .collect(), 405 ) 406 } 407} 408 409#[cfg(test)] 410mod tests { 411 use super::build::{SolidLabeling, assemble, edge_length}; 412 use super::{BrepError, BrepLoop, BrepSolid, LabelKind, MeshError, SolidMesh}; 413 use bone_types::{ 414 AngleTolerance, BrepLoopId, ChordHeightTolerance, EdgeLabel, EdgeRole, FaceLabel, FaceRole, 415 FeatureId, LoopIndex, SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole, 416 }; 417 use slotmap::SlotMap; 418 use std::collections::{HashMap, HashSet}; 419 use truck_modeling::{Face, Point3, Shell, Solid, Vector3, builder}; 420 421 const LOW: f64 = 0.25; 422 const HIGH: f64 = 0.75; 423 424 struct Profile { 425 feature: FeatureId, 426 edges: [SketchEntityId; 4], 427 corners: [SketchEntityId; 4], 428 } 429 430 fn profile() -> Profile { 431 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 432 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 433 Profile { 434 feature: features.insert(()), 435 edges: [ 436 entities.insert(()), 437 entities.insert(()), 438 entities.insert(()), 439 entities.insert(()), 440 ], 441 corners: [ 442 entities.insert(()), 443 entities.insert(()), 444 entities.insert(()), 445 entities.insert(()), 446 ], 447 } 448 } 449 450 fn unit_cube() -> Solid { 451 let corner = builder::vertex(Point3::new(0.0, 0.0, 0.0)); 452 let edge = builder::tsweep(&corner, Vector3::new(1.0, 0.0, 0.0)); 453 let face = builder::tsweep(&edge, Vector3::new(0.0, 1.0, 0.0)); 454 builder::tsweep(&face, Vector3::new(0.0, 0.0, 1.0)) 455 } 456 457 fn iteration_fingerprint(solid: &Solid) -> String { 458 let faces = solid 459 .boundaries() 460 .iter() 461 .flat_map(Shell::face_iter) 462 .map(|face| { 463 let centroid = face 464 .boundaries() 465 .iter() 466 .flat_map(|wire| wire.vertex_iter().map(|v| v.point())) 467 .fold((0.0, 0.0, 0.0, 0.0), |(x, y, z, n), p| { 468 (x + p.x, y + p.y, z + p.z, n + 1.0) 469 }); 470 format!("f({:.3},{:.3},{:.3})", centroid.0, centroid.1, centroid.2) 471 }) 472 .collect::<Vec<_>>() 473 .join(" "); 474 let vertices = solid 475 .vertex_iter() 476 .map(|v| { 477 let p = v.point(); 478 format!("v({:.3},{:.3},{:.3})", p.x, p.y, p.z) 479 }) 480 .collect::<Vec<_>>() 481 .join(" "); 482 let edges = solid 483 .edge_iter() 484 .map(|e| { 485 let a = e.front().point(); 486 let b = e.back().point(); 487 format!("e({:.3},{:.3}->{:.3},{:.3})", a.x, a.y, b.x, b.y) 488 }) 489 .collect::<Vec<_>>() 490 .join(" "); 491 format!("faces:{faces}\nverts:{vertices}\nedges:{edges}") 492 } 493 494 #[test] 495 fn truck_solid_iteration_order_survives_serde() { 496 let solid = unit_cube(); 497 let before = iteration_fingerprint(&solid); 498 let Ok(text) = ron::to_string(&solid) else { 499 panic!("serialize truck solid"); 500 }; 501 let Ok(restored) = ron::from_str::<Solid>(&text) else { 502 panic!("deserialize truck solid"); 503 }; 504 let after = iteration_fingerprint(&restored); 505 assert_eq!(before, after); 506 } 507 508 fn corner_index(x: f64, y: f64) -> usize { 509 usize::from(x > 0.5) * 2 + usize::from(y > 0.5) 510 } 511 512 fn wall_index(footprint: &[(f64, f64)]) -> usize { 513 if footprint.iter().all(|&(_, y)| y < LOW) { 514 0 515 } else if footprint.iter().all(|&(x, _)| x > HIGH) { 516 1 517 } else if footprint.iter().all(|&(_, y)| y > HIGH) { 518 2 519 } else { 520 3 521 } 522 } 523 524 fn label_solid(solid: &Solid, profile: &Profile) -> SolidLabeling { 525 let faces: HashMap<_, _> = solid 526 .boundaries() 527 .iter() 528 .flat_map(Shell::face_iter) 529 .map(|face| { 530 let points: Vec<Point3> = face 531 .boundaries() 532 .iter() 533 .flat_map(|wire| wire.vertex_iter().map(|vertex| vertex.point())) 534 .collect(); 535 let role = if points.iter().all(|p| p.z < LOW) { 536 FaceRole::StartCap 537 } else if points.iter().all(|p| p.z > HIGH) { 538 FaceRole::EndCap 539 } else { 540 let footprint: Vec<(f64, f64)> = points.iter().map(|p| (p.x, p.y)).collect(); 541 FaceRole::Side { 542 loop_index: LoopIndex::OUTER, 543 from: profile.edges[wall_index(&footprint)], 544 } 545 }; 546 ( 547 face.id(), 548 FaceLabel { 549 feature: profile.feature, 550 role, 551 }, 552 ) 553 }) 554 .collect(); 555 556 let edges: HashMap<_, _> = solid 557 .edge_iter() 558 .map(|edge| { 559 let a = edge.front().point(); 560 let b = edge.back().point(); 561 let footprint = [(a.x, a.y), (b.x, b.y)]; 562 let role = if a.z < LOW && b.z < LOW { 563 EdgeRole::StartCapEdge { 564 from: profile.edges[wall_index(&footprint)], 565 } 566 } else if a.z > HIGH && b.z > HIGH { 567 EdgeRole::EndCapEdge { 568 from: profile.edges[wall_index(&footprint)], 569 } 570 } else { 571 EdgeRole::SideEdge { 572 from: profile.corners[corner_index(a.x, a.y)], 573 side: SideKind::Corner, 574 } 575 }; 576 ( 577 edge.id(), 578 EdgeLabel { 579 feature: profile.feature, 580 role, 581 }, 582 ) 583 }) 584 .collect(); 585 586 let vertices: HashMap<_, _> = solid 587 .vertex_iter() 588 .map(|vertex| { 589 let p = vertex.point(); 590 let from = profile.corners[corner_index(p.x, p.y)]; 591 let role = if p.z < 0.5 { 592 VertexRole::StartCapVertex { 593 from, 594 side: SideKind::Corner, 595 } 596 } else { 597 VertexRole::EndCapVertex { 598 from, 599 side: SideKind::Corner, 600 } 601 }; 602 ( 603 vertex.id(), 604 VertexLabel { 605 feature: profile.feature, 606 role, 607 }, 608 ) 609 }) 610 .collect(); 611 612 SolidLabeling { 613 faces, 614 edges, 615 vertices, 616 closed_curves: HashSet::new(), 617 edge_curves: HashMap::new(), 618 } 619 } 620 621 fn open_shell(solid: &Solid) -> Solid { 622 let faces: Vec<Face> = solid 623 .boundaries() 624 .iter() 625 .flat_map(Shell::face_iter) 626 .take(5) 627 .cloned() 628 .collect(); 629 Solid::new_unchecked(vec![faces.into_iter().collect::<Shell>()]) 630 } 631 632 #[test] 633 fn unit_cube_iteration_is_label_ordered() { 634 let profile = profile(); 635 let solid = unit_cube(); 636 let labeling = label_solid(&solid, &profile); 637 let Ok(brep) = assemble(solid, &labeling) else { 638 panic!("unit cube labels are complete"); 639 }; 640 641 let faces = brep 642 .iter_faces() 643 .map(|face| format!(" {}", face.label())) 644 .collect::<Vec<_>>() 645 .join("\n"); 646 let edges = brep 647 .iter_edges() 648 .map(|edge| format!(" {}", edge.label())) 649 .collect::<Vec<_>>() 650 .join("\n"); 651 let vertices = brep 652 .iter_vertices() 653 .map(|vertex| format!(" {}", vertex.label())) 654 .collect::<Vec<_>>() 655 .join("\n"); 656 let shells = brep 657 .iter_shells() 658 .map(|shell| format!(" shell faces={}", shell.faces().len())) 659 .collect::<Vec<_>>() 660 .join("\n"); 661 662 insta::assert_snapshot!(format!( 663 "shells:\n{shells}\nfaces:\n{faces}\nedges:\n{edges}\nvertices:\n{vertices}" 664 )); 665 } 666 667 #[test] 668 fn unit_cube_topology_counts() { 669 let profile = profile(); 670 let solid = unit_cube(); 671 let labeling = label_solid(&solid, &profile); 672 let Ok(brep) = assemble(solid, &labeling) else { 673 panic!("unit cube labels are complete"); 674 }; 675 assert_eq!(brep.iter_shells().count(), 1); 676 assert_eq!(brep.iter_faces().count(), 6); 677 assert_eq!(brep.iter_loops().count(), 6); 678 assert_eq!(brep.iter_edges().count(), 12); 679 assert_eq!(brep.iter_vertices().count(), 8); 680 let Some(first) = brep.iter_faces().next() else { 681 panic!("cube has faces"); 682 }; 683 let Some(last) = brep.iter_faces().last() else { 684 panic!("cube has faces"); 685 }; 686 assert_eq!(first.label().role, FaceRole::StartCap); 687 assert_eq!(last.label().role, FaceRole::EndCap); 688 } 689 690 #[test] 691 fn loop_iteration_follows_face_order() { 692 let profile = profile(); 693 let solid = unit_cube(); 694 let labeling = label_solid(&solid, &profile); 695 let Ok(brep) = assemble(solid, &labeling) else { 696 panic!("unit cube labels are complete"); 697 }; 698 let grouped_by_face: Vec<BrepLoopId> = brep 699 .iter_faces() 700 .flat_map(|face| face.loops().iter().copied()) 701 .collect(); 702 let iterated: Vec<BrepLoopId> = brep.iter_loops().map(BrepLoop::id).collect(); 703 assert_eq!(iterated.len(), 6); 704 assert_eq!(grouped_by_face, iterated); 705 } 706 707 #[test] 708 fn unit_cube_validates() { 709 let profile = profile(); 710 let solid = unit_cube(); 711 let labeling = label_solid(&solid, &profile); 712 let Ok(brep) = assemble(solid, &labeling) else { 713 panic!("unit cube labels are complete"); 714 }; 715 assert!(brep.validate(Tolerance::new(1e-9)).is_ok()); 716 } 717 718 #[test] 719 fn open_shell_is_not_closed() { 720 let profile = profile(); 721 let solid = open_shell(&unit_cube()); 722 let labeling = label_solid(&solid, &profile); 723 let Ok(brep) = assemble(solid, &labeling) else { 724 panic!("open shell labels are complete"); 725 }; 726 assert!(matches!( 727 brep.validate(Tolerance::new(1e-9)), 728 Err(BrepError::ShellNotClosed) 729 )); 730 } 731 732 #[test] 733 fn missing_label_is_reported() { 734 let profile = profile(); 735 let solid = unit_cube(); 736 let mut labeling = label_solid(&solid, &profile); 737 labeling.faces.clear(); 738 assert!(matches!( 739 assemble(solid, &labeling), 740 Err(BrepError::MissingLabel { 741 kind: LabelKind::Face 742 }) 743 )); 744 } 745 746 #[test] 747 fn shared_face_label_groups_into_one() { 748 let profile = profile(); 749 let solid = unit_cube(); 750 let mut labeling = label_solid(&solid, &profile); 751 let Some(shared) = labeling.faces.values().copied().next() else { 752 panic!("cube has faces"); 753 }; 754 labeling 755 .faces 756 .values_mut() 757 .for_each(|label| *label = shared); 758 let Ok(brep) = assemble(solid, &labeling) else { 759 panic!("faces sharing one label group rather than collide"); 760 }; 761 assert_eq!(brep.iter_faces().count(), 1); 762 } 763 764 #[test] 765 fn oversized_tolerance_flags_degenerate_edge() { 766 let profile = profile(); 767 let solid = unit_cube(); 768 let labeling = label_solid(&solid, &profile); 769 let Ok(brep) = assemble(solid, &labeling) else { 770 panic!("unit cube labels are complete"); 771 }; 772 assert!(matches!( 773 brep.validate(Tolerance::new(2.0)), 774 Err(BrepError::DegenerateEdge { .. }) 775 )); 776 } 777 778 #[test] 779 fn closed_curve_edge_is_not_degenerate() { 780 let start = builder::vertex(Point3::new(1.0, 0.0, 0.0)); 781 let end = builder::vertex(Point3::new(1.0, 0.05, 0.0)); 782 let arc = builder::circle_arc(&start, &end, Point3::new(-1.0, 0.0, 0.0)); 783 let gap = end.point() - start.point(); 784 let chord = gap.x.hypot(gap.y).hypot(gap.z); 785 assert!(chord < 0.1); 786 assert!(edge_length(&arc) > 5.0); 787 } 788 789 #[test] 790 fn empty_solid_is_not_closed() { 791 let solid = Solid::new_unchecked(vec![]); 792 let labeling = SolidLabeling { 793 faces: HashMap::new(), 794 edges: HashMap::new(), 795 vertices: HashMap::new(), 796 closed_curves: HashSet::new(), 797 edge_curves: HashMap::new(), 798 }; 799 let Ok(brep) = assemble(solid, &labeling) else { 800 panic!("empty solid has nothing to label"); 801 }; 802 assert!(matches!( 803 brep.validate(Tolerance::new(1e-9)), 804 Err(BrepError::ShellNotClosed) 805 )); 806 } 807 808 #[test] 809 fn missing_edge_label_is_reported() { 810 let profile = profile(); 811 let solid = unit_cube(); 812 let mut labeling = label_solid(&solid, &profile); 813 labeling.edges.clear(); 814 assert!(matches!( 815 assemble(solid, &labeling), 816 Err(BrepError::MissingLabel { 817 kind: LabelKind::Edge 818 }) 819 )); 820 } 821 822 #[test] 823 fn missing_vertex_label_is_reported() { 824 let profile = profile(); 825 let solid = unit_cube(); 826 let mut labeling = label_solid(&solid, &profile); 827 labeling.vertices.clear(); 828 assert!(matches!( 829 assemble(solid, &labeling), 830 Err(BrepError::MissingLabel { 831 kind: LabelKind::Vertex 832 }) 833 )); 834 } 835 836 #[test] 837 fn shared_edge_label_groups_into_one() { 838 let profile = profile(); 839 let solid = unit_cube(); 840 let mut labeling = label_solid(&solid, &profile); 841 let Some(shared) = labeling.edges.values().copied().next() else { 842 panic!("cube has edges"); 843 }; 844 labeling 845 .edges 846 .values_mut() 847 .for_each(|label| *label = shared); 848 let Ok(brep) = assemble(solid, &labeling) else { 849 panic!("edges sharing one label group rather than collide"); 850 }; 851 assert_eq!(brep.iter_edges().count(), 1); 852 } 853 854 #[test] 855 fn shared_vertex_label_groups_into_one() { 856 let profile = profile(); 857 let solid = unit_cube(); 858 let mut labeling = label_solid(&solid, &profile); 859 let Some(shared) = labeling.vertices.values().copied().next() else { 860 panic!("cube has vertices"); 861 }; 862 labeling 863 .vertices 864 .values_mut() 865 .for_each(|label| *label = shared); 866 let Ok(brep) = assemble(solid, &labeling) else { 867 panic!("vertices sharing one label group rather than collide"); 868 }; 869 assert_eq!(brep.iter_vertices().count(), 1); 870 } 871 872 fn unit_cube_brep() -> BrepSolid { 873 let profile = profile(); 874 let solid = unit_cube(); 875 let labeling = label_solid(&solid, &profile); 876 let Ok(brep) = assemble(solid, &labeling) else { 877 panic!("unit cube labels are complete"); 878 }; 879 brep 880 } 881 882 fn tessellate_cube(chord: ChordHeightTolerance, angle: AngleTolerance) -> SolidMesh { 883 let Ok(mesh) = unit_cube_brep().tessellate(chord, angle) else { 884 panic!("unit cube tessellates"); 885 }; 886 mesh 887 } 888 889 #[test] 890 fn tessellate_unit_cube_emits_per_face_slabs() { 891 let brep = unit_cube_brep(); 892 let Ok(mesh) = brep.tessellate( 893 ChordHeightTolerance::from_mm(0.05), 894 AngleTolerance::from_radians(0.2), 895 ) else { 896 panic!("unit cube tessellates"); 897 }; 898 let face_count = brep.iter_faces().count(); 899 assert_eq!(mesh.faces().len(), face_count); 900 mesh.faces().iter().for_each(|slab| { 901 assert!(!slab.triangles().is_empty(), "slab has triangles"); 902 assert!(!slab.positions().is_empty(), "slab has positions"); 903 assert_eq!(slab.positions().len(), slab.normals().len()); 904 }); 905 } 906 907 #[test] 908 fn tessellate_unit_cube_validates() { 909 let mesh = tessellate_cube( 910 ChordHeightTolerance::from_mm(0.05), 911 AngleTolerance::from_radians(0.2), 912 ); 913 assert!(mesh.validate(Tolerance::new(1e-9)).is_ok()); 914 } 915 916 #[test] 917 fn tessellate_is_deterministic() { 918 let chord = ChordHeightTolerance::from_mm(0.05); 919 let angle = AngleTolerance::from_radians(0.2); 920 let first = tessellate_cube(chord, angle); 921 let second = tessellate_cube(chord, angle); 922 assert_eq!(first, second); 923 assert_eq!(first.generation(), second.generation()); 924 } 925 926 #[test] 927 fn tessellate_generation_changes_with_chord() { 928 let coarse = tessellate_cube( 929 ChordHeightTolerance::from_mm(0.1), 930 AngleTolerance::from_radians(0.2), 931 ); 932 let fine = tessellate_cube( 933 ChordHeightTolerance::from_mm(0.001), 934 AngleTolerance::from_radians(0.2), 935 ); 936 assert_ne!(coarse.generation(), fine.generation()); 937 } 938 939 #[test] 940 fn tessellate_generation_changes_with_angle() { 941 let chord = ChordHeightTolerance::from_mm(0.05); 942 let lo = tessellate_cube(chord, AngleTolerance::from_radians(0.01)); 943 let hi = tessellate_cube(chord, AngleTolerance::from_radians(1.0)); 944 assert_ne!(lo.generation(), hi.generation()); 945 } 946 947 #[test] 948 fn tessellate_oversized_tolerance_flags_degenerate() { 949 let mesh = tessellate_cube( 950 ChordHeightTolerance::from_mm(0.05), 951 AngleTolerance::from_radians(0.2), 952 ); 953 assert!(matches!( 954 mesh.validate(Tolerance::new(10.0)), 955 Err(MeshError::DegenerateTriangle { .. }) 956 )); 957 } 958 959 proptest::proptest! { 960 #[test] 961 fn tessellate_proptest_is_deterministic( 962 chord_mm in 0.001f64..=0.2f64, 963 angle_rad in 0.01f64..=0.5f64, 964 ) { 965 let chord = ChordHeightTolerance::from_mm(chord_mm); 966 let angle = AngleTolerance::from_radians(angle_rad); 967 let first = tessellate_cube(chord, angle); 968 let second = tessellate_cube(chord, angle); 969 proptest::prop_assert_eq!(first, second); 970 } 971 } 972}