Another project
0

Configure Feed

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

feat(types): topology labels, 3d ids, tolerance newtypes

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

author
Lewis
date (May 23, 2026, 5:12 PM +0300) commit 4046d99e parent 0946c999 change-id wzrnszuz
+714 -15
+1
Cargo.lock
··· 471 471 dependencies = [ 472 472 "insta", 473 473 "nalgebra", 474 + "ron", 474 475 "serde", 475 476 "slotmap", 476 477 "thiserror 2.0.18",
+1
crates/bone-types/Cargo.toml
··· 16 16 17 17 [dev-dependencies] 18 18 insta = { workspace = true } 19 + ron = { workspace = true } 19 20 20 21 [features] 21 22 testing = ["dep:tracing", "dep:tracing-subscriber"]
+192
crates/bone-types/src/label.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + use crate::{FeatureId, SketchEntityId}; 4 + 5 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 6 + pub struct LoopIndex(u16); 7 + 8 + impl LoopIndex { 9 + pub const OUTER: Self = Self(0); 10 + 11 + #[must_use] 12 + pub const fn new(value: u16) -> Self { 13 + Self(value) 14 + } 15 + 16 + #[must_use] 17 + pub const fn value(self) -> u16 { 18 + self.0 19 + } 20 + } 21 + 22 + impl core::fmt::Display for LoopIndex { 23 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 24 + write!(f, "loop#{}", self.0) 25 + } 26 + } 27 + 28 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 29 + pub struct ImportOrdinal(u32); 30 + 31 + impl ImportOrdinal { 32 + #[must_use] 33 + pub const fn new(value: u32) -> Self { 34 + Self(value) 35 + } 36 + 37 + #[must_use] 38 + pub const fn value(self) -> u32 { 39 + self.0 40 + } 41 + } 42 + 43 + impl core::fmt::Display for ImportOrdinal { 44 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 45 + write!(f, "import#{}", self.0) 46 + } 47 + } 48 + 49 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 50 + pub enum SideKind { 51 + Corner, 52 + Seam, 53 + } 54 + 55 + impl SideKind { 56 + #[must_use] 57 + pub const fn label(self) -> &'static str { 58 + match self { 59 + Self::Corner => "corner", 60 + Self::Seam => "seam", 61 + } 62 + } 63 + } 64 + 65 + impl core::fmt::Display for SideKind { 66 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 67 + f.write_str(self.label()) 68 + } 69 + } 70 + 71 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 72 + #[serde(deny_unknown_fields)] 73 + pub enum FaceRole { 74 + StartCap, 75 + Side { 76 + loop_index: LoopIndex, 77 + from: SketchEntityId, 78 + }, 79 + EndCap, 80 + Imported { 81 + ordinal: ImportOrdinal, 82 + }, 83 + } 84 + 85 + impl core::fmt::Display for FaceRole { 86 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 87 + match self { 88 + Self::StartCap => f.write_str("start_cap"), 89 + Self::Side { loop_index, from } => write!(f, "side({loop_index}, from={from:?})"), 90 + Self::EndCap => f.write_str("end_cap"), 91 + Self::Imported { ordinal } => write!(f, "imported({ordinal})"), 92 + } 93 + } 94 + } 95 + 96 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 97 + #[serde(deny_unknown_fields)] 98 + pub enum EdgeRole { 99 + StartCapEdge { 100 + from: SketchEntityId, 101 + }, 102 + SideEdge { 103 + from: SketchEntityId, 104 + side: SideKind, 105 + }, 106 + EndCapEdge { 107 + from: SketchEntityId, 108 + }, 109 + Imported { 110 + ordinal: ImportOrdinal, 111 + }, 112 + } 113 + 114 + impl core::fmt::Display for EdgeRole { 115 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 116 + match self { 117 + Self::StartCapEdge { from } => write!(f, "start_cap_edge(from={from:?})"), 118 + Self::SideEdge { from, side } => write!(f, "side_edge({side}, from={from:?})"), 119 + Self::EndCapEdge { from } => write!(f, "end_cap_edge(from={from:?})"), 120 + Self::Imported { ordinal } => write!(f, "imported({ordinal})"), 121 + } 122 + } 123 + } 124 + 125 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 126 + #[serde(deny_unknown_fields)] 127 + pub enum VertexRole { 128 + StartCapVertex { 129 + from: SketchEntityId, 130 + side: SideKind, 131 + }, 132 + EndCapVertex { 133 + from: SketchEntityId, 134 + side: SideKind, 135 + }, 136 + Imported { 137 + ordinal: ImportOrdinal, 138 + }, 139 + } 140 + 141 + impl core::fmt::Display for VertexRole { 142 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 143 + match self { 144 + Self::StartCapVertex { from, side } => { 145 + write!(f, "start_cap_vertex({side}, from={from:?})") 146 + } 147 + Self::EndCapVertex { from, side } => { 148 + write!(f, "end_cap_vertex({side}, from={from:?})") 149 + } 150 + Self::Imported { ordinal } => write!(f, "imported({ordinal})"), 151 + } 152 + } 153 + } 154 + 155 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 156 + #[serde(deny_unknown_fields)] 157 + pub struct FaceLabel { 158 + pub feature: FeatureId, 159 + pub role: FaceRole, 160 + } 161 + 162 + impl core::fmt::Display for FaceLabel { 163 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 164 + write!(f, "face[{:?}]:{}", self.feature, self.role) 165 + } 166 + } 167 + 168 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 169 + #[serde(deny_unknown_fields)] 170 + pub struct EdgeLabel { 171 + pub feature: FeatureId, 172 + pub role: EdgeRole, 173 + } 174 + 175 + impl core::fmt::Display for EdgeLabel { 176 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 177 + write!(f, "edge[{:?}]:{}", self.feature, self.role) 178 + } 179 + } 180 + 181 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 182 + #[serde(deny_unknown_fields)] 183 + pub struct VertexLabel { 184 + pub feature: FeatureId, 185 + pub role: VertexRole, 186 + } 187 + 188 + impl core::fmt::Display for VertexLabel { 189 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 190 + write!(f, "vertex[{:?}]:{}", self.feature, self.role) 191 + } 192 + }
+323 -10
crates/bone-types/src/lib.rs
··· 3 3 pub use uom::si::length::millimeter; 4 4 5 5 pub mod dimensioned_serde; 6 + pub mod label; 6 7 pub mod schema; 7 8 pub mod solver; 8 9 pub mod space; 9 10 11 + pub use label::{ 12 + EdgeLabel, EdgeRole, FaceLabel, FaceRole, ImportOrdinal, LoopIndex, SideKind, VertexLabel, 13 + VertexRole, 14 + }; 10 15 pub use schema::{SchemaHeader, SchemaVersion}; 11 16 pub use solver::{ 12 17 BudgetCeiling, DegreesOfFreedom, NewtonDamping, NewtonStepTolerance, Parameter, ParameterIndex, 13 18 ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed, 14 19 }; 15 - pub use space::{Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3, Vec2}; 20 + pub use space::{ 21 + Aabb3, AxisAngle, OrientedBox3, Plane3, Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3, 22 + Vec2, Vec3, 23 + }; 16 24 17 25 #[cfg(feature = "testing")] 18 26 pub mod testing; ··· 23 31 NonOrthogonalPlaneAxes(f64), 24 32 #[error("axis vector is zero-length")] 25 33 ZeroLengthAxis, 34 + #[error("oriented box half-extent must be finite and non-negative: {0}")] 35 + InvalidHalfExtent(f64), 26 36 } 27 37 28 38 pub type Result<T, E = TypesError> = core::result::Result<T, E>; ··· 43 53 pub struct SketchRelationId; 44 54 pub struct SketchDimensionId; 45 55 pub struct SketchParameterId; 56 + pub struct ExtrudeId; 57 + pub struct BodyId; 58 + pub struct BrepShellId; 59 + pub struct BrepFaceId; 60 + pub struct BrepEdgeId; 61 + pub struct BrepVertexId; 62 + pub struct BrepLoopId; 46 63 } 47 64 48 65 impl SketchId { ··· 110 127 } 111 128 } 112 129 130 + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] 131 + pub struct ChordHeightTolerance(Length); 132 + 133 + impl ChordHeightTolerance { 134 + #[must_use] 135 + pub fn new(length: Length) -> Self { 136 + Self(length) 137 + } 138 + 139 + #[must_use] 140 + pub fn from_mm(value: f64) -> Self { 141 + Self(Length::new::<millimeter>(value)) 142 + } 143 + 144 + #[must_use] 145 + pub fn length(self) -> Length { 146 + self.0 147 + } 148 + 149 + #[must_use] 150 + pub fn millimeters(self) -> f64 { 151 + self.0.get::<millimeter>() 152 + } 153 + } 154 + 155 + impl core::fmt::Display for ChordHeightTolerance { 156 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 157 + write!(f, "chord_tol={} mm", self.0.get::<millimeter>()) 158 + } 159 + } 160 + 113 161 #[cfg(test)] 114 162 mod tests { 115 163 use super::{ 116 - Angle, AngleTolerance, DegreesOfFreedom, DocumentId, EdgeId, FaceId, FeatureId, Length, 117 - LoopId, NodeId, Parameter, Point2, Point3, ShellId, SketchDimensionId, SketchEntityId, 118 - SketchId, SketchParameterId, SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, 119 - Tolerance, UnitVec2, UnitVec3, Vec2, VertexId, WireId, degree, millimeter, radian, 164 + Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, 165 + BrepShellId, BrepVertexId, ChordHeightTolerance, DegreesOfFreedom, DocumentId, EdgeId, 166 + EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, FaceRole, FeatureId, ImportOrdinal, 167 + Length, LoopId, LoopIndex, NodeId, OrientedBox3, Parameter, Plane3, Point2, Point3, 168 + ShellId, SideKind, SketchDimensionId, SketchEntityId, SketchId, SketchParameterId, 169 + SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, Tolerance, UnitVec2, UnitVec3, 170 + Vec2, Vec3, VertexId, VertexLabel, VertexRole, WireId, degree, millimeter, radian, 120 171 }; 121 172 use slotmap::Key; 122 173 ··· 137 188 assert!(SketchRelationId::null().is_null()); 138 189 assert!(SketchDimensionId::null().is_null()); 139 190 assert!(SketchParameterId::null().is_null()); 191 + assert!(ExtrudeId::null().is_null()); 192 + assert!(BodyId::null().is_null()); 193 + assert!(BrepShellId::null().is_null()); 194 + assert!(BrepFaceId::null().is_null()); 195 + assert!(BrepEdgeId::null().is_null()); 196 + assert!(BrepVertexId::null().is_null()); 197 + assert!(BrepLoopId::null().is_null()); 140 198 } 141 199 142 200 #[test] ··· 290 348 291 349 #[test] 292 350 fn sketch_plane_basis_reorthonormalizes_within_tolerance() { 293 - let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0) else { 351 + let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9)) 352 + else { 294 353 panic!("input vector is nonzero"); 295 354 }; 296 355 let Ok(basis) = SketchPlaneBasis::new( ··· 310 369 311 370 #[test] 312 371 fn sketch_plane_basis_rejects_non_orthogonal_axes() { 313 - let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0) else { 372 + let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else { 314 373 panic!("input vector is nonzero"); 315 374 }; 316 375 let result = SketchPlaneBasis::new( ··· 323 382 } 324 383 325 384 #[test] 326 - fn unit_vec3_rejects_zero_axis() { 327 - let r = UnitVec3::try_from_components(0.0, 0.0, 0.0); 328 - assert!(r.is_err()); 385 + fn unit_vec3_rejects_zero_and_near_zero_within_tolerance() { 386 + let tol = Tolerance::new(1e-9); 387 + assert!(UnitVec3::try_from_components(0.0, 0.0, 0.0, tol).is_err()); 388 + assert!(UnitVec3::try_from_components(1e-13, 0.0, 0.0, tol).is_err()); 389 + assert!(UnitVec3::try_from_components(1.0, 0.0, 0.0, tol).is_ok()); 329 390 } 330 391 331 392 #[test] ··· 347 408 let r = SolverResidual::new(1.5e-6); 348 409 assert_eq!(format!("{r}"), "res=0.0000015"); 349 410 assert!((r.value() - 1.5e-6).abs() < f64::EPSILON); 411 + } 412 + 413 + #[test] 414 + fn chord_height_tolerance_display_and_value() { 415 + let c = ChordHeightTolerance::from_mm(0.01); 416 + assert!((c.millimeters() - 0.01).abs() < 1e-15); 417 + assert_eq!(format!("{c}"), "chord_tol=0.01 mm"); 418 + let c2 = ChordHeightTolerance::new(Length::new::<millimeter>(0.05)); 419 + assert!((c2.length().get::<millimeter>() - 0.05).abs() < 1e-15); 420 + } 421 + 422 + #[test] 423 + fn vec3_ops_and_normalize() { 424 + let vec = Vec3::from_mm(1.0, 2.0, 2.0); 425 + assert!((vec.norm_mm() - 3.0).abs() < 1e-12); 426 + let unit_x = Vec3::from_mm(1.0, 0.0, 0.0); 427 + let unit_y = Vec3::from_mm(0.0, 1.0, 0.0); 428 + assert!((unit_x.cross(unit_y).z().get::<millimeter>() - 1.0).abs() < 1e-12); 429 + assert!(unit_x.dot_mm2(unit_y).abs() < 1e-12); 430 + let Ok(normalized) = vec.try_normalize(Tolerance::new(1e-9)) else { 431 + panic!("nonzero"); 432 + }; 433 + let (nx, ny, nz) = normalized.components(); 434 + assert!((nx * nx + ny * ny + nz * nz - 1.0).abs() < 1e-12); 435 + assert!(Vec3::zero().try_normalize(Tolerance::new(1e-9)).is_err()); 436 + let far = Point3::from_mm(5.0, 5.0, 5.0); 437 + let near = Point3::from_mm(1.0, 2.0, 3.0); 438 + let delta: Vec3 = far - near; 439 + assert!((delta.x().get::<millimeter>() - 4.0).abs() < 1e-12); 440 + assert!(((near + delta).x().get::<millimeter>() - 5.0).abs() < 1e-12); 441 + } 442 + 443 + #[test] 444 + fn unit_vec3_reverse_and_into_vec() { 445 + let zaxis = UnitVec3::z_axis(); 446 + assert!((zaxis.reversed().components().2 + 1.0).abs() < 1e-12); 447 + let scaled = zaxis.into_vec(Length::new::<millimeter>(4.0)); 448 + assert!(scaled.x().get::<millimeter>().abs() < 1e-12); 449 + assert!((scaled.z().get::<millimeter>() - 4.0).abs() < 1e-12); 450 + } 451 + 452 + #[test] 453 + fn plane3_orthonormalizes_and_rejects_skew() { 454 + let origin = Point3::from_mm(1.0, 2.0, 3.0); 455 + let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9)) 456 + else { 457 + panic!("nonzero"); 458 + }; 459 + let Ok(plane) = Plane3::new(origin, UnitVec3::x_axis(), nearly, Tolerance::new(1e-9)) 460 + else { 461 + panic!("within tolerance"); 462 + }; 463 + assert!(plane.x_axis().dot(plane.y_axis()).abs() < 1e-15); 464 + let (nx, ny, nz) = plane.normal().components(); 465 + assert!(nx.abs() < 1e-12 && ny.abs() < 1e-12 && (nz - 1.0).abs() < 1e-12); 466 + let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else { 467 + panic!("nonzero"); 468 + }; 469 + assert!(Plane3::new(origin, UnitVec3::x_axis(), skew, Tolerance::new(1e-9)).is_err()); 470 + } 471 + 472 + #[test] 473 + fn axis_angle_accessors() { 474 + let aa = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5)); 475 + assert!((aa.angle().get::<radian>() - 0.5).abs() < 1e-15); 476 + assert!((aa.axis().components().2 - 1.0).abs() < 1e-12); 477 + } 478 + 479 + #[test] 480 + fn oriented_box_from_aabb_matches_center_and_half() { 481 + let aabb = Aabb3::from_corners( 482 + Point3::from_mm(-2.0, -4.0, 0.0), 483 + Point3::from_mm(2.0, 4.0, 10.0), 484 + ); 485 + let obox = OrientedBox3::from_aabb(aabb); 486 + let (cx, cy, cz) = obox.center().coords_mm(); 487 + assert!(cx.abs() < 1e-12 && cy.abs() < 1e-12 && (cz - 5.0).abs() < 1e-12); 488 + let (hx, hy, hz) = obox.half_extents().coords_mm(); 489 + assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12); 490 + assert!((obox.frame().normal().components().2 - 1.0).abs() < 1e-12); 491 + } 492 + 493 + #[test] 494 + fn aabb3_from_points_union_contains() { 495 + let pts = [ 496 + Point3::from_mm(0.0, 0.0, 0.0), 497 + Point3::from_mm(2.0, -1.0, 5.0), 498 + Point3::from_mm(-3.0, 4.0, 1.0), 499 + ]; 500 + let Some(bb) = Aabb3::from_points(pts) else { 501 + panic!("nonempty"); 502 + }; 503 + let (lx, ly, lz) = bb.min().coords_mm(); 504 + let (hx, hy, hz) = bb.max().coords_mm(); 505 + assert!((lx + 3.0).abs() < 1e-12 && (ly + 1.0).abs() < 1e-12 && lz.abs() < 1e-12); 506 + assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12); 507 + assert!(bb.contains(Point3::from_mm(0.0, 0.0, 2.0))); 508 + assert!(!bb.contains(Point3::from_mm(10.0, 0.0, 0.0))); 509 + let (cx, cy, cz) = bb.center().coords_mm(); 510 + assert!((cx + 0.5).abs() < 1e-12 && (cy - 1.5).abs() < 1e-12 && (cz - 2.5).abs() < 1e-12); 511 + assert!((bb.extent().x().get::<millimeter>() - 5.0).abs() < 1e-12); 512 + assert!(Aabb3::from_points(core::iter::empty()).is_none()); 513 + } 514 + 515 + #[test] 516 + fn face_role_ord_matches_canonical_order() { 517 + let from = SketchEntityId::default(); 518 + let start = FaceRole::StartCap; 519 + let side0 = FaceRole::Side { 520 + loop_index: LoopIndex::OUTER, 521 + from, 522 + }; 523 + let side1 = FaceRole::Side { 524 + loop_index: LoopIndex::new(1), 525 + from, 526 + }; 527 + let end = FaceRole::EndCap; 528 + let imported = FaceRole::Imported { 529 + ordinal: ImportOrdinal::new(0), 530 + }; 531 + assert!(start < side0); 532 + assert!(side0 < side1); 533 + assert!(side1 < end); 534 + assert!(end < imported); 535 + } 536 + 537 + #[test] 538 + fn edge_role_ord_matches_canonical_order() { 539 + let from = SketchEntityId::default(); 540 + let start = EdgeRole::StartCapEdge { from }; 541 + let side = EdgeRole::SideEdge { 542 + from, 543 + side: SideKind::Corner, 544 + }; 545 + let end = EdgeRole::EndCapEdge { from }; 546 + let imported = EdgeRole::Imported { 547 + ordinal: ImportOrdinal::new(0), 548 + }; 549 + assert!(start < side); 550 + assert!(side < end); 551 + assert!(end < imported); 552 + } 553 + 554 + #[test] 555 + fn vertex_role_ord_matches_canonical_order() { 556 + let from = SketchEntityId::default(); 557 + let start = VertexRole::StartCapVertex { 558 + from, 559 + side: SideKind::Corner, 560 + }; 561 + let end = VertexRole::EndCapVertex { 562 + from, 563 + side: SideKind::Corner, 564 + }; 565 + let imported = VertexRole::Imported { 566 + ordinal: ImportOrdinal::new(0), 567 + }; 568 + assert!(start < end); 569 + assert!(end < imported); 570 + } 571 + 572 + #[test] 573 + fn oriented_box_rejects_invalid_half_extent() { 574 + let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()); 575 + assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, 2.0, 3.0)).is_ok()); 576 + assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, -2.0, 3.0)).is_err()); 577 + assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, f64::NAN, 3.0)).is_err()); 578 + assert!(OrientedBox3::new(frame, Vec3::from_mm(f64::INFINITY, 2.0, 3.0)).is_err()); 579 + } 580 + 581 + #[test] 582 + fn oriented_box_deserialize_rejects_invalid_half_extent() { 583 + let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()); 584 + let Ok(obox) = OrientedBox3::new(frame, Vec3::from_mm(5.0, 5.0, 5.0)) else { 585 + panic!("non-negative half-extents"); 586 + }; 587 + let Ok(good) = ron::to_string(&obox) else { 588 + panic!("serialize oriented box"); 589 + }; 590 + assert!(ron::from_str::<OrientedBox3>(&good).is_ok()); 591 + let bad = good.replace('5', "-5"); 592 + assert!(ron::from_str::<OrientedBox3>(&bad).is_err()); 593 + } 594 + 595 + #[test] 596 + fn labels_ron_roundtrip() { 597 + let feature = FeatureId::default(); 598 + let from = SketchEntityId::default(); 599 + let face = FaceLabel { 600 + feature, 601 + role: FaceRole::Side { 602 + loop_index: LoopIndex::OUTER, 603 + from, 604 + }, 605 + }; 606 + let Ok(text) = ron::to_string(&face) else { 607 + panic!("serialize face label"); 608 + }; 609 + let Ok(back) = ron::from_str::<FaceLabel>(&text) else { 610 + panic!("deserialize face label"); 611 + }; 612 + assert_eq!(face, back); 613 + 614 + let edge = EdgeLabel { 615 + feature, 616 + role: EdgeRole::SideEdge { 617 + from, 618 + side: SideKind::Seam, 619 + }, 620 + }; 621 + let Ok(text) = ron::to_string(&edge) else { 622 + panic!("serialize edge label"); 623 + }; 624 + let Ok(back) = ron::from_str::<EdgeLabel>(&text) else { 625 + panic!("deserialize edge label"); 626 + }; 627 + assert_eq!(edge, back); 628 + 629 + let vertex = VertexLabel { 630 + feature, 631 + role: VertexRole::EndCapVertex { 632 + from, 633 + side: SideKind::Corner, 634 + }, 635 + }; 636 + let Ok(text) = ron::to_string(&vertex) else { 637 + panic!("serialize vertex label"); 638 + }; 639 + let Ok(back) = ron::from_str::<VertexLabel>(&text) else { 640 + panic!("deserialize vertex label"); 641 + }; 642 + assert_eq!(vertex, back); 643 + } 644 + 645 + #[test] 646 + fn role_rejects_unknown_field() { 647 + let label = FaceLabel { 648 + feature: FeatureId::default(), 649 + role: FaceRole::Side { 650 + loop_index: LoopIndex::OUTER, 651 + from: SketchEntityId::default(), 652 + }, 653 + }; 654 + let Ok(text) = ron::to_string(&label) else { 655 + panic!("serialize face label"); 656 + }; 657 + let Some(idx) = text.find("Side(") else { 658 + panic!("expected Side variant in {text}"); 659 + }; 660 + let mut munged = text.clone(); 661 + munged.insert_str(idx + "Side(".len(), "bogus:42,"); 662 + assert!(ron::from_str::<FaceLabel>(&munged).is_err()); 350 663 } 351 664 }
+13
crates/bone-types/tests/snapshots/surface__geometry3_surface.snap
··· 1 + --- 2 + source: crates/bone-types/tests/surface.rs 3 + expression: surface 4 + --- 5 + plane_debug = Plane3 { origin: Point3(1 mm, 0 mm, 0 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) } 6 + plane_display = plane{ o=(1 mm, 0 mm, 0 mm), x=[1, 0, 0], y=[0, 1, 0] } 7 + plane_normal = [0, 0, 1] 8 + axisangle_debug = AxisAngle(axis=UnitVec3([[0.0, 0.0, 1.0]]), angle=0.5 rad) 9 + axisangle_disp = axisangle{ axis=[0, 0, 1], angle=0.5 rad } 10 + obox_debug = OrientedBox3 { frame: Plane3 { origin: Point3(0 mm, 0 mm, 5 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, half_extents: Vec3(2 mm, 4 mm, 5 mm) } 11 + obox_display = obox{ c=(0 mm, 0 mm, 5 mm), half=<2 mm, 4 mm, 5 mm> } 12 + obox_center = (0 mm, 0 mm, 5 mm) 13 + obox_half = <2 mm, 4 mm, 5 mm>
+7
crates/bone-types/tests/snapshots/surface__id_debug_surface.snap
··· 17 17 SketchRelationId::null = SketchRelationId(null) 18 18 SketchDimensionId::null = SketchDimensionId(null) 19 19 SketchParameterId::null = SketchParameterId(null) 20 + ExtrudeId::null = ExtrudeId(null) 21 + BodyId::null = BodyId(null) 22 + BrepShellId::null = BrepShellId(null) 23 + BrepFaceId::null = BrepFaceId(null) 24 + BrepEdgeId::null = BrepEdgeId(null) 25 + BrepVertexId::null = BrepVertexId(null) 26 + BrepLoopId::null = BrepLoopId(null) 20 27 FaceId::default = FaceId(null) 21 28 FaceId::null.is_null = true 22 29 EdgeId::null.is_null = true
+15
crates/bone-types/tests/snapshots/surface__label_surface.snap
··· 1 + --- 2 + source: crates/bone-types/tests/surface.rs 3 + expression: surface 4 + --- 5 + loop_index_disp = loop#2 6 + loop_outer_disp = loop#0 7 + import_ordinal = import#7 8 + side_kind_corner= corner 9 + side_kind_seam = seam 10 + face_start_disp = face[FeatureId(null)]:start_cap 11 + face_start_dbg = FaceLabel { feature: FeatureId(null), role: StartCap } 12 + face_side_disp = face[FeatureId(null)]:side(loop#0, from=SketchEntityId(null)) 13 + face_imp_disp = face[FeatureId(null)]:imported(import#7) 14 + edge_side_disp = edge[FeatureId(null)]:side_edge(seam, from=SketchEntityId(null)) 15 + vertex_end_disp = vertex[FeatureId(null)]:end_cap_vertex(corner, from=SketchEntityId(null))
+16
crates/bone-types/tests/snapshots/surface__space3_surface.snap
··· 1 + --- 2 + source: crates/bone-types/tests/surface.rs 3 + expression: surface 4 + --- 5 + vec3_debug = Vec3(3 mm, 0 mm, 4 mm) 6 + vec3_display = <3 mm, 0 mm, 4 mm> 7 + vec3_norm_mm = 5 8 + vec3_cross_z = 1 9 + unit3_reversed = [0, 0, -1] 10 + unit3_into_vec5 = <0 mm, 0 mm, 5 mm> 11 + aabb_debug = Aabb3 { min: Point3(-3 mm, -1 mm, 0 mm), max: Point3(2 mm, 4 mm, 5 mm) } 12 + aabb_display = aabb[(-3 mm, -1 mm, 0 mm)..(2 mm, 4 mm, 5 mm)] 13 + aabb_center = (-0.5 mm, 1.5 mm, 2.5 mm) 14 + aabb_extent = <5 mm, 5 mm, 5 mm> 15 + chord_display = chord_tol=0.01 mm 16 + chord_mm = 0.01
+146 -5
crates/bone-types/tests/surface.rs
··· 1 1 use bone_types::{ 2 - Angle, AngleTolerance, BudgetCeiling, DegreesOfFreedom, DocumentId, EdgeId, FaceId, FeatureId, 3 - Length, LoopId, NewtonDamping, NewtonStepTolerance, NodeId, Parameter, ParentIndex, Point2, 4 - Point3, ShellId, SketchDimensionId, SketchEntityId, SketchId, SketchItemId, SketchParameterId, 5 - SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, Tolerance, UnitVec2, UnitVec3, 6 - Vec2, VertexId, WireId, degree, millimeter, radian, 2 + Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, 3 + BrepShellId, BrepVertexId, BudgetCeiling, ChordHeightTolerance, DegreesOfFreedom, DocumentId, 4 + EdgeId, EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, FaceRole, FeatureId, ImportOrdinal, 5 + Length, LoopId, LoopIndex, NewtonDamping, NewtonStepTolerance, NodeId, OrientedBox3, Parameter, 6 + ParentIndex, Plane3, Point2, Point3, ShellId, SideKind, SketchDimensionId, SketchEntityId, 7 + SketchId, SketchItemId, SketchParameterId, SketchPlaneBasis, SketchRelationId, SolidId, 8 + SolverResidual, Tolerance, UnitVec2, UnitVec3, Vec2, Vec3, VertexId, VertexLabel, VertexRole, 9 + WireId, degree, millimeter, radian, 7 10 }; 8 11 use slotmap::Key; 9 12 ··· 25 28 SketchRelationId::null = {:?}\n\ 26 29 SketchDimensionId::null = {:?}\n\ 27 30 SketchParameterId::null = {:?}\n\ 31 + ExtrudeId::null = {:?}\n\ 32 + BodyId::null = {:?}\n\ 33 + BrepShellId::null = {:?}\n\ 34 + BrepFaceId::null = {:?}\n\ 35 + BrepEdgeId::null = {:?}\n\ 36 + BrepVertexId::null = {:?}\n\ 37 + BrepLoopId::null = {:?}\n\ 28 38 FaceId::default = {:?}\n\ 29 39 FaceId::null.is_null = {}\n\ 30 40 EdgeId::null.is_null = {}\n\ ··· 44 54 SketchRelationId::null(), 45 55 SketchDimensionId::null(), 46 56 SketchParameterId::null(), 57 + ExtrudeId::null(), 58 + BodyId::null(), 59 + BrepShellId::null(), 60 + BrepFaceId::null(), 61 + BrepEdgeId::null(), 62 + BrepVertexId::null(), 63 + BrepLoopId::null(), 47 64 FaceId::default(), 48 65 FaceId::null().is_null(), 49 66 EdgeId::null().is_null(), ··· 195 212 ); 196 213 insta::assert_snapshot!(surface); 197 214 } 215 + 216 + #[test] 217 + fn space3_surface() { 218 + let v3 = Vec3::from_mm(3.0, 0.0, 4.0); 219 + let unit = UnitVec3::z_axis(); 220 + let bb = Aabb3::from_corners( 221 + Point3::from_mm(2.0, -1.0, 5.0), 222 + Point3::from_mm(-3.0, 4.0, 0.0), 223 + ); 224 + let chord = ChordHeightTolerance::from_mm(0.01); 225 + let cross_z = Vec3::from_mm(1.0, 0.0, 0.0) 226 + .cross(Vec3::from_mm(0.0, 1.0, 0.0)) 227 + .z() 228 + .get::<millimeter>(); 229 + let surface = format!( 230 + "vec3_debug = {v3:?}\n\ 231 + vec3_display = {v3}\n\ 232 + vec3_norm_mm = {vn}\n\ 233 + vec3_cross_z = {cross_z}\n\ 234 + unit3_reversed = {urev}\n\ 235 + unit3_into_vec5 = {uvec}\n\ 236 + aabb_debug = {bb:?}\n\ 237 + aabb_display = {bb}\n\ 238 + aabb_center = {center}\n\ 239 + aabb_extent = {extent}\n\ 240 + chord_display = {chord}\n\ 241 + chord_mm = {chmm}", 242 + vn = v3.norm_mm(), 243 + urev = unit.reversed(), 244 + uvec = unit.into_vec(Length::new::<millimeter>(5.0)), 245 + center = bb.center(), 246 + extent = bb.extent(), 247 + chmm = chord.millimeters(), 248 + ); 249 + insta::assert_snapshot!(surface); 250 + } 251 + 252 + #[test] 253 + fn geometry3_surface() { 254 + let Ok(plane) = Plane3::new( 255 + Point3::from_mm(1.0, 0.0, 0.0), 256 + UnitVec3::x_axis(), 257 + UnitVec3::y_axis(), 258 + Tolerance::new(1e-9), 259 + ) else { 260 + panic!("orthonormal axes"); 261 + }; 262 + let axis_angle = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5)); 263 + let obox = OrientedBox3::from_aabb(Aabb3::from_corners( 264 + Point3::from_mm(-2.0, -4.0, 0.0), 265 + Point3::from_mm(2.0, 4.0, 10.0), 266 + )); 267 + let surface = format!( 268 + "plane_debug = {plane:?}\n\ 269 + plane_display = {plane}\n\ 270 + plane_normal = {pn}\n\ 271 + axisangle_debug = {axis_angle:?}\n\ 272 + axisangle_disp = {axis_angle}\n\ 273 + obox_debug = {obox:?}\n\ 274 + obox_display = {obox}\n\ 275 + obox_center = {oc}\n\ 276 + obox_half = {oh}", 277 + pn = plane.normal(), 278 + oc = obox.center(), 279 + oh = obox.half_extents(), 280 + ); 281 + insta::assert_snapshot!(surface); 282 + } 283 + 284 + #[test] 285 + fn label_surface() { 286 + let feature = FeatureId::default(); 287 + let from = SketchEntityId::default(); 288 + let face_start = FaceLabel { 289 + feature, 290 + role: FaceRole::StartCap, 291 + }; 292 + let face_side = FaceLabel { 293 + feature, 294 + role: FaceRole::Side { 295 + loop_index: LoopIndex::OUTER, 296 + from, 297 + }, 298 + }; 299 + let face_imp = FaceLabel { 300 + feature, 301 + role: FaceRole::Imported { 302 + ordinal: ImportOrdinal::new(7), 303 + }, 304 + }; 305 + let edge_side = EdgeLabel { 306 + feature, 307 + role: EdgeRole::SideEdge { 308 + from, 309 + side: SideKind::Seam, 310 + }, 311 + }; 312 + let vert_end = VertexLabel { 313 + feature, 314 + role: VertexRole::EndCapVertex { 315 + from, 316 + side: SideKind::Corner, 317 + }, 318 + }; 319 + let surface = format!( 320 + "loop_index_disp = {li}\n\ 321 + loop_outer_disp = {lo}\n\ 322 + import_ordinal = {io}\n\ 323 + side_kind_corner= {skc}\n\ 324 + side_kind_seam = {sks}\n\ 325 + face_start_disp = {face_start}\n\ 326 + face_start_dbg = {face_start:?}\n\ 327 + face_side_disp = {face_side}\n\ 328 + face_imp_disp = {face_imp}\n\ 329 + edge_side_disp = {edge_side}\n\ 330 + vertex_end_disp = {vert_end}", 331 + li = LoopIndex::new(2), 332 + lo = LoopIndex::OUTER, 333 + io = ImportOrdinal::new(7), 334 + skc = SideKind::Corner, 335 + sks = SideKind::Seam, 336 + ); 337 + insta::assert_snapshot!(surface); 338 + }