1use ogeom_algo::{Built, normals_oppose, project_on_surface, restate_geometry};
11use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
12use ogeom_geom::{
13 ConeSurface, Curve, CylinderSurface, PlaneSurface, SphereSurface, Surface as _,
14 SurfaceGeometry, TorusSurface,
15};
16use ogeom_math::{Axis, Cone, Cylinder, Direction, Frame, Plane, Point, Sphere, Torus, Vector};
17use ogeom_topo::{Model, Shape};
18
19pub fn restrict_degree(
31 model: &mut Model,
32 shape: &Shape,
33 max_degree: usize,
34 tolerance: f64,
35 tol: Tolerances,
36) -> OgeomResult<Built> {
37 if max_degree == 0 {
38 ogeom_bail!(Construction, "a degree limit of zero leaves nothing");
39 }
40 if !(tolerance.is_finite() && tolerance > 0.0) {
41 ogeom_bail!(Construction, "a tolerance of {tolerance} is not a distance");
42 }
43 let surface = |s: &SurfaceGeometry| -> OgeomResult<Option<(SurfaceGeometry, bool)>> {
44 let SurfaceGeometry::BSpline(b) = s else {
45 return Ok(None);
46 };
47 if b.u_knots().degree() <= max_degree && b.v_knots().degree() <= max_degree {
48 return Ok(None);
49 }
50 let fitted = b.restricted_to_degree(max_degree, tolerance, tol)?;
51 if !fitted.met {
52 ogeom_bail!(
53 NotDone,
54 "a patch fitted at degree {max_degree} stays {} away",
55 fitted.error
56 );
57 }
58 let restated = SurfaceGeometry::BSpline(fitted.curve);
59 let flipped = normals_oppose(s, &restated, tol)?;
60 Ok(Some((restated, flipped)))
61 };
62 let curve = |c: &Curve, range: (f64, f64)| -> OgeomResult<Option<(Curve, (f64, f64))>> {
63 let Curve::BSpline(b) = c else {
64 return Ok(None);
65 };
66 if b.degree() <= max_degree {
67 return Ok(None);
68 }
69 let piece = b.segment(range, tol)?;
72 let fitted = piece.restricted_to_degree(max_degree, tolerance, tol)?;
73 if !fitted.met {
74 ogeom_bail!(
75 NotDone,
76 "a curve fitted at degree {max_degree} stays {} away",
77 fitted.error
78 );
79 }
80 let restated = Curve::BSpline(fitted.curve);
81 let domain = ogeom_geom::Curve3d::domain(&restated);
82 Ok(Some((restated, domain)))
83 };
84 restate_geometry(model, shape, &surface, &curve, tol)
85}
86
87pub fn swept_to_elementary(
96 model: &mut Model,
97 shape: &Shape,
98 tol: Tolerances,
99) -> OgeomResult<Built> {
100 let surface = |s: &SurfaceGeometry| -> OgeomResult<Option<(SurfaceGeometry, bool)>> {
101 let Some(candidate) = elementary(s, tol)? else {
102 return Ok(None);
103 };
104 for p in interior_samples(s, 5, tol)? {
107 if project_on_surface(&candidate, p, 16, tol)?.distance > tol.confusion() {
108 return Ok(None);
109 }
110 }
111 let flipped = normals_oppose(s, &candidate, tol)?;
112 Ok(Some((candidate, flipped)))
113 };
114 let curve = |_: &Curve, _: (f64, f64)| -> OgeomResult<Option<(Curve, (f64, f64))>> { Ok(None) };
115 restate_geometry(model, shape, &surface, &curve, tol)
116}
117
118fn elementary(s: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<Option<SurfaceGeometry>> {
120 let samples = interior_samples(s, 5, tol)?;
121 let unwrap = |c: &Curve| -> Curve {
122 match c {
123 Curve::Trimmed(t) => t.basis().clone(),
124 other => other.clone(),
125 }
126 };
127 let found = match s {
128 SurfaceGeometry::Extrusion(e) => match unwrap(e.curve()) {
129 Curve::Line(l) => {
130 let axis = l.axis();
131 let normal = axis.direction.vector().cross(e.direction().vector());
132 let Ok(z) = Direction::new(normal, tol) else {
133 return Ok(None);
134 };
135 let frame = Frame::new(axis.location, z, axis.direction, tol)?;
136 Some(plane_over(frame, &samples)?)
137 }
138 Curve::Circle(c) => {
139 let circle = c.circle();
140 let frame = circle.frame();
141 if frame.z().vector().cross(e.direction().vector()).magnitude() > tol.angular() {
142 return Ok(None);
143 }
144 let axis = Frame::new(frame.origin(), e.direction(), frame.x(), tol)?;
145 let heights = heights(&axis, &samples);
146 Some(
147 CylinderSurface::new(Cylinder::new(axis, circle.radius(), tol)?, heights)?
148 .into(),
149 )
150 }
151 _ => None,
152 },
153 SurfaceGeometry::Revolution(r) => {
154 let axis = r.axis();
155 match unwrap(r.curve()) {
156 Curve::Line(l) => revolved_line(axis, l.axis(), &samples, tol)?,
157 Curve::Circle(c) => revolved_circle(axis, c.circle(), tol)?,
158 _ => None,
159 }
160 }
161 _ => None,
162 };
163 Ok(found)
164}
165
166fn revolved_line(
169 axis: Axis,
170 line: Axis,
171 samples: &[Point],
172 tol: Tolerances,
173) -> OgeomResult<Option<SurfaceGeometry>> {
174 let z = axis.direction.vector();
175 let a = line.location;
176 let b = a + line.direction.vector();
177 let height = |p: Point| (p - axis.location).dot(z);
178 let radius = |p: Point| p.distance(axis.project(p));
179 let Some(off) = [a, b]
181 .into_iter()
182 .map(|p| p - axis.project(p))
183 .find(|v| v.magnitude() > tol.confusion())
184 else {
185 return Ok(None);
186 };
187 let x = Direction::new(off, tol)?;
188 let (dh, dr) = (height(b) - height(a), radius(b) - radius(a));
189 if dh.abs() <= tol.confusion() {
190 let frame = Frame::new(axis.project(a), axis.direction, x, tol)?;
191 return Ok(Some(plane_over(frame, samples)?));
192 }
193 let frame = Frame::new(axis.project(a), axis.direction, x, tol)?;
194 let heights = heights(&frame, samples);
195 if dr.abs() <= tol.confusion() {
196 return Ok(Some(
197 CylinderSurface::new(Cylinder::new(frame, radius(a), tol)?, heights)?.into(),
198 ));
199 }
200 let half_angle = (dr / dh).atan();
201 let Ok(cone) = Cone::new(frame, radius(a), half_angle, tol) else {
202 return Ok(None);
203 };
204 Ok(Some(ConeSurface::new(cone, heights)?.into()))
205}
206
207fn revolved_circle(
210 axis: Axis,
211 circle: ogeom_math::Circle,
212 tol: Tolerances,
213) -> OgeomResult<Option<SurfaceGeometry>> {
214 let centre = circle.centre();
215 let foot = axis.project(centre);
216 let off = centre - foot;
217 if off.magnitude() <= tol.confusion() {
218 let x = Direction::new(circle.frame().x().vector(), tol)?;
219 let x = if x.vector().cross(axis.direction.vector()).magnitude() > tol.angular() {
220 x
221 } else {
222 Direction::new(circle.frame().y().vector(), tol)?
223 };
224 let side = x.vector() - axis.direction.vector() * x.vector().dot(axis.direction.vector());
225 let frame = Frame::new(foot, axis.direction, Direction::new(side, tol)?, tol)?;
226 return Ok(Some(
227 SphereSurface::new(Sphere::new(frame, circle.radius(), tol)?).into(),
228 ));
229 }
230 let frame = Frame::new(foot, axis.direction, Direction::new(off, tol)?, tol)?;
231 let Ok(torus) = Torus::new(frame, off.magnitude(), circle.radius(), tol) else {
232 return Ok(None);
233 };
234 Ok(Some(TorusSurface::new(torus).into()))
235}
236
237fn plane_over(frame: Frame, samples: &[Point]) -> OgeomResult<SurfaceGeometry> {
239 let along = |p: Point, d: Vector| (p - frame.origin()).dot(d);
240 let range = |d: Vector| {
241 let (lo, hi) = samples
242 .iter()
243 .fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), p| {
244 (l.min(along(*p, d)), h.max(along(*p, d)))
245 });
246 let margin = (hi - lo) * 0.5 + 1.0;
247 (lo - margin, hi + margin)
248 };
249 Ok(PlaneSurface::over(
250 Plane::new(frame),
251 range(frame.x().vector()),
252 range(frame.y().vector()),
253 )?
254 .into())
255}
256
257fn heights(frame: &Frame, samples: &[Point]) -> (f64, f64) {
259 let z = frame.z().vector();
260 let (lo, hi) = samples
261 .iter()
262 .map(|p| (*p - frame.origin()).dot(z))
263 .fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), v| {
264 (l.min(v), h.max(v))
265 });
266 let margin = (hi - lo) * 0.5 + 1.0;
267 (lo - margin, hi + margin)
268}
269
270fn interior_samples(s: &SurfaceGeometry, n: usize, tol: Tolerances) -> OgeomResult<Vec<Point>> {
272 let ((u0, u1), (v0, v1)) = s.domain();
273 let mut out = Vec::with_capacity((n + 1) * (n + 1));
274 for i in 0..=n {
275 for j in 0..=n {
276 #[allow(clippy::cast_precision_loss, reason = "a sample index")]
277 let (fu, fv) = (i as f64 / n as f64, j as f64 / n as f64);
278 out.push(s.point_at(u0 + (u1 - u0) * fu, v0 + (v1 - v0) * fv, tol)?);
279 }
280 }
281 Ok(out)
282}