diff --git a/main/opengeometry/src/brep/builder/fixtures.rs b/main/opengeometry/src/brep/builder/fixtures.rs new file mode 100644 index 00000000..5cf3d585 --- /dev/null +++ b/main/opengeometry/src/brep/builder/fixtures.rs @@ -0,0 +1,299 @@ +use super::boundary::{boundary, plane_boundary, uv_line}; +use super::brep_builder::{Builder, Use}; +use crate::brep::accuracy::Accuracy; +use crate::brep::frame::Frame3; +use crate::brep::geometry::{CurveGeometry, SurfaceGeometry, Wrap}; +use crate::brep::topology::{BrepEnvelope, Orientation}; +use crate::math::{Interval, Point3}; + +pub(crate) struct Sector { + pub(crate) inner: f64, + pub(crate) thickness: f64, + pub(crate) wrap_radius: f64, + pub(crate) length: f64, + pub(crate) rise: f64, + pub(crate) width: f64, +} + +pub(crate) const SECTOR: Sector = Sector { + inner: 2.0, + thickness: 2.0, + wrap_radius: 2.8, + length: 4.0, + rise: 1.0, + width: 10.0, +}; + +const SECTOR_ACCURACY: Accuracy = Accuracy { + geometric: 1e-6, + intersection: 1e-7, + tessellation: 1e-3, + exchange: 1e-5, +}; + +impl Sector { + pub(crate) fn wrap(&self) -> Wrap { + Wrap { + frame: Frame3::from_axis([1.0, 2.0, 3.0], [0.2, 1.0, 0.1], [1.0, 0.0, 0.3]) + .expect("sector frame"), + radius: self.wrap_radius, + } + } + + pub(crate) fn volume(&self) -> f64 { + let area = self.length * self.width - self.length * self.rise / 2.0; + let outer = self.inner + self.thickness; + area * (outer * outer - self.inner * self.inner) / (2.0 * self.wrap_radius) + } + + fn slant(&self) -> f64 { + self.length.hypot(self.rise) + } + + fn levels(&self) -> [f64; 2] { + [self.inner, self.inner + self.thickness] + } + + fn corners(&self) -> [[f64; 2]; 4] { + [ + [0.0, 0.0], + [self.length, self.rise], + [self.length, self.width], + [0.0, self.width], + ] + } +} + +struct Edges { + vertices: Vec<[u32; 2]>, + ab: [u32; 2], + bc: [u32; 2], + cd: [u32; 2], + da: [u32; 2], + up: Vec, +} + +fn line(b: &mut Builder, origin: Point3, direction: Point3, length: f64) -> u32 { + b.edge( + CurveGeometry::Line { origin, direction }, + Interval::new(0.0, length).expect("line range"), + false, + ) +} + +fn level_edges(b: &mut Builder, s: &Sector, z: f64) -> [u32; 4] { + let w = s.wrap(); + let f = w.frame; + let d = [s.length / s.slant(), s.rise / s.slant(), 0.0]; + let ab = b.edge( + CurveGeometry::Wrapped { + wrap: w, + curve: Box::new(CurveGeometry::Line { + origin: [0.0, 0.0, z], + direction: d, + }), + }, + Interval::new(0.0, s.slant()).expect("slant range"), + false, + ); + let bc = line(b, w.point([s.length, s.rise, z]), f.z, s.width - s.rise); + let da = line(b, w.point([0.0, 0.0, z]), f.z, s.width); + let cd = b.edge( + CurveGeometry::Circle { + frame: Frame3 { + origin: f.point([0.0, 0.0, s.width]), + ..f + }, + radius: z, + }, + Interval::new(0.0, s.length / s.wrap_radius).expect("arc range"), + false, + ); + [ab, bc, cd, da] +} + +fn edges(b: &mut Builder, s: &Sector) -> Edges { + let w = s.wrap(); + let vertices = s + .corners() + .iter() + .map(|c| s.levels().map(|z| b.vertex(w.point([c[0], c[1], z])))) + .collect(); + let [inner, outer] = s.levels().map(|z| level_edges(b, s, z)); + let radial = |x: f64| { + w.frame + .vector([(x / s.wrap_radius).cos(), (x / s.wrap_radius).sin(), 0.0]) + }; + let up = s + .corners() + .iter() + .map(|c| line(b, w.point([c[0], c[1], s.inner]), radial(c[0]), s.thickness)) + .collect(); + Edges { + vertices, + ab: [inner[0], outer[0]], + bc: [inner[1], outer[1]], + cd: [inner[2], outer[2]], + da: [inner[3], outer[3]], + up, + } +} + +fn cylinder_faces(b: &mut Builder, s: &Sector, e: &Edges) { + let (fwd, rev) = (Orientation::Forward, Orientation::Reverse); + let end = s.length / s.wrap_radius; + let ab = uv_line( + [0.0, 0.0], + [s.length / s.slant() / s.wrap_radius, s.rise / s.slant()], + ); + let bc = uv_line([end, s.rise], [0.0, 1.0]); + let cd = uv_line([0.0, s.width], [1.0, 0.0]); + let da = uv_line([0.0, 0.0], [0.0, 1.0]); + let v = |corner: usize, level: usize| e.vertices[corner][level]; + let inner = vec![ + boundary(e.da[0], v(0, 0), v(3, 0), fwd, da.clone()), + boundary(e.cd[0], v(3, 0), v(2, 0), fwd, cd.clone()), + boundary(e.bc[0], v(2, 0), v(1, 0), rev, bc.clone()), + boundary(e.ab[0], v(1, 0), v(0, 0), rev, ab.clone()), + ]; + let outer = vec![ + boundary(e.ab[1], v(0, 1), v(1, 1), fwd, ab), + boundary(e.bc[1], v(1, 1), v(2, 1), fwd, bc), + boundary(e.cd[1], v(2, 1), v(3, 1), rev, cd), + boundary(e.da[1], v(3, 1), v(0, 1), rev, da), + ]; + let pad = 1e-3; + for (level, uses) in [inner, outer].into_iter().enumerate() { + let face = b.brep.topology.faces.len(); + b.face( + if level == 0 { "inner" } else { "outer" }, + SurfaceGeometry::Cylinder { + frame: s.wrap().frame, + radius: s.levels()[level], + }, + [[-pad, end + pad], [-pad, s.width + pad]], + uses, + ) + .expect("cylinder face"); + if level == 0 { + b.brep.topology.faces[face].sense = rev; + } + } +} + +fn plane(b: &mut Builder, key: &str, frame: Frame3, uses: &[(u32, u32, u32, Orientation)]) { + let uses: Vec = uses + .iter() + .map(|&(e, from, to, sense)| { + plane_boundary(b, frame, e, from, to, sense).expect("plane boundary") + }) + .collect(); + let big = 4.0 * (SECTOR.inner + SECTOR.thickness + SECTOR.width); + b.face( + key, + SurfaceGeometry::Plane { frame }, + [[-big, big], [-big, big]], + uses, + ) + .expect("plane face"); +} + +fn plane_faces(b: &mut Builder, s: &Sector, e: &Edges) { + let (fwd, rev) = (Orientation::Forward, Orientation::Reverse); + let f = s.wrap().frame; + let v = |corner: usize, level: usize| e.vertices[corner][level]; + let end = s.length / s.wrap_radius; + let start = Frame3::from_axis(f.origin, f.vector([0.0, -1.0, 0.0]), f.x).expect("start"); + let radial = f.vector([end.cos(), end.sin(), 0.0]); + let finish = + Frame3::from_axis(f.origin, f.vector([-end.sin(), end.cos(), 0.0]), radial).expect("end"); + let start_uses = [ + (e.up[0], v(0, 0), v(0, 1), fwd), + (e.da[1], v(0, 1), v(3, 1), fwd), + (e.up[3], v(3, 1), v(3, 0), rev), + (e.da[0], v(3, 0), v(0, 0), rev), + ]; + plane(b, "start", start, &start_uses); + let end_uses = [ + (e.bc[0], v(1, 0), v(2, 0), fwd), + (e.up[2], v(2, 0), v(2, 1), fwd), + (e.bc[1], v(2, 1), v(1, 1), rev), + (e.up[1], v(1, 1), v(1, 0), rev), + ]; + plane(b, "end", finish, &end_uses); + let far = Frame3 { + origin: f.point([0.0, 0.0, s.width]), + ..f + }; + let uses = [ + (e.cd[0], v(2, 0), v(3, 0), rev), + (e.up[3], v(3, 0), v(3, 1), fwd), + (e.cd[1], v(3, 1), v(2, 1), fwd), + (e.up[2], v(2, 1), v(2, 0), rev), + ]; + plane(b, "far", far, &uses); +} + +fn slanted_face(b: &mut Builder, s: &Sector, e: &Edges) { + let (fwd, rev) = (Orientation::Forward, Orientation::Reverse); + let slant = s.slant(); + let [r, top] = s.levels(); + let flat = Frame3 { + origin: [0.0; 3], + x: [s.length / slant, s.rise / slant, 0.0], + y: [0.0, 0.0, 1.0], + z: [s.rise / slant, -s.length / slant, 0.0], + }; + let v = |corner: usize, level: usize| e.vertices[corner][level]; + let uses = vec![ + boundary( + e.ab[0], + v(0, 0), + v(1, 0), + fwd, + uv_line([0.0, r], [1.0, 0.0]), + ), + boundary( + e.up[1], + v(1, 0), + v(1, 1), + fwd, + uv_line([slant, r], [0.0, 1.0]), + ), + boundary( + e.ab[1], + v(1, 1), + v(0, 1), + rev, + uv_line([0.0, top], [1.0, 0.0]), + ), + boundary( + e.up[0], + v(0, 1), + v(0, 0), + rev, + uv_line([0.0, r], [0.0, 1.0]), + ), + ]; + let pad = 1e-3; + b.face( + "slant", + SurfaceGeometry::Wrapped { + wrap: s.wrap(), + surface: Box::new(SurfaceGeometry::Plane { frame: flat }), + }, + [[-pad, slant + pad], [r - pad, top + pad]], + uses, + ) + .expect("slanted face"); +} + +pub(crate) fn wrapped_sector() -> BrepEnvelope { + let s = &SECTOR; + let mut b = Builder::new("sector".into(), SECTOR_ACCURACY).expect("builder"); + let e = edges(&mut b, s); + cylinder_faces(&mut b, s, &e); + plane_faces(&mut b, s, &e); + slanted_face(&mut b, s, &e); + b.finish_solid().expect("wrapped sector") +} diff --git a/main/opengeometry/src/brep/builder/mod.rs b/main/opengeometry/src/brep/builder/mod.rs index dfb958c2..8ee37e4f 100644 --- a/main/opengeometry/src/brep/builder/mod.rs +++ b/main/opengeometry/src/brep/builder/mod.rs @@ -1,9 +1,13 @@ mod boundary; mod brep_builder; #[cfg(test)] +mod fixtures; +#[cfg(test)] mod tests; mod wire; pub(crate) use boundary::{boundary, dimensions, padded_uv_bounds, plane_boundary, uv_line}; pub(crate) use brep_builder::{Builder, Use}; +#[cfg(test)] +pub(crate) use fixtures::{wrapped_sector, SECTOR}; pub(crate) use wire::WireUse; diff --git a/main/opengeometry/src/brep/error.rs b/main/opengeometry/src/brep/error.rs index 2f504256..698451f2 100644 --- a/main/opengeometry/src/brep/error.rs +++ b/main/opengeometry/src/brep/error.rs @@ -50,6 +50,9 @@ pub(super) fn positive(value: f64) -> Result<(), GeometryError> { } Ok(()) } +pub(crate) fn unsupported_wrapped(operation: &str) -> GeometryError { + GeometryError::UnsupportedGeometry(format!("{operation} of wrapped geometry")) +} pub(crate) fn coverage_gap(a: &str, b: &str) -> GeometryError { GeometryError::CoverageGap { families: [a.into(), b.into()], diff --git a/main/opengeometry/src/brep/geometry/curve.rs b/main/opengeometry/src/brep/geometry/curve.rs index 934a4719..98bcf9c2 100644 --- a/main/opengeometry/src/brep/geometry/curve.rs +++ b/main/opengeometry/src/brep/geometry/curve.rs @@ -1,4 +1,5 @@ use super::turned::{narrowed, turned_axis}; +use super::wrap::Wrap; use crate::brep::bounds::PatchBounds; use crate::brep::error::GeometryError; use crate::brep::frame::{checked, Frame3}; @@ -24,6 +25,10 @@ pub enum CurveGeometry { Intersection { definition: u32, }, + Wrapped { + wrap: Wrap, + curve: Box, + }, } pub(crate) trait Curve { @@ -65,10 +70,39 @@ impl CurveGeometry { } } Self::Intersection { .. } => {} + Self::Wrapped { wrap, curve } => { + wrap.validate()?; + if matches!(**curve, Self::Wrapped { .. } | Self::Intersection { .. }) { + return Err(GeometryError::InvalidGeometry( + "a wrapped curve must wrap a line, circle or ellipse".into(), + )); + } + curve.validate()?; + } }; Ok(()) } + pub(crate) fn derivative_bounds(&self, range: Interval) -> Result<(f64, f64), GeometryError> { + Ok(match self { + Self::Line { .. } => (1.0, 0.0), + Self::Circle { radius, .. } => (*radius, *radius), + Self::Ellipse { major_radius, .. } => (*major_radius, *major_radius), + Self::Intersection { definition } => { + return Err(GeometryError::MissingReference { + kind: "intersection".into(), + index: *definition, + }) + } + Self::Wrapped { wrap, curve } => { + let (speed, acceleration) = curve.derivative_bounds(range)?; + let z = curve.elementary_bounds(range)?.axes[2]; + let (first, second) = wrap.derivative_bounds(z.lo.abs().max(z.hi.abs())); + (first * speed, second * speed * speed + first * acceleration) + } + }) + } + pub(crate) fn elementary_point(&self, t: f64) -> Result { self.validate()?; finite(t)?; @@ -82,6 +116,7 @@ impl CurveGeometry { major_radius: a, minor_radius: b, } => frame.point([a * t.cos(), b * t.sin(), 0.0]), + Self::Wrapped { wrap, curve } => wrap.point(curve.elementary_point(t)?), Self::Intersection { definition } => { return Err(GeometryError::MissingReference { kind: "intersection".into(), @@ -104,6 +139,9 @@ impl CurveGeometry { major_radius: a, minor_radius: b, } => frame.vector([-a * t.sin(), b * t.cos(), 0.0]), + Self::Wrapped { wrap, curve } => { + wrap.push(curve.elementary_point(t)?, curve.elementary_tangent(t)?) + } Self::Intersection { definition } => { return Err(GeometryError::MissingReference { kind: "intersection".into(), @@ -132,6 +170,7 @@ impl CurveGeometry { major_radius: a, minor_radius: b, } => conic_bounds(frame, [*a, *b], range), + Self::Wrapped { wrap, curve } => wrap.enclose(curve.elementary_bounds(range)?), Self::Intersection { definition } => Err(GeometryError::MissingReference { kind: "intersection".into(), index: *definition, @@ -153,18 +192,12 @@ impl CurveGeometry { "deflection must be finite and positive".into(), )); } - let bound = match self { - Self::Line { .. } => 0.0, - Self::Circle { radius, .. } => *radius, - Self::Ellipse { major_radius, .. } => *major_radius, - Self::Intersection { definition } => { - return Err(GeometryError::MissingReference { - kind: "intersection".into(), - index: *definition, - }) - } + let bound = self.derivative_bounds(range)?.1; + let closed = match self { + Self::Wrapped { curve, .. } => !matches!(**curve, Self::Line { .. }), + _ => true, }; - let minimum = if bound > 0.0 && range.width() >= std::f64::consts::TAU - 1e-12 { + let minimum = if closed && bound > 0.0 && range.width() >= std::f64::consts::TAU - 1e-12 { 3.0 } else { 1.0 diff --git a/main/opengeometry/src/brep/geometry/mod.rs b/main/opengeometry/src/brep/geometry/mod.rs index a3e64e14..315aa206 100644 --- a/main/opengeometry/src/brep/geometry/mod.rs +++ b/main/opengeometry/src/brep/geometry/mod.rs @@ -7,6 +7,7 @@ mod surface; mod tests; mod trace; mod turned; +mod wrap; pub(crate) use chord::{bounds_chord_deviation, point_segment_distance}; pub(crate) use curve::Curve; @@ -16,3 +17,4 @@ pub use store::GeometryStore; pub use surface::{Surface, SurfaceChart, SurfaceGeometry, SurfaceJet2}; pub use trace::IntersectionPoint; pub(crate) use trace::SsiBudget; +pub use wrap::Wrap; diff --git a/main/opengeometry/src/brep/geometry/store.rs b/main/opengeometry/src/brep/geometry/store.rs index 22a53bcc..2ace8c82 100644 --- a/main/opengeometry/src/brep/geometry/store.rs +++ b/main/opengeometry/src/brep/geometry/store.rs @@ -31,6 +31,15 @@ impl GeometryStore { intersections: Vec::new(), } } + pub(crate) fn has_wrapped(&self) -> bool { + self.surfaces + .iter() + .any(|s| matches!(s, SurfaceGeometry::Wrapped { .. })) + || self + .curves + .iter() + .any(|c| matches!(c, CurveGeometry::Wrapped { .. })) + } pub fn surface(&self, id: u32) -> Result<&SurfaceGeometry, GeometryError> { self.surfaces .get(id as usize) diff --git a/main/opengeometry/src/brep/geometry/surface.rs b/main/opengeometry/src/brep/geometry/surface.rs index 9566cd8a..084545f1 100644 --- a/main/opengeometry/src/brep/geometry/surface.rs +++ b/main/opengeometry/src/brep/geometry/surface.rs @@ -1,7 +1,8 @@ -use super::turned::{narrowed, reached, ring_axis, swept_axis}; +use super::turned::{lift, narrowed, reached, ring_axis, swept_axis}; +use super::wrap::Wrap; use crate::brep::bounds::PatchBounds; use crate::brep::error::GeometryError; -use crate::brep::frame::{checked, Frame3, UVBox, UV}; +use crate::brep::frame::{checked, unit, Frame3, UVBox, UV}; use crate::math::{add, finite, norm, scale, Interval, Point3}; use serde::{Deserialize, Serialize}; @@ -57,6 +58,10 @@ pub enum SurfaceGeometry { major_radius: f64, minor_radius: f64, }, + Wrapped { + wrap: Wrap, + surface: Box, + }, } #[derive(Clone, Copy, Debug)] @@ -86,6 +91,7 @@ impl SurfaceGeometry { | Self::Cylinder { frame, .. } | Self::Cone { frame, .. } | Self::Torus { frame, .. } => frame, + Self::Wrapped { wrap, .. } => &wrap.frame, } } pub(crate) fn validate(&self) -> Result<(), GeometryError> { @@ -110,6 +116,7 @@ impl SurfaceGeometry { && *minor_radius > 0.0 && major_radius > minor_radius } + Self::Wrapped { wrap, surface } => return wrapped_validate(wrap, surface), }; if !valid { return Err(GeometryError::InvalidGeometry( @@ -120,6 +127,10 @@ impl SurfaceGeometry { } fn check_uv(&self, uv: UV) -> Result<(), GeometryError> { + if let Self::Wrapped { wrap, surface } = self { + wrap.validate()?; + return surface.check_uv(uv); + } self.validate()?; for x in uv { finite(x)?; @@ -135,18 +146,15 @@ impl SurfaceGeometry { } pub(crate) fn parameter_in_domain(&self, uv: UV) -> bool { + if let Self::Wrapped { surface, .. } = self { + return surface.parameter_in_domain(uv); + } uv.into_iter().all(f64::is_finite) && (!matches!(self, Self::Sphere { .. }) || uv[1].abs() <= std::f64::consts::FRAC_PI_2) && (!matches!(self, Self::Cone { .. }) || uv[1] >= 0.0) } } -fn lift(angle: f64, hint: Option) -> f64 { - hint.map_or(angle, |h| { - angle + ((h - angle) / std::f64::consts::TAU).round() * std::f64::consts::TAU - }) -} - impl Surface for SurfaceGeometry { fn point_at(&self, uv: UV) -> Result { Ok(self.derivatives(uv)?.point) @@ -215,6 +223,7 @@ impl Surface for SurfaceGeometry { add(scale(radial, -a * cv), scale(z, -a * sv)), ) } + Self::Wrapped { wrap, surface } => return wrapped_derivatives(wrap, surface, uv), }; let f = self.frame(); Ok(SurfaceJet2 { @@ -245,6 +254,7 @@ impl Surface for SurfaceGeometry { -semi_angle.sin(), ] } + Self::Wrapped { wrap, surface } => return wrapped_normal(wrap, surface, uv), }; checked(self.frame().vector(n)) } @@ -257,6 +267,7 @@ impl Surface for SurfaceGeometry { finite(v)?; } } + let world = p; let p = checked(self.frame().local(p))?; let radial = p[0].hypot(p[1]); let u = lift(p[1].atan2(p[0]), hint.map(|h| h[0])); @@ -290,6 +301,7 @@ impl Surface for SurfaceGeometry { lift(p[2].atan2(radial - major_radius), hint.map(|h| h[1])), ] } + Self::Wrapped { wrap, surface } => return wrapped_project(wrap, surface, world, hint), }; for x in result { finite(x)?; @@ -304,6 +316,7 @@ impl Surface for SurfaceGeometry { Self::Cylinder { .. } => &CYLINDER_CHART, Self::Cone { .. } => &CONE_CHART, Self::Torus { .. } => &TORUS_CHART, + Self::Wrapped { surface, .. } => surface.charts(), } } @@ -351,6 +364,7 @@ impl Surface for SurfaceGeometry { v.sin()?.mul_interval(a)?, ] } + Self::Wrapped { wrap, surface } => return wrap.enclose(surface.enclose(domain)?), }; let bounds = self.frame().bounds(local)?; self.tightened(bounds, domain) @@ -390,3 +404,52 @@ impl SurfaceGeometry { Ok(bounds) } } + +fn wrapped_validate(wrap: &Wrap, surface: &SurfaceGeometry) -> Result<(), GeometryError> { + wrap.validate()?; + if matches!(surface, SurfaceGeometry::Wrapped { .. }) { + return Err(GeometryError::InvalidGeometry( + "a wrapped surface cannot wrap another wrapped surface".into(), + )); + } + surface.validate() +} + +fn wrapped_derivatives( + wrap: &Wrap, + surface: &SurfaceGeometry, + uv: UV, +) -> Result { + let f = surface.derivatives(uv)?; + let p = f.point; + let second = |a, b, d| add(wrap.second(p, a, b), wrap.push(p, d)); + Ok(SurfaceJet2 { + point: checked(wrap.point(p))?, + du: checked(wrap.push(p, f.du))?, + dv: checked(wrap.push(p, f.dv))?, + duu: checked(second(f.du, f.du, f.duu))?, + duv: checked(second(f.du, f.dv, f.duv))?, + dvv: checked(second(f.dv, f.dv, f.dvv))?, + }) +} + +fn wrapped_normal(wrap: &Wrap, surface: &SurfaceGeometry, uv: UV) -> Result { + let p = surface.point_at(uv)?; + if p[2] <= 0.0 { + return Err(GeometryError::SingularParameterization); + } + unit(wrap.push_normal(p, surface.normal_at(uv)?)) +} + +fn wrapped_project( + wrap: &Wrap, + surface: &SurfaceGeometry, + p: Point3, + hint: Option, +) -> Result { + let hint_x = match hint { + Some(h) => Some(surface.point_at(h)?[0]), + None => None, + }; + surface.project(wrap.flatten(p, hint_x)?, hint) +} diff --git a/main/opengeometry/src/brep/geometry/tests.rs b/main/opengeometry/src/brep/geometry/tests.rs index 817998be..de517ec0 100644 --- a/main/opengeometry/src/brep/geometry/tests.rs +++ b/main/opengeometry/src/brep/geometry/tests.rs @@ -1,5 +1,6 @@ use super::curve::CurveGeometry; use super::surface::{Surface, SurfaceGeometry}; +use super::wrap::Wrap; use crate::brep::error::GeometryError; use crate::brep::frame::Frame3; use crate::math::{dot, norm, scale, sub, Interval}; @@ -19,9 +20,107 @@ fn surfaces() -> Vec { major_radius: 3.0, minor_radius: 1.0, }, + SurfaceGeometry::Wrapped { + wrap: wrap(), + surface: Box::new(SurfaceGeometry::Cylinder { + frame: Frame3 { + origin: [1.0, 0.5, 3.0], + ..Frame3::IDENTITY + }, + radius: 0.8, + }), + }, + SurfaceGeometry::Wrapped { + wrap: wrap(), + surface: Box::new(SurfaceGeometry::Plane { + frame: Frame3::from_axis([0.3, -0.2, 2.5], [1.0, 2.0, 0.3], [0.0, 0.0, 1.0]) + .unwrap(), + }), + }, ] } +fn wrap() -> Wrap { + Wrap { + frame: Frame3::from_axis([3.0, -2.0, 7.0], [1.0, 2.0, 3.0], [0.0, 1.0, 0.0]).unwrap(), + radius: 2.6, + } +} + +#[test] +fn a_wrap_keeps_arc_length_on_its_radius_and_round_trips() { + let w = wrap(); + let p = [1.3, -0.4, 2.6]; + let chord = norm(sub(w.point(p), w.point([0.0, -0.4, 2.6]))); + assert!((chord - 2.0 * 2.6 * (1.3_f64 / 5.2).sin()).abs() < 1e-12); + let back = w.flatten(w.point(p), None).unwrap(); + assert!(norm(sub(back, p)) < 1e-12); + let far = [2.6 * 4.0, 0.0, 3.0]; + assert!(norm(sub(w.flatten(w.point(far), Some(far[0])).unwrap(), far)) < 1e-12); + let curve = CurveGeometry::Wrapped { + wrap: w, + curve: Box::new(CurveGeometry::Line { + origin: [0.0, 0.0, 2.0], + direction: [0.6, 0.8, 0.0], + }), + }; + let speed = |t: f64| norm(curve.elementary_tangent(t).unwrap()); + assert!((speed(0.1) - speed(3.0)).abs() < 1e-12); + let h = 1e-6; + let fd = scale( + sub( + curve.elementary_point(1.0 + h).unwrap(), + curve.elementary_point(1.0 - h).unwrap(), + ), + 0.5 / h, + ); + assert!(norm(sub(fd, curve.elementary_tangent(1.0).unwrap())) < 1e-8); + let bounds = curve + .elementary_bounds(Interval::new(0.0, 4.0).unwrap()) + .unwrap(); + for i in 0..=40 { + assert!(bounds.contains(curve.elementary_point(i as f64 / 10.0).unwrap())); + } + let nested = CurveGeometry::Wrapped { + wrap: w, + curve: Box::new(curve), + }; + assert!(nested.validate().is_err()); + let nested_surface = SurfaceGeometry::Wrapped { + wrap: w, + surface: Box::new(surfaces().pop().unwrap()), + }; + assert!(nested_surface.validate().is_err()); +} + +#[test] +fn wrapped_circle_samples_meet_their_chord_error() { + let curve = CurveGeometry::Wrapped { + wrap: wrap(), + curve: Box::new(CurveGeometry::Circle { + frame: Frame3 { + origin: [1.0, 0.5, 3.0], + ..Frame3::IDENTITY + }, + radius: 0.8, + }), + }; + let range = Interval::new(0.0, std::f64::consts::TAU).unwrap(); + let samples = curve.sample_elementary(range, 1e-3, 100_000).unwrap(); + let n = samples.len() - 1; + for i in 0..n { + for k in 1..8 { + let t = range.width() * (i as f64 + k as f64 / 8.0) / n as f64; + let p = curve.elementary_point(t).unwrap(); + let (a, b) = (samples[i], samples[i + 1]); + let ab = sub(b, a); + let along = dot(sub(p, a), ab) / dot(ab, ab); + let foot = [0, 1, 2].map(|j| a[j] + along * ab[j]); + assert!(norm(sub(p, foot)) <= 1e-3); + } + } +} + #[test] fn derivatives_projection_and_normals_for_all_families() { let uv = [0.7, 0.4]; diff --git a/main/opengeometry/src/brep/geometry/turned.rs b/main/opengeometry/src/brep/geometry/turned.rs index c19ddfe1..5f0953cf 100644 --- a/main/opengeometry/src/brep/geometry/turned.rs +++ b/main/opengeometry/src/brep/geometry/turned.rs @@ -2,6 +2,12 @@ use crate::brep::error::GeometryError; use crate::brep::frame::{Frame3, UVBox}; use crate::math::Interval; +pub(super) fn lift(angle: f64, hint: Option) -> f64 { + hint.map_or(angle, |h| { + angle + ((h - angle) / std::f64::consts::TAU).round() * std::f64::consts::TAU + }) +} + pub(super) fn reached(frame: &Frame3, axis: usize) -> usize { [frame.x[axis], frame.y[axis], frame.z[axis]] .iter() diff --git a/main/opengeometry/src/brep/geometry/wrap.rs b/main/opengeometry/src/brep/geometry/wrap.rs new file mode 100644 index 00000000..f8038810 --- /dev/null +++ b/main/opengeometry/src/brep/geometry/wrap.rs @@ -0,0 +1,86 @@ +use super::turned::lift; +use crate::brep::bounds::PatchBounds; +use crate::brep::error::GeometryError; +use crate::brep::frame::{checked, Frame3}; +use crate::math::{add, scale, Interval, Point3}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Wrap { + pub frame: Frame3, + pub radius: f64, +} + +impl Wrap { + pub(crate) fn validate(&self) -> Result<(), GeometryError> { + self.frame.validate()?; + (self.radius.is_finite() && self.radius > 0.0) + .then_some(()) + .ok_or_else(|| GeometryError::InvalidGeometry("wrap radius must be positive".into())) + } + + fn axes(&self, p: Point3) -> [Point3; 3] { + let (s, c) = (p[0] / self.radius).sin_cos(); + [[-s, c, 0.0], [0.0, 0.0, 1.0], [c, s, 0.0]] + } + + pub fn point(&self, p: Point3) -> Point3 { + let (s, c) = (p[0] / self.radius).sin_cos(); + self.frame.point([p[2] * c, p[2] * s, p[1]]) + } + + pub(crate) fn push(&self, p: Point3, v: Point3) -> Point3 { + let [t, a, r] = self.axes(p); + let local = add( + add(scale(t, v[0] * p[2] / self.radius), scale(a, v[1])), + scale(r, v[2]), + ); + self.frame.vector(local) + } + + pub(crate) fn second(&self, p: Point3, a: Point3, b: Point3) -> Point3 { + let [t, _, r] = self.axes(p); + let xx = scale(r, -p[2] / (self.radius * self.radius)); + let xz = scale(t, 1.0 / self.radius); + self.frame.vector(add( + scale(xx, a[0] * b[0]), + scale(xz, a[0] * b[2] + a[2] * b[0]), + )) + } + + pub(crate) fn push_normal(&self, p: Point3, n: Point3) -> Point3 { + let [t, a, r] = self.axes(p); + let k = p[2] / self.radius; + self.frame.vector(add( + add(scale(t, n[0]), scale(a, k * n[1])), + scale(r, k * n[2]), + )) + } + + pub fn flatten(&self, q: Point3, hint_x: Option) -> Result { + let l = self.frame.local(q); + let z = l[0].hypot(l[1]); + if z == 0.0 { + return Err(GeometryError::SingularParameterization); + } + let angle = lift(l[1].atan2(l[0]), hint_x.map(|x| x / self.radius)); + checked([angle * self.radius, l[2], z]) + } + + pub(crate) fn enclose(&self, flat: PatchBounds) -> Result { + let [x, y, z] = flat.axes; + let angle = x.mul_interval(Interval::point(1.0 / self.radius)?)?; + let (c, s) = (angle.cos()?, angle.sin()?); + self.frame + .bounds([c.mul_interval(z)?, s.mul_interval(z)?, y]) + } + + pub(crate) fn derivative_bounds(&self, z_max: f64) -> (f64, f64) { + let z = z_max.abs(); + ( + (z / self.radius).max(1.0), + z / (self.radius * self.radius) + 1.0 / self.radius, + ) + } +} diff --git a/main/opengeometry/src/brep/mod.rs b/main/opengeometry/src/brep/mod.rs index 3978682a..f166284d 100644 --- a/main/opengeometry/src/brep/mod.rs +++ b/main/opengeometry/src/brep/mod.rs @@ -21,8 +21,10 @@ pub use bounds::PatchBounds; pub(super) use builder::{ boundary, dimensions, padded_uv_bounds, plane_boundary, uv_line, Builder, Use, WireUse, }; -pub(super) use error::coverage_gap; +#[cfg(test)] +pub(super) use builder::{wrapped_sector, SECTOR}; pub use error::GeometryError; +pub(super) use error::{coverage_gap, unsupported_wrapped}; pub use frame::Frame3; pub(super) use frame::{checked, unit, UVBox, GROUND, UV}; pub(super) use geometry::{ @@ -31,7 +33,7 @@ pub(super) use geometry::{ }; pub use geometry::{ CurveGeometry, GeometryStore, IntersectionPoint, Surface, SurfaceChart, SurfaceGeometry, - SurfaceJet2, + SurfaceJet2, Wrap, }; pub(super) use pcurve::{period_lifted_uv, IntersectionSide, PcurveGeometry}; pub use placement::{mirror::mirrored, placed}; diff --git a/main/opengeometry/src/brep/placement/mirror.rs b/main/opengeometry/src/brep/placement/mirror.rs index b277334c..dc3fc537 100644 --- a/main/opengeometry/src/brep/placement/mirror.rs +++ b/main/opengeometry/src/brep/placement/mirror.rs @@ -1,5 +1,5 @@ use super::check_coordinate_precision; -use crate::brep::error::GeometryError; +use crate::brep::error::{unsupported_wrapped, GeometryError}; use crate::brep::frame::{checked, unit, Frame3, UV}; use crate::brep::geometry::{CurveGeometry, IntersectionDefinition, SurfaceGeometry}; use crate::brep::pcurve::PcurveGeometry; @@ -107,8 +107,8 @@ pub fn mirrored( .revision .checked_add(1) .ok_or_else(|| GeometryError::LimitExceeded("BRep revision overflow".into()))?; - reflect_surfaces(&mut output.geometry.surfaces, reflection); - reflect_curves(&mut output.geometry.curves, reflection); + reflect_surfaces(&mut output.geometry.surfaces, reflection)?; + reflect_curves(&mut output.geometry.curves, reflection)?; for vertex in &mut output.topology.vertices { vertex.position = reflection.point(vertex.position); } @@ -129,7 +129,10 @@ pub fn mirrored( Ok(output) } -fn reflect_surfaces(surfaces: &mut [SurfaceGeometry], reflection: Reflection) { +fn reflect_surfaces( + surfaces: &mut [SurfaceGeometry], + reflection: Reflection, +) -> Result<(), GeometryError> { for surface in surfaces { match surface { SurfaceGeometry::Plane { frame } @@ -137,11 +140,16 @@ fn reflect_surfaces(surfaces: &mut [SurfaceGeometry], reflection: Reflection) { | SurfaceGeometry::Cylinder { frame, .. } | SurfaceGeometry::Cone { frame, .. } | SurfaceGeometry::Torus { frame, .. } => *frame = reflection.surface_frame(*frame), + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("mirroring")), } } + Ok(()) } -fn reflect_curves(curves: &mut [CurveGeometry], reflection: Reflection) { +fn reflect_curves( + curves: &mut [CurveGeometry], + reflection: Reflection, +) -> Result<(), GeometryError> { for curve in curves { match curve { CurveGeometry::Line { origin, direction } => { @@ -152,8 +160,10 @@ fn reflect_curves(curves: &mut [CurveGeometry], reflection: Reflection) { *frame = reflection.curve_frame(*frame) } CurveGeometry::Intersection { .. } => {} + CurveGeometry::Wrapped { .. } => return Err(unsupported_wrapped("mirroring")), } } + Ok(()) } fn remap_pcurves(input: &BrepEnvelope, output: &mut BrepEnvelope) { diff --git a/main/opengeometry/src/brep/placement/mod.rs b/main/opengeometry/src/brep/placement/mod.rs index 3356992b..1e2d09b6 100644 --- a/main/opengeometry/src/brep/placement/mod.rs +++ b/main/opengeometry/src/brep/placement/mod.rs @@ -2,7 +2,7 @@ pub(super) mod mirror; #[cfg(test)] mod tests; -use super::error::GeometryError; +use super::error::{unsupported_wrapped, GeometryError}; use super::frame::Frame3; use super::geometry::{CurveGeometry, IntersectionDefinition, SurfaceGeometry}; use super::pcurve::PcurveGeometry; @@ -21,6 +21,9 @@ pub fn placed( "placement scale must be positive and finite".into(), )); } + if scale != 1.0 && input.geometry.has_wrapped() { + return Err(unsupported_wrapped("scaling")); + } let mut output = input.clone(); output.revision = input .revision @@ -107,6 +110,9 @@ fn place_surfaces(surfaces: &mut [SurfaceGeometry], placement: Frame3, scale: f6 *major_radius *= scale; *minor_radius *= scale; } + SurfaceGeometry::Wrapped { wrap, .. } => { + transform_frame(&mut wrap.frame, placement, 1.0) + } } } } @@ -139,6 +145,7 @@ fn place_curves(curves: &mut [CurveGeometry], placement: Frame3, scale: f64) { *minor_radius *= scale; } CurveGeometry::Intersection { .. } => {} + CurveGeometry::Wrapped { wrap, .. } => transform_frame(&mut wrap.frame, placement, 1.0), } } } diff --git a/main/opengeometry/src/brep/placement/tests.rs b/main/opengeometry/src/brep/placement/tests.rs index 2afa0728..04bd1780 100644 --- a/main/opengeometry/src/brep/placement/tests.rs +++ b/main/opengeometry/src/brep/placement/tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::brep::geometry::{Curve, Surface}; use crate::brep::topology::coarse_accuracy; +use crate::brep::wrapped_sector; use crate::math::sub; use crate::primitives; @@ -125,3 +126,31 @@ fn invalid_scale_revision_overflow_and_precision_are_structured_errors() { Err(GeometryError::LimitExceeded(_)) )); } + +#[test] +fn wrapped_geometry_moves_rigidly_and_refuses_scaling_and_mirroring() { + let brep = wrapped_sector(); + let placement = Frame3::from_axis([10.0, -3.0, 5.0], [1.0, 2.0, 3.0], [0.0, 1.0, 0.0]).unwrap(); + let moved = placed(&brep, placement, 1.0).unwrap(); + let slant = brep.topology.faces.len() - 1; + let surface = |b: &BrepEnvelope| { + b.geometry + .surface(b.topology.faces[slant].surface) + .unwrap() + .clone() + }; + let uv = [1.7, 2.9]; + let expected = placement.point(surface(&brep).point_at(uv).unwrap()); + assert!(norm(sub(surface(&moved).point_at(uv).unwrap(), expected)) < 1e-12); + for (before, after) in brep.topology.vertices.iter().zip(&moved.topology.vertices) { + assert!(norm(sub(placement.point(before.position), after.position)) < 1e-12); + } + assert!(matches!( + placed(&brep, placement, 2.0), + Err(GeometryError::UnsupportedGeometry(_)) + )); + assert!(matches!( + mirror::mirrored(&brep, [0.0; 3], [1.0, 0.0, 0.0]), + Err(GeometryError::UnsupportedGeometry(_)) + )); +} diff --git a/main/opengeometry/src/exchange/fit_bezier/mod.rs b/main/opengeometry/src/exchange/fit_bezier/mod.rs new file mode 100644 index 00000000..3b0c41ce --- /dev/null +++ b/main/opengeometry/src/exchange/fit_bezier/mod.rs @@ -0,0 +1,252 @@ +#[cfg(test)] +mod tests; + +use crate::brep::{CurveGeometry, GeometryError, Surface, SurfaceGeometry, SurfaceJet2, UVBox}; +use crate::math::{norm, sub, Interval, Point3}; + +const SAMPLES: usize = 16; +const MAX_DEPTH: usize = 40; + +#[derive(Clone, Debug)] +pub(super) struct FittedCurve { + pub(super) controls: Vec, + pub(super) knots: Vec, + pub(super) multiplicities: Vec, + pub(super) error_bound: f64, +} + +#[derive(Clone, Debug)] +pub(super) struct FittedSurface { + pub(super) controls: Vec>, + pub(super) knots: [Vec; 2], + pub(super) multiplicities: [Vec; 2], + pub(super) error_bound: f64, +} + +struct Grid { + values: [Vec; 2], + step: [f64; 2], +} + +fn bernstein(t: f64) -> [f64; 4] { + let s = 1.0 - t; + [s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t] +} + +fn knot_vector(values: &[f64]) -> (Vec, Vec) { + let last = values.len() - 1; + let multiplicities = (0..=last) + .map(|i| if i == 0 || i == last { 4 } else { 3 }) + .collect(); + (values.to_vec(), multiplicities) +} + +fn positive_budget(tolerance: f64, budget: usize) -> Result<(), GeometryError> { + if !tolerance.is_finite() || tolerance <= 0.0 || budget == 0 { + return Err(GeometryError::InvalidGeometry( + "B-spline fitting requires a positive tolerance and budget".into(), + )); + } + Ok(()) +} + +fn hermite_span( + curve: &CurveGeometry, + a: f64, + b: f64, +) -> Result<([Point3; 4], f64), GeometryError> { + let h = b - a; + let (p0, p3) = (curve.elementary_point(a)?, curve.elementary_point(b)?); + let (t0, t1) = (curve.elementary_tangent(a)?, curve.elementary_tangent(b)?); + let c = [ + p0, + std::array::from_fn(|i| p0[i] + t0[i] * h / 3.0), + std::array::from_fn(|i| p3[i] - t1[i] * h / 3.0), + p3, + ]; + let mut error: f64 = 0.0; + for k in 1..SAMPLES { + let s = k as f64 / SAMPLES as f64; + let w = bernstein(s); + let fitted: Point3 = std::array::from_fn(|i| (0..4).map(|j| w[j] * c[j][i]).sum()); + error = error.max(norm(sub(fitted, curve.elementary_point(a + s * h)?))); + } + Ok((c, error)) +} + +pub(super) fn fit_curve( + curve: &CurveGeometry, + range: Interval, + tolerance: f64, + max_segments: usize, +) -> Result { + positive_budget(tolerance, max_segments)?; + let mut stack = vec![(range.lo, range.hi, 0usize)]; + let mut accepted = Vec::new(); + while let Some((a, b, depth)) = stack.pop() { + let (c, error) = hermite_span(curve, a, b)?; + if error <= tolerance { + accepted.push((a, c, error)); + continue; + } + if depth >= MAX_DEPTH || accepted.len() + stack.len() + 2 > max_segments { + return Err(GeometryError::LimitExceeded( + "B-spline curve fit spans".into(), + )); + } + let m = 0.5 * (a + b); + stack.push((m, b, depth + 1)); + stack.push((a, m, depth + 1)); + } + accepted.sort_by(|x, y| x.0.total_cmp(&y.0)); + let mut controls = Vec::with_capacity(accepted.len() * 3 + 1); + let mut error_bound: f64 = 0.0; + for (index, (_, c, error)) in accepted.iter().enumerate() { + controls.extend_from_slice(&c[usize::from(index > 0)..]); + error_bound = error_bound.max(*error); + } + let mut values: Vec = accepted.iter().map(|x| x.0).collect(); + values.push(range.hi); + let (knots, multiplicities) = knot_vector(&values); + Ok(FittedCurve { + controls, + knots, + multiplicities, + error_bound, + }) +} + +fn grid(domain: UVBox, n: [usize; 2]) -> Grid { + Grid { + values: std::array::from_fn(|axis| { + (0..=n[axis]) + .map(|i| domain[axis].lo + domain[axis].width() * i as f64 / n[axis] as f64) + .collect() + }), + step: std::array::from_fn(|axis| domain[axis].width() / n[axis] as f64), + } +} + +fn corner(index: usize) -> (usize, f64) { + match index % 3 { + 0 => (index / 3, 0.0), + 1 => (index / 3, 1.0), + _ => (index / 3 + 1, -1.0), + } +} + +fn grid_jets( + surface: &SurfaceGeometry, + grid: &Grid, +) -> Result>, GeometryError> { + grid.values[0] + .iter() + .map(|&u| { + grid.values[1] + .iter() + .map(|&v| Ok((surface.point_at([u, v])?, surface.derivatives([u, v])?))) + .collect() + }) + .collect() +} + +fn hermite_controls( + surface: &SurfaceGeometry, + grid: &Grid, +) -> Result>, GeometryError> { + let jets = grid_jets(surface, grid)?; + let [nu, nv] = grid.values.each_ref().map(|v| v.len() - 1); + Ok((0..=3 * nu) + .map(|i| { + (0..=3 * nv) + .map(|j| { + let ((ci, su), (cj, sv)) = (corner(i), corner(j)); + let (p, jet) = &jets[ci][cj]; + let (a, b) = (su * grid.step[0] / 3.0, sv * grid.step[1] / 3.0); + std::array::from_fn(|k| { + p[k] + a * jet.du[k] + b * jet.dv[k] + a * b * jet.duv[k] + }) + }) + .collect() + }) + .collect()) +} + +fn patch_point(controls: &[Vec], patch: [usize; 2], s: f64, t: f64) -> Point3 { + let (wu, wv) = (bernstein(s), bernstein(t)); + std::array::from_fn(|k| { + (0..4) + .map(|x| { + (0..4) + .map(|y| wu[x] * wv[y] * controls[3 * patch[0] + x][3 * patch[1] + y][k]) + .sum::() + }) + .sum() + }) +} + +fn patch_errors( + surface: &SurfaceGeometry, + grid: &Grid, + controls: &[Vec], +) -> Result<[f64; 3], GeometryError> { + let mut errors = [0.0_f64; 3]; + for pi in 0..grid.values[0].len() - 1 { + for pj in 0..grid.values[1].len() - 1 { + for a in 0..=SAMPLES { + for b in 0..=SAMPLES { + let (s, t) = (a as f64 / SAMPLES as f64, b as f64 / SAMPLES as f64); + let exact = surface.point_at([ + grid.values[0][pi] + s * grid.step[0], + grid.values[1][pj] + t * grid.step[1], + ])?; + let e = norm(sub(patch_point(controls, [pi, pj], s, t), exact)); + errors[0] = errors[0].max(e); + if b == 0 || b == SAMPLES { + errors[1] = errors[1].max(e); + } + if a == 0 || a == SAMPLES { + errors[2] = errors[2].max(e); + } + } + } + } + } + Ok(errors) +} + +pub(super) fn fit_surface( + surface: &SurfaceGeometry, + domain: UVBox, + tolerance: f64, + max_patches: usize, +) -> Result { + positive_budget(tolerance, max_patches)?; + let mut n = [1usize, 1usize]; + loop { + let grid = grid(domain, n); + let controls = hermite_controls(surface, &grid)?; + let [error, along_u, along_v] = patch_errors(surface, &grid, &controls)?; + if error <= tolerance { + let [ku, kv] = grid.values.each_ref().map(|v| knot_vector(v)); + return Ok(FittedSurface { + controls, + knots: [ku.0, kv.0], + multiplicities: [ku.1, kv.1], + error_bound: error, + }); + } + let (refine_u, refine_v) = (along_u > tolerance / 4.0, along_v > tolerance / 4.0); + if refine_u || !refine_v { + n[0] *= 2; + } + if refine_v || !refine_u { + n[1] *= 2; + } + if n[0].saturating_mul(n[1]) > max_patches { + return Err(GeometryError::LimitExceeded( + "B-spline surface fit patches".into(), + )); + } + } +} diff --git a/main/opengeometry/src/exchange/fit_bezier/tests.rs b/main/opengeometry/src/exchange/fit_bezier/tests.rs new file mode 100644 index 00000000..cd30e15b --- /dev/null +++ b/main/opengeometry/src/exchange/fit_bezier/tests.rs @@ -0,0 +1,72 @@ +use super::*; +use crate::brep::{Frame3, Wrap}; + +fn wrap() -> Wrap { + Wrap { + frame: Frame3::from_axis([1.0, 2.0, 3.0], [0.2, 1.0, 0.1], [1.0, 0.0, 0.3]).unwrap(), + radius: 2.8, + } +} + +#[test] +fn a_wrapped_plane_fits_within_tolerance_between_its_samples() { + let surface = SurfaceGeometry::Wrapped { + wrap: wrap(), + surface: Box::new(SurfaceGeometry::Plane { + frame: Frame3::from_axis([0.0; 3], [0.24, -0.97, 0.0], [0.97, 0.24, 0.0]).unwrap(), + }), + }; + let domain = [ + Interval::new(-0.4, 4.5).unwrap(), + Interval::new(1.8, 4.2).unwrap(), + ]; + let fitted = fit_surface(&surface, domain, 1e-6, 1 << 16).unwrap(); + assert!(fitted.error_bound <= 1e-6); + let n = [ + (fitted.controls.len() - 1) / 3, + (fitted.controls[0].len() - 1) / 3, + ]; + let step = [0, 1].map(|i| domain[i].width() / n[i] as f64); + for i in 0..n[0] { + for j in 0..n[1] { + for (s, t) in [(0.37, 0.61), (0.93, 0.05), (0.5, 0.5)] { + let uv = [ + domain[0].lo + (i as f64 + s) * step[0], + domain[1].lo + (j as f64 + t) * step[1], + ]; + let p = patch_point(&fitted.controls, [i, j], s, t); + assert!(norm(sub(p, surface.point_at(uv).unwrap())) <= 2e-6); + } + } + } + assert_eq!(fitted.knots[0].len(), n[0] + 1); + assert_eq!(fitted.multiplicities[1].iter().sum::(), 3 * n[1] + 5); +} + +#[test] +fn a_wrapped_circle_fits_with_joined_spans() { + let curve = CurveGeometry::Wrapped { + wrap: wrap(), + curve: Box::new(CurveGeometry::Circle { + frame: Frame3 { + origin: [1.0, 5.0, 3.0], + ..Frame3::IDENTITY + }, + radius: 0.5, + }), + }; + let range = Interval::new(0.0, std::f64::consts::TAU).unwrap(); + let fitted = fit_curve(&curve, range, 1e-6, 20_000).unwrap(); + assert!(fitted.error_bound <= 1e-6); + let spans = fitted.knots.len() - 1; + assert_eq!(fitted.controls.len(), 3 * spans + 1); + assert!(norm(sub(fitted.controls[0], fitted.controls[3 * spans])) < 1e-12); + for k in 0..spans { + let (a, b) = (fitted.knots[k], fitted.knots[k + 1]); + let w = bernstein(0.3); + let p: Point3 = + std::array::from_fn(|i| (0..4).map(|j| w[j] * fitted.controls[3 * k + j][i]).sum()); + let exact = curve.elementary_point(a + 0.3 * (b - a)).unwrap(); + assert!(norm(sub(p, exact)) <= 2e-6); + } +} diff --git a/main/opengeometry/src/exchange/ifc_body/curves.rs b/main/opengeometry/src/exchange/ifc_body/curves.rs index 2fcdbd0a..24d88c5a 100644 --- a/main/opengeometry/src/exchange/ifc_body/curves.rs +++ b/main/opengeometry/src/exchange/ifc_body/curves.rs @@ -1,5 +1,7 @@ use super::entities::{enumeration, real, reals, reference, references, Entities, IfcValue}; -use crate::brep::{BrepEnvelope, CurveGeometry, EdgeGeometry, GeometryError, Orientation}; +use crate::brep::{ + unsupported_wrapped, BrepEnvelope, CurveGeometry, EdgeGeometry, GeometryError, Orientation, +}; use crate::exchange::fit_curve::{fit_intersection_curve, FittedIntersectionCurve}; use crate::math::{norm, Point3}; use std::f64::consts::TAU; @@ -133,6 +135,7 @@ fn add_curve(entities: &mut Entities, geometry: &CurveGeometry) -> Result return Err(unsupported_wrapped("IFC export")), }) } diff --git a/main/opengeometry/src/exchange/ifc_body/surfaces.rs b/main/opengeometry/src/exchange/ifc_body/surfaces.rs index 3c69c9f0..4b34896c 100644 --- a/main/opengeometry/src/exchange/ifc_body/surfaces.rs +++ b/main/opengeometry/src/exchange/ifc_body/surfaces.rs @@ -1,5 +1,5 @@ use super::entities::{enumeration, real, reference, references, Entities, IfcValue}; -use crate::brep::{BrepEnvelope, Frame3, GeometryError, SurfaceGeometry}; +use crate::brep::{unsupported_wrapped, BrepEnvelope, Frame3, GeometryError, SurfaceGeometry}; use crate::math::scale; pub(super) fn add_surfaces( @@ -39,6 +39,7 @@ pub(super) fn add_surfaces( let height = cone_height(brep, face.surface)?; add_cone(entities, *frame, *semi_angle, height) } + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("IFC export")), }; surfaces[face.surface as usize] = Some(id); } diff --git a/main/opengeometry/src/exchange/mod.rs b/main/opengeometry/src/exchange/mod.rs index 346095bb..a8a33bb1 100644 --- a/main/opengeometry/src/exchange/mod.rs +++ b/main/opengeometry/src/exchange/mod.rs @@ -1,3 +1,4 @@ +mod fit_bezier; mod fit_curve; pub mod ifc_body; mod part21; diff --git a/main/opengeometry/src/exchange/preflight.rs b/main/opengeometry/src/exchange/preflight.rs index 97fa7bbe..7384e593 100644 --- a/main/opengeometry/src/exchange/preflight.rs +++ b/main/opengeometry/src/exchange/preflight.rs @@ -51,6 +51,10 @@ pub(super) fn preflight(brep: &BrepEnvelope, scale: f64) -> Result { &frame.origin } + CurveGeometry::Wrapped { wrap, .. } => { + frame_bound = frame_bound.max(frame_error(wrap.frame, bounds)?); + &wrap.frame.origin + } CurveGeometry::Intersection { .. } => continue, }; magnitude = origin.iter().fold(magnitude, |m, v| m.max(v.abs())); @@ -219,6 +223,11 @@ fn surface_numbers(surface: &SurfaceGeometry, numbers: &mut Vec) { frame_numbers(*frame, numbers); numbers.extend([major_radius, minor_radius]); } + SurfaceGeometry::Wrapped { wrap, surface } => { + frame_numbers(wrap.frame, numbers); + numbers.push(wrap.radius); + surface_numbers(surface, numbers); + } } } @@ -243,6 +252,11 @@ fn curve_numbers(curve: &CurveGeometry, numbers: &mut Vec) { numbers.extend([major_radius, minor_radius]); } CurveGeometry::Intersection { definition } => index_numbers([definition], numbers), + CurveGeometry::Wrapped { wrap, curve } => { + frame_numbers(wrap.frame, numbers); + numbers.push(wrap.radius); + curve_numbers(curve, numbers); + } } } diff --git a/main/opengeometry/src/exchange/step/body.rs b/main/opengeometry/src/exchange/step/body.rs index 9166646b..fd97fb9b 100644 --- a/main/opengeometry/src/exchange/step/body.rs +++ b/main/opengeometry/src/exchange/step/body.rs @@ -1,6 +1,7 @@ use super::entities::{ bspline_curve, curve, pcurve, placement, point, real, refs, sense, PcurveOnSurface, }; +use super::wrapped::{add_wrapped_curve, add_wrapped_surface}; use crate::brep::{ BrepEnvelope, CurveGeometry, Edge, EdgeGeometry, GeometryError, Orientation, PcurveGeometry, SurfaceGeometry, @@ -42,7 +43,7 @@ pub(super) fn emit_body( mut exchange_bound: f64, ) -> Result { let fitting_budget = brep.accuracy.exchange - exchange_bound; - let surfaces = add_surfaces(writer, brep, scale)?; + let (surfaces, surface_error) = add_surfaces(writer, brep, scale, fitting_budget)?; let vertices = add_vertices(writer, brep, scale); let body_entities = BodyEntities { brep, @@ -58,7 +59,7 @@ pub(super) fn emit_body( pcurveless_edges, } = add_edges(writer, &body_entities, fitting_budget)?; let (faces, collapsed_chart_uses) = add_faces(writer, brep, &surfaces, &vertices, &edges)?; - exchange_bound += fitting_error; + exchange_bound += fitting_error.max(surface_error); if exchange_bound > brep.accuracy.exchange { return Err(GeometryError::LimitExceeded( "STEP fitted curves exceed the exchange error budget".into(), @@ -86,13 +87,22 @@ fn add_surfaces( writer: &mut Part21Writer, brep: &BrepEnvelope, scale: f64, -) -> Result>, GeometryError> { + fitting_budget: f64, +) -> Result<(Vec>, f64), GeometryError> { let mut surfaces = vec![None; brep.geometry.surfaces.len()]; + let mut fitting_error: f64 = 0.0; for face in &brep.topology.faces { if surfaces[face.surface as usize].is_some() { continue; } let surface = brep.geometry.surface(face.surface)?; + if let SurfaceGeometry::Wrapped { .. } = surface { + let (id, error) = + add_wrapped_surface(writer, brep, face.surface, fitting_budget, scale)?; + fitting_error = fitting_error.max(error); + surfaces[face.surface as usize] = Some(id); + continue; + } let frame = placement(writer, *surface.frame(), scale); let expression = match surface { SurfaceGeometry::Plane { .. } => format!("PLANE('',#{frame})"), @@ -114,10 +124,15 @@ fn add_surfaces( real(major_radius * scale), real(minor_radius * scale) ), + SurfaceGeometry::Wrapped { .. } => { + return Err(GeometryError::UnsupportedGeometry( + "STEP wrapped surfaces require B-spline fitting".into(), + )) + } }; surfaces[face.surface as usize] = Some(writer.add_entity(expression)); } - Ok(surfaces) + Ok((surfaces, fitting_error)) } fn add_vertices(writer: &mut Part21Writer, brep: &BrepEnvelope, scale: f64) -> Vec { @@ -176,6 +191,12 @@ fn add_edges( fitted_curves[curve_id as usize] = Some(fitted); id } + CurveGeometry::Wrapped { .. } => { + let (id, error) = + add_wrapped_curve(writer, brep, curve_id, fitting_budget, scale)?; + fitting_error = fitting_error.max(error); + id + } _ => curve(writer, geometry, scale)?, }; curves[curve_id as usize] = Some(id); diff --git a/main/opengeometry/src/exchange/step/entities.rs b/main/opengeometry/src/exchange/step/entities.rs index b8d863b7..5f22753d 100644 --- a/main/opengeometry/src/exchange/step/entities.rs +++ b/main/opengeometry/src/exchange/step/entities.rs @@ -46,7 +46,7 @@ pub(super) fn curve( real(minor_radius * scale) )) } - CurveGeometry::Intersection { .. } => { + CurveGeometry::Intersection { .. } | CurveGeometry::Wrapped { .. } => { return Err(GeometryError::UnsupportedGeometry( "STEP numerical branches require bounded export-curve fitting".into(), )) diff --git a/main/opengeometry/src/exchange/step/mod.rs b/main/opengeometry/src/exchange/step/mod.rs index 1d5fbeeb..24504180 100644 --- a/main/opengeometry/src/exchange/step/mod.rs +++ b/main/opengeometry/src/exchange/step/mod.rs @@ -4,6 +4,7 @@ mod product; mod report; #[cfg(test)] mod tests; +mod wrapped; pub(crate) use report::{StepBodyInput, StepExportFailure}; pub use report::{StepBodyReport, StepExportReport, StepReport, StepSkipped}; diff --git a/main/opengeometry/src/exchange/step/tests.rs b/main/opengeometry/src/exchange/step/tests.rs index de60a67d..686b0b72 100644 --- a/main/opengeometry/src/exchange/step/tests.rs +++ b/main/opengeometry/src/exchange/step/tests.rs @@ -1,7 +1,7 @@ use super::{export_bodies, export_step, StepBodyInput}; use crate::brep::{ - coarse_accuracy, BrepEnvelope, Curve, CurveGeometry, EdgeGeometry, Frame3, GeometryError, - IntersectionDefinition, IntersectionSide, PcurveGeometry, Surface, TraceAnchor, + coarse_accuracy, wrapped_sector, BrepEnvelope, Curve, CurveGeometry, EdgeGeometry, Frame3, + GeometryError, IntersectionDefinition, IntersectionSide, PcurveGeometry, Surface, TraceAnchor, }; use crate::math::Interval; use crate::operations::modifying::boolean::{boolean_spheres, BooleanOp}; @@ -224,3 +224,14 @@ fn two_million_entity_export_is_limit_exceeded() { matches!(&failure.error, GeometryError::LimitExceeded(message) if message.contains("2,000,000")) ); } + +#[test] +fn a_wrapped_side_exports_as_b_splines_within_the_exchange_budget() { + let brep = wrapped_sector(); + let (step, report) = export_step(&brep, "metre").unwrap(); + assert_eq!(report.faces, 6); + assert_eq!(step.matches("B_SPLINE_SURFACE_WITH_KNOTS").count(), 1); + assert_eq!(step.matches("B_SPLINE_CURVE_WITH_KNOTS").count(), 2); + assert_eq!(step.matches("=PCURVE(").count(), 2 * report.edges); + assert!(report.exchange_error_bound <= brep.accuracy.exchange); +} diff --git a/main/opengeometry/src/exchange/step/wrapped.rs b/main/opengeometry/src/exchange/step/wrapped.rs new file mode 100644 index 00000000..7b80fad0 --- /dev/null +++ b/main/opengeometry/src/exchange/step/wrapped.rs @@ -0,0 +1,126 @@ +use super::entities::{bspline_curve, point, real, refs}; +use crate::brep::{BrepEnvelope, EdgeGeometry, GeometryError, Surface, SurfaceGeometry, UVBox}; +use crate::exchange::fit_bezier::{fit_curve, fit_surface, FittedSurface}; +use crate::exchange::part21::Part21Writer; +use crate::math::Interval; + +const MAX_PATCHES: usize = 1 << 16; +const MAX_SPANS: usize = 20_000; +const REACH: f64 = 0.1; + +fn check_budget(budget: f64) -> Result<(), GeometryError> { + if budget <= 0.0 { + return Err(GeometryError::LimitExceeded( + "STEP exchange budget leaves no room for wrapped geometry fitting".into(), + )); + } + Ok(()) +} + +pub(super) fn add_wrapped_surface( + writer: &mut Part21Writer, + brep: &BrepEnvelope, + surface_id: u32, + budget: f64, + scale: f64, +) -> Result<(usize, f64), GeometryError> { + check_budget(budget)?; + let surface = brep.geometry.surface(surface_id)?; + let domain = reached_past(surface, support_domain(brep, surface_id)?)?; + let fitted = fit_surface(surface, domain, budget, MAX_PATCHES)?; + Ok((bspline_surface(writer, &fitted, scale), fitted.error_bound)) +} + +fn support_domain(brep: &BrepEnvelope, surface_id: u32) -> Result { + brep.topology + .faces + .iter() + .filter(|face| face.surface == surface_id) + .map(|face| face.trim.uv_bounds) + .reduce(|a, b| [a[0].hull(b[0]), a[1].hull(b[1])]) + .ok_or_else(|| GeometryError::InvalidTopology("STEP surface has no face".into())) +} + +fn reached_past(surface: &SurfaceGeometry, domain: UVBox) -> Result { + let periods = surface + .charts() + .first() + .ok_or_else(|| GeometryError::InvalidGeometry("surface has no parameter chart".into()))? + .periods; + let side = |i: usize| { + let d = domain[i]; + let room = periods[i].map_or(f64::INFINITY, |p| (p - d.width()) / 2.0); + let pad = (REACH * d.width()).min(0.9 * room.max(0.0)); + Interval::new(d.lo - pad, d.hi + pad) + }; + Ok([side(0)?, side(1)?]) +} + +fn bspline_surface(writer: &mut Part21Writer, fitted: &FittedSurface, scale: f64) -> usize { + let rows = fitted + .controls + .iter() + .map(|row| { + let ids: Vec<_> = row + .iter() + .map(|p| point(writer, &p.map(|v| v * scale))) + .collect(); + format!("({})", refs(&ids)) + }) + .collect::>() + .join(","); + let list = |values: &[usize]| { + values + .iter() + .map(usize::to_string) + .collect::>() + .join(",") + }; + let reals = |values: &[f64]| { + values + .iter() + .map(|v| real(*v)) + .collect::>() + .join(",") + }; + writer.add_entity(format!( + "B_SPLINE_SURFACE_WITH_KNOTS('',3,3,({rows}),.UNSPECIFIED.,.F.,.F.,.F.,({}),({}),({}),({}),.UNSPECIFIED.)", + list(&fitted.multiplicities[0]), + list(&fitted.multiplicities[1]), + reals(&fitted.knots[0]), + reals(&fitted.knots[1]), + )) +} + +pub(super) fn add_wrapped_curve( + writer: &mut Part21Writer, + brep: &BrepEnvelope, + curve_id: u32, + budget: f64, + scale: f64, +) -> Result<(usize, f64), GeometryError> { + check_budget(budget)?; + let range = brep + .topology + .edges + .iter() + .filter_map(|edge| match edge.geometry { + EdgeGeometry::Curve { curve, range } if curve == curve_id => Some(range), + _ => None, + }) + .reduce(Interval::hull) + .ok_or_else(|| GeometryError::InvalidTopology("STEP curve has no edge".into()))?; + let fitted = fit_curve( + &brep.geometry.curves[curve_id as usize], + range, + budget, + MAX_SPANS, + )?; + let controls = fitted + .controls + .iter() + .map(|control| control.map(|value| value * scale).to_vec()) + .collect::>(); + let id = bspline_curve(writer, &controls, &fitted.knots, &fitted.multiplicities)?; + Ok((id, fitted.error_bound)) +} diff --git a/main/opengeometry/src/intersection/ssi/fallback.rs b/main/opengeometry/src/intersection/ssi/fallback.rs index 084dbf79..13ef6cb0 100644 --- a/main/opengeometry/src/intersection/ssi/fallback.rs +++ b/main/opengeometry/src/intersection/ssi/fallback.rs @@ -59,9 +59,11 @@ fn finite_target_domain( Interval::new(0.0, std::f64::consts::TAU)?, Interval::new(0.0, maximum_radius + margin)?, ]), - SurfaceGeometry::Sphere { .. } | SurfaceGeometry::Torus { .. } => Err( - GeometryError::UnsupportedGeometry("SSI trim surface".into()), - ), + SurfaceGeometry::Sphere { .. } + | SurfaceGeometry::Torus { .. } + | SurfaceGeometry::Wrapped { .. } => Err(GeometryError::UnsupportedGeometry( + "SSI trim surface".into(), + )), } } diff --git a/main/opengeometry/src/intersection/ssi/mod.rs b/main/opengeometry/src/intersection/ssi/mod.rs index 32d2b1f2..e51dea15 100644 --- a/main/opengeometry/src/intersection/ssi/mod.rs +++ b/main/opengeometry/src/intersection/ssi/mod.rs @@ -7,8 +7,8 @@ mod tests; use super::ssi_result::{GeometryResult, SsiCurve, SsiResult}; use crate::brep::{ - Accuracy, CurveGeometry, Frame3, GeometryError, GeometryStore, PcurveGeometry, Surface, - SurfaceGeometry, + unsupported_wrapped, Accuracy, CurveGeometry, Frame3, GeometryError, GeometryStore, + PcurveGeometry, Surface, SurfaceGeometry, }; use crate::math::{dot, norm, sub, Interval, Point3}; use coaxial::{ @@ -104,6 +104,7 @@ fn analytic_intersection( major_radius, minor_radius, } => torus_pairs(*frame, *major_radius, *minor_radius, sb), + SurfaceGeometry::Wrapped { .. } => Err(unsupported_wrapped("intersection")), } } @@ -131,6 +132,7 @@ fn plane_pairs( major_radius, minor_radius, } => plane_torus(plane, *torus, *major_radius, *minor_radius)?, + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } @@ -158,6 +160,7 @@ fn sphere_pairs( major_radius, minor_radius, } => coaxial_sphere_torus(sphere, sphere_radius, *torus, *major_radius, *minor_radius)?, + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } @@ -193,6 +196,7 @@ fn cylinder_pairs( *major_radius, *minor_radius, )?, + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } @@ -220,6 +224,7 @@ fn cone_pairs( major_radius, minor_radius, } => coaxial_cone_torus(cone, semi_angle, *torus, *major_radius, *minor_radius)?, + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } @@ -263,6 +268,7 @@ fn torus_pairs( *other_major, *other_minor, )?, + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } diff --git a/main/opengeometry/src/intersection/universal_ssi/implicit_bounds.rs b/main/opengeometry/src/intersection/universal_ssi/implicit_bounds.rs index c383eebb..28707d04 100644 --- a/main/opengeometry/src/intersection/universal_ssi/implicit_bounds.rs +++ b/main/opengeometry/src/intersection/universal_ssi/implicit_bounds.rs @@ -1,4 +1,4 @@ -use crate::brep::{GeometryError, PatchBounds, SurfaceGeometry}; +use crate::brep::{unsupported_wrapped, GeometryError, PatchBounds, SurfaceGeometry}; use crate::math::{Interval, Point3}; fn local_intervals( @@ -59,6 +59,7 @@ pub(super) fn implicit_interval( .add_interval(z2)? .sub_interval(Interval::point(minor_radius * minor_radius)?)? } + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }) } @@ -87,6 +88,7 @@ pub(super) fn implicit_value( let radial = x.mul_add(x, y * y); (radial.sqrt() - major_radius).powi(2) + z * z - minor_radius * minor_radius } + SurfaceGeometry::Wrapped { .. } => return Err(unsupported_wrapped("intersection")), }; if value.is_finite() { Ok(value) @@ -109,5 +111,6 @@ pub(super) fn surface_scale(surface: &SurfaceGeometry) -> f64 { minor_radius, .. } => major_radius + minor_radius, + SurfaceGeometry::Wrapped { wrap, .. } => wrap.radius, } } diff --git a/main/opengeometry/src/operations/modifying/boolean/batch/mod.rs b/main/opengeometry/src/operations/modifying/boolean/batch/mod.rs index dc5228b1..80c84e87 100644 --- a/main/opengeometry/src/operations/modifying/boolean/batch/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/batch/mod.rs @@ -5,7 +5,9 @@ use super::dispatch::{boolean_brep_outcome_with_trace, record_handler, HandlerId use super::handlers::{boolean_rectilinear, subtract_vertical_arc_extrusion_batch}; use super::operands::{all_planar, full_cylinder, unique_sources}; use super::types::{BooleanOp, BooleanResult}; -use crate::brep::{Accuracy, BrepEnvelope, GeometryError, PatchBounds, SurfaceGeometry}; +use crate::brep::{ + unsupported_wrapped, Accuracy, BrepEnvelope, GeometryError, PatchBounds, SurfaceGeometry, +}; use prismatic_profile::subtract_prismatic_profile_batch; pub(super) use prismatic_profile::profile_edges; @@ -52,6 +54,9 @@ fn subtract_planar_cutters_inner( id: String, trace: &mut Trace, ) -> Result { + if host.geometry.has_wrapped() || cutters.iter().any(|c| c.geometry.has_wrapped()) { + return Err(unsupported_wrapped("booleans")); + } let mut nested = Trace::default(); let result = record_handler(trace, HandlerId::SubtractPlanarCutters, || { planar_cutters_boolean(host, cutters, id, &mut nested) diff --git a/main/opengeometry/src/operations/modifying/boolean/dispatch/mod.rs b/main/opengeometry/src/operations/modifying/boolean/dispatch/mod.rs index 01188f82..dd0b2155 100644 --- a/main/opengeometry/src/operations/modifying/boolean/dispatch/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/dispatch/mod.rs @@ -7,7 +7,7 @@ pub(super) use trace::{record_handler, Trace}; use super::handlers::{coincident_boolean, disjoint_boolean}; use super::types::{BooleanOp, BooleanResult}; -use crate::brep::{BrepEnvelope, GeometryError}; +use crate::brep::{unsupported_wrapped, BrepEnvelope, GeometryError}; use table::{generic_boolean, specialized_boolean}; use trace::record_optional; @@ -60,6 +60,9 @@ fn boolean_brep_inner( ) -> Result { a.validate()?; b.validate()?; + if a.geometry.has_wrapped() || b.geometry.has_wrapped() { + return Err(unsupported_wrapped("booleans")); + } if a.geometry == b.geometry && a.topology == b.topology && a.solids == b.solids { return record_handler(trace, HandlerId::CoincidentBoolean, || { coincident_boolean(a, b, operation, id) diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/sphere_sphere.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/sphere_sphere.rs index 24977e0d..f8b4cae8 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/sphere_sphere.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/sphere_sphere.rs @@ -99,6 +99,7 @@ fn sphere_operands<'a>( Some(SurfaceGeometry::Cone { .. }) => "cone", Some(SurfaceGeometry::Torus { .. }) => "torus", Some(SurfaceGeometry::Plane { .. }) => "planar/compound solid", + Some(SurfaceGeometry::Wrapped { .. }) => "wrapped", None => "empty solid", } .to_string() diff --git a/main/opengeometry/src/operations/modifying/boolean/shell.rs b/main/opengeometry/src/operations/modifying/boolean/shell.rs index 9ad7f441..905c94ef 100644 --- a/main/opengeometry/src/operations/modifying/boolean/shell.rs +++ b/main/opengeometry/src/operations/modifying/boolean/shell.rs @@ -5,6 +5,7 @@ use super::operands::{ full_planar_extrusion, full_sphere, full_torus, prismatic_gap, unique_sources, BoxInput, }; use super::types::{BooleanOp, BooleanResult, FaceMapping}; +use crate::brep::unsupported_wrapped; use crate::brep::{BrepEnvelope, EdgeGeometry, Frame3, GeometryError, SurfaceGeometry}; use crate::geom2d::{ inset_curved_region, inset_tolerance, offset_from, oriented_ring, pinched, self_intersects2, @@ -84,6 +85,7 @@ fn offset_cavity( inner_id: String, ) -> Result { let body = match input.geometry.surfaces.first() { + Some(_) if input.geometry.has_wrapped() => Err(unsupported_wrapped("shelling")), Some(SurfaceGeometry::Sphere { .. }) => sphere_cavity(input, thickness, inner_id), Some(SurfaceGeometry::Cylinder { .. }) => cylinder_cavity(input, thickness, inner_id), Some(SurfaceGeometry::Torus { .. }) => torus_cavity(input, thickness, inner_id), @@ -96,6 +98,7 @@ fn offset_cavity( Err(error) => Err(error), }, Some(SurfaceGeometry::Cone { .. }) => cone_cavity(input, thickness, inner_id), + Some(SurfaceGeometry::Wrapped { .. }) => Err(unsupported_wrapped("shelling")), None => Err(GeometryError::CoverageGap { families: ["restricted analytic shell".into(), "input solid".into()], }), @@ -472,6 +475,7 @@ fn revolved(input: &BrepEnvelope) -> bool { | SurfaceGeometry::Cylinder { frame, .. } | SurfaceGeometry::Cone { frame, .. } | SurfaceGeometry::Torus { frame, .. } => frame, + SurfaceGeometry::Wrapped { wrap, .. } => wrap.frame, }; let curved = |surface: &&SurfaceGeometry| !matches!(surface, SurfaceGeometry::Plane { .. }); let surfaces = &input.geometry.surfaces; diff --git a/main/opengeometry/src/operations/modifying/boolean/tests/shell.rs b/main/opengeometry/src/operations/modifying/boolean/tests/shell.rs index 254f61cf..f45a6190 100644 --- a/main/opengeometry/src/operations/modifying/boolean/tests/shell.rs +++ b/main/opengeometry/src/operations/modifying/boolean/tests/shell.rs @@ -1,6 +1,7 @@ use super::support::{extrusion, sphere}; -use crate::brep::{FaceRole, Frame3, GeometryError}; +use crate::brep::{wrapped_sector, FaceRole, Frame3, GeometryError}; use crate::operations::modifying::boolean::shell::shell_brep; +use crate::operations::modifying::boolean::{boolean_brep, BooleanOp}; use crate::primitives; use crate::tessellation::tessellate; @@ -68,3 +69,27 @@ fn restricted_shell_offsets_supported_closed_families() { Err(GeometryError::UnresolvedIntersection(_)) )); } + +#[test] +fn booleans_and_shells_refuse_wrapped_geometry() { + let sector = wrapped_sector(); + let cube = primitives::cuboid( + "cube".into(), + Frame3::IDENTITY, + [1.0, 1.0, 1.0], + sector.accuracy, + ) + .unwrap(); + let refused = |result: Result<_, GeometryError>| { + matches!(result, Err(GeometryError::UnsupportedGeometry(_))) + }; + assert!(refused( + boolean_brep(§or, &cube, BooleanOp::Union, "u".into()).map(|_| ()) + )); + assert!(refused( + boolean_brep(&cube, §or, BooleanOp::Subtraction, "s".into()).map(|_| ()) + )); + assert!(refused( + shell_brep(§or, 0.1, "shell".into()).map(|_| ()) + )); +} diff --git a/main/opengeometry/src/query/classify.rs b/main/opengeometry/src/query/classify.rs index a3d2a575..97e18065 100644 --- a/main/opengeometry/src/query/classify.rs +++ b/main/opengeometry/src/query/classify.rs @@ -1,6 +1,7 @@ use super::classification::PointClassification; use super::face_trim::face_contains_uv; use super::ray_roots::{ray_box_domain, support_roots}; +use super::wrapped_roots::{wrapped_ray_roots, Ray}; use crate::brep::{ unit, BrepEnvelope, Face, GeometryError, SolidRegion, Surface, SurfaceGeometry, UV, }; @@ -16,16 +17,21 @@ fn classify_with_ray<'a>( return Ok(PointClassification::Outside); }; let mut hits: Vec<(f64, Point3)> = Vec::new(); + let ray = Ray { + origin: point, + direction, + domain, + tolerance: brep.accuracy.intersection, + }; for face in faces { let surface = brep.geometry.surface(face.surface)?; - let Some(roots) = support_roots( - surface, - point, - direction, - domain, - brep.accuracy.intersection, - )? - else { + let roots = match surface { + SurfaceGeometry::Wrapped { .. } => { + wrapped_ray_roots(surface, face.trim.uv_bounds, &ray)? + } + _ => support_roots(surface, point, direction, domain, ray.tolerance)?, + }; + let Some(roots) = roots else { return Ok(PointClassification::Unknown); }; for parameter in roots { diff --git a/main/opengeometry/src/query/mod.rs b/main/opengeometry/src/query/mod.rs index 88746abc..8346e65f 100644 --- a/main/opengeometry/src/query/mod.rs +++ b/main/opengeometry/src/query/mod.rs @@ -5,6 +5,7 @@ mod measure; mod ray_roots; #[cfg(test)] mod tests; +mod wrapped_roots; pub use classification::PointClassification; pub use classify::classify_point; diff --git a/main/opengeometry/src/query/ray_roots.rs b/main/opengeometry/src/query/ray_roots.rs index 4fb9de5a..55fa2a76 100644 --- a/main/opengeometry/src/query/ray_roots.rs +++ b/main/opengeometry/src/query/ray_roots.rs @@ -68,6 +68,7 @@ pub(super) fn support_roots( domain, tolerance, )?), + SurfaceGeometry::Wrapped { .. } => None, }; Ok(result.map(|roots| { roots diff --git a/main/opengeometry/src/query/tests.rs b/main/opengeometry/src/query/tests.rs index a1330868..20850437 100644 --- a/main/opengeometry/src/query/tests.rs +++ b/main/opengeometry/src/query/tests.rs @@ -1,6 +1,6 @@ use super::classification::PointClassification; use super::classify::{classify_point, classify_point_validated}; -use crate::brep::{coarse_accuracy, unit, Accuracy, BrepEnvelope, Frame3}; +use crate::brep::{coarse_accuracy, unit, wrapped_sector, Accuracy, BrepEnvelope, Frame3, SECTOR}; use crate::math::{add, cross, dot, scale, Point3}; use crate::primitives; @@ -431,3 +431,28 @@ fn cuboid_corners_edges_and_face_points_are_boundary() { .collect(), ); } + +#[test] +fn points_on_either_side_of_a_wrapped_wall_classify_correctly() { + let brep = wrapped_sector(); + let w = SECTOR.wrap(); + let mid = SECTOR.inner + SECTOR.thickness / 2.0; + for x in [0.5, 2.0, 3.5] { + let y = x * SECTOR.rise / SECTOR.length; + let inside = w.point([x, y + 0.01, mid]); + let outside = w.point([x, y - 0.01, mid]); + assert_eq!( + classify_point(&brep, inside).unwrap(), + PointClassification::Inside + ); + assert_eq!( + classify_point(&brep, outside).unwrap(), + PointClassification::Outside + ); + } + let middle = w.point([SECTOR.length / 2.0, SECTOR.width / 2.0, mid]); + assert_eq!( + classify_point(&brep, middle).unwrap(), + PointClassification::Inside + ); +} diff --git a/main/opengeometry/src/query/wrapped_roots.rs b/main/opengeometry/src/query/wrapped_roots.rs new file mode 100644 index 00000000..21666f80 --- /dev/null +++ b/main/opengeometry/src/query/wrapped_roots.rs @@ -0,0 +1,123 @@ +use crate::brep::{GeometryError, PatchBounds, Surface, SurfaceGeometry, UVBox}; +use crate::math::{add, cross, dot, norm, scale, sub, Interval, Point3}; + +const MAX_VISITS: usize = 50_000; +const MAX_DEPTH: usize = 24; +const LEAF_FRACTION: f64 = 1.0 / 64.0; +const NEWTON_STEPS: usize = 32; + +pub(super) struct Ray { + pub(super) origin: Point3, + pub(super) direction: Point3, + pub(super) domain: Interval, + pub(super) tolerance: f64, +} + +fn ray_in_box(bounds: PatchBounds, ray: &Ray) -> Option { + let (o, d) = (ray.origin, ray.direction); + let (mut lo, mut hi) = (ray.domain.lo, ray.domain.hi); + for axis in 0..3 { + let (a, b) = (bounds.axes[axis].lo, bounds.axes[axis].hi); + if d[axis] == 0.0 { + if o[axis] < a || o[axis] > b { + return None; + } + continue; + } + let (mut t0, mut t1) = ((a - o[axis]) / d[axis], (b - o[axis]) / d[axis]); + if t0 > t1 { + std::mem::swap(&mut t0, &mut t1); + } + lo = lo.max(t0); + hi = hi.min(t1); + } + Interval::new(lo, hi).ok().filter(|_| lo <= hi) +} + +fn diagonal(bounds: PatchBounds) -> f64 { + norm(std::array::from_fn(|i| bounds.axes[i].width())) +} + +pub(super) fn wrapped_ray_roots( + surface: &SurfaceGeometry, + uv: UVBox, + ray: &Ray, +) -> Result>, GeometryError> { + let leaf = diagonal(surface.enclose(uv)?) * LEAF_FRACTION; + let mut roots: Vec = Vec::new(); + let mut stack = vec![(uv, 0usize)]; + let mut visits = 0usize; + while let Some((b, depth)) = stack.pop() { + visits += 1; + if visits > MAX_VISITS { + return Ok(None); + } + let bounds = surface.enclose(b)?; + let Some(span) = ray_in_box(bounds, ray) else { + continue; + }; + if diagonal(bounds) <= leaf || depth >= MAX_DEPTH { + let start = [b[0].midpoint(), b[1].midpoint(), span.midpoint()]; + if let Some([u, v, t]) = ray_newton(surface, start, ray)? { + if uv[0].contains(u) + && uv[1].contains(v) + && ray.domain.contains(t) + && roots.iter().all(|r| (r - t).abs() > ray.tolerance) + { + roots.push(t); + } + } + continue; + } + for child in halves(surface, b)? { + stack.push((child, depth + 1)); + } + } + Ok(Some(roots)) +} + +fn halves(surface: &SurfaceGeometry, b: UVBox) -> Result<[UVBox; 2], GeometryError> { + let jet = surface.derivatives([b[0].midpoint(), b[1].midpoint()])?; + let axis = usize::from(norm(jet.dv) * b[1].width() > norm(jet.du) * b[0].width()); + let m = b[axis].midpoint(); + let (mut low, mut high) = (b, b); + low[axis] = Interval::new(b[axis].lo, m)?; + high[axis] = Interval::new(m, b[axis].hi)?; + Ok([low, high]) +} + +fn ray_newton( + surface: &SurfaceGeometry, + start: [f64; 3], + ray: &Ray, +) -> Result, GeometryError> { + let mut x = start; + for _ in 0..NEWTON_STEPS { + let uv = [x[0], x[1]]; + let (point, jet) = match surface + .point_at(uv) + .and_then(|p| Ok((p, surface.derivatives(uv)?))) + { + Ok(value) => value, + Err(GeometryError::SingularParameterization) => return Ok(None), + Err(error) => return Err(error), + }; + let r = sub(point, add(ray.origin, scale(ray.direction, x[2]))); + if norm(r) <= ray.tolerance { + return Ok(Some(x)); + } + let m = [jet.du, jet.dv, scale(ray.direction, -1.0)]; + let det = dot(m[0], cross(m[1], m[2])); + if det.abs() <= f64::EPSILON * norm(m[0]) * norm(m[1]) { + return Ok(None); + } + let rhs = scale(r, -1.0); + x[0] += dot(rhs, cross(m[1], m[2])) / det; + x[1] += dot(m[0], cross(rhs, m[2])) / det; + x[2] += dot(m[0], cross(m[1], rhs)) / det; + if x.iter().any(|v| !v.is_finite()) { + return Ok(None); + } + } + Ok(None) +} diff --git a/main/opengeometry/src/tessellation/area_bound.rs b/main/opengeometry/src/tessellation/area_bound.rs index 4dd3c848..715a421e 100644 --- a/main/opengeometry/src/tessellation/area_bound.rs +++ b/main/opengeometry/src/tessellation/area_bound.rs @@ -132,6 +132,7 @@ fn deviation( sense, deflection, ), + SurfaceGeometry::Wrapped { .. } => None, } } diff --git a/main/opengeometry/src/tessellation/deviation.rs b/main/opengeometry/src/tessellation/deviation.rs index b0c2878d..126a223b 100644 --- a/main/opengeometry/src/tessellation/deviation.rs +++ b/main/opengeometry/src/tessellation/deviation.rs @@ -184,6 +184,18 @@ pub(super) fn edge_count( CurveGeometry::Intersection { definition } => { bounded_intersection_count(brep, *definition, curve, range, error, max_triangles) } + geometry @ CurveGeometry::Wrapped { curve: flat, .. } => { + let arc = matches!( + **flat, + CurveGeometry::Circle { .. } | CurveGeometry::Ellipse { .. } + ); + turn_count( + range.width() * (geometry.derivative_bounds(range)?.1 / (8.0 * error)).sqrt(), + if arc { range.width() } else { 0.0 }, + 1, + max_triangles, + ) + } }, } } diff --git a/main/opengeometry/src/tessellation/grid.rs b/main/opengeometry/src/tessellation/grid.rs index d0e50772..be2c0e45 100644 --- a/main/opengeometry/src/tessellation/grid.rs +++ b/main/opengeometry/src/tessellation/grid.rs @@ -1,3 +1,4 @@ +use super::wrapped::second_derivative_bounds; use crate::brep::{ period_lifted_uv, BrepEnvelope, Face, GeometryError, PcurveGeometry, Surface, SurfaceGeometry, }; @@ -123,6 +124,7 @@ pub(super) fn grid_size( turn_count(u * ((r + a) / (4.0 * error)).sqrt(), u, 3, max_triangles)?, turn_count(v * (a / (4.0 * error)).sqrt(), v, 3, max_triangles)?, ), + SurfaceGeometry::Wrapped { .. } => wrapped_grid(surface, bounds, error, max_triangles)?, SurfaceGeometry::Plane { .. } => { return Err(GeometryError::InvalidGeometry( "plane does not use a curved grid".into(), @@ -141,6 +143,25 @@ pub(super) fn grid_size( Ok([nu, nv]) } +fn wrapped_grid( + surface: &SurfaceGeometry, + bounds: [Interval; 2], + error: f64, + max_triangles: usize, +) -> Result<(usize, usize), GeometryError> { + let second = second_derivative_bounds(surface, bounds)?; + let periods = surface.charts()[0].periods; + let side = |i: usize, minimum: usize| { + let width = bounds[i].width(); + let value = width * (second[i] / (8.0 * error)).sqrt(); + match periods[i] { + Some(_) => turn_count(value, width, minimum, max_triangles), + None => count(value, minimum, max_triangles), + } + }; + Ok((side(0, 3)?, side(1, 1)?)) +} + pub(super) fn turn_count( value: f64, sweep: f64, diff --git a/main/opengeometry/src/tessellation/mod.rs b/main/opengeometry/src/tessellation/mod.rs index 6c718024..bd3b3efc 100644 --- a/main/opengeometry/src/tessellation/mod.rs +++ b/main/opengeometry/src/tessellation/mod.rs @@ -16,6 +16,7 @@ mod tessellate; mod tests; mod trim; mod trimmed_face; +mod wrapped; pub use measure::{measure, Measure}; pub use mesh::Tessellation; diff --git a/main/opengeometry/src/tessellation/tests/mod.rs b/main/opengeometry/src/tessellation/tests/mod.rs index 31eb3c09..e013c164 100644 --- a/main/opengeometry/src/tessellation/tests/mod.rs +++ b/main/opengeometry/src/tessellation/tests/mod.rs @@ -2,6 +2,7 @@ mod display; mod seams; mod short_loops; mod support; +mod wrapped; use super::cache::TessellationCache; use super::tessellate::tessellate; diff --git a/main/opengeometry/src/tessellation/tests/wrapped.rs b/main/opengeometry/src/tessellation/tests/wrapped.rs new file mode 100644 index 00000000..3a443cf7 --- /dev/null +++ b/main/opengeometry/src/tessellation/tests/wrapped.rs @@ -0,0 +1,24 @@ +use crate::brep::{wrapped_sector, SECTOR}; +use crate::math::Point3; +use crate::tessellation::tessellate::tessellate; + +#[test] +fn a_wrapped_side_meshes_to_the_exact_volume() { + let mesh = tessellate(&wrapped_sector(), 1e-3, 1_000_000).unwrap(); + let p = |i: u32| -> Point3 { std::array::from_fn(|j| mesh.positions[i as usize * 3 + j]) }; + let volume: f64 = mesh + .indices + .chunks_exact(3) + .map(|t| { + let (a, b, c) = (p(t[0]), p(t[1]), p(t[2])); + (a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + + a[2] * (b[0] * c[1] - b[1] * c[0])) + / 6.0 + }) + .sum(); + let exact = SECTOR.volume(); + assert!( + ((volume - exact) / exact).abs() < 1e-4, + "{volume} vs {exact}" + ); +} diff --git a/main/opengeometry/src/tessellation/trim.rs b/main/opengeometry/src/tessellation/trim.rs index 75e87714..2f59d6af 100644 --- a/main/opengeometry/src/tessellation/trim.rs +++ b/main/opengeometry/src/tessellation/trim.rs @@ -1,3 +1,4 @@ +use super::wrapped::chord_deviation; use crate::brep::{ period_lifted_uv, BrepEnvelope, EdgeGeometry, Face, GeometryError, HalfEdge, Orientation, Surface, SurfaceGeometry, @@ -262,6 +263,7 @@ pub(super) fn trimmed_surface_edge_deviation( } }; let acceleration = match surface { + SurfaceGeometry::Wrapped { .. } => return chord_deviation(surface, uv[a], uv[b]), SurfaceGeometry::Plane { .. } => 0.0, SurfaceGeometry::Sphere { radius, .. } => { let cos_v = maximum_trig(0.0, f64::cos); diff --git a/main/opengeometry/src/tessellation/wrapped.rs b/main/opengeometry/src/tessellation/wrapped.rs new file mode 100644 index 00000000..89197a3e --- /dev/null +++ b/main/opengeometry/src/tessellation/wrapped.rs @@ -0,0 +1,40 @@ +use crate::brep::{GeometryError, Surface, SurfaceGeometry}; +use crate::math::{norm, Interval, Point3}; + +const MARGIN: f64 = 1.5; +const GRID: usize = 8; + +pub(super) fn second_derivative_bounds( + surface: &SurfaceGeometry, + bounds: [Interval; 2], +) -> Result<[f64; 2], GeometryError> { + let mut maximum = [0.0_f64; 2]; + for i in 0..=GRID { + for j in 0..=GRID { + let uv = [ + bounds[0].lo + bounds[0].width() * i as f64 / GRID as f64, + bounds[1].lo + bounds[1].width() * j as f64 / GRID as f64, + ]; + let jet = surface.derivatives(uv)?; + maximum[0] = maximum[0].max(norm(jet.duu)); + maximum[1] = maximum[1].max(norm(jet.dvv)); + } + } + Ok(maximum.map(|m| m * MARGIN)) +} + +pub(super) fn chord_deviation(surface: &SurfaceGeometry, a: [f64; 2], b: [f64; 2]) -> f64 { + let (du, dv) = (b[0] - a[0], b[1] - a[1]); + let mut maximum = 0.0_f64; + for k in 0..=2 { + let t = k as f64 / 2.0; + let Ok(jet) = surface.derivatives([a[0] + t * du, a[1] + t * dv]) else { + return f64::INFINITY; + }; + let acceleration: Point3 = std::array::from_fn(|i| { + jet.duu[i] * du * du + 2.0 * jet.duv[i] * du * dv + jet.dvv[i] * dv * dv + }); + maximum = maximum.max(norm(acceleration)); + } + maximum * MARGIN / 8.0 +} diff --git a/main/opengeometry/tests/creating/sweep_bends.rs b/main/opengeometry/tests/creating/sweep_bends.rs index af26e7b3..ff79e516 100644 --- a/main/opengeometry/tests/creating/sweep_bends.rs +++ b/main/opengeometry/tests/creating/sweep_bends.rs @@ -99,6 +99,7 @@ fn surface_kinds(body: &BrepEnvelope) -> Vec<&'static str> { SurfaceGeometry::Cone { .. } => "cone", SurfaceGeometry::Torus { .. } => "torus", SurfaceGeometry::Sphere { .. } => "sphere", + SurfaceGeometry::Wrapped { .. } => "wrapped", }) .collect::>(); kinds.sort_unstable(); diff --git a/main/opengeometry/tests/primitives.rs b/main/opengeometry/tests/primitives.rs index e6a03995..78aa1e46 100644 --- a/main/opengeometry/tests/primitives.rs +++ b/main/opengeometry/tests/primitives.rs @@ -279,6 +279,7 @@ fn surface_kinds(body: &BrepEnvelope) -> Vec<&'static str> { SurfaceGeometry::Cylinder { .. } => "cylinder", SurfaceGeometry::Cone { .. } => "cone", SurfaceGeometry::Torus { .. } => "torus", + SurfaceGeometry::Wrapped { .. } => "wrapped", }) .collect::>(); kinds.sort();