Another project
1pub use uom::si::angle::{degree, radian};
2pub use uom::si::f64::{Angle, Length};
3use uom::si::length::meter;
4pub use uom::si::length::millimeter;
5
6pub mod camera;
7pub mod cancel;
8pub mod color;
9pub mod content;
10pub mod dimensioned_serde;
11pub mod display;
12pub mod history;
13pub mod icon;
14pub mod label;
15pub mod reference;
16pub mod schema;
17pub mod solver;
18pub mod space;
19pub mod step;
20
21pub use camera::{
22 Camera3, CubicEasing, OrbitState, Projection, ProjectionKind, StandardView, ZoomFactor,
23};
24pub use cancel::{Cancel, CancelFlag};
25pub use color::LinearRgba;
26pub use content::SolidKey;
27pub use display::{DisplayMode, ShadingModel};
28pub use history::{
29 BuildFailure, FeatureGeneration, RebuildError, RebuildStatus, RebuildWarning, RollbackMarker,
30 SuppressionState,
31};
32pub use icon::{IconId, IconTile};
33pub use label::{
34 EdgeLabel, EdgeRole, FaceLabel, FaceRole, ImportOrdinal, LoopIndex, SideKind, VertexLabel,
35 VertexRole,
36};
37pub use reference::{
38 EdgeFingerprint, EntityFingerprint, EntityRef, FaceFingerprint, FaceRef, MatchScore,
39 Resolution, VertexFingerprint,
40};
41pub use schema::{SchemaHeader, SchemaVersion};
42pub use solver::{
43 BudgetCeiling, DegreesOfFreedom, NewtonDamping, NewtonStepTolerance, Parameter, ParameterIndex,
44 ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed,
45};
46pub use space::{
47 Aabb3, AxisAngle, OrientedBox3, Plane3, Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3,
48 Vec2, Vec3,
49};
50pub use step::{
51 StepEntityId, StepEntityKind, StepFileHeader, StepFileName, StepOrganization,
52 StepOriginatingSystem, StepSchema,
53};
54
55#[cfg(feature = "testing")]
56pub mod testing;
57
58#[derive(Debug, thiserror::Error)]
59pub enum TypesError {
60 #[error("sketch plane axes not orthogonal: |x·y|={0}")]
61 NonOrthogonalPlaneAxes(f64),
62 #[error("axis vector is zero-length")]
63 ZeroLengthAxis,
64 #[error("axis vector is not unit length: norm {0}")]
65 NonUnitAxis(f64),
66 #[error("oriented box half-extent must be finite and non-negative: {0}")]
67 InvalidHalfExtent(f64),
68 #[error("camera eye and target coincide: separation {0} mm")]
69 DegenerateCamera(f64),
70 #[error("camera eye or target is not finite")]
71 CameraNotFinite,
72 #[error("camera up direction is parallel to the view direction")]
73 CameraUpParallelToView,
74 #[error("orthographic half-height must be finite and positive: {0} mm")]
75 NonPositiveOrthoHeight(f64),
76 #[error("perspective field of view must be within (0, pi) radians: {0}")]
77 FovOutOfRange(f64),
78 #[error("perspective near distance must be finite and positive: {0} mm")]
79 NonPositiveNear(f64),
80 #[error("perspective far distance must exceed the near distance: near {near} mm, far {far} mm")]
81 FarNotBeyondNear { near: f64, far: f64 },
82 #[error("zoom factor must be finite and positive: {0}")]
83 NonPositiveZoom(f64),
84 #[error("orbit pan target is not finite")]
85 OrbitPanTargetNotFinite,
86 #[error("orbit rotation is not finite")]
87 OrbitRotationNotFinite,
88 #[error("view-projection matrix is not invertible")]
89 NonInvertibleViewProjection,
90 #[error("viewport pixel coordinate is not finite: ({x}, {y})")]
91 NonFiniteViewportPixel { x: f64, y: f64 },
92 #[error("viewport extent has a zero dimension: {width} x {height}")]
93 ZeroViewportExtent { width: u32, height: u32 },
94 #[error("step entity instance name must be positive")]
95 ZeroStepEntityId,
96 #[error("length must be finite and positive: {0} m")]
97 NonPositiveLength(f64),
98 #[error("normal-to view requires a reference plane")]
99 NormalToRequiresPlane,
100 #[error("cubic easing needs finite controls with x1,x2 in 0..=1: ({x1},{y1}) ({x2},{y2})")]
101 InvalidEasingControl { x1: f64, y1: f64, x2: f64, y2: f64 },
102 #[error("match score must be finite and within 0..=1: {0}")]
103 MatchScoreOutOfRange(f64),
104}
105
106pub type Result<T, E = TypesError> = core::result::Result<T, E>;
107
108slotmap::new_key_type! {
109 pub struct FaceId;
110 pub struct EdgeId;
111 pub struct VertexId;
112 pub struct ShellId;
113 pub struct SolidId;
114 pub struct LoopId;
115 pub struct WireId;
116 pub struct FeatureId;
117 pub struct NodeId;
118 pub struct DocumentId;
119 pub struct SketchId;
120 pub struct SketchEntityId;
121 pub struct SketchRelationId;
122 pub struct SketchDimensionId;
123 pub struct SketchParameterId;
124 pub struct ExtrudeId;
125 pub struct BodyId;
126 pub struct BrepShellId;
127 pub struct BrepFaceId;
128 pub struct BrepEdgeId;
129 pub struct BrepVertexId;
130 pub struct BrepLoopId;
131}
132
133macro_rules! impl_as_u64 {
134 ($($key:ty),+ $(,)?) => {
135 $(impl $key {
136 /// Encodes this id as an opaque `u64`, stable for the same slotmap slot+version
137 /// within a single process. Use only for deterministic widget-key derivation; the
138 /// representation is not portable across builds or persisted artifacts.
139 #[must_use]
140 pub fn as_u64(self) -> u64 {
141 use slotmap::Key;
142 self.data().as_ffi()
143 }
144 })+
145 };
146}
147
148impl_as_u64!(SketchId, ExtrudeId, BrepEdgeId, BrepVertexId);
149
150#[derive(Copy, Clone, Debug, PartialEq, Eq)]
151pub struct BrepSlot(u32);
152
153impl core::fmt::Display for BrepSlot {
154 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155 self.0.fmt(f)
156 }
157}
158
159macro_rules! impl_slot {
160 ($($key:ty),+ $(,)?) => {
161 $(impl $key {
162 #[must_use]
163 pub fn slot(self) -> BrepSlot {
164 use slotmap::Key;
165 let low = self.data().as_ffi() & u64::from(u32::MAX);
166 BrepSlot(u32::try_from(low).expect("masked value fits in u32"))
167 }
168 })+
169 };
170}
171
172impl_slot!(BrepEdgeId, BrepVertexId);
173
174#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
175pub struct Tolerance(f64);
176
177impl Tolerance {
178 #[must_use]
179 pub const fn new(value: f64) -> Self {
180 Self(value)
181 }
182
183 #[must_use]
184 pub const fn value(self) -> f64 {
185 self.0
186 }
187}
188
189impl core::fmt::Display for Tolerance {
190 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191 write!(f, "tol={}", self.0)
192 }
193}
194
195#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
196pub struct AngleTolerance(f64);
197
198impl AngleTolerance {
199 pub const ZERO: Self = Self(0.0);
200
201 #[must_use]
202 pub const fn from_radians(value: f64) -> Self {
203 Self(value)
204 }
205
206 #[must_use]
207 pub const fn radians(self) -> f64 {
208 self.0
209 }
210
211 #[must_use]
212 pub fn from_arc_length(linear: Tolerance, radius: Length) -> Self {
213 let r = radius.get::<millimeter>().abs();
214 if r > linear.value() {
215 Self(linear.value() / r)
216 } else {
217 Self(linear.value())
218 }
219 }
220}
221
222impl core::fmt::Display for AngleTolerance {
223 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224 write!(f, "tol_angle={} rad", self.0)
225 }
226}
227
228#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
229pub struct ChordHeightTolerance(Length);
230
231impl ChordHeightTolerance {
232 #[must_use]
233 pub fn new(length: Length) -> Self {
234 Self(length)
235 }
236
237 #[must_use]
238 pub fn from_mm(value: f64) -> Self {
239 Self(Length::new::<millimeter>(value))
240 }
241
242 #[must_use]
243 pub fn length(self) -> Length {
244 self.0
245 }
246
247 #[must_use]
248 pub fn millimeters(self) -> f64 {
249 self.0.get::<millimeter>()
250 }
251}
252
253impl core::fmt::Display for ChordHeightTolerance {
254 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
255 write!(f, "chord_tol={} mm", self.0.get::<millimeter>())
256 }
257}
258
259#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
260#[serde(try_from = "f64", into = "f64")]
261pub struct PositiveLength(Length);
262
263impl PositiveLength {
264 pub fn new(length: Length) -> Result<Self> {
265 let meters = length.get::<meter>();
266 if meters.is_finite() && meters > 0.0 {
267 Ok(Self(length))
268 } else {
269 Err(TypesError::NonPositiveLength(meters))
270 }
271 }
272
273 #[must_use]
274 pub fn get(self) -> Length {
275 self.0
276 }
277}
278
279impl From<PositiveLength> for f64 {
280 fn from(value: PositiveLength) -> Self {
281 value.0.get::<meter>()
282 }
283}
284
285impl TryFrom<f64> for PositiveLength {
286 type Error = TypesError;
287
288 fn try_from(value: f64) -> Result<Self> {
289 Self::new(Length::new::<meter>(value))
290 }
291}
292
293#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
294pub struct CreaseAngle(f64);
295
296impl CreaseAngle {
297 pub const FLAT: Self = Self(0.0);
298
299 #[must_use]
300 pub const fn from_radians(value: f64) -> Self {
301 Self(value)
302 }
303
304 #[must_use]
305 pub fn from_dot(dot: f64) -> Self {
306 Self(dot.clamp(-1.0, 1.0).acos())
307 }
308
309 #[must_use]
310 pub fn angle(self) -> Angle {
311 Angle::new::<radian>(self.0)
312 }
313
314 #[must_use]
315 pub const fn radians(self) -> f64 {
316 self.0
317 }
318}
319
320impl core::fmt::Display for CreaseAngle {
321 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322 write!(f, "crease={} rad", self.0)
323 }
324}
325
326#[derive(
327 Copy,
328 Clone,
329 Debug,
330 PartialEq,
331 Eq,
332 PartialOrd,
333 Ord,
334 Hash,
335 Default,
336 serde::Serialize,
337 serde::Deserialize,
338)]
339pub struct MeshGeneration(u64);
340
341impl MeshGeneration {
342 #[must_use]
343 pub const fn new(value: u64) -> Self {
344 Self(value)
345 }
346
347 #[must_use]
348 pub const fn value(self) -> u64 {
349 self.0
350 }
351}
352
353#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
354pub struct GeometryGeneration(u64);
355
356impl GeometryGeneration {
357 #[must_use]
358 pub fn from_solid_key(key: SolidKey) -> Self {
359 let folded = key
360 .bytes()
361 .chunks_exact(8)
362 .map(|word| {
363 let mut bytes = [0u8; 8];
364 bytes.copy_from_slice(word);
365 u64::from_le_bytes(bytes)
366 })
367 .fold(0u64, core::ops::BitXor::bitxor);
368 Self(folded)
369 }
370
371 #[must_use]
372 pub const fn value(self) -> u64 {
373 self.0
374 }
375}
376
377impl core::fmt::Display for MeshGeneration {
378 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379 write!(f, "mesh_gen={}", self.0)
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::BrepSlot;
386 use super::{
387 Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId,
388 BrepShellId, BrepVertexId, BuildFailure, Camera3, ChordHeightTolerance, CreaseAngle,
389 CubicEasing, DegreesOfFreedom, DisplayMode, DocumentId, EdgeFingerprint, EdgeId, EdgeLabel,
390 EdgeRole, EntityFingerprint, EntityRef, ExtrudeId, FaceFingerprint, FaceId, FaceLabel,
391 FaceRef, FaceRole, FeatureGeneration, FeatureId, ImportOrdinal, Length, LoopId, LoopIndex,
392 MatchScore, MeshGeneration, NodeId, OrbitState, OrientedBox3, Parameter, Plane3, Point2,
393 Point3, PositiveLength, Projection, ProjectionKind, RebuildError, RebuildStatus,
394 RebuildWarning, Resolution, RollbackMarker, ShadingModel, ShellId, SideKind,
395 SketchDimensionId, SketchEntityId, SketchId, SketchParameterId, SketchPlaneBasis,
396 SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, StepEntityKind,
397 StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, StepSchema,
398 SuppressionState, Tolerance, UnitVec2, UnitVec3, Vec2, Vec3, VertexFingerprint, VertexId,
399 VertexLabel, VertexRole, WireId, ZoomFactor, degree, millimeter, radian,
400 };
401 use slotmap::{Key, SlotMap};
402 use uom::si::length::meter;
403
404 fn ortho_camera() -> Camera3 {
405 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(25.0)) else {
406 panic!("25 mm half-height is positive");
407 };
408 let Ok(camera) = Camera3::new(
409 Point3::from_mm(0.0, 0.0, 10.0),
410 Point3::origin(),
411 UnitVec3::y_axis(),
412 projection,
413 ) else {
414 panic!("eye and target are 10 mm apart");
415 };
416 camera
417 }
418
419 #[test]
420 fn id_null_is_null() {
421 assert!(FaceId::null().is_null());
422 assert!(EdgeId::null().is_null());
423 assert!(VertexId::null().is_null());
424 assert!(ShellId::null().is_null());
425 assert!(SolidId::null().is_null());
426 assert!(LoopId::null().is_null());
427 assert!(WireId::null().is_null());
428 assert!(FeatureId::null().is_null());
429 assert!(NodeId::null().is_null());
430 assert!(DocumentId::null().is_null());
431 assert!(SketchId::null().is_null());
432 assert!(SketchEntityId::null().is_null());
433 assert!(SketchRelationId::null().is_null());
434 assert!(SketchDimensionId::null().is_null());
435 assert!(SketchParameterId::null().is_null());
436 assert!(ExtrudeId::null().is_null());
437 assert!(BodyId::null().is_null());
438 assert!(BrepShellId::null().is_null());
439 assert!(BrepFaceId::null().is_null());
440 assert!(BrepEdgeId::null().is_null());
441 assert!(BrepVertexId::null().is_null());
442 assert!(BrepLoopId::null().is_null());
443 }
444
445 #[test]
446 fn brep_slot_is_the_one_based_insertion_index() {
447 let mut edges: SlotMap<BrepEdgeId, ()> = SlotMap::with_key();
448 let slots: Vec<BrepSlot> = (0..3).map(|_| edges.insert(()).slot()).collect();
449 assert_eq!(slots, vec![BrepSlot(1), BrepSlot(2), BrepSlot(3)]);
450 }
451
452 #[test]
453 fn ids_are_distinct_types() {
454 let _f: FaceId = FaceId::null();
455 let _e: EdgeId = EdgeId::null();
456 let _s: SketchEntityId = SketchEntityId::null();
457 let _r: SketchRelationId = SketchRelationId::null();
458 }
459
460 #[test]
461 fn length_mm_roundtrip() {
462 let l = Length::new::<millimeter>(12.5);
463 assert!((l.get::<millimeter>() - 12.5).abs() < f64::EPSILON);
464 }
465
466 #[test]
467 fn angle_deg_converts_to_rad() {
468 let a = Angle::new::<degree>(180.0);
469 assert!((a.get::<radian>() - core::f64::consts::PI).abs() < 1e-12);
470 }
471
472 #[test]
473 fn tolerance_display() {
474 let t = Tolerance::new(1e-6);
475 assert_eq!(format!("{t}"), "tol=0.000001");
476 assert!((t.value() - 1e-6).abs() < f64::EPSILON);
477 }
478
479 #[test]
480 fn angle_tolerance_from_arc_length_scales_by_radius() {
481 let linear = Tolerance::new(1e-3);
482 let r = Length::new::<millimeter>(10.0);
483 let a = AngleTolerance::from_arc_length(linear, r);
484 assert!((a.radians() - 1e-4).abs() < 1e-15);
485 }
486
487 #[test]
488 fn angle_tolerance_degenerate_radius_falls_back_to_linear() {
489 let linear = Tolerance::new(1e-3);
490 let r = Length::new::<millimeter>(0.0);
491 let a = AngleTolerance::from_arc_length(linear, r);
492 assert!((a.radians() - 1e-3).abs() < 1e-15);
493 }
494
495 #[test]
496 fn angle_tolerance_zero_constant() {
497 assert!(AngleTolerance::ZERO.radians().abs() < f64::EPSILON);
498 }
499
500 #[test]
501 fn crease_angle_flat_is_zero() {
502 assert!(CreaseAngle::FLAT.radians().abs() < f64::EPSILON);
503 }
504
505 #[test]
506 fn crease_angle_from_dot_right_angle() {
507 let a = CreaseAngle::from_dot(0.0);
508 assert!((a.radians() - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
509 }
510
511 #[test]
512 fn crease_angle_from_dot_clamps_outside_unit() {
513 let a = CreaseAngle::from_dot(1.5);
514 assert!(a.radians().abs() < 1e-12);
515 let b = CreaseAngle::from_dot(-1.5);
516 assert!((b.radians() - core::f64::consts::PI).abs() < 1e-12);
517 }
518
519 #[test]
520 fn point2_minus_point2_is_vec2() {
521 let a = Point2::from_mm(10.0, 5.0);
522 let b = Point2::from_mm(4.0, 2.0);
523 let d: Vec2 = a - b;
524 assert!((d.x().get::<millimeter>() - 6.0).abs() < 1e-12);
525 assert!((d.y().get::<millimeter>() - 3.0).abs() < 1e-12);
526 }
527
528 #[test]
529 fn point2_plus_vec2_is_point2() {
530 let p = Point2::from_mm(1.0, 2.0);
531 let v = Vec2::from_mm(10.0, 20.0);
532 let q: Point2 = p + v;
533 assert!((q.x().get::<millimeter>() - 11.0).abs() < 1e-12);
534 assert!((q.y().get::<millimeter>() - 22.0).abs() < 1e-12);
535 }
536
537 #[test]
538 fn vec2_perp_and_dot() {
539 let v = Vec2::from_mm(3.0, 4.0);
540 assert!((v.norm_mm() - 5.0).abs() < 1e-12);
541 let p = v.perp_ccw();
542 assert!((p.x().get::<millimeter>() + 4.0).abs() < 1e-12);
543 assert!((p.y().get::<millimeter>() - 3.0).abs() < 1e-12);
544 assert!(v.dot_mm2(p).abs() < 1e-12);
545 assert!((v.cross_z_mm2(Vec2::from_mm(1.0, 0.0)) + 4.0).abs() < 1e-12);
546 }
547
548 #[test]
549 fn vec2_try_normalize_rejects_zero() {
550 let v = Vec2::zero();
551 let r = v.try_normalize(Tolerance::new(1e-9));
552 assert!(r.is_err());
553 }
554
555 #[test]
556 fn vec2_try_normalize_unit() {
557 let v = Vec2::from_mm(0.0, 7.0);
558 let Ok(u) = v.try_normalize(Tolerance::new(1e-9)) else {
559 panic!("nonzero");
560 };
561 let (ux, uy) = u.components();
562 assert!(ux.abs() < 1e-12);
563 assert!((uy - 1.0).abs() < 1e-12);
564 }
565
566 #[test]
567 fn unit_vec2_axes_are_orthogonal() {
568 let x = UnitVec2::x_axis();
569 let y = UnitVec2::y_axis();
570 assert!(x.dot(y).abs() < 1e-15);
571 assert_eq!(x.perp_ccw().components(), y.components());
572 }
573
574 #[test]
575 fn unit_vec2_rejects_zero_axis() {
576 let r = UnitVec2::try_from_components(0.0, 0.0, Tolerance::new(1e-9));
577 assert!(r.is_err());
578 }
579
580 #[test]
581 fn unit_vec2_into_vec_scales_by_length() {
582 let u = UnitVec2::x_axis();
583 let v = u.into_vec(Length::new::<millimeter>(3.0));
584 assert!((v.x().get::<millimeter>() - 3.0).abs() < 1e-12);
585 assert!(v.y().get::<millimeter>().abs() < 1e-12);
586 }
587
588 #[test]
589 fn vec2_scalar_and_negation() {
590 let v = Vec2::from_mm(3.0, -4.0);
591 let w = 2.0 * (-v);
592 assert!((w.x().get::<millimeter>() + 6.0).abs() < 1e-12);
593 assert!((w.y().get::<millimeter>() - 8.0).abs() < 1e-12);
594 }
595
596 #[test]
597 fn point3_roundtrip() {
598 let p = Point3::from_mm(1.5, 2.5, 3.5);
599 assert!((p.x().get::<millimeter>() - 1.5).abs() < 1e-12);
600 assert!((p.y().get::<millimeter>() - 2.5).abs() < 1e-12);
601 assert!((p.z().get::<millimeter>() - 3.5).abs() < 1e-12);
602 }
603
604 #[test]
605 fn sketch_plane_basis_accepts_orthogonal_axes() {
606 let Ok(basis) = SketchPlaneBasis::new(
607 Point3::origin(),
608 UnitVec3::x_axis(),
609 UnitVec3::y_axis(),
610 Tolerance::new(1e-9),
611 ) else {
612 panic!("x and y unit axes are orthogonal");
613 };
614 let (nx, ny, nz) = basis.normal().components();
615 assert!((nx).abs() < 1e-12);
616 assert!((ny).abs() < 1e-12);
617 assert!((nz - 1.0).abs() < 1e-12);
618 }
619
620 #[test]
621 fn sketch_plane_basis_reorthonormalizes_within_tolerance() {
622 let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9))
623 else {
624 panic!("input vector is nonzero");
625 };
626 let Ok(basis) = SketchPlaneBasis::new(
627 Point3::origin(),
628 UnitVec3::x_axis(),
629 nearly,
630 Tolerance::new(1e-9),
631 ) else {
632 panic!("within tolerance");
633 };
634 let dot_after = basis.x_axis().dot(basis.y_axis()).abs();
635 assert!(
636 dot_after < 1e-15,
637 "re-orthonormalized y should be orthogonal, got {dot_after}"
638 );
639 }
640
641 #[test]
642 fn sketch_plane_basis_rejects_non_orthogonal_axes() {
643 let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else {
644 panic!("input vector is nonzero");
645 };
646 let result = SketchPlaneBasis::new(
647 Point3::origin(),
648 UnitVec3::x_axis(),
649 skew,
650 Tolerance::new(1e-9),
651 );
652 assert!(result.is_err());
653 }
654
655 #[test]
656 fn unit_vec3_rejects_zero_and_near_zero_within_tolerance() {
657 let tol = Tolerance::new(1e-9);
658 assert!(UnitVec3::try_from_components(0.0, 0.0, 0.0, tol).is_err());
659 assert!(UnitVec3::try_from_components(1e-13, 0.0, 0.0, tol).is_err());
660 assert!(UnitVec3::try_from_components(1.0, 0.0, 0.0, tol).is_ok());
661 }
662
663 #[test]
664 fn unit_vec3_deserialize_preserves_irrational_components_bit_exact() {
665 let Ok(original) = UnitVec3::try_from_components(1.0, 2.0, 3.0, Tolerance::new(1e-9))
666 else {
667 panic!("normalizable direction");
668 };
669 let Ok(text) = ron::to_string(&original) else {
670 panic!("serialize");
671 };
672 let Ok(back) = ron::from_str::<UnitVec3>(&text) else {
673 panic!("deserialize");
674 };
675 let (ox, oy, oz) = original.components();
676 let (bx, by, bz) = back.components();
677 assert_eq!(ox.to_bits(), bx.to_bits());
678 assert_eq!(oy.to_bits(), by.to_bits());
679 assert_eq!(oz.to_bits(), bz.to_bits());
680 }
681
682 #[test]
683 fn unit_vec3_deserialize_rejects_non_unit_components() {
684 let probe = UnitVec3::new_unchecked(0.6, 0.8, 0.0);
685 let Ok(text) = ron::to_string(&probe) else {
686 panic!("serialize");
687 };
688 assert!(ron::from_str::<UnitVec3>(&text).is_ok());
689 let non_unit = text.replacen("0.6", "0.9", 1);
690 assert_ne!(text, non_unit, "probe value present for mutation");
691 assert!(ron::from_str::<UnitVec3>(&non_unit).is_err());
692 }
693
694 #[test]
695 fn parameter_display_and_value() {
696 let p = Parameter::new(0.25);
697 assert_eq!(format!("{p}"), "param=0.25");
698 assert!((p.value() - 0.25).abs() < f64::EPSILON);
699 }
700
701 #[test]
702 fn degrees_of_freedom_display_and_value() {
703 let d = DegreesOfFreedom::new(7);
704 assert_eq!(format!("{d}"), "dof=7");
705 assert_eq!(d.value(), 7);
706 }
707
708 #[test]
709 fn solver_residual_display_and_value() {
710 let r = SolverResidual::new(1.5e-6);
711 assert_eq!(format!("{r}"), "res=0.0000015");
712 assert!((r.value() - 1.5e-6).abs() < f64::EPSILON);
713 }
714
715 #[test]
716 fn mesh_generation_orders_by_value() {
717 let a = MeshGeneration::new(7);
718 let b = MeshGeneration::new(42);
719 assert!(a < b);
720 assert_eq!(format!("{a}"), "mesh_gen=7");
721 assert_eq!(a.value(), 7);
722 assert_eq!(MeshGeneration::default(), MeshGeneration::new(0));
723 }
724
725 #[test]
726 fn chord_height_tolerance_display_and_value() {
727 let c = ChordHeightTolerance::from_mm(0.01);
728 assert!((c.millimeters() - 0.01).abs() < 1e-15);
729 assert_eq!(format!("{c}"), "chord_tol=0.01 mm");
730 let c2 = ChordHeightTolerance::new(Length::new::<millimeter>(0.05));
731 assert!((c2.length().get::<millimeter>() - 0.05).abs() < 1e-15);
732 }
733
734 #[test]
735 fn vec3_ops_and_normalize() {
736 let vec = Vec3::from_mm(1.0, 2.0, 2.0);
737 assert!((vec.norm_mm() - 3.0).abs() < 1e-12);
738 let unit_x = Vec3::from_mm(1.0, 0.0, 0.0);
739 let unit_y = Vec3::from_mm(0.0, 1.0, 0.0);
740 assert!((unit_x.cross(unit_y).z().get::<millimeter>() - 1.0).abs() < 1e-12);
741 assert!(unit_x.dot_mm2(unit_y).abs() < 1e-12);
742 let Ok(normalized) = vec.try_normalize(Tolerance::new(1e-9)) else {
743 panic!("nonzero");
744 };
745 let (nx, ny, nz) = normalized.components();
746 assert!((nx * nx + ny * ny + nz * nz - 1.0).abs() < 1e-12);
747 assert!(Vec3::zero().try_normalize(Tolerance::new(1e-9)).is_err());
748 let far = Point3::from_mm(5.0, 5.0, 5.0);
749 let near = Point3::from_mm(1.0, 2.0, 3.0);
750 let delta: Vec3 = far - near;
751 assert!((delta.x().get::<millimeter>() - 4.0).abs() < 1e-12);
752 assert!(((near + delta).x().get::<millimeter>() - 5.0).abs() < 1e-12);
753 }
754
755 #[test]
756 fn unit_vec3_reverse_and_into_vec() {
757 let zaxis = UnitVec3::z_axis();
758 assert!((zaxis.reversed().components().2 + 1.0).abs() < 1e-12);
759 let scaled = zaxis.into_vec(Length::new::<millimeter>(4.0));
760 assert!(scaled.x().get::<millimeter>().abs() < 1e-12);
761 assert!((scaled.z().get::<millimeter>() - 4.0).abs() < 1e-12);
762 }
763
764 #[test]
765 fn plane3_orthonormalizes_and_rejects_skew() {
766 let origin = Point3::from_mm(1.0, 2.0, 3.0);
767 let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9))
768 else {
769 panic!("nonzero");
770 };
771 let Ok(plane) = Plane3::new(origin, UnitVec3::x_axis(), nearly, Tolerance::new(1e-9))
772 else {
773 panic!("within tolerance");
774 };
775 assert!(plane.x_axis().dot(plane.y_axis()).abs() < 1e-15);
776 let (nx, ny, nz) = plane.normal().components();
777 assert!(nx.abs() < 1e-12 && ny.abs() < 1e-12 && (nz - 1.0).abs() < 1e-12);
778 let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else {
779 panic!("nonzero");
780 };
781 assert!(Plane3::new(origin, UnitVec3::x_axis(), skew, Tolerance::new(1e-9)).is_err());
782 }
783
784 #[test]
785 fn axis_angle_accessors() {
786 let aa = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5));
787 assert!((aa.angle().get::<radian>() - 0.5).abs() < 1e-15);
788 assert!((aa.axis().components().2 - 1.0).abs() < 1e-12);
789 }
790
791 #[test]
792 fn oriented_box_from_aabb_matches_center_and_half() {
793 let aabb = Aabb3::from_corners(
794 Point3::from_mm(-2.0, -4.0, 0.0),
795 Point3::from_mm(2.0, 4.0, 10.0),
796 );
797 let obox = OrientedBox3::from_aabb(aabb);
798 let (cx, cy, cz) = obox.center().coords_mm();
799 assert!(cx.abs() < 1e-12 && cy.abs() < 1e-12 && (cz - 5.0).abs() < 1e-12);
800 let (hx, hy, hz) = obox.half_extents().coords_mm();
801 assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12);
802 assert!((obox.frame().normal().components().2 - 1.0).abs() < 1e-12);
803 }
804
805 #[test]
806 fn aabb3_from_points_union_contains() {
807 let pts = [
808 Point3::from_mm(0.0, 0.0, 0.0),
809 Point3::from_mm(2.0, -1.0, 5.0),
810 Point3::from_mm(-3.0, 4.0, 1.0),
811 ];
812 let Some(bb) = Aabb3::from_points(pts) else {
813 panic!("nonempty");
814 };
815 let (lx, ly, lz) = bb.min().coords_mm();
816 let (hx, hy, hz) = bb.max().coords_mm();
817 assert!((lx + 3.0).abs() < 1e-12 && (ly + 1.0).abs() < 1e-12 && lz.abs() < 1e-12);
818 assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12);
819 assert!(bb.contains(Point3::from_mm(0.0, 0.0, 2.0)));
820 assert!(!bb.contains(Point3::from_mm(10.0, 0.0, 0.0)));
821 let (cx, cy, cz) = bb.center().coords_mm();
822 assert!((cx + 0.5).abs() < 1e-12 && (cy - 1.5).abs() < 1e-12 && (cz - 2.5).abs() < 1e-12);
823 assert!((bb.extent().x().get::<millimeter>() - 5.0).abs() < 1e-12);
824 assert!(Aabb3::from_points(core::iter::empty()).is_none());
825 }
826
827 #[test]
828 fn face_role_ord_matches_canonical_order() {
829 let from = SketchEntityId::default();
830 let start = FaceRole::StartCap;
831 let side0 = FaceRole::Side {
832 loop_index: LoopIndex::OUTER,
833 from,
834 };
835 let side1 = FaceRole::Side {
836 loop_index: LoopIndex::new(1),
837 from,
838 };
839 let end = FaceRole::EndCap;
840 let imported = FaceRole::Imported {
841 ordinal: ImportOrdinal::new(0),
842 };
843 assert!(start < side0);
844 assert!(side0 < side1);
845 assert!(side1 < end);
846 assert!(end < imported);
847 }
848
849 #[test]
850 fn edge_role_ord_matches_canonical_order() {
851 let from = SketchEntityId::default();
852 let start = EdgeRole::StartCapEdge { from };
853 let side = EdgeRole::SideEdge {
854 from,
855 side: SideKind::Corner,
856 };
857 let end = EdgeRole::EndCapEdge { from };
858 let imported = EdgeRole::Imported {
859 ordinal: ImportOrdinal::new(0),
860 };
861 assert!(start < side);
862 assert!(side < end);
863 assert!(end < imported);
864 }
865
866 #[test]
867 fn vertex_role_ord_matches_canonical_order() {
868 let from = SketchEntityId::default();
869 let start = VertexRole::StartCapVertex {
870 from,
871 side: SideKind::Corner,
872 };
873 let end = VertexRole::EndCapVertex {
874 from,
875 side: SideKind::Corner,
876 };
877 let imported = VertexRole::Imported {
878 ordinal: ImportOrdinal::new(0),
879 };
880 assert!(start < end);
881 assert!(end < imported);
882 }
883
884 #[test]
885 fn oriented_box_rejects_invalid_half_extent() {
886 let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis());
887 assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, 2.0, 3.0)).is_ok());
888 assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, -2.0, 3.0)).is_err());
889 assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, f64::NAN, 3.0)).is_err());
890 assert!(OrientedBox3::new(frame, Vec3::from_mm(f64::INFINITY, 2.0, 3.0)).is_err());
891 }
892
893 #[test]
894 fn oriented_box_deserialize_rejects_invalid_half_extent() {
895 let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis());
896 let Ok(obox) = OrientedBox3::new(frame, Vec3::from_mm(5.0, 5.0, 5.0)) else {
897 panic!("non-negative half-extents");
898 };
899 let Ok(good) = ron::to_string(&obox) else {
900 panic!("serialize oriented box");
901 };
902 assert!(ron::from_str::<OrientedBox3>(&good).is_ok());
903 let bad = good.replace('5', "-5");
904 assert!(ron::from_str::<OrientedBox3>(&bad).is_err());
905 }
906
907 #[test]
908 fn labels_ron_roundtrip() {
909 let feature = FeatureId::default();
910 let from = SketchEntityId::default();
911 let face = FaceLabel {
912 feature,
913 role: FaceRole::Side {
914 loop_index: LoopIndex::OUTER,
915 from,
916 },
917 };
918 let Ok(text) = ron::to_string(&face) else {
919 panic!("serialize face label");
920 };
921 let Ok(back) = ron::from_str::<FaceLabel>(&text) else {
922 panic!("deserialize face label");
923 };
924 assert_eq!(face, back);
925
926 let edge = EdgeLabel {
927 feature,
928 role: EdgeRole::SideEdge {
929 from,
930 side: SideKind::Seam,
931 },
932 };
933 let Ok(text) = ron::to_string(&edge) else {
934 panic!("serialize edge label");
935 };
936 let Ok(back) = ron::from_str::<EdgeLabel>(&text) else {
937 panic!("deserialize edge label");
938 };
939 assert_eq!(edge, back);
940
941 let vertex = VertexLabel {
942 feature,
943 role: VertexRole::EndCapVertex {
944 from,
945 side: SideKind::Corner,
946 },
947 };
948 let Ok(text) = ron::to_string(&vertex) else {
949 panic!("serialize vertex label");
950 };
951 let Ok(back) = ron::from_str::<VertexLabel>(&text) else {
952 panic!("deserialize vertex label");
953 };
954 assert_eq!(vertex, back);
955 }
956
957 #[test]
958 fn role_rejects_unknown_field() {
959 let label = FaceLabel {
960 feature: FeatureId::default(),
961 role: FaceRole::Side {
962 loop_index: LoopIndex::OUTER,
963 from: SketchEntityId::default(),
964 },
965 };
966 let Ok(text) = ron::to_string(&label) else {
967 panic!("serialize face label");
968 };
969 let Some(idx) = text.find("Side(") else {
970 panic!("expected Side variant in {text}");
971 };
972 let mut munged = text.clone();
973 munged.insert_str(idx + "Side(".len(), "bogus:42,");
974 assert!(ron::from_str::<FaceLabel>(&munged).is_err());
975 }
976
977 #[test]
978 fn camera_new_rejects_coincident_eye_and_target() {
979 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
980 panic!("10 mm half-height is positive");
981 };
982 let coincident = Camera3::new(
983 Point3::from_mm(1.0, 2.0, 3.0),
984 Point3::from_mm(1.0, 2.0, 3.0),
985 UnitVec3::y_axis(),
986 projection,
987 );
988 assert!(coincident.is_err());
989 }
990
991 #[test]
992 fn camera_accessors_round_trip_fields() {
993 let camera = ortho_camera();
994 assert!((camera.eye().z().get::<millimeter>() - 10.0).abs() < 1e-12);
995 assert!(camera.target().coords_mm().0.abs() < 1e-12);
996 assert!((camera.up().components().1 - 1.0).abs() < 1e-12);
997 let ProjectionKind::Orthographic { half_height } = camera.projection().kind() else {
998 panic!("constructed an orthographic camera");
999 };
1000 assert!((half_height.get::<millimeter>() - 25.0).abs() < 1e-12);
1001 }
1002
1003 #[test]
1004 fn camera_ron_round_trip() {
1005 let camera = ortho_camera();
1006 let Ok(text) = ron::to_string(&camera) else {
1007 panic!("serialize camera");
1008 };
1009 let Ok(back) = ron::from_str::<Camera3>(&text) else {
1010 panic!("deserialize camera");
1011 };
1012 assert_eq!(camera, back);
1013 }
1014
1015 #[test]
1016 fn camera_deserialize_rejects_degenerate() {
1017 let Ok(text) = ron::to_string(&ortho_camera()) else {
1018 panic!("serialize camera");
1019 };
1020 assert!(ron::from_str::<Camera3>(&text).is_ok());
1021 let collapsed = text.replace("z:10.0", "z:0.0");
1022 assert!(ron::from_str::<Camera3>(&collapsed).is_err());
1023 }
1024
1025 #[test]
1026 fn orbit_state_identity_is_unrotated() {
1027 let orbit = OrbitState::identity();
1028 assert!((orbit.rotation().angle().get::<radian>()).abs() < 1e-12);
1029 assert!((orbit.zoom().value() - 1.0).abs() < 1e-12);
1030 assert!(orbit.pan_target().coords_mm().0.abs() < 1e-12);
1031 }
1032
1033 #[test]
1034 fn orbit_state_rotated_accumulates_angle() {
1035 let quarter = AxisAngle::new(
1036 UnitVec3::z_axis(),
1037 Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
1038 );
1039 let once = OrbitState::identity().rotated(quarter);
1040 let twice = once.rotated(quarter);
1041 assert!(
1042 (once.rotation().angle().get::<radian>() - core::f64::consts::FRAC_PI_2).abs() < 1e-12
1043 );
1044 assert!((twice.rotation().angle().get::<radian>() - core::f64::consts::PI).abs() < 1e-12);
1045 assert!((twice.rotation().axis().components().2 - 1.0).abs() < 1e-12);
1046 }
1047
1048 #[test]
1049 fn orbit_state_ron_round_trip() {
1050 let Ok(zoom) = ZoomFactor::new(2.5) else {
1051 panic!("positive zoom");
1052 };
1053 let Ok(orbit) = OrbitState::new(
1054 AxisAngle::new(
1055 UnitVec3::z_axis(),
1056 Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
1057 ),
1058 zoom,
1059 Point3::from_mm(1.0, 2.0, 3.0),
1060 ) else {
1061 panic!("finite pan target");
1062 };
1063 let Ok(text) = ron::to_string(&orbit) else {
1064 panic!("serialize orbit");
1065 };
1066 let Ok(back) = ron::from_str::<OrbitState>(&text) else {
1067 panic!("deserialize orbit");
1068 };
1069 assert_eq!(orbit, back);
1070 }
1071
1072 #[test]
1073 fn standard_view_labels_are_distinct() {
1074 let views = [
1075 StandardView::Front,
1076 StandardView::Back,
1077 StandardView::Left,
1078 StandardView::Right,
1079 StandardView::Top,
1080 StandardView::Bottom,
1081 StandardView::Isometric,
1082 StandardView::NormalTo,
1083 ];
1084 let labels: std::collections::BTreeSet<&str> = views.iter().map(|v| v.label()).collect();
1085 assert_eq!(labels.len(), views.len());
1086 }
1087
1088 #[test]
1089 fn step_entity_id_display() {
1090 let Ok(id) = StepEntityId::new(123) else {
1091 panic!("123 is a positive instance name");
1092 };
1093 assert_eq!(format!("{id}"), "#123");
1094 let Ok(seven) = StepEntityId::new(7) else {
1095 panic!("7 is a positive instance name");
1096 };
1097 assert_eq!(seven.value(), 7);
1098 }
1099
1100 #[test]
1101 fn step_entity_id_rejects_zero() {
1102 assert!(StepEntityId::new(0).is_err());
1103 assert!(StepEntityId::new(1).is_ok());
1104 assert!(ron::from_str::<StepEntityId>("0").is_err());
1105 assert!(ron::from_str::<StepEntityId>("123").is_ok());
1106 }
1107
1108 #[test]
1109 fn step_schema_labels() {
1110 assert_eq!(StepSchema::Ap214.label(), "AP214");
1111 assert_eq!(StepSchema::Ap242E2.label(), "AP242E2");
1112 }
1113
1114 #[test]
1115 fn step_entity_kind_labels_and_round_trip() {
1116 assert_eq!(
1117 StepEntityKind::CylindricalSurface.label(),
1118 "a cylindrical surface"
1119 );
1120 assert_eq!(StepEntityKind::ConicCurve.label(), "a conic curve");
1121 let Ok(text) = ron::to_string(&StepEntityKind::ToroidalSurface) else {
1122 panic!("serialize step entity kind");
1123 };
1124 let Ok(back) = ron::from_str::<StepEntityKind>(&text) else {
1125 panic!("deserialize step entity kind");
1126 };
1127 assert_eq!(StepEntityKind::ToroidalSurface, back);
1128 }
1129
1130 #[test]
1131 fn step_file_header_ron_round_trip() {
1132 let header = StepFileHeader {
1133 schema: StepSchema::Ap214,
1134 originating_system: StepOriginatingSystem::new("bone 0.0.0"),
1135 organization: StepOrganization::new("witchcraft.systems"),
1136 file_name: StepFileName::new("limpet.step"),
1137 };
1138 let Ok(text) = ron::to_string(&header) else {
1139 panic!("serialize step header");
1140 };
1141 let Ok(back) = ron::from_str::<StepFileHeader>(&text) else {
1142 panic!("deserialize step header");
1143 };
1144 assert_eq!(header, back);
1145 assert_eq!(header.file_name.as_str(), "limpet.step");
1146 }
1147
1148 #[test]
1149 fn display_mode_default_and_labels() {
1150 assert_eq!(DisplayMode::DEFAULT, DisplayMode::ShadedWithEdges);
1151 assert_eq!(DisplayMode::HiddenLineGray.label(), "hidden_line_gray");
1152 assert_eq!(ShadingModel::Gouraud.label(), "gouraud");
1153 }
1154
1155 #[test]
1156 fn camera_rejects_non_finite_eye() {
1157 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
1158 panic!("10 mm half-height is positive");
1159 };
1160 let nan = Camera3::new(
1161 Point3::from_mm(f64::NAN, 0.0, 0.0),
1162 Point3::origin(),
1163 UnitVec3::y_axis(),
1164 projection,
1165 );
1166 assert!(nan.is_err());
1167 let infinite = Camera3::new(
1168 Point3::from_mm(f64::INFINITY, 0.0, 0.0),
1169 Point3::origin(),
1170 UnitVec3::y_axis(),
1171 projection,
1172 );
1173 assert!(infinite.is_err());
1174 }
1175
1176 #[test]
1177 fn camera_rejects_up_parallel_to_view() {
1178 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
1179 panic!("10 mm half-height is positive");
1180 };
1181 let parallel = Camera3::new(
1182 Point3::from_mm(0.0, 0.0, 10.0),
1183 Point3::origin(),
1184 UnitVec3::z_axis(),
1185 projection,
1186 );
1187 assert!(parallel.is_err());
1188 }
1189
1190 #[test]
1191 fn projection_constructors_validate() {
1192 let mm = |v| Length::new::<millimeter>(v);
1193 assert!(Projection::orthographic(mm(25.0)).is_ok());
1194 assert!(Projection::orthographic(mm(0.0)).is_err());
1195 assert!(Projection::orthographic(mm(-1.0)).is_err());
1196 assert!(Projection::orthographic(Length::new::<millimeter>(f64::NAN)).is_err());
1197 assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(1.0), mm(1000.0)).is_ok());
1198 assert!(Projection::perspective(Angle::new::<degree>(400.0), mm(1.0), mm(1000.0)).is_err());
1199 assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(10.0), mm(1.0)).is_err());
1200 assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(0.0), mm(1000.0)).is_err());
1201 }
1202
1203 #[test]
1204 fn projection_deserialize_rejects_degenerate() {
1205 let Ok(valid) = Projection::orthographic(Length::new::<millimeter>(25.0)) else {
1206 panic!("25 mm half-height is positive");
1207 };
1208 let Ok(text) = ron::to_string(&valid) else {
1209 panic!("serialize projection");
1210 };
1211 assert!(ron::from_str::<Projection>(&text).is_ok());
1212 let degenerate = text.replace("0.025", "-0.025");
1213 assert!(ron::from_str::<Projection>(°enerate).is_err());
1214 }
1215
1216 #[test]
1217 fn zoom_factor_rejects_non_positive() {
1218 assert!(ZoomFactor::new(2.5).is_ok());
1219 assert!(ZoomFactor::new(0.0).is_err());
1220 assert!(ZoomFactor::new(-1.0).is_err());
1221 assert!(ZoomFactor::new(f64::NAN).is_err());
1222 assert!(ZoomFactor::new(f64::INFINITY).is_err());
1223 }
1224
1225 #[test]
1226 fn cubic_easing_constrains_x_controls_to_the_unit_interval() {
1227 assert!(CubicEasing::new(0.2, 0.0, 0.0, 1.0).is_ok());
1228 assert!(CubicEasing::new(0.0, -2.0, 1.0, 3.0).is_ok());
1229 assert!(CubicEasing::new(1.5, 0.0, 0.5, 1.0).is_err());
1230 assert!(CubicEasing::new(0.2, 0.0, -0.5, 1.0).is_err());
1231 assert!(CubicEasing::new(f64::NAN, 0.0, 0.5, 1.0).is_err());
1232 assert!(CubicEasing::new(0.2, f64::INFINITY, 0.5, 1.0).is_err());
1233 }
1234
1235 #[test]
1236 fn zoom_factor_deserialize_rejects_non_positive() {
1237 assert!(ron::from_str::<ZoomFactor>("2.5").is_ok());
1238 assert!(ron::from_str::<ZoomFactor>("0.0").is_err());
1239 assert!(ron::from_str::<ZoomFactor>("-1.0").is_err());
1240 }
1241
1242 #[test]
1243 fn positive_length_rejects_non_positive_and_non_finite() {
1244 assert!(PositiveLength::new(Length::new::<meter>(0.5)).is_ok());
1245 assert!(PositiveLength::new(Length::new::<meter>(f64::MIN_POSITIVE)).is_ok());
1246 assert!(PositiveLength::new(Length::new::<meter>(0.0)).is_err());
1247 assert!(PositiveLength::new(Length::new::<meter>(-1.0)).is_err());
1248 assert!(PositiveLength::new(Length::new::<meter>(f64::INFINITY)).is_err());
1249 assert!(PositiveLength::new(Length::new::<meter>(f64::NEG_INFINITY)).is_err());
1250 assert!(PositiveLength::new(Length::new::<meter>(f64::NAN)).is_err());
1251 }
1252
1253 #[test]
1254 fn positive_length_round_trips_through_meters() {
1255 let Ok(length) = PositiveLength::new(Length::new::<meter>(0.0123)) else {
1256 panic!("0.0123 m is positive");
1257 };
1258 assert!((f64::from(length) - 0.0123).abs() < f64::EPSILON);
1259 assert!((length.get().get::<meter>() - 0.0123).abs() < f64::EPSILON);
1260 }
1261
1262 #[test]
1263 fn positive_length_deserialize_rejects_non_positive() {
1264 assert!(ron::from_str::<PositiveLength>("0.5").is_ok());
1265 assert!(ron::from_str::<PositiveLength>("0.0").is_err());
1266 assert!(ron::from_str::<PositiveLength>("-1.0").is_err());
1267 assert!(ron::from_str::<PositiveLength>("inf").is_err());
1268 assert!(ron::from_str::<PositiveLength>("NaN").is_err());
1269 }
1270
1271 #[test]
1272 fn orbit_state_deserialize_normalizes_non_unit_quaternion() {
1273 let nonunit = "(qw:2.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
1274 let Ok(orbit) = ron::from_str::<OrbitState>(nonunit) else {
1275 panic!("non-unit quaternion should normalize, not fail");
1276 };
1277 let Ok(text) = ron::to_string(&orbit) else {
1278 panic!("serialize orbit");
1279 };
1280 assert!(
1281 text.contains("qw:1"),
1282 "quaternion should re-normalize to unit, got {text}"
1283 );
1284 }
1285
1286 #[test]
1287 fn orbit_state_deserialize_rejects_zero_quaternion() {
1288 let zero = "(qw:0.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
1289 assert!(ron::from_str::<OrbitState>(zero).is_err());
1290 }
1291
1292 #[test]
1293 fn orbit_state_rejects_non_finite_pan_target() {
1294 let Ok(zoom) = ZoomFactor::new(1.0) else {
1295 panic!("positive zoom");
1296 };
1297 let rotation = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.0));
1298 assert!(OrbitState::new(rotation, zoom, Point3::from_mm(f64::NAN, 0.0, 0.0)).is_err());
1299 assert!(OrbitState::new(rotation, zoom, Point3::from_mm(f64::INFINITY, 0.0, 0.0)).is_err());
1300 assert!(OrbitState::new(rotation, zoom, Point3::origin()).is_ok());
1301 }
1302
1303 #[test]
1304 fn orbit_state_deserialize_rejects_non_finite_pan_target() {
1305 let finite = "(qw:1.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
1306 assert!(ron::from_str::<OrbitState>(finite).is_ok());
1307 let non_finite = "(qw:1.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:inf))";
1308 assert!(ron::from_str::<OrbitState>(non_finite).is_err());
1309 }
1310
1311 #[test]
1312 fn orbit_state_deserialize_rejects_non_finite_rotation() {
1313 let inf = "(qw:inf,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
1314 assert!(ron::from_str::<OrbitState>(inf).is_err());
1315 let nan = "(qw:1.0,qx:NaN,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
1316 assert!(ron::from_str::<OrbitState>(nan).is_err());
1317 }
1318
1319 #[test]
1320 fn orbit_state_new_rejects_non_finite_rotation() {
1321 let Ok(zoom) = ZoomFactor::new(1.0) else {
1322 panic!("positive zoom");
1323 };
1324 let bad = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(f64::INFINITY));
1325 assert!(OrbitState::new(bad, zoom, Point3::origin()).is_err());
1326 let good = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5));
1327 assert!(OrbitState::new(good, zoom, Point3::origin()).is_ok());
1328 }
1329
1330 #[test]
1331 fn unit_vec3_rotated_quarter_turn_about_z() {
1332 let q = AxisAngle::new(
1333 UnitVec3::z_axis(),
1334 Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
1335 );
1336 let (cx, cy, cz) = UnitVec3::x_axis().rotated(q).components();
1337 assert!(
1338 cx.abs() < 1e-12 && (cy - 1.0).abs() < 1e-12 && cz.abs() < 1e-12,
1339 "x rotated +90 deg about z is y: ({cx},{cy},{cz})"
1340 );
1341 }
1342
1343 #[test]
1344 fn point3_rotated_about_pivot_quarter_turn() {
1345 let pivot = Point3::from_mm(1.0, 0.0, 0.0);
1346 let q = AxisAngle::new(
1347 UnitVec3::z_axis(),
1348 Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
1349 );
1350 let (x, y, z) = Point3::from_mm(2.0, 0.0, 0.0)
1351 .rotated_about(pivot, q)
1352 .coords_mm();
1353 assert!(
1354 (x - 1.0).abs() < 1e-12 && (y - 1.0).abs() < 1e-12 && z.abs() < 1e-12,
1355 "(2,0,0) about (1,0,0) +90 deg z is (1,1,0): ({x},{y},{z})"
1356 );
1357 }
1358
1359 #[test]
1360 fn camera_perspective_ron_round_trip() {
1361 let Ok(persp) = Projection::perspective(
1362 Angle::new::<degree>(60.0),
1363 Length::new::<millimeter>(1.0),
1364 Length::new::<millimeter>(1000.0),
1365 ) else {
1366 panic!("valid perspective");
1367 };
1368 let Ok(camera) = Camera3::new(
1369 Point3::from_mm(0.0, 0.0, 10.0),
1370 Point3::origin(),
1371 UnitVec3::y_axis(),
1372 persp,
1373 ) else {
1374 panic!("eye and target are 10 mm apart");
1375 };
1376 let Ok(text) = ron::to_string(&camera) else {
1377 panic!("serialize camera");
1378 };
1379 let Ok(back) = ron::from_str::<Camera3>(&text) else {
1380 panic!("deserialize camera");
1381 };
1382 assert_eq!(camera, back);
1383 }
1384
1385 fn assert_ron_round_trip<T>(value: &T)
1386 where
1387 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + core::fmt::Debug,
1388 {
1389 let Ok(text) = ron::to_string(value) else {
1390 panic!("serialize {value:?}");
1391 };
1392 let Ok(back) = ron::from_str::<T>(&text) else {
1393 panic!("deserialize {text}");
1394 };
1395 assert_eq!(value, &back);
1396 }
1397
1398 #[test]
1399 fn discrete_types_ron_round_trip() {
1400 [
1401 DisplayMode::Wireframe,
1402 DisplayMode::HiddenLineRemoved,
1403 DisplayMode::HiddenLineGray,
1404 DisplayMode::ShadedWithEdges,
1405 DisplayMode::ShadedNoEdges,
1406 ]
1407 .iter()
1408 .for_each(assert_ron_round_trip);
1409 [
1410 ShadingModel::Flat,
1411 ShadingModel::Gouraud,
1412 ShadingModel::Phong,
1413 ]
1414 .iter()
1415 .for_each(assert_ron_round_trip);
1416 [
1417 StandardView::Front,
1418 StandardView::Back,
1419 StandardView::Left,
1420 StandardView::Right,
1421 StandardView::Top,
1422 StandardView::Bottom,
1423 StandardView::Isometric,
1424 StandardView::NormalTo,
1425 ]
1426 .iter()
1427 .for_each(assert_ron_round_trip);
1428 [StepSchema::Ap214, StepSchema::Ap242E2]
1429 .iter()
1430 .for_each(assert_ron_round_trip);
1431 let Ok(entity) = StepEntityId::new(123) else {
1432 panic!("123 is a positive instance name");
1433 };
1434 assert_ron_round_trip(&entity);
1435 }
1436
1437 fn sample_face_fingerprint() -> FaceFingerprint {
1438 let plane = Plane3::new_unchecked(
1439 Point3::from_mm(1.0, 2.0, 3.0),
1440 UnitVec3::x_axis(),
1441 UnitVec3::y_axis(),
1442 );
1443 FaceFingerprint {
1444 plane,
1445 centroid: Point3::from_mm(4.0, 5.0, 6.0),
1446 }
1447 }
1448
1449 fn sample_face_ref() -> EntityRef {
1450 EntityRef::Face(
1451 FaceLabel {
1452 feature: FeatureId::default(),
1453 role: FaceRole::EndCap,
1454 },
1455 sample_face_fingerprint(),
1456 )
1457 }
1458
1459 #[test]
1460 fn entity_ref_round_trips_all_kinds() {
1461 assert_ron_round_trip(&sample_face_ref());
1462 assert_ron_round_trip(&EntityRef::Edge(
1463 EdgeLabel {
1464 feature: FeatureId::default(),
1465 role: EdgeRole::EndCapEdge {
1466 from: SketchEntityId::default(),
1467 },
1468 },
1469 EdgeFingerprint {
1470 sample: Point3::from_mm(1.0, 0.0, 0.0),
1471 direction: UnitVec3::z_axis(),
1472 },
1473 ));
1474 assert_ron_round_trip(&EntityRef::Vertex(
1475 VertexLabel {
1476 feature: FeatureId::default(),
1477 role: VertexRole::EndCapVertex {
1478 from: SketchEntityId::default(),
1479 side: SideKind::Corner,
1480 },
1481 },
1482 VertexFingerprint {
1483 point: Point3::origin(),
1484 },
1485 ));
1486 }
1487
1488 #[test]
1489 fn face_ref_round_trips_and_projects_to_an_entity_ref() {
1490 let face = FaceRef::new(
1491 FaceLabel {
1492 feature: FeatureId::default(),
1493 role: FaceRole::EndCap,
1494 },
1495 sample_face_fingerprint(),
1496 );
1497 assert_ron_round_trip(&face);
1498 assert_eq!(face.entity_ref(), sample_face_ref());
1499 }
1500
1501 #[test]
1502 fn entity_ref_fingerprint_projects_its_kind() {
1503 assert_eq!(
1504 sample_face_ref().fingerprint(),
1505 EntityFingerprint::Face(sample_face_fingerprint())
1506 );
1507 }
1508
1509 #[test]
1510 fn entity_ref_display_shows_the_label() {
1511 let expected = format!("face[{:?}]:{}", FeatureId::default(), FaceRole::EndCap);
1512 assert_eq!(format!("{}", sample_face_ref()), expected);
1513 }
1514
1515 #[test]
1516 fn fingerprint_rejects_unknown_field() {
1517 let Ok(text) = ron::to_string(&sample_face_fingerprint()) else {
1518 panic!("serialize face fingerprint");
1519 };
1520 assert!(ron::from_str::<FaceFingerprint>(&text).is_ok());
1521 let mut munged = text.clone();
1522 munged.insert_str(1, "bogus:42,");
1523 assert!(ron::from_str::<FaceFingerprint>(&munged).is_err());
1524 }
1525
1526 #[test]
1527 fn match_score_rejects_out_of_range_and_non_finite() {
1528 assert!(MatchScore::new(0.0).is_ok());
1529 assert!(MatchScore::new(1.0).is_ok());
1530 assert!(MatchScore::new(0.75).is_ok());
1531 assert!(MatchScore::new(-0.1).is_err());
1532 assert!(MatchScore::new(1.5).is_err());
1533 assert!(MatchScore::new(f64::NAN).is_err());
1534 assert!(MatchScore::new(f64::INFINITY).is_err());
1535 assert!(ron::from_str::<MatchScore>("0.5").is_ok());
1536 assert!(ron::from_str::<MatchScore>("1.5").is_err());
1537 assert!(ron::from_str::<MatchScore>("-0.1").is_err());
1538 assert!(ron::from_str::<MatchScore>("inf").is_err());
1539 assert!(ron::from_str::<MatchScore>("NaN").is_err());
1540 let Ok(score) = MatchScore::new(0.5) else {
1541 panic!("0.5 is in range");
1542 };
1543 assert!((score.value() - 0.5).abs() < f64::EPSILON);
1544 }
1545
1546 #[test]
1547 fn resolution_id_and_dangling() {
1548 let resolved: Resolution<FeatureId> = Resolution::Resolved(FeatureId::default());
1549 assert_eq!(resolved.id(), Some(FeatureId::default()));
1550 assert!(!resolved.is_dangling());
1551 assert_ron_round_trip(&resolved);
1552
1553 let Ok(score) = MatchScore::new(0.9) else {
1554 panic!("0.9 is in range");
1555 };
1556 let repaired: Resolution<FeatureId> = Resolution::Repaired {
1557 id: FeatureId::default(),
1558 score,
1559 };
1560 assert_eq!(repaired.id(), Some(FeatureId::default()));
1561
1562 let dangling: Resolution<FeatureId> = Resolution::Dangling {
1563 last_known: sample_face_ref(),
1564 };
1565 assert_eq!(dangling.id(), None);
1566 assert!(dangling.is_dangling());
1567 assert_ron_round_trip(&dangling);
1568 assert_ron_round_trip(&repaired);
1569 }
1570
1571 #[test]
1572 fn resolution_repaired_rejects_unknown_field() {
1573 let Ok(score) = MatchScore::new(0.9) else {
1574 panic!("0.9 is in range");
1575 };
1576 let repaired: Resolution<FeatureId> = Resolution::Repaired {
1577 id: FeatureId::default(),
1578 score,
1579 };
1580 let Ok(text) = ron::to_string(&repaired) else {
1581 panic!("serialize repaired resolution");
1582 };
1583 assert!(ron::from_str::<Resolution<FeatureId>>(&text).is_ok());
1584 let Some(idx) = text.find("Repaired(") else {
1585 panic!("expected Repaired variant in {text}");
1586 };
1587 let mut munged = text.clone();
1588 munged.insert_str(idx + "Repaired(".len(), "bogus:42,");
1589 assert!(ron::from_str::<Resolution<FeatureId>>(&munged).is_err());
1590 }
1591
1592 #[test]
1593 fn feature_generation_is_a_monotone_mark() {
1594 assert_eq!(FeatureGeneration::START, FeatureGeneration::default());
1595 assert_eq!(FeatureGeneration::START.value(), 0);
1596 let mark = FeatureGeneration::new(7);
1597 assert_eq!(mark.succ(), FeatureGeneration::new(8));
1598 assert!(mark < mark.succ());
1599 assert_eq!(mark.raised_to(FeatureGeneration::new(3)), mark);
1600 assert_eq!(
1601 mark.raised_to(FeatureGeneration::new(12)),
1602 FeatureGeneration::new(12)
1603 );
1604 let Ok(text) = ron::to_string(&mark) else {
1605 panic!("serialize feature generation");
1606 };
1607 assert_eq!(text, "7");
1608 assert_ron_round_trip(&mark);
1609 assert_eq!(
1610 FeatureGeneration::new(u64::MAX).succ(),
1611 FeatureGeneration::new(u64::MAX)
1612 );
1613 }
1614
1615 #[test]
1616 fn rollback_marker_defaults_to_end() {
1617 assert_eq!(RollbackMarker::default(), RollbackMarker::AtEnd);
1618 assert_eq!(RollbackMarker::AtEnd.feature(), None);
1619 assert_eq!(
1620 RollbackMarker::Above(FeatureId::default()).feature(),
1621 Some(FeatureId::default())
1622 );
1623 assert!(RollbackMarker::AtEnd < RollbackMarker::Above(FeatureId::default()));
1624 assert_ron_round_trip(&RollbackMarker::AtEnd);
1625 assert_ron_round_trip(&RollbackMarker::Above(FeatureId::default()));
1626 }
1627
1628 #[test]
1629 fn suppression_state_toggles() {
1630 assert_eq!(SuppressionState::default(), SuppressionState::Active);
1631 assert!(!SuppressionState::Active.is_suppressed());
1632 assert!(SuppressionState::Suppressed.is_suppressed());
1633 assert_eq!(
1634 SuppressionState::Active.toggled(),
1635 SuppressionState::Suppressed
1636 );
1637 assert_eq!(
1638 SuppressionState::Suppressed.toggled(),
1639 SuppressionState::Active
1640 );
1641 assert!(SuppressionState::Active < SuppressionState::Suppressed);
1642 assert_ron_round_trip(&SuppressionState::Active);
1643 assert_ron_round_trip(&SuppressionState::Suppressed);
1644 }
1645
1646 #[test]
1647 fn rebuild_status_honors_warning_versus_error() {
1648 let dangling = RebuildStatus::Error(RebuildError::DanglingReference(sample_face_ref()));
1649 assert!(dangling.is_error());
1650 assert!(!dangling.builds_geometry());
1651
1652 let non_planar = RebuildStatus::Error(RebuildError::NonPlanarSketchTarget);
1653 assert!(!non_planar.builds_geometry());
1654
1655 let build_failed = RebuildStatus::Error(RebuildError::Build(BuildFailure::Kernel));
1656 assert!(build_failed.is_error());
1657 assert!(!build_failed.builds_geometry());
1658
1659 let Ok(score) = MatchScore::new(0.8) else {
1660 panic!("0.8 is in range");
1661 };
1662 let repaired = RebuildStatus::Warning(RebuildWarning::RepairedReference {
1663 reference: sample_face_ref(),
1664 score,
1665 });
1666 assert!(!repaired.is_error());
1667 assert!(repaired.builds_geometry());
1668
1669 assert!(RebuildStatus::UpToDate.builds_geometry());
1670 assert!(RebuildStatus::NeedsRebuild.builds_geometry());
1671
1672 assert_eq!(dangling.worse_of(repaired), dangling);
1673 assert_eq!(repaired.worse_of(dangling), dangling);
1674 assert_eq!(repaired.worse_of(RebuildStatus::NeedsRebuild), repaired);
1675 assert_eq!(
1676 RebuildStatus::NeedsRebuild.worse_of(RebuildStatus::UpToDate),
1677 RebuildStatus::NeedsRebuild
1678 );
1679 assert_eq!(
1680 RebuildStatus::UpToDate.worse_of(RebuildStatus::UpToDate),
1681 RebuildStatus::UpToDate
1682 );
1683
1684 assert_ron_round_trip(&dangling);
1685 assert_ron_round_trip(&non_planar);
1686 assert_ron_round_trip(&build_failed);
1687 assert_ron_round_trip(&RebuildStatus::Error(RebuildError::Build(
1688 BuildFailure::UnsolvedSketch,
1689 )));
1690 assert_ron_round_trip(&repaired);
1691 assert_ron_round_trip(&RebuildStatus::UpToDate);
1692 assert_ron_round_trip(&RebuildStatus::NeedsRebuild);
1693 }
1694}