diff --git a/crates/bone-types/src/history.rs b/crates/bone-types/src/history.rs index d740621..3b864bb 100644 --- a/crates/bone-types/src/history.rs +++ b/crates/bone-types/src/history.rs @@ -103,6 +103,7 @@ impl core::fmt::Display for SuppressionState { pub enum BuildFailure { UnsolvedSketch, Kernel, + CutWithoutBody, } impl core::fmt::Display for BuildFailure { @@ -110,6 +111,7 @@ impl core::fmt::Display for BuildFailure { match self { Self::UnsolvedSketch => f.write_str("sketch did not solve"), Self::Kernel => f.write_str("solid build failed"), + Self::CutWithoutBody => f.write_str("cut has no body to remove material from"), } } } diff --git a/crates/bone-types/src/icon.rs b/crates/bone-types/src/icon.rs index 88286a7..f7b5ddb 100644 --- a/crates/bone-types/src/icon.rs +++ b/crates/bone-types/src/icon.rs @@ -63,6 +63,11 @@ icon_ids! { ViewSettings, Check, Cross, + RevolvedBossBase, + RevolvedCut, + Centerline, + Spline, + Ellipse, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] diff --git a/crates/bone-types/src/label.rs b/crates/bone-types/src/label.rs index bf6b177..d40ef91 100644 --- a/crates/bone-types/src/label.rs +++ b/crates/bone-types/src/label.rs @@ -46,6 +46,60 @@ impl core::fmt::Display for ImportOrdinal { } } +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct BodyOrdinal(u16); + +impl BodyOrdinal { + #[must_use] + pub const fn new(value: u16) -> Self { + Self(value) + } + + #[must_use] + pub const fn origin() -> Self { + Self(0) + } + + #[must_use] + pub const fn value(self) -> u16 { + self.0 + } + + #[must_use] + pub const fn is_origin(&self) -> bool { + self.0 == 0 + } +} + +impl core::fmt::Display for BodyOrdinal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "body#{}", self.0) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct CutKey([u64; 3]); + +impl CutKey { + #[must_use] + pub const fn new(bits: [u64; 3]) -> Self { + Self(bits) + } + + #[must_use] + pub const fn bits(self) -> [u64; 3] { + self.0 + } +} + +impl core::fmt::Display for CutKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:x}:{:x}:{:x}", self.0[0], self.0[1], self.0[2]) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum SideKind { Corner, @@ -106,17 +160,30 @@ pub enum EdgeRole { EndCapEdge { from: SketchEntityId, }, + Intersection { + a: FaceLabel, + b: FaceLabel, + }, Imported { ordinal: ImportOrdinal, }, } +impl EdgeRole { + #[must_use] + pub fn intersection(a: FaceLabel, b: FaceLabel) -> Self { + let (a, b) = if a <= b { (a, b) } else { (b, a) }; + Self::Intersection { a, b } + } +} + impl core::fmt::Display for EdgeRole { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::StartCapEdge { from } => write!(f, "start_cap_edge(from={from:?})"), Self::SideEdge { from, side } => write!(f, "side_edge({side}, from={from:?})"), Self::EndCapEdge { from } => write!(f, "end_cap_edge(from={from:?})"), + Self::Intersection { a, b } => write!(f, "intersection({a}, {b})"), Self::Imported { ordinal } => write!(f, "imported({ordinal})"), } } @@ -133,6 +200,15 @@ pub enum VertexRole { from: SketchEntityId, side: SideKind, }, + Seam { + from: SketchEntityId, + }, + Intersection { + faces: [FaceLabel; 3], + }, + Cut { + key: CutKey, + }, Imported { ordinal: ImportOrdinal, }, @@ -147,6 +223,11 @@ impl core::fmt::Display for VertexRole { Self::EndCapVertex { from, side } => { write!(f, "end_cap_vertex({side}, from={from:?})") } + Self::Seam { from } => write!(f, "seam(from={from:?})"), + Self::Intersection { faces: [a, b, c] } => { + write!(f, "intersection({a}, {b}, {c})") + } + Self::Cut { key } => write!(f, "cut({key})"), Self::Imported { ordinal } => write!(f, "imported({ordinal})"), } } diff --git a/crates/bone-types/src/lib.rs b/crates/bone-types/src/lib.rs index 9180514..295f74c 100644 --- a/crates/bone-types/src/lib.rs +++ b/crates/bone-types/src/lib.rs @@ -31,12 +31,12 @@ pub use history::{ }; pub use icon::{IconId, IconTile}; pub use label::{ - EdgeLabel, EdgeRole, FaceLabel, FaceRole, ImportOrdinal, LoopIndex, SideKind, VertexLabel, - VertexRole, + BodyOrdinal, CutKey, EdgeLabel, EdgeRole, FaceLabel, FaceRole, ImportOrdinal, LoopIndex, + SideKind, VertexLabel, VertexRole, }; pub use reference::{ - EdgeFingerprint, EntityFingerprint, EntityRef, FaceFingerprint, FaceRef, MatchScore, - Resolution, VertexFingerprint, + DatumTarget, EdgeFingerprint, EdgeRef, EntityFingerprint, EntityRef, FaceFingerprint, FaceRef, + MatchScore, Resolution, VertexFingerprint, VertexRef, }; pub use schema::{SchemaHeader, SchemaVersion}; pub use solver::{ @@ -44,8 +44,8 @@ pub use solver::{ ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed, }; pub use space::{ - Aabb3, AxisAngle, OrientedBox3, Plane3, Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3, - Vec2, Vec3, + Aabb3, Axis3, AxisAngle, CoordinateSystem3, OrientedBox3, Plane3, Point2, Point3, + SketchPlaneBasis, UnitVec2, UnitVec3, Vec2, Vec3, }; pub use step::{ StepEntityId, StepEntityKind, StepFileHeader, StepFileName, StepOrganization, @@ -122,6 +122,8 @@ slotmap::new_key_type! { pub struct SketchDimensionId; pub struct SketchParameterId; pub struct ExtrudeId; + pub struct RevolveId; + pub struct DatumId; pub struct BodyId; pub struct BrepShellId; pub struct BrepFaceId; @@ -145,7 +147,14 @@ macro_rules! impl_as_u64 { }; } -impl_as_u64!(SketchId, ExtrudeId, BrepEdgeId, BrepVertexId); +impl_as_u64!( + SketchId, + ExtrudeId, + RevolveId, + DatumId, + BrepEdgeId, + BrepVertexId +); #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct BrepSlot(u32); @@ -368,6 +377,11 @@ impl GeometryGeneration { Self(folded) } + #[must_use] + pub const fn from_bits(bits: u64) -> Self { + Self(bits) + } + #[must_use] pub const fn value(self) -> u64 { self.0 @@ -387,20 +401,38 @@ mod tests { Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, BuildFailure, Camera3, ChordHeightTolerance, CreaseAngle, CubicEasing, DegreesOfFreedom, DisplayMode, DocumentId, EdgeFingerprint, EdgeId, EdgeLabel, - EdgeRole, EntityFingerprint, EntityRef, ExtrudeId, FaceFingerprint, FaceId, FaceLabel, - FaceRef, FaceRole, FeatureGeneration, FeatureId, ImportOrdinal, Length, LoopId, LoopIndex, - MatchScore, MeshGeneration, NodeId, OrbitState, OrientedBox3, Parameter, Plane3, Point2, - Point3, PositiveLength, Projection, ProjectionKind, RebuildError, RebuildStatus, + EdgeRef, EdgeRole, EntityFingerprint, EntityRef, ExtrudeId, FaceFingerprint, FaceId, + FaceLabel, FaceRef, FaceRole, FeatureGeneration, FeatureId, ImportOrdinal, Length, LoopId, + LoopIndex, MatchScore, MeshGeneration, NodeId, OrbitState, OrientedBox3, Parameter, Plane3, + Point2, Point3, PositiveLength, Projection, ProjectionKind, RebuildError, RebuildStatus, RebuildWarning, Resolution, RollbackMarker, ShadingModel, ShellId, SideKind, SketchDimensionId, SketchEntityId, SketchId, SketchParameterId, SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, StepEntityKind, StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, StepSchema, SuppressionState, Tolerance, UnitVec2, UnitVec3, Vec2, Vec3, VertexFingerprint, VertexId, - VertexLabel, VertexRole, WireId, ZoomFactor, degree, millimeter, radian, + VertexLabel, VertexRef, VertexRole, WireId, ZoomFactor, degree, millimeter, radian, }; use slotmap::{Key, SlotMap}; use uom::si::length::meter; + #[test] + fn orthonormal_basis_is_perpendicular_to_the_normal_and_itself() { + let s = 14.0_f64.sqrt(); + let oblique = UnitVec3::new_unchecked(1.0 / s, 2.0 / s, 3.0 / s); + let normals = [ + UnitVec3::x_axis(), + UnitVec3::y_axis(), + UnitVec3::z_axis(), + oblique, + ]; + normals.into_iter().for_each(|n| { + let (u, v) = n.orthonormal_basis(); + assert!(n.dot(u).abs() < 1e-9, "u perpendicular to normal"); + assert!(n.dot(v).abs() < 1e-9, "v perpendicular to normal"); + assert!(u.dot(v).abs() < 1e-9, "u perpendicular to v"); + }); + } + fn ortho_camera() -> Camera3 { let Ok(projection) = Projection::orthographic(Length::new::(25.0)) else { panic!("25 mm half-height is positive"); @@ -416,6 +448,31 @@ mod tests { camera } + #[test] + fn plane3_project_inverts_point_at_local() { + let plane = Plane3::new_unchecked( + Point3::from_mm(1.0, 2.0, 3.0), + UnitVec3::y_axis(), + UnitVec3::z_axis(), + ); + let lifted = plane.point_at_local(4.0, -5.0, 0.0); + let (u, v) = plane.project(lifted).coords_mm(); + assert!( + (u - 4.0).abs() < 1e-9 && (v + 5.0).abs() < 1e-9, + "({u}, {v})" + ); + } + + #[test] + fn plane3_project_drops_the_normal_component() { + let plane = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()); + let (u, v) = plane.project(Point3::from_mm(3.0, -4.0, 7.0)).coords_mm(); + assert!( + (u - 3.0).abs() < 1e-9 && (v + 4.0).abs() < 1e-9, + "({u}, {v})" + ); + } + #[test] fn id_null_is_null() { assert!(FaceId::null().is_null()); @@ -1447,19 +1504,19 @@ mod tests { } fn sample_face_ref() -> EntityRef { - EntityRef::Face( + EntityRef::Face(FaceRef::new( FaceLabel { feature: FeatureId::default(), role: FaceRole::EndCap, }, sample_face_fingerprint(), - ) + )) } #[test] fn entity_ref_round_trips_all_kinds() { assert_ron_round_trip(&sample_face_ref()); - assert_ron_round_trip(&EntityRef::Edge( + assert_ron_round_trip(&EntityRef::Edge(EdgeRef::new( EdgeLabel { feature: FeatureId::default(), role: EdgeRole::EndCapEdge { @@ -1470,8 +1527,8 @@ mod tests { sample: Point3::from_mm(1.0, 0.0, 0.0), direction: UnitVec3::z_axis(), }, - )); - assert_ron_round_trip(&EntityRef::Vertex( + ))); + assert_ron_round_trip(&EntityRef::Vertex(VertexRef::new( VertexLabel { feature: FeatureId::default(), role: VertexRole::EndCapVertex { @@ -1482,7 +1539,7 @@ mod tests { VertexFingerprint { point: Point3::origin(), }, - )); + ))); } #[test] diff --git a/crates/bone-types/src/reference.rs b/crates/bone-types/src/reference.rs index 3c25101..fc7ef85 100644 --- a/crates/bone-types/src/reference.rs +++ b/crates/bone-types/src/reference.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::{EdgeLabel, FaceLabel, Plane3, Point3, Result, TypesError, UnitVec3, VertexLabel}; +use crate::{ + BodyOrdinal, EdgeLabel, FaceLabel, FeatureId, Plane3, Point3, Result, TypesError, UnitVec3, + VertexLabel, +}; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -59,18 +62,27 @@ impl core::fmt::Display for EntityFingerprint { #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum EntityRef { - Face(FaceLabel, FaceFingerprint), - Edge(EdgeLabel, EdgeFingerprint), - Vertex(VertexLabel, VertexFingerprint), + Face(FaceRef), + Edge(EdgeRef), + Vertex(VertexRef), } impl EntityRef { #[must_use] pub fn fingerprint(self) -> EntityFingerprint { match self { - Self::Face(_, g) => EntityFingerprint::Face(g), - Self::Edge(_, g) => EntityFingerprint::Edge(g), - Self::Vertex(_, g) => EntityFingerprint::Vertex(g), + Self::Face(reference) => EntityFingerprint::Face(reference.fingerprint), + Self::Edge(reference) => EntityFingerprint::Edge(reference.fingerprint), + Self::Vertex(reference) => EntityFingerprint::Vertex(reference.fingerprint), + } + } + + #[must_use] + pub const fn ordinal(self) -> BodyOrdinal { + match self { + Self::Face(reference) => reference.ordinal, + Self::Edge(reference) => reference.ordinal, + Self::Vertex(reference) => reference.ordinal, } } } @@ -78,9 +90,38 @@ impl EntityRef { impl core::fmt::Display for EntityRef { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Face(label, _) => label.fmt(f), - Self::Edge(label, _) => label.fmt(f), - Self::Vertex(label, _) => label.fmt(f), + Self::Face(reference) => reference.label.fmt(f), + Self::Edge(reference) => reference.label.fmt(f), + Self::Vertex(reference) => reference.label.fmt(f), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum DatumTarget { + Feature(FeatureId), + Entity(EntityRef), +} + +impl DatumTarget { + #[must_use] + pub fn source_feature(self) -> FeatureId { + match self { + Self::Feature(feature) => feature, + Self::Entity(entity) => match entity { + EntityRef::Face(reference) => reference.label.feature, + EntityRef::Edge(reference) => reference.label.feature, + EntityRef::Vertex(reference) => reference.label.feature, + }, + } + } +} + +impl core::fmt::Display for DatumTarget { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Feature(feature) => write!(f, "feature {feature:?}"), + Self::Entity(entity) => entity.fmt(f), } } } @@ -90,17 +131,28 @@ impl core::fmt::Display for EntityRef { pub struct FaceRef { pub label: FaceLabel, pub fingerprint: FaceFingerprint, + #[serde(default, skip_serializing_if = "BodyOrdinal::is_origin")] + pub ordinal: BodyOrdinal, } impl FaceRef { #[must_use] pub const fn new(label: FaceLabel, fingerprint: FaceFingerprint) -> Self { - Self { label, fingerprint } + Self { + label, + fingerprint, + ordinal: BodyOrdinal::origin(), + } + } + + #[must_use] + pub const fn at(self, ordinal: BodyOrdinal) -> Self { + Self { ordinal, ..self } } #[must_use] pub const fn entity_ref(self) -> EntityRef { - EntityRef::Face(self.label, self.fingerprint) + EntityRef::Face(self) } } @@ -110,6 +162,78 @@ impl core::fmt::Display for FaceRef { } } +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EdgeRef { + pub label: EdgeLabel, + pub fingerprint: EdgeFingerprint, + #[serde(default, skip_serializing_if = "BodyOrdinal::is_origin")] + pub ordinal: BodyOrdinal, +} + +impl EdgeRef { + #[must_use] + pub const fn new(label: EdgeLabel, fingerprint: EdgeFingerprint) -> Self { + Self { + label, + fingerprint, + ordinal: BodyOrdinal::origin(), + } + } + + #[must_use] + pub const fn at(self, ordinal: BodyOrdinal) -> Self { + Self { ordinal, ..self } + } + + #[must_use] + pub const fn entity_ref(self) -> EntityRef { + EntityRef::Edge(self) + } +} + +impl core::fmt::Display for EdgeRef { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.label.fmt(f) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VertexRef { + pub label: VertexLabel, + pub fingerprint: VertexFingerprint, + #[serde(default, skip_serializing_if = "BodyOrdinal::is_origin")] + pub ordinal: BodyOrdinal, +} + +impl VertexRef { + #[must_use] + pub const fn new(label: VertexLabel, fingerprint: VertexFingerprint) -> Self { + Self { + label, + fingerprint, + ordinal: BodyOrdinal::origin(), + } + } + + #[must_use] + pub const fn at(self, ordinal: BodyOrdinal) -> Self { + Self { ordinal, ..self } + } + + #[must_use] + pub const fn entity_ref(self) -> EntityRef { + EntityRef::Vertex(self) + } +} + +impl core::fmt::Display for VertexRef { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.label.fmt(f) + } +} + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] #[serde(try_from = "f64", into = "f64")] pub struct MatchScore(f64); diff --git a/crates/bone-types/src/schema.rs b/crates/bone-types/src/schema.rs index 02fe86e..2d45f74 100644 --- a/crates/bone-types/src/schema.rs +++ b/crates/bone-types/src/schema.rs @@ -30,7 +30,7 @@ pub struct SchemaHeader { impl SchemaHeader { pub const BONE_DOCUMENT_NAME: &'static str = "bone-document"; pub const BONE_DOCUMENT_MAJOR: u32 = 1; - pub const BONE_DOCUMENT_MINOR: u32 = 4; + pub const BONE_DOCUMENT_MINOR: u32 = 5; #[must_use] pub fn bone_document() -> Self { diff --git a/crates/bone-types/src/space.rs b/crates/bone-types/src/space.rs index cad1309..604d518 100644 --- a/crates/bone-types/src/space.rs +++ b/crates/bone-types/src/space.rs @@ -541,6 +541,23 @@ impl UnitVec3 { .map(Self) .ok_or(TypesError::ZeroLengthAxis) } + + #[must_use] + pub fn orthonormal_basis(self) -> (Self, Self) { + const TOL: Tolerance = Tolerance::new(1e-12); + let world = [Self::x_axis(), Self::y_axis(), Self::z_axis()] + .into_iter() + .min_by(|a, b| { + self.dot(*a) + .abs() + .partial_cmp(&self.dot(*b).abs()) + .unwrap_or(core::cmp::Ordering::Equal) + }) + .unwrap_or_else(Self::x_axis); + let u = self.cross(world, TOL).unwrap_or_else(|_| Self::x_axis()); + let v = self.cross(u, TOL).unwrap_or_else(|_| Self::y_axis()); + (u, v) + } } impl core::fmt::Display for UnitVec3 { @@ -605,6 +622,15 @@ impl SketchPlaneBasis { Ok(Self { origin, x, y }) } + #[must_use] + pub fn xy() -> Self { + Self { + origin: Point3::origin(), + x: UnitVec3::x_axis(), + y: UnitVec3::y_axis(), + } + } + #[must_use] pub fn origin(self) -> Point3 { self.origin @@ -970,6 +996,19 @@ impl Plane3 { oz + u_mm * xz + v_mm * yz + normal_offset_mm * nz, ) } + + #[must_use] + pub fn project(self, point: Point3) -> Point2 { + let (ox, oy, oz) = self.origin.coords_mm(); + let (px, py, pz) = point.coords_mm(); + let (dx, dy, dz) = (px - ox, py - oy, pz - oz); + let (xx, xy, xz) = self.x.components(); + let (yx, yy, yz) = self.y.components(); + Point2::from_mm( + dx.mul_add(xx, dy.mul_add(xy, dz * xz)), + dx.mul_add(yx, dy.mul_add(yy, dz * yz)), + ) + } } impl core::fmt::Display for Plane3 { @@ -1141,3 +1180,163 @@ impl core::fmt::Display for OrientedBox3 { ) } } + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(from = "Axis3Wire", into = "Axis3Wire")] +pub struct Axis3 { + origin: Point3, + direction: UnitVec3, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename = "Axis3", deny_unknown_fields)] +struct Axis3Wire { + origin: Point3, + direction: UnitVec3, +} + +impl From for Axis3Wire { + fn from(a: Axis3) -> Self { + Self { + origin: a.origin, + direction: a.direction, + } + } +} + +impl From for Axis3 { + fn from(w: Axis3Wire) -> Self { + Self { + origin: w.origin, + direction: w.direction, + } + } +} + +impl Axis3 { + #[must_use] + pub fn new(origin: Point3, direction: UnitVec3) -> Self { + Self { origin, direction } + } + + pub fn through(from: Point3, to: Point3, tolerance: Tolerance) -> Result { + Ok(Self { + origin: from, + direction: (to - from).try_normalize(tolerance)?, + }) + } + + #[must_use] + pub fn origin(self) -> Point3 { + self.origin + } + + #[must_use] + pub fn direction(self) -> UnitVec3 { + self.direction + } + + #[must_use] + pub fn point_at(self, distance_mm: f64) -> Point3 { + self.origin + self.direction.into_vec(mm(distance_mm)) + } +} + +impl core::fmt::Display for Axis3 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "axis{{ o={}, d={} }}", self.origin, self.direction) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "CoordinateSystem3Wire", into = "CoordinateSystem3Wire")] +pub struct CoordinateSystem3 { + origin: Point3, + x: UnitVec3, + y: UnitVec3, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename = "CoordinateSystem3", deny_unknown_fields)] +struct CoordinateSystem3Wire { + origin: Point3, + x_axis: UnitVec3, + y_axis: UnitVec3, +} + +impl From for CoordinateSystem3Wire { + fn from(c: CoordinateSystem3) -> Self { + Self { + origin: c.origin, + x_axis: c.x, + y_axis: c.y, + } + } +} + +impl TryFrom for CoordinateSystem3 { + type Error = TypesError; + fn try_from(w: CoordinateSystem3Wire) -> Result { + Self::new(w.origin, w.x_axis, w.y_axis, PLANE_ORTHOGONALITY_TOLERANCE) + } +} + +impl CoordinateSystem3 { + pub fn new(origin: Point3, x: UnitVec3, y: UnitVec3, tolerance: Tolerance) -> Result { + let (x, y) = orthonormalize(x, y, tolerance)?; + Ok(Self { origin, x, y }) + } + + #[must_use] + pub fn new_unchecked(origin: Point3, x: UnitVec3, y: UnitVec3) -> Self { + debug_assert!( + x.dot(y).abs() <= PLANE_ORTHOGONALITY_TOLERANCE.value(), + "CoordinateSystem3::new_unchecked requires orthonormal axes" + ); + Self { origin, x, y } + } + + #[must_use] + pub fn origin(self) -> Point3 { + self.origin + } + + #[must_use] + pub fn x_axis(self) -> UnitVec3 { + self.x + } + + #[must_use] + pub fn y_axis(self) -> UnitVec3 { + self.y + } + + #[must_use] + pub fn z_axis(self) -> UnitVec3 { + UnitVec3(Unit::new_normalize(self.x.0.cross(&self.y.0))) + } + + #[must_use] + pub fn xy_plane(self) -> Plane3 { + Plane3::new_unchecked(self.origin, self.x, self.y) + } + + #[must_use] + pub fn point_at_local(self, x_mm: f64, y_mm: f64, z_mm: f64) -> Point3 { + let (ox, oy, oz) = self.origin.coords_mm(); + let (xx, xy, xz) = self.x.components(); + let (yx, yy, yz) = self.y.components(); + let (zx, zy, zz) = self.z_axis().components(); + Point3::from_mm( + ox + x_mm * xx + y_mm * yx + z_mm * zx, + oy + x_mm * xy + y_mm * yy + z_mm * zy, + oz + x_mm * xz + y_mm * yz + z_mm * zz, + ) + } +} + +impl core::fmt::Display for CoordinateSystem3 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "csys{{ o={}, x={}, y={} }}", self.origin, self.x, self.y) + } +} diff --git a/crates/bone-types/tests/snapshots/surface__reference_surface.snap b/crates/bone-types/tests/snapshots/surface__reference_surface.snap index 41122d0..a289aa6 100644 --- a/crates/bone-types/tests/snapshots/surface__reference_surface.snap +++ b/crates/bone-types/tests/snapshots/surface__reference_surface.snap @@ -7,9 +7,9 @@ edge_fp_display = edge@(1 mm, 0 mm, 0 mm) vertex_fp_display = vertex@(0 mm, 0 mm, 0 mm) entity_fp_display = face@(4 mm, 5 mm, 6 mm) face_ref_display = face[FeatureId(null)]:end_cap -face_ref_debug = Face(FaceLabel { feature: FeatureId(null), role: EndCap }, FaceFingerprint { plane: Plane3 { origin: Point3(1 mm, 2 mm, 3 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, centroid: Point3(4 mm, 5 mm, 6 mm) }) +face_ref_debug = Face(FaceRef { label: FaceLabel { feature: FeatureId(null), role: EndCap }, fingerprint: FaceFingerprint { plane: Plane3 { origin: Point3(1 mm, 2 mm, 3 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, centroid: Point3(4 mm, 5 mm, 6 mm) }, ordinal: BodyOrdinal(0) }) match_score_disp = score=0.875 match_score_value = 0.875 resolved_debug = Resolved(FeatureId(null)) repaired_debug = Repaired { id: FeatureId(null), score: MatchScore(0.875) } -dangling_debug = Dangling { last_known: Face(FaceLabel { feature: FeatureId(null), role: EndCap }, FaceFingerprint { plane: Plane3 { origin: Point3(1 mm, 2 mm, 3 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, centroid: Point3(4 mm, 5 mm, 6 mm) }) } +dangling_debug = Dangling { last_known: Face(FaceRef { label: FaceLabel { feature: FeatureId(null), role: EndCap }, fingerprint: FaceFingerprint { plane: Plane3 { origin: Point3(1 mm, 2 mm, 3 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, centroid: Point3(4 mm, 5 mm, 6 mm) }, ordinal: BodyOrdinal(0) }) } diff --git a/crates/bone-types/tests/surface.rs b/crates/bone-types/tests/surface.rs index d7c06b4..358e20d 100644 --- a/crates/bone-types/tests/surface.rs +++ b/crates/bone-types/tests/surface.rs @@ -2,15 +2,15 @@ use bone_types::{ Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, BudgetCeiling, Camera3, ChordHeightTolerance, DegreesOfFreedom, DisplayMode, DocumentId, EdgeFingerprint, EdgeId, EdgeLabel, EdgeRole, EntityRef, ExtrudeId, - FaceFingerprint, FaceId, FaceLabel, FaceRole, FeatureGeneration, FeatureId, ImportOrdinal, - Length, LoopId, LoopIndex, MatchScore, NewtonDamping, NewtonStepTolerance, NodeId, OrbitState, - OrientedBox3, Parameter, ParentIndex, Plane3, Point2, Point3, Projection, RebuildError, - RebuildStatus, RebuildWarning, Resolution, RollbackMarker, ShadingModel, ShellId, SideKind, - SketchDimensionId, SketchEntityId, SketchId, SketchItemId, SketchParameterId, SketchPlaneBasis, - SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, StepFileHeader, - StepFileName, StepOrganization, StepOriginatingSystem, StepSchema, SuppressionState, Tolerance, - UnitVec2, UnitVec3, Vec2, Vec3, VertexFingerprint, VertexId, VertexLabel, VertexRole, WireId, - ZoomFactor, degree, millimeter, radian, + FaceFingerprint, FaceId, FaceLabel, FaceRef, FaceRole, FeatureGeneration, FeatureId, + ImportOrdinal, Length, LoopId, LoopIndex, MatchScore, NewtonDamping, NewtonStepTolerance, + NodeId, OrbitState, OrientedBox3, Parameter, ParentIndex, Plane3, Point2, Point3, Projection, + RebuildError, RebuildStatus, RebuildWarning, Resolution, RollbackMarker, ShadingModel, ShellId, + SideKind, SketchDimensionId, SketchEntityId, SketchId, SketchItemId, SketchParameterId, + SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, + StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, StepSchema, + SuppressionState, Tolerance, UnitVec2, UnitVec3, Vec2, Vec3, VertexFingerprint, VertexId, + VertexLabel, VertexRole, WireId, ZoomFactor, degree, millimeter, radian, }; use slotmap::Key; @@ -440,13 +440,13 @@ fn reference_surface() { let vertex_fp = VertexFingerprint { point: Point3::origin(), }; - let face_ref = EntityRef::Face( + let face_ref = EntityRef::Face(FaceRef::new( FaceLabel { feature: FeatureId::default(), role: FaceRole::EndCap, }, face_fp, - ); + )); let Ok(score) = MatchScore::new(0.875) else { panic!("0.875 is in range"); }; @@ -478,7 +478,7 @@ fn reference_surface() { #[test] fn history_surface() { - let face_ref = EntityRef::Face( + let face_ref = EntityRef::Face(FaceRef::new( FaceLabel { feature: FeatureId::default(), role: FaceRole::EndCap, @@ -487,7 +487,7 @@ fn history_surface() { plane: Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()), centroid: Point3::from_mm(1.0, 2.0, 3.0), }, - ); + )); let Ok(score) = MatchScore::new(0.5) else { panic!("0.5 is in range"); };