diff --git a/main/opengeometry/src/brep/builder/boundary.rs b/main/opengeometry/src/brep/builder/boundary.rs index 35668587..6e6737d3 100644 --- a/main/opengeometry/src/brep/builder/boundary.rs +++ b/main/opengeometry/src/brep/builder/boundary.rs @@ -15,7 +15,7 @@ pub(crate) fn dimensions(values: &[f64]) -> Result<(), GeometryError> { Ok(()) } -pub(crate) fn plane_boundary( +pub fn plane_boundary( builder: &Builder, frame: Frame3, edge: u32, @@ -50,16 +50,10 @@ pub(crate) fn plane_boundary( Ok(boundary(edge, from, to, sense, pcurve)) } -pub(crate) fn uv_line(origin: [f64; 2], direction: [f64; 2]) -> PcurveGeometry { +pub fn uv_line(origin: [f64; 2], direction: [f64; 2]) -> PcurveGeometry { PcurveGeometry::Line2 { origin, direction } } -pub(crate) fn boundary( - edge: u32, - from: u32, - to: u32, - sense: Orientation, - pcurve: PcurveGeometry, -) -> Use { +pub fn boundary(edge: u32, from: u32, to: u32, sense: Orientation, pcurve: PcurveGeometry) -> Use { Use { edge, from, diff --git a/main/opengeometry/src/brep/builder/brep_builder.rs b/main/opengeometry/src/brep/builder/brep_builder.rs index 568f4bcc..54bf2fb2 100644 --- a/main/opengeometry/src/brep/builder/brep_builder.rs +++ b/main/opengeometry/src/brep/builder/brep_builder.rs @@ -8,7 +8,7 @@ use crate::brep::topology::{ }; use crate::math::{Interval, Point3}; -pub(crate) struct Use { +pub struct Use { pub(super) edge: u32, pub(super) from: u32, pub(super) to: u32, @@ -16,16 +16,19 @@ pub(crate) struct Use { pub(super) pcurve: PcurveGeometry, } -pub(crate) struct Builder { +pub struct Builder { pub(crate) brep: BrepEnvelope, } impl Builder { - pub(crate) fn new(id: String, accuracy: Accuracy) -> Result { + pub fn new(id: String, accuracy: Accuracy) -> Result { Ok(Self { brep: BrepEnvelope::new(id, accuracy)?, }) } - pub(crate) fn vertex(&mut self, p: Point3) -> u32 { + pub fn brep(&self) -> &BrepEnvelope { + &self.brep + } + pub fn vertex(&mut self, p: Point3) -> u32 { let id = self.brep.topology.vertices.len() as u32; self.brep.topology.vertices.push(Vertex { id, @@ -35,7 +38,7 @@ impl Builder { }); id } - pub(crate) fn edge(&mut self, curve: CurveGeometry, range: Interval, seam: bool) -> u32 { + pub fn edge(&mut self, curve: CurveGeometry, range: Interval, seam: bool) -> u32 { let curve_id = self.brep.geometry.curves.len() as u32; self.brep.geometry.curves.push(curve); self.edge_geometry( @@ -129,23 +132,23 @@ impl Builder { }); Ok(loop_id) } - pub(crate) fn face( + pub fn face( &mut self, key: &str, surface: SurfaceGeometry, bounds: [[f64; 2]; 2], uses: Vec, - ) -> Result<(), GeometryError> { + ) -> Result { self.face_with_holes(key, surface, bounds, uses, Vec::new()) } - pub(crate) fn face_with_holes( + pub fn face_with_holes( &mut self, key: &str, surface: SurfaceGeometry, bounds: [[f64; 2]; 2], outer_uses: Vec, hole_uses: Vec>, - ) -> Result<(), GeometryError> { + ) -> Result { let surface_id = self.brep.geometry.surfaces.len() as u32; self.brep.geometry.surfaces.push(surface); let face = self.brep.topology.faces.len() as u32; @@ -180,9 +183,21 @@ impl Builder { reversed: false, }, }); + Ok(face) + } + pub fn set_face_sense(&mut self, face: u32, sense: Orientation) -> Result<(), GeometryError> { + self.brep + .topology + .faces + .get_mut(face as usize) + .ok_or(GeometryError::MissingReference { + kind: "face".into(), + index: face, + })? + .sense = sense; Ok(()) } - pub(crate) fn finish_solid(mut self) -> Result { + pub fn finish_solid(mut self) -> Result { let faces = (0..self.brep.topology.faces.len() as u32).collect(); self.brep.topology.shells.push(Shell { id: 0, diff --git a/main/opengeometry/src/brep/builder/mod.rs b/main/opengeometry/src/brep/builder/mod.rs index dfb958c2..ff481e55 100644 --- a/main/opengeometry/src/brep/builder/mod.rs +++ b/main/opengeometry/src/brep/builder/mod.rs @@ -4,6 +4,7 @@ mod brep_builder; mod tests; mod wire; -pub(crate) use boundary::{boundary, dimensions, padded_uv_bounds, plane_boundary, uv_line}; -pub(crate) use brep_builder::{Builder, Use}; +pub use boundary::{boundary, plane_boundary, uv_line}; +pub(crate) use boundary::{dimensions, padded_uv_bounds}; +pub use brep_builder::{Builder, Use}; pub(crate) use wire::WireUse; diff --git a/main/opengeometry/src/brep/mod.rs b/main/opengeometry/src/brep/mod.rs index 3978682a..67425b9b 100644 --- a/main/opengeometry/src/brep/mod.rs +++ b/main/opengeometry/src/brep/mod.rs @@ -18,9 +18,8 @@ mod validate; pub use accuracy::Accuracy; pub use body_type::BodyType; pub use bounds::PatchBounds; -pub(super) use builder::{ - boundary, dimensions, padded_uv_bounds, plane_boundary, uv_line, Builder, Use, WireUse, -}; +pub use builder::{boundary, plane_boundary, uv_line, Builder, Use}; +pub(super) use builder::{dimensions, padded_uv_bounds, WireUse}; pub(super) use error::coverage_gap; pub use error::GeometryError; pub use frame::Frame3; @@ -33,15 +32,16 @@ pub use geometry::{ CurveGeometry, GeometryStore, IntersectionPoint, Surface, SurfaceChart, SurfaceGeometry, SurfaceJet2, }; -pub(super) use pcurve::{period_lifted_uv, IntersectionSide, PcurveGeometry}; +pub(super) use pcurve::period_lifted_uv; +pub use pcurve::{IntersectionSide, PcurveGeometry}; pub use placement::{mirror::mirrored, placed}; pub use similarity::Similarity3; #[cfg(test)] pub(super) use topology::{circular_face, coarse_accuracy, fine_accuracy}; pub use topology::{ - BrepEnvelope, Edge, Face, FaceProvenance, FaceRole, FaceSource, HalfEdge, Topology, Vertex, + BrepEnvelope, Edge, Face, FaceProvenance, FaceRole, FaceSource, HalfEdge, Orientation, + Topology, Vertex, }; pub(super) use topology::{ - EdgeGeometry, GeometryQuality, HalfEdgeGeometryUse, Loop, Orientation, Shell, SolidRegion, - TrimRegion, Wire, + EdgeGeometry, GeometryQuality, HalfEdgeGeometryUse, Loop, Shell, SolidRegion, TrimRegion, Wire, }; diff --git a/main/opengeometry/src/brep/pcurve.rs b/main/opengeometry/src/brep/pcurve.rs index e0046c5c..39f2919a 100644 --- a/main/opengeometry/src/brep/pcurve.rs +++ b/main/opengeometry/src/brep/pcurve.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind", deny_unknown_fields)] -pub(crate) enum PcurveGeometry { +pub enum PcurveGeometry { Line2 { origin: UV, direction: UV, @@ -27,7 +27,7 @@ pub(crate) enum PcurveGeometry { }, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) enum IntersectionSide { +pub enum IntersectionSide { A, B, } diff --git a/main/opengeometry/src/brep/topology/mod.rs b/main/opengeometry/src/brep/topology/mod.rs index b8818406..9ec9c942 100644 --- a/main/opengeometry/src/brep/topology/mod.rs +++ b/main/opengeometry/src/brep/topology/mod.rs @@ -16,5 +16,5 @@ pub(crate) use envelope::GeometryQuality; pub(super) use envelope::SCHEMA_VERSION; #[cfg(test)] pub(crate) use fixtures::{circular_face, coarse_accuracy, fine_accuracy}; -pub(crate) use orientation::Orientation; +pub use orientation::Orientation; pub use provenance::{FaceProvenance, FaceRole, FaceSource}; diff --git a/main/opengeometry/src/brep/topology/orientation.rs b/main/opengeometry/src/brep/topology/orientation.rs index f5b93c0b..cc6df414 100644 --- a/main/opengeometry/src/brep/topology/orientation.rs +++ b/main/opengeometry/src/brep/topology/orientation.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) enum Orientation { +pub enum Orientation { Forward, Reverse, } diff --git a/main/opengeometry/src/math/interval.rs b/main/opengeometry/src/math/interval.rs index 7fca2806..51a73e2d 100644 --- a/main/opengeometry/src/math/interval.rs +++ b/main/opengeometry/src/math/interval.rs @@ -27,7 +27,7 @@ fn next_down(x: f64) -> f64 { } impl Interval { - pub(crate) fn new(lo: f64, hi: f64) -> Result { + pub fn new(lo: f64, hi: f64) -> Result { finite(lo)?; finite(hi)?; if lo > hi { diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/embedded.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/embedded.rs index 0168a48a..fc2fae68 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/embedded.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/embedded.rs @@ -346,7 +346,8 @@ fn add_two_arc_plane_face( SurfaceGeometry::Plane { frame }, two_arc_plane_bounds(frame, arcs, tolerance), uses, - ) + )?; + Ok(()) } fn set_embedded_provenance( diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/parallel.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/parallel.rs index 085a1dba..56515abc 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/parallel.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/parallel.rs @@ -410,7 +410,8 @@ fn add_parallel_caps( SurfaceGeometry::Plane { frame: upper_frame }, planar_bounds, upper_uses, - ) + )?; + Ok(()) } fn parallel_cap_bounds( diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/transverse/host.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/transverse/host.rs index 2faa65f4..94b35df0 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/transverse/host.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/cylinder_cylinder/transverse/host.rs @@ -120,7 +120,8 @@ pub(super) fn add_host_lateral_face( ), ], host_holes, - ) + )?; + Ok(()) } pub(super) fn add_host_caps( @@ -167,5 +168,6 @@ pub(super) fn add_host_caps( axis_b: [0.0, host.radius], }, )], - ) + )?; + Ok(()) } diff --git a/main/opengeometry/src/primitives/internal/arc_edged_extrusion.rs b/main/opengeometry/src/primitives/internal/arc_edged_extrusion.rs index 452b488b..284c462c 100644 --- a/main/opengeometry/src/primitives/internal/arc_edged_extrusion.rs +++ b/main/opengeometry/src/primitives/internal/arc_edged_extrusion.rs @@ -297,7 +297,8 @@ fn add_cap_faces( [bounds[0], [-bounds[1][1], -bounds[1][0]]], bottom_outer, bottom_holes, - ) + )?; + Ok(()) } fn cap_uses( diff --git a/main/opengeometry/src/primitives/internal/cylinder_sector.rs b/main/opengeometry/src/primitives/internal/cylinder_sector.rs index 31116a28..a354194a 100644 --- a/main/opengeometry/src/primitives/internal/cylinder_sector.rs +++ b/main/opengeometry/src/primitives/internal/cylinder_sector.rs @@ -197,7 +197,8 @@ fn add_lateral_face( uv_line([lo, 0.0], [0.0, 1.0]), ), ], - ) + )?; + Ok(()) } fn add_sector_caps( @@ -258,7 +259,8 @@ fn add_sector_caps( SurfaceGeometry::Plane { frame: top_frame }, [[-radius, radius]; 2], top_uses, - ) + )?; + Ok(()) } fn add_radial_faces( @@ -328,7 +330,8 @@ fn add_radial_faces( SurfaceGeometry::Plane { frame: end_frame }, [[0.0, height], [0.0, radius]], end_uses, - ) + )?; + Ok(()) } fn tangent(frame: Frame3, angle: f64) -> Point3 { diff --git a/main/opengeometry/src/primitives/internal/cylinder_with_circular_hole.rs b/main/opengeometry/src/primitives/internal/cylinder_with_circular_hole.rs index 6c1913f3..a24132c1 100644 --- a/main/opengeometry/src/primitives/internal/cylinder_with_circular_hole.rs +++ b/main/opengeometry/src/primitives/internal/cylinder_with_circular_hole.rs @@ -282,5 +282,6 @@ fn add_annulus_caps( [[-outer_radius, outer_radius]; 2], outer_upper, vec![inner_upper], - ) + )?; + Ok(()) } diff --git a/main/opengeometry/src/primitives/internal/extruded_loop.rs b/main/opengeometry/src/primitives/internal/extruded_loop.rs index 769eaee1..e3a1e28e 100644 --- a/main/opengeometry/src/primitives/internal/extruded_loop.rs +++ b/main/opengeometry/src/primitives/internal/extruded_loop.rs @@ -83,5 +83,6 @@ pub(super) fn add_plane_side( SurfaceGeometry::Plane { frame: side_frame }, [[-g, norm(sub(destination, origin)) + g], [-g, height + g]], uses, - ) + )?; + Ok(()) } diff --git a/main/opengeometry/src/primitives/internal/linear_extrusion.rs b/main/opengeometry/src/primitives/internal/linear_extrusion.rs index 4fb7a05b..93935218 100644 --- a/main/opengeometry/src/primitives/internal/linear_extrusion.rs +++ b/main/opengeometry/src/primitives/internal/linear_extrusion.rs @@ -188,7 +188,8 @@ fn add_cap_faces( bottom_bounds, bottom_outer, bottom_holes, - ) + )?; + Ok(()) } fn cap_uses( diff --git a/main/opengeometry/src/primitives/internal/revolved_profile.rs b/main/opengeometry/src/primitives/internal/revolved_profile.rs index 9eb030c3..e25bca40 100644 --- a/main/opengeometry/src/primitives/internal/revolved_profile.rs +++ b/main/opengeometry/src/primitives/internal/revolved_profile.rs @@ -375,7 +375,8 @@ fn add_plane_face( [[-radius, radius]; 2], vec![outer], inner.into_iter().map(|hole| vec![hole]).collect(), - ) + )?; + Ok(()) } fn add_lateral_face( diff --git a/main/opengeometry/src/primitives/revolved_section.rs b/main/opengeometry/src/primitives/revolved_section.rs index 85bd8941..85c6f7d1 100644 --- a/main/opengeometry/src/primitives/revolved_section.rs +++ b/main/opengeometry/src/primitives/revolved_section.rs @@ -163,7 +163,8 @@ fn add_lower_cap( axis_b: [0.0, -r0], }, )], - ) + )?; + Ok(()) } fn add_upper_cap( @@ -188,5 +189,6 @@ fn add_upper_cap( axis_b: [0.0, r1], }, )], - ) + )?; + Ok(()) } diff --git a/main/opengeometry/tests/public_brep_builder.rs b/main/opengeometry/tests/public_brep_builder.rs new file mode 100644 index 00000000..a8f8e15a --- /dev/null +++ b/main/opengeometry/tests/public_brep_builder.rs @@ -0,0 +1,142 @@ +use opengeometry::brep::{ + plane_boundary, Builder, CurveGeometry, Frame3, GeometryError, Orientation, SurfaceGeometry, +}; +use opengeometry::exchange::export_step; +use opengeometry::math::{Interval, Point3}; +use opengeometry::query::{classify_point, PointClassification}; +use opengeometry_test_support::accuracy::accuracy; +use opengeometry_test_support::volume::estimate; + +const FACES: [(&str, [usize; 4], Point3); 6] = [ + ("bottom", [0, 2, 3, 1], [0.0, 0.0, -1.0]), + ("top", [4, 5, 7, 6], [0.0, 0.0, 1.0]), + ("left", [0, 4, 6, 2], [-1.0, 0.0, 0.0]), + ("right", [1, 3, 7, 5], [1.0, 0.0, 0.0]), + ("front", [0, 1, 5, 4], [0.0, -1.0, 0.0]), + ("back", [2, 6, 7, 3], [0.0, 1.0, 0.0]), +]; + +fn sub(a: Point3, b: Point3) -> Point3 { + [a[0] - b[0], a[1] - b[1], a[2] - b[2]] +} + +fn dot(a: Point3, b: Point3) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +fn corners(size: Point3) -> Vec { + (0..8) + .map(|i| { + std::array::from_fn(|axis| { + if i & (1 << axis) == 0 { + 0.0 + } else { + size[axis] + } + }) + }) + .collect() +} + +fn box_edges(builder: &mut Builder, points: &[Point3], size: Point3) -> [[u32; 3]; 8] { + let axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + let mut edges = [[u32::MAX; 3]; 8]; + for i in 0..8 { + for axis in (0..3).filter(|axis| i & (1 << axis) == 0) { + let line = CurveGeometry::Line { + origin: points[i], + direction: axes[axis], + }; + edges[i][axis] = builder.edge(line, Interval::new(0.0, size[axis]).unwrap(), false); + } + } + edges +} + +fn box_builder(size: Point3) -> Result<(Builder, Vec), GeometryError> { + let mut builder = Builder::new("box".into(), accuracy(1e-6))?; + let points = corners(size); + for point in &points { + builder.vertex(*point); + } + let edges = box_edges(&mut builder, &points, size); + let mut faces = Vec::new(); + for (key, corner, normal) in FACES { + let x = sub(points[corner[1]], points[corner[0]]); + let frame = Frame3::from_axis(points[corner[0]], normal, x)?; + let mut uses = Vec::new(); + let mut uv = Vec::new(); + for i in 0..4 { + let (from, to) = (corner[i], corner[(i + 1) % 4]); + let axis = (from ^ to).trailing_zeros() as usize; + let sense = if from < to { + Orientation::Forward + } else { + Orientation::Reverse + }; + let edge = edges[from.min(to)][axis]; + uses.push(plane_boundary( + &builder, + frame, + edge, + from as u32, + to as u32, + sense, + )?); + let local = sub(points[from], frame.origin); + uv.push([dot(local, frame.x), dot(local, frame.y)]); + } + let bounds = std::array::from_fn(|axis| { + let values = uv.iter().map(|point| point[axis]); + let lo = values.clone().fold(f64::INFINITY, f64::min); + [lo - 1e-7, values.fold(f64::NEG_INFINITY, f64::max) + 1e-7] + }); + faces.push(builder.face(key, SurfaceGeometry::Plane { frame }, bounds, uses)?); + } + Ok((builder, faces)) +} + +#[test] +fn a_box_built_through_the_public_builder_is_a_valid_exportable_solid() { + let (builder, faces) = box_builder([2.0, 3.0, 4.0]).unwrap(); + assert_eq!(faces, [0, 1, 2, 3, 4, 5]); + assert_eq!(builder.brep().topology.faces.len(), 6); + let brep = builder.finish_solid().unwrap(); + brep.validate().unwrap(); + assert_eq!(brep.topology.edges.len(), 12); + assert_eq!(brep.topology.halfedges.len(), 24); + let volume = estimate(&brep, 1e-3); + assert!((volume.value - 24.0).abs() <= volume.error_bound.max(1e-9)); + let inside = classify_point(&brep, [1.0, 1.5, 2.0]).unwrap(); + assert_eq!(inside, PointClassification::Inside); + let outside = classify_point(&brep, [3.0, 1.5, 2.0]).unwrap(); + assert_eq!(outside, PointClassification::Outside); + let (step, _) = export_step(&brep, "millimetre").unwrap(); + assert_eq!(step.matches("=ADVANCED_FACE(").count(), 6); +} + +#[test] +fn a_face_sense_is_set_by_face_id() { + let (mut builder, faces) = box_builder([1.0, 1.0, 1.0]).unwrap(); + let top = [0.5, 0.5, 1.0]; + let normal = |builder: &Builder| builder.brep().face_normal_at(faces[1], top).unwrap(); + assert!((normal(&builder)[2] - 1.0).abs() < 1e-12); + builder + .set_face_sense(faces[1], Orientation::Reverse) + .unwrap(); + assert!((normal(&builder)[2] + 1.0).abs() < 1e-12); + assert!(matches!( + builder.set_face_sense(6, Orientation::Reverse), + Err(GeometryError::MissingReference { .. }) + )); +} + +#[test] +fn an_edge_used_by_a_third_loop_is_rejected() { + let (mut builder, _) = box_builder([1.0, 1.0, 1.0]).unwrap(); + let frame = Frame3::from_axis([0.0; 3], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]).unwrap(); + let reuse = plane_boundary(&builder, frame, 0, 0, 1, Orientation::Forward).unwrap(); + let plane = SurfaceGeometry::Plane { frame }; + let result = builder.face("extra", plane, [[-1.0, 1.0], [-1.0, 1.0]], vec![reuse]); + assert!(matches!(result, Err(GeometryError::InvalidTopology(_)))); +}