Another project
0

Configure Feed

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

fix(kernel): angle tolerance

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 29, 2026, 11:45 PM +0300) commit 8615bdf9 parent d137ebdf change-id yunrznwv
+667 -35
+37
Cargo.lock
··· 438 438 "serde", 439 439 "slotmap", 440 440 "thiserror 2.0.18", 441 + "truck-meshalgo", 441 442 "truck-modeling", 442 443 "uom", 443 444 ] ··· 3135 3136 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" 3136 3137 3137 3138 [[package]] 3139 + name = "robust" 3140 + version = "1.2.0" 3141 + source = "registry+https://github.com/rust-lang/crates.io-index" 3142 + checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" 3143 + 3144 + [[package]] 3138 3145 name = "ron" 3139 3146 version = "0.12.1" 3140 3147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3450 3457 checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" 3451 3458 dependencies = [ 3452 3459 "serde", 3460 + ] 3461 + 3462 + [[package]] 3463 + name = "spade" 3464 + version = "2.15.1" 3465 + source = "registry+https://github.com/rust-lang/crates.io-index" 3466 + checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" 3467 + dependencies = [ 3468 + "hashbrown 0.16.1", 3469 + "num-traits", 3470 + "robust", 3471 + "smallvec", 3453 3472 ] 3454 3473 3455 3474 [[package]] ··· 3794 3813 "thiserror 1.0.69", 3795 3814 "truck-base", 3796 3815 "truck-derivers", 3816 + ] 3817 + 3818 + [[package]] 3819 + name = "truck-meshalgo" 3820 + version = "0.4.0" 3821 + source = "registry+https://github.com/rust-lang/crates.io-index" 3822 + checksum = "e18f325738e822c79bb1a48897ec789cbf50cbeeb0841105295f9da85bdf3613" 3823 + dependencies = [ 3824 + "array-macro", 3825 + "derive_more", 3826 + "itertools", 3827 + "rayon", 3828 + "rustc-hash 2.1.2", 3829 + "spade", 3830 + "truck-base", 3831 + "truck-geometry", 3832 + "truck-polymesh", 3833 + "truck-topology", 3797 3834 ] 3798 3835 3799 3836 [[package]]
+1 -5
crates/bone-app/src/sketch_mode.rs
··· 399 399 400 400 #[test] 401 401 fn entities_table_holds_day_one_set() { 402 - assert_eq!( 403 - SketchTool::ENTITIES.len(), 404 - 12, 405 - "day-1 entity tools" 406 - ); 402 + assert_eq!(SketchTool::ENTITIES.len(), 12, "day-1 entity tools"); 407 403 } 408 404 409 405 #[test]
+1
crates/bone-kernel/Cargo.toml
··· 10 10 serde = { workspace = true } 11 11 slotmap = { workspace = true } 12 12 thiserror = { workspace = true } 13 + truck-meshalgo = { version = "=0.4.0", default-features = false, features = ["tessellation"] } 13 14 truck-modeling = "=0.6.0" 14 15 uom = { workspace = true } 15 16
+27 -2
crates/bone-kernel/src/brep/build.rs
··· 22 22 pub(crate) struct Arena { 23 23 solid: Solid, 24 24 edges: Vec<Edge>, 25 + face_index: HashMap<FaceID, BrepFaceId>, 25 26 } 26 27 27 28 impl Arena { ··· 32 33 pub(crate) fn edge(&self, handle: EdgeArenaHandle) -> &Edge { 33 34 &self.edges[handle.0] 34 35 } 36 + 37 + pub(crate) fn solid(&self) -> &Solid { 38 + &self.solid 39 + } 40 + 41 + pub(crate) fn face_index(&self) -> &HashMap<FaceID, BrepFaceId> { 42 + &self.face_index 43 + } 35 44 } 36 45 37 46 const LENGTH_DIVISION_TOLERANCE: Tolerance = Tolerance::new(1.0e-4); ··· 91 100 arena: Arena { 92 101 solid, 93 102 edges: edges.arena, 103 + face_index: faces.face_index, 94 104 }, 95 105 shells: faces.shells, 96 106 faces: faces.faces, ··· 199 209 }, 200 210 )?; 201 211 202 - let (map, label_to_id) = accum.into_iter().fold( 212 + let mut ordered_edges: Vec<(EdgeLabel, EdgeAccum)> = accum.into_iter().collect(); 213 + ordered_edges.sort_by_key(|(label, _)| *label); 214 + let (map, label_to_id) = ordered_edges.into_iter().fold( 203 215 (SlotMap::with_key(), HashMap::<EdgeLabel, BrepEdgeId>::new()), 204 216 |(mut map, mut label_to_id), (label, slot)| { 205 217 let vertices = logical_endpoints(&slot.incident); ··· 251 263 faces: SlotMap<BrepFaceId, BrepFace>, 252 264 loops: SlotMap<BrepLoopId, BrepLoop>, 253 265 shells: SlotMap<BrepShellId, BrepShell>, 266 + face_index: HashMap<FaceID, BrepFaceId>, 254 267 } 255 268 256 269 #[derive(Default)] ··· 258 271 adjacency: HashMap<EdgeID, Vec<FaceLabel>>, 259 272 patches: HashMap<FaceLabel, Vec<Vec<Vec<EdgeID>>>>, 260 273 shell_members: Vec<Vec<FaceLabel>>, 274 + face_to_label: Vec<(FaceID, FaceLabel)>, 261 275 } 262 276 263 277 fn build_faces( ··· 269 283 adjacency, 270 284 patches, 271 285 shell_members, 286 + face_to_label, 272 287 } = gather_faces(solid, labeling)?; 273 288 274 - let (faces, loops, label_to_face) = patches.into_iter().fold( 289 + let mut ordered_patches: Vec<(FaceLabel, Vec<Vec<Vec<EdgeID>>>)> = 290 + patches.into_iter().collect(); 291 + ordered_patches.sort_by_key(|(label, _)| *label); 292 + let (faces, loops, label_to_face) = ordered_patches.into_iter().fold( 275 293 ( 276 294 SlotMap::with_key(), 277 295 SlotMap::with_key(), ··· 305 323 }, 306 324 ); 307 325 326 + let face_index = face_to_label 327 + .into_iter() 328 + .map(|(face_id, label)| (face_id, label_to_face[&label])) 329 + .collect(); 330 + 308 331 Ok(FaceTable { 309 332 faces, 310 333 loops, 311 334 shells, 335 + face_index, 312 336 }) 313 337 } 314 338 ··· 335 359 gathered.adjacency.entry(*edge_id).or_default().push(label); 336 360 }); 337 361 gathered.patches.entry(label).or_default().push(wires); 362 + gathered.face_to_label.push((face.id(), label)); 338 363 Ok::<FaceLabel, BrepError>(label) 339 364 }) 340 365 .collect::<Result<Vec<_>, _>>()?;
+69
crates/bone-kernel/src/brep/convert.rs
··· 1 + #[cfg(test)] 2 + use bone_types::Vec3; 3 + use bone_types::{Point3, Tolerance, UnitVec3}; 4 + use truck_modeling::{Point3 as TruckPoint3, Vector3 as TruckVector3}; 5 + 6 + pub(crate) fn point_from_truck(p: TruckPoint3) -> Point3 { 7 + Point3::from_mm(p.x, p.y, p.z) 8 + } 9 + 10 + pub(crate) fn try_unit_from_truck(v: TruckVector3, tolerance: Tolerance) -> Option<UnitVec3> { 11 + UnitVec3::try_from_components(v.x, v.y, v.z, tolerance).ok() 12 + } 13 + 14 + #[cfg(test)] 15 + pub(crate) fn point_to_truck(p: Point3) -> TruckPoint3 { 16 + let (x, y, z) = p.coords_mm(); 17 + TruckPoint3::new(x, y, z) 18 + } 19 + 20 + #[cfg(test)] 21 + pub(crate) fn vec_from_truck(v: TruckVector3) -> Vec3 { 22 + Vec3::from_mm(v.x, v.y, v.z) 23 + } 24 + 25 + #[cfg(test)] 26 + pub(crate) fn vec_to_truck(v: Vec3) -> TruckVector3 { 27 + let (x, y, z) = v.coords_mm(); 28 + TruckVector3::new(x, y, z) 29 + } 30 + 31 + #[cfg(test)] 32 + mod tests { 33 + use super::*; 34 + use proptest::prelude::*; 35 + 36 + fn finite_coord() -> impl Strategy<Value = f64> { 37 + prop::num::f64::POSITIVE | prop::num::f64::NEGATIVE | prop::num::f64::ZERO 38 + } 39 + 40 + proptest! { 41 + #[test] 42 + fn point_roundtrips_to_ulp( 43 + x in finite_coord(), 44 + y in finite_coord(), 45 + z in finite_coord(), 46 + ) { 47 + let bone = Point3::from_mm(x, y, z); 48 + let back = point_from_truck(point_to_truck(bone)); 49 + let (bx, by, bz) = back.coords_mm(); 50 + prop_assert!(bx.to_bits() == x.to_bits()); 51 + prop_assert!(by.to_bits() == y.to_bits()); 52 + prop_assert!(bz.to_bits() == z.to_bits()); 53 + } 54 + 55 + #[test] 56 + fn vec_roundtrips_to_ulp( 57 + x in finite_coord(), 58 + y in finite_coord(), 59 + z in finite_coord(), 60 + ) { 61 + let bone = Vec3::from_mm(x, y, z); 62 + let back = vec_from_truck(vec_to_truck(bone)); 63 + let (bx, by, bz) = back.coords_mm(); 64 + prop_assert!(bx.to_bits() == x.to_bits()); 65 + prop_assert!(by.to_bits() == y.to_bits()); 66 + prop_assert!(bz.to_bits() == z.to_bits()); 67 + } 68 + } 69 + }
+14 -7
crates/bone-kernel/src/brep/eval.rs
··· 31 31 ) -> Result<BrepSolid, BrepError> { 32 32 let plan = sweep_plan(feature)?; 33 33 let resolved = resolve_profile(profile)?; 34 - let (solid, labeling) = 35 - build_solid(profile.plane(), &resolved, plan, extrude, closed_curves(profile))?; 34 + let (solid, labeling) = build_solid( 35 + profile.plane(), 36 + &resolved, 37 + plan, 38 + extrude, 39 + closed_curves(profile), 40 + )?; 36 41 assemble(solid, &labeling) 37 42 } 38 43 ··· 379 384 380 385 fn junction_entry(segments: &[Segment], index: usize) -> (SideKind, SketchEntityId) { 381 386 let segment = segments[index]; 382 - segment.corner.map_or( 383 - (SideKind::Seam, segment.curve_entity), 384 - |corner| (SideKind::Corner, corner), 385 - ) 387 + segment 388 + .corner 389 + .map_or((SideKind::Seam, segment.curve_entity), |corner| { 390 + (SideKind::Corner, corner) 391 + }) 386 392 } 387 393 388 394 fn sub_edges(curve: Curve2Kind, reversed: bool) -> Vec<(Point2, Point2, Option<Point2>)> { ··· 458 464 }) 459 465 .collect(); 460 466 461 - let face = builder::try_attach_plane(&wires).map_err(|_| invalid(ProfileDefect::SelfIntersectingLoop))?; 467 + let face = builder::try_attach_plane(&wires) 468 + .map_err(|_| invalid(ProfileDefect::SelfIntersectingLoop))?; 462 469 let (nx, ny, nz) = plane.normal().components(); 463 470 let sweep = Vector3::new(nx * plan.depth_mm, ny * plan.depth_mm, nz * plan.depth_mm); 464 471 let solid = builder::tsweep(&face, sweep);
+126 -7
crates/bone-kernel/src/brep/mod.rs
··· 1 1 use std::collections::HashSet; 2 2 3 3 use bone_types::{ 4 - Aabb3, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, EdgeRole, 5 - FaceLabel, Point3, SideKind, Tolerance, VertexLabel, 4 + Aabb3, AngleTolerance, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, 5 + ChordHeightTolerance, EdgeLabel, EdgeRole, FaceLabel, SideKind, Tolerance, VertexLabel, 6 6 }; 7 7 use slotmap::SlotMap; 8 8 use truck_modeling::ShellCondition; 9 9 10 10 mod build; 11 + pub(crate) mod convert; 11 12 pub mod eval; 12 13 pub mod profile; 14 + pub mod tessellate; 13 15 use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length, edge_points}; 16 + use convert::point_from_truck; 17 + pub use tessellate::{FaceMesh, MeshError, SolidMesh}; 14 18 15 19 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 16 20 pub enum TruckGap { ··· 274 278 .collect(); 275 279 self.iter_edges().try_for_each(|edge| { 276 280 if loop_edges.contains(&edge.id) 277 - || matches!(edge.label.role, EdgeRole::SideEdge { side: SideKind::Seam, .. }) 281 + || matches!( 282 + edge.label.role, 283 + EdgeRole::SideEdge { 284 + side: SideKind::Seam, 285 + .. 286 + } 287 + ) 278 288 { 279 289 Ok(()) 280 290 } else { ··· 300 310 edge.handles.iter().flat_map(|handle| { 301 311 edge_points(self.arena.edge(*handle)) 302 312 .into_iter() 303 - .map(|p| Point3::from_mm(p.x, p.y, p.z)) 313 + .map(point_from_truck) 304 314 }) 305 315 }); 306 316 Aabb3::from_points(points) 307 317 } 318 + 319 + pub fn tessellate( 320 + &self, 321 + chord: ChordHeightTolerance, 322 + angle: AngleTolerance, 323 + ) -> Result<SolidMesh, MeshError> { 324 + tessellate::tessellate_solid(self, chord, angle) 325 + } 308 326 } 309 327 310 328 #[cfg(test)] 311 329 mod tests { 312 330 use super::build::{SolidLabeling, assemble, edge_length}; 313 - use super::{BrepError, BrepLoop, LabelKind}; 331 + use super::{BrepError, BrepLoop, BrepSolid, LabelKind, MeshError, SolidMesh}; 314 332 use bone_types::{ 315 - BrepLoopId, EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, LoopIndex, SideKind, 316 - SketchEntityId, Tolerance, VertexLabel, VertexRole, 333 + AngleTolerance, BrepLoopId, ChordHeightTolerance, EdgeLabel, EdgeRole, FaceLabel, FaceRole, 334 + FeatureId, LoopIndex, SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole, 317 335 }; 318 336 use slotmap::SlotMap; 319 337 use std::collections::{HashMap, HashSet}; ··· 715 733 panic!("vertices sharing one label group rather than collide"); 716 734 }; 717 735 assert_eq!(brep.iter_vertices().count(), 1); 736 + } 737 + 738 + fn unit_cube_brep() -> BrepSolid { 739 + let profile = profile(); 740 + let solid = unit_cube(); 741 + let labeling = label_solid(&solid, &profile); 742 + let Ok(brep) = assemble(solid, &labeling) else { 743 + panic!("unit cube labels are complete"); 744 + }; 745 + brep 746 + } 747 + 748 + fn tessellate_cube(chord: ChordHeightTolerance, angle: AngleTolerance) -> SolidMesh { 749 + let Ok(mesh) = unit_cube_brep().tessellate(chord, angle) else { 750 + panic!("unit cube tessellates"); 751 + }; 752 + mesh 753 + } 754 + 755 + #[test] 756 + fn tessellate_unit_cube_emits_per_face_slabs() { 757 + let brep = unit_cube_brep(); 758 + let Ok(mesh) = brep.tessellate( 759 + ChordHeightTolerance::from_mm(0.05), 760 + AngleTolerance::from_radians(0.2), 761 + ) else { 762 + panic!("unit cube tessellates"); 763 + }; 764 + let face_count = brep.iter_faces().count(); 765 + assert_eq!(mesh.faces().len(), face_count); 766 + mesh.faces().iter().for_each(|slab| { 767 + assert!(!slab.triangles().is_empty(), "slab has triangles"); 768 + assert!(!slab.positions().is_empty(), "slab has positions"); 769 + assert_eq!(slab.positions().len(), slab.normals().len()); 770 + }); 771 + } 772 + 773 + #[test] 774 + fn tessellate_unit_cube_validates() { 775 + let mesh = tessellate_cube( 776 + ChordHeightTolerance::from_mm(0.05), 777 + AngleTolerance::from_radians(0.2), 778 + ); 779 + assert!(mesh.validate(Tolerance::new(1e-9)).is_ok()); 780 + } 781 + 782 + #[test] 783 + fn tessellate_is_deterministic() { 784 + let chord = ChordHeightTolerance::from_mm(0.05); 785 + let angle = AngleTolerance::from_radians(0.2); 786 + let first = tessellate_cube(chord, angle); 787 + let second = tessellate_cube(chord, angle); 788 + assert_eq!(first, second); 789 + assert_eq!(first.generation(), second.generation()); 790 + } 791 + 792 + #[test] 793 + fn tessellate_generation_changes_with_chord() { 794 + let coarse = tessellate_cube( 795 + ChordHeightTolerance::from_mm(0.1), 796 + AngleTolerance::from_radians(0.2), 797 + ); 798 + let fine = tessellate_cube( 799 + ChordHeightTolerance::from_mm(0.001), 800 + AngleTolerance::from_radians(0.2), 801 + ); 802 + assert_ne!(coarse.generation(), fine.generation()); 803 + } 804 + 805 + #[test] 806 + fn tessellate_generation_changes_with_angle() { 807 + let chord = ChordHeightTolerance::from_mm(0.05); 808 + let lo = tessellate_cube(chord, AngleTolerance::from_radians(0.01)); 809 + let hi = tessellate_cube(chord, AngleTolerance::from_radians(1.0)); 810 + assert_ne!(lo.generation(), hi.generation()); 811 + } 812 + 813 + #[test] 814 + fn tessellate_oversized_tolerance_flags_degenerate() { 815 + let mesh = tessellate_cube( 816 + ChordHeightTolerance::from_mm(0.05), 817 + AngleTolerance::from_radians(0.2), 818 + ); 819 + assert!(matches!( 820 + mesh.validate(Tolerance::new(10.0)), 821 + Err(MeshError::DegenerateTriangle { .. }) 822 + )); 823 + } 824 + 825 + proptest::proptest! { 826 + #[test] 827 + fn tessellate_proptest_is_deterministic( 828 + chord_mm in 0.001f64..=0.2f64, 829 + angle_rad in 0.01f64..=0.5f64, 830 + ) { 831 + let chord = ChordHeightTolerance::from_mm(chord_mm); 832 + let angle = AngleTolerance::from_radians(angle_rad); 833 + let first = tessellate_cube(chord, angle); 834 + let second = tessellate_cube(chord, angle); 835 + proptest::prop_assert_eq!(first, second); 836 + } 718 837 } 719 838 }
+326
crates/bone-kernel/src/brep/tessellate.rs
··· 1 + use std::collections::HashMap; 2 + use std::hash::{Hash, Hasher}; 3 + 4 + use bone_types::{ 5 + AngleTolerance, BrepFaceId, ChordHeightTolerance, FaceLabel, MeshGeneration, Point3, Tolerance, 6 + UnitVec3, 7 + }; 8 + use slotmap::Key; 9 + use truck_meshalgo::prelude::PolygonMesh; 10 + use truck_meshalgo::tessellation::RobustMeshableShape; 11 + use truck_modeling::{Invertible, TOLERANCE}; 12 + 13 + use super::BrepSolid; 14 + use super::convert::{point_from_truck, try_unit_from_truck}; 15 + 16 + const UNIT_NORMAL_TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 17 + 18 + #[derive(Clone, Debug, PartialEq)] 19 + pub struct FaceMesh { 20 + face: BrepFaceId, 21 + label: FaceLabel, 22 + positions: Vec<Point3>, 23 + normals: Vec<UnitVec3>, 24 + triangles: Vec<[u32; 3]>, 25 + } 26 + 27 + impl FaceMesh { 28 + #[must_use] 29 + pub fn face(&self) -> BrepFaceId { 30 + self.face 31 + } 32 + 33 + #[must_use] 34 + pub fn label(&self) -> FaceLabel { 35 + self.label 36 + } 37 + 38 + #[must_use] 39 + pub fn positions(&self) -> &[Point3] { 40 + &self.positions 41 + } 42 + 43 + #[must_use] 44 + pub fn normals(&self) -> &[UnitVec3] { 45 + &self.normals 46 + } 47 + 48 + #[must_use] 49 + pub fn triangles(&self) -> &[[u32; 3]] { 50 + &self.triangles 51 + } 52 + } 53 + 54 + #[derive(Clone, Debug, PartialEq)] 55 + pub struct SolidMesh { 56 + faces: Vec<FaceMesh>, 57 + generation: MeshGeneration, 58 + } 59 + 60 + impl SolidMesh { 61 + #[must_use] 62 + pub fn faces(&self) -> &[FaceMesh] { 63 + &self.faces 64 + } 65 + 66 + #[must_use] 67 + pub fn generation(&self) -> MeshGeneration { 68 + self.generation 69 + } 70 + 71 + pub fn validate(&self, tolerance: Tolerance) -> Result<(), MeshError> { 72 + self.faces 73 + .iter() 74 + .try_for_each(|slab| validate_slab(slab, tolerance)) 75 + } 76 + } 77 + 78 + #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] 79 + pub enum MeshError { 80 + #[error("face {0:?} produced no triangles")] 81 + EmptyFace(BrepFaceId), 82 + #[error("face {face:?} triangle {triangle} has an edge below tolerance")] 83 + DegenerateTriangle { face: BrepFaceId, triangle: usize }, 84 + #[error("face {face:?} triangle {triangle} winds against its surface normal")] 85 + NonCcwTriangle { face: BrepFaceId, triangle: usize }, 86 + #[error("face {face:?} has an edge incident to more than two triangles")] 87 + NonManifoldEdge { face: BrepFaceId }, 88 + #[error("face {face:?} surface tessellation produced no polygon")] 89 + SurfaceTessellationFailed { face: BrepFaceId }, 90 + #[error("face {face:?} surface emitted a normal that is not unit length")] 91 + InvalidSurfaceNormal { face: BrepFaceId }, 92 + #[error("face {face:?} slab exceeds u32 vertex addressing")] 93 + SlabTooLarge { face: BrepFaceId }, 94 + } 95 + 96 + pub(super) fn tessellate_solid( 97 + brep: &BrepSolid, 98 + chord: ChordHeightTolerance, 99 + angle: AngleTolerance, 100 + ) -> Result<SolidMesh, MeshError> { 101 + let tol = effective_tolerance(brep, chord, angle); 102 + let original = brep.arena.solid(); 103 + let meshed = original.robust_triangulation(tol); 104 + 105 + let face_index = brep.arena.face_index(); 106 + let mut slabs: HashMap<BrepFaceId, FaceMesh> = brep 107 + .iter_faces() 108 + .map(|face| { 109 + ( 110 + face.id(), 111 + FaceMesh { 112 + face: face.id(), 113 + label: face.label(), 114 + positions: Vec::new(), 115 + normals: Vec::new(), 116 + triangles: Vec::new(), 117 + }, 118 + ) 119 + }) 120 + .collect(); 121 + 122 + original 123 + .boundaries() 124 + .iter() 125 + .zip(meshed.boundaries().iter()) 126 + .flat_map(|(orig_shell, mesh_shell)| orig_shell.face_iter().zip(mesh_shell.face_iter())) 127 + .try_for_each(|(orig_face, mesh_face)| { 128 + let brep_face = *face_index 129 + .get(&orig_face.id()) 130 + .unwrap_or_else(|| unreachable!("face_index covers every truck face in the arena")); 131 + let Some(mut polygon) = mesh_face.surface() else { 132 + return Err(MeshError::SurfaceTessellationFailed { face: brep_face }); 133 + }; 134 + if !mesh_face.orientation() { 135 + polygon.invert(); 136 + } 137 + let slab = slabs.get_mut(&brep_face).unwrap_or_else(|| { 138 + unreachable!("slabs pre-filled from iter_faces over the same slotmap") 139 + }); 140 + append_polygon(slab, &polygon) 141 + })?; 142 + 143 + let faces: Vec<FaceMesh> = brep 144 + .iter_faces() 145 + .map(|face| { 146 + slabs 147 + .remove(&face.id()) 148 + .unwrap_or_else(|| unreachable!("slabs pre-filled from iter_faces")) 149 + }) 150 + .collect(); 151 + faces.iter().try_for_each(|slab| { 152 + if slab.triangles.is_empty() { 153 + Err(MeshError::EmptyFace(slab.face)) 154 + } else { 155 + Ok(()) 156 + } 157 + })?; 158 + let generation = derive_generation(&faces, chord, angle); 159 + Ok(SolidMesh { faces, generation }) 160 + } 161 + 162 + fn effective_tolerance( 163 + brep: &BrepSolid, 164 + chord: ChordHeightTolerance, 165 + angle: AngleTolerance, 166 + ) -> f64 { 167 + let chord_mm = chord.millimeters(); 168 + let angle_chord = brep.bounding_box().map_or(f64::INFINITY, |bbox| { 169 + let radius = bbox.extent().norm_mm() / 2.0; 170 + radius * (1.0 - (angle.radians() / 2.0).cos()) 171 + }); 172 + chord_mm.min(angle_chord).max(TOLERANCE) 173 + } 174 + 175 + fn append_polygon(slab: &mut FaceMesh, polygon: &PolygonMesh) -> Result<(), MeshError> { 176 + let mut lookup: HashMap<(usize, Option<usize>), u32> = HashMap::new(); 177 + let positions = polygon.positions(); 178 + let normals = polygon.normals(); 179 + polygon.tri_faces().iter().try_for_each(|tri| { 180 + let [v0, v1, v2] = *tri; 181 + let a = append_vertex(slab, positions, normals, v0, &mut lookup)?; 182 + let b = append_vertex(slab, positions, normals, v1, &mut lookup)?; 183 + let c = append_vertex(slab, positions, normals, v2, &mut lookup)?; 184 + slab.triangles.push([a, b, c]); 185 + Ok(()) 186 + }) 187 + } 188 + 189 + fn append_vertex( 190 + slab: &mut FaceMesh, 191 + positions: &[truck_modeling::Point3], 192 + normals: &[truck_modeling::Vector3], 193 + v: truck_meshalgo::prelude::StandardVertex, 194 + lookup: &mut HashMap<(usize, Option<usize>), u32>, 195 + ) -> Result<u32, MeshError> { 196 + if let Some(existing) = lookup.get(&(v.pos, v.nor)) { 197 + return Ok(*existing); 198 + } 199 + let Some(nor_index) = v.nor else { 200 + return Err(MeshError::InvalidSurfaceNormal { face: slab.face }); 201 + }; 202 + let Some(normal) = try_unit_from_truck(normals[nor_index], UNIT_NORMAL_TOLERANCE) else { 203 + return Err(MeshError::InvalidSurfaceNormal { face: slab.face }); 204 + }; 205 + let index = u32::try_from(slab.positions.len()) 206 + .map_err(|_| MeshError::SlabTooLarge { face: slab.face })?; 207 + slab.positions.push(point_from_truck(positions[v.pos])); 208 + slab.normals.push(normal); 209 + lookup.insert((v.pos, v.nor), index); 210 + Ok(index) 211 + } 212 + 213 + fn derive_generation( 214 + faces: &[FaceMesh], 215 + chord: ChordHeightTolerance, 216 + angle: AngleTolerance, 217 + ) -> MeshGeneration { 218 + let mut hasher = std::collections::hash_map::DefaultHasher::new(); 219 + chord.millimeters().to_bits().hash(&mut hasher); 220 + angle.radians().to_bits().hash(&mut hasher); 221 + faces.iter().for_each(|slab| { 222 + slab.face.data().as_ffi().hash(&mut hasher); 223 + slab.label.hash(&mut hasher); 224 + slab.positions.iter().for_each(|p| { 225 + let (x, y, z) = p.coords_mm(); 226 + x.to_bits().hash(&mut hasher); 227 + y.to_bits().hash(&mut hasher); 228 + z.to_bits().hash(&mut hasher); 229 + }); 230 + slab.normals.iter().for_each(|n| { 231 + let (x, y, z) = n.components(); 232 + x.to_bits().hash(&mut hasher); 233 + y.to_bits().hash(&mut hasher); 234 + z.to_bits().hash(&mut hasher); 235 + }); 236 + slab.triangles.iter().for_each(|tri| tri.hash(&mut hasher)); 237 + }); 238 + MeshGeneration::new(hasher.finish()) 239 + } 240 + 241 + fn validate_slab(slab: &FaceMesh, tolerance: Tolerance) -> Result<(), MeshError> { 242 + slab.triangles 243 + .iter() 244 + .enumerate() 245 + .try_for_each(|(triangle_index, indices)| { 246 + let p = corners(slab, *indices); 247 + check_non_degenerate(slab.face, triangle_index, p, tolerance)?; 248 + check_ccw_winding(slab, triangle_index, *indices, p) 249 + })?; 250 + check_manifold(slab) 251 + } 252 + 253 + type Corners = [Point3; 3]; 254 + 255 + fn corners(slab: &FaceMesh, indices: [u32; 3]) -> Corners { 256 + indices.map(|i| slab.positions[i as usize]) 257 + } 258 + 259 + fn check_non_degenerate( 260 + face: BrepFaceId, 261 + triangle: usize, 262 + corners: Corners, 263 + tolerance: Tolerance, 264 + ) -> Result<(), MeshError> { 265 + let min_sq = tolerance.value() * tolerance.value(); 266 + let edge_sq = |a: usize, b: usize| { 267 + let d = corners[b] - corners[a]; 268 + d.norm_squared_mm2() 269 + }; 270 + if edge_sq(0, 1) <= min_sq || edge_sq(1, 2) <= min_sq || edge_sq(2, 0) <= min_sq { 271 + Err(MeshError::DegenerateTriangle { face, triangle }) 272 + } else { 273 + Ok(()) 274 + } 275 + } 276 + 277 + fn check_ccw_winding( 278 + slab: &FaceMesh, 279 + triangle: usize, 280 + indices: [u32; 3], 281 + corners: Corners, 282 + ) -> Result<(), MeshError> { 283 + let edge_a = corners[1] - corners[0]; 284 + let edge_b = corners[2] - corners[0]; 285 + let cross = edge_a.cross(edge_b); 286 + let normal_sum = indices.iter().fold([0.0_f64; 3], |acc, idx| { 287 + let (nx, ny, nz) = slab.normals[*idx as usize].components(); 288 + [acc[0] + nx, acc[1] + ny, acc[2] + nz] 289 + }); 290 + let (cx, cy, cz) = cross.coords_mm(); 291 + let dot = cx * normal_sum[0] + cy * normal_sum[1] + cz * normal_sum[2]; 292 + if dot > 0.0 { 293 + Ok(()) 294 + } else { 295 + Err(MeshError::NonCcwTriangle { 296 + face: slab.face, 297 + triangle, 298 + }) 299 + } 300 + } 301 + 302 + fn check_manifold(slab: &FaceMesh) -> Result<(), MeshError> { 303 + let counts = slab 304 + .triangles 305 + .iter() 306 + .flat_map(|tri| { 307 + [ 308 + edge_key(tri[0], tri[1]), 309 + edge_key(tri[1], tri[2]), 310 + edge_key(tri[2], tri[0]), 311 + ] 312 + }) 313 + .fold(HashMap::<(u32, u32), u32>::new(), |mut counts, key| { 314 + *counts.entry(key).or_insert(0) += 1; 315 + counts 316 + }); 317 + if counts.values().any(|count| *count > 2) { 318 + Err(MeshError::NonManifoldEdge { face: slab.face }) 319 + } else { 320 + Ok(()) 321 + } 322 + } 323 + 324 + fn edge_key(a: u32, b: u32) -> (u32, u32) { 325 + if a < b { (a, b) } else { (b, a) } 326 + }
+2 -2
crates/bone-kernel/src/lib.rs
··· 27 27 pub use brep::eval::evaluate_extrude; 28 28 pub use brep::profile::{ExtrudeProfile, ProfileEdge, ProfileLoop}; 29 29 pub use brep::{ 30 - BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, LabelKind, 31 - ProfileDefect, TruckGap, 30 + BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, FaceMesh, LabelKind, 31 + MeshError, ProfileDefect, SolidMesh, TruckGap, 32 32 }; 33 33 pub use circle2::Circle2; 34 34 pub use circle3::Circle3;
+26 -5
crates/bone-kernel/tests/extrude.rs
··· 113 113 .map(|index| { 114 114 let corner = ids.entity(); 115 115 let segment = ids.entity(); 116 - ProfileEdge::new(line(corners[index], corners[(index + 1) % count]), segment, corner) 116 + ProfileEdge::new( 117 + line(corners[index], corners[(index + 1) % count]), 118 + segment, 119 + corner, 120 + ) 117 121 }) 118 122 .collect(); 119 123 ProfileLoop::Open(edges) ··· 246 250 let arc_entity = ids.entity(); 247 251 let line_corner = ids.entity(); 248 252 let line_entity = ids.entity(); 249 - let curved = ProfileEdge::new(arc(point(0.0, 0.0), 5.0, 0.0, 180.0), arc_entity, arc_corner); 253 + let curved = ProfileEdge::new( 254 + arc(point(0.0, 0.0), 5.0, 0.0, 180.0), 255 + arc_entity, 256 + arc_corner, 257 + ); 250 258 let chord = ProfileEdge::new( 251 259 line(point(-5.0, 0.0), point(5.0, 0.0)), 252 260 line_entity, ··· 266 274 let down_entity = ids.entity(); 267 275 let out_corner = ids.entity(); 268 276 let out_entity = ids.entity(); 269 - let curved = ProfileEdge::new(arc(point(0.0, 0.0), 5.0, 0.0, 270.0), arc_entity, arc_corner); 277 + let curved = ProfileEdge::new( 278 + arc(point(0.0, 0.0), 5.0, 0.0, 270.0), 279 + arc_entity, 280 + arc_corner, 281 + ); 270 282 let radius_down = ProfileEdge::new( 271 283 line(point(0.0, -5.0), point(0.0, 0.0)), 272 284 down_entity, 273 285 down_corner, 274 286 ); 275 - let radius_out = ProfileEdge::new(line(point(0.0, 0.0), point(5.0, 0.0)), out_entity, out_corner); 287 + let radius_out = ProfileEdge::new( 288 + line(point(0.0, 0.0), point(5.0, 0.0)), 289 + out_entity, 290 + out_corner, 291 + ); 276 292 let profile = ExtrudeProfile::new( 277 293 xy_plane(), 278 294 vec![ProfileLoop::Open(vec![curved, radius_down, radius_out])], ··· 405 421 let mut ids = Ids::new(); 406 422 let bowtie = polygon( 407 423 &mut ids, 408 - &[point(0.0, 0.0), point(4.0, 0.0), point(1.0, 3.0), point(3.0, 3.0)], 424 + &[ 425 + point(0.0, 0.0), 426 + point(4.0, 0.0), 427 + point(1.0, 3.0), 428 + point(3.0, 3.0), 429 + ], 409 430 ); 410 431 let profile = ExtrudeProfile::new(xy_plane(), vec![bowtie]); 411 432 let extrude = ids.feature();
+38 -7
crates/bone-types/src/lib.rs
··· 223 223 } 224 224 } 225 225 226 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 227 + pub struct MeshGeneration(u64); 228 + 229 + impl MeshGeneration { 230 + #[must_use] 231 + pub const fn new(value: u64) -> Self { 232 + Self(value) 233 + } 234 + 235 + #[must_use] 236 + pub const fn value(self) -> u64 { 237 + self.0 238 + } 239 + } 240 + 241 + impl core::fmt::Display for MeshGeneration { 242 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 243 + write!(f, "mesh_gen={}", self.0) 244 + } 245 + } 246 + 226 247 #[cfg(test)] 227 248 mod tests { 228 249 use super::{ 229 250 Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, 230 251 BrepShellId, BrepVertexId, Camera3, ChordHeightTolerance, DegreesOfFreedom, DisplayMode, 231 252 DocumentId, EdgeId, EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, FaceRole, FeatureId, 232 - ImportOrdinal, Length, LoopId, LoopIndex, NodeId, OrbitState, OrientedBox3, Parameter, 233 - Plane3, Point2, Point3, PositiveLength, Projection, ProjectionKind, ShadingModel, ShellId, 234 - SideKind, SketchDimensionId, SketchEntityId, SketchId, SketchParameterId, SketchPlaneBasis, 235 - SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, StepFileHeader, 236 - StepFileName, StepOrganization, StepOriginatingSystem, StepSchema, Tolerance, UnitVec2, 237 - UnitVec3, Vec2, Vec3, VertexId, VertexLabel, VertexRole, WireId, ZoomFactor, degree, 238 - millimeter, radian, 253 + ImportOrdinal, Length, LoopId, LoopIndex, MeshGeneration, NodeId, OrbitState, OrientedBox3, 254 + Parameter, Plane3, Point2, Point3, PositiveLength, Projection, ProjectionKind, 255 + ShadingModel, ShellId, SideKind, SketchDimensionId, SketchEntityId, SketchId, 256 + SketchParameterId, SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, 257 + StandardView, StepEntityId, StepFileHeader, StepFileName, StepOrganization, 258 + StepOriginatingSystem, StepSchema, Tolerance, UnitVec2, UnitVec3, Vec2, Vec3, VertexId, 259 + VertexLabel, VertexRole, WireId, ZoomFactor, degree, millimeter, radian, 239 260 }; 240 261 use slotmap::Key; 241 262 use uom::si::length::meter; ··· 492 513 let r = SolverResidual::new(1.5e-6); 493 514 assert_eq!(format!("{r}"), "res=0.0000015"); 494 515 assert!((r.value() - 1.5e-6).abs() < f64::EPSILON); 516 + } 517 + 518 + #[test] 519 + fn mesh_generation_orders_by_value() { 520 + let a = MeshGeneration::new(7); 521 + let b = MeshGeneration::new(42); 522 + assert!(a < b); 523 + assert_eq!(format!("{a}"), "mesh_gen=7"); 524 + assert_eq!(a.value(), 7); 525 + assert_eq!(MeshGeneration::default(), MeshGeneration::new(0)); 495 526 } 496 527 497 528 #[test]