1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
11use ogeom_geom::{Curve3d as _, Surface as _};
12use ogeom_math::{Aabb, Point, Point2, Vector, solve};
13use ogeom_mesh::Deflection;
14use ogeom_topo::{EdgeRepr, Model, NodeData, SameKey, Shape, ShapeType, explore_unique};
15
16const AXES: [Vector; 3] = [Vector::X, Vector::Y, Vector::Z];
17
18fn ruled(surface: &ogeom_geom::SurfaceGeometry) -> bool {
21 use ogeom_geom::SurfaceGeometry as S;
22 match surface {
23 S::Plane(_) | S::Cylinder(_) | S::Cone(_) | S::Extrusion(_) => true,
24 S::Trimmed(t) => ruled(t.basis()),
25 S::Sphere(_) | S::Torus(_) | S::BSpline(_) | S::Revolution(_) | S::Offset(_) => false,
26 }
27}
28
29pub fn tight_bounds(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Aabb> {
43 let mut edges = EdgeBoxes::default();
44 let mut bound = Aabb::EMPTY;
45 let keep = tol == model.tolerances();
46 for face in explore_unique(model, shape, ShapeType::Face)? {
47 let placement = face.transform(model.datums())?;
48 let found = if keep && keeps_axes(&placement) {
49 let kept = model.kept_face_box(&face, |bare| face_box(model, bare, tol, &mut edges))?;
50 kept.transformed(&placement)
51 } else {
52 face_box(model, &face, tol, &mut edges)?
53 };
54 bound = bound.union(&found);
55 }
56 let (loose_edges, loose_vertices) = outside_faces(model, shape)?;
57 for edge in &loose_edges {
58 bound = bound.union(&edge_box(model, edge, tol, &mut edges)?);
59 }
60 for vertex in &loose_vertices {
61 if let Some(point) = vertex_point(model, vertex)? {
62 bound = bound.with_point(point);
63 }
64 }
65 if bound.is_empty() {
66 ogeom_bail!(Construction, "the shape holds nothing to bound");
67 }
68 Ok(bound)
69}
70
71pub fn face_bounds(model: &Model, face: &Shape) -> OgeomResult<Aabb> {
86 let tol = model.tolerances();
87 let mut edges = EdgeBoxes::default();
88 model.kept_face_box(face, |bare| face_box(model, bare, tol, &mut edges))?;
89 if let Some(kept) = model.face_bounds(face) {
90 return Ok(kept);
91 }
92 let tolerance = model.tolerance_of(face)?.map_or(0.0, |t| t.get());
93 Ok(face_box(model, face, tol, &mut edges)?.expanded(tolerance))
94}
95
96fn keeps_axes(placement: &ogeom_math::Transform) -> bool {
98 use ogeom_math::TransformKind as K;
99 matches!(
100 placement.kind(),
101 K::Identity | K::Translation | K::Scale | K::PointMirror
102 )
103}
104
105type EdgeBoxes = ogeom_core::FastMap<SameKey, Aabb>;
107
108fn face_box(
110 model: &Model,
111 face: &Shape,
112 tol: Tolerances,
113 edges: &mut EdgeBoxes,
114) -> OgeomResult<Aabb> {
115 let mut bound = Aabb::EMPTY;
116 for edge in explore_unique(model, face, ShapeType::Edge)? {
117 bound = bound.union(&edge_box(model, &edge, tol, edges)?);
118 }
119 let mut points = Vec::new();
120 face_extremes(model, face, tol, &mut points)?;
121 for point in points {
122 bound = bound.with_point(point);
123 }
124 Ok(bound)
125}
126
127fn edge_box(
129 model: &Model,
130 edge: &Shape,
131 tol: Tolerances,
132 edges: &mut EdgeBoxes,
133) -> OgeomResult<Aabb> {
134 let key = SameKey(edge.clone());
135 if let Some(known) = edges.get(&key) {
136 return Ok(*known);
137 }
138 let mut points = Vec::new();
139 for vertex in model.children_of(edge)? {
140 if let Some(point) = vertex_point(model, &vertex)? {
141 points.push(point);
142 }
143 }
144 edge_extremes(model, edge, tol, &mut points)?;
145 let found = Aabb::of_points(&points);
146 edges.insert(key, found);
147 Ok(found)
148}
149
150fn vertex_point(model: &Model, vertex: &Shape) -> OgeomResult<Option<Point>> {
152 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
153 return Ok(None);
154 };
155 Ok(Some(vertex.transform(model.datums())?.apply(data.point)))
156}
157
158fn outside_faces(model: &Model, shape: &Shape) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
160 let mut edges = Vec::new();
161 let mut vertices = Vec::new();
162 let mut seen = ogeom_core::FastSet::default();
163 let mut stack = vec![shape.clone()];
164 while let Some(at) = stack.pop() {
165 let Some(node) = model.node(&at) else {
166 ogeom_bail!(Dangling, "shape refers to a node not in this model");
167 };
168 match node.kind() {
169 ShapeType::Face => {}
170 ShapeType::Edge => {
171 if seen.insert(SameKey(at.clone())) {
172 edges.push(at);
173 }
174 }
175 ShapeType::Vertex => {
176 if seen.insert(SameKey(at.clone())) {
177 vertices.push(at);
178 }
179 }
180 _ => stack.extend(model.children_of(&at)?),
181 }
182 }
183 Ok((edges, vertices))
184}
185
186fn edge_extremes(
189 model: &Model,
190 edge: &Shape,
191 tol: Tolerances,
192 points: &mut Vec<Point>,
193) -> OgeomResult<()> {
194 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
195 return Ok(());
196 };
197 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
198 return Ok(());
199 };
200 let Some(geometry) = model.geometry().curve(*curve) else {
201 ogeom_bail!(Dangling, "curve is not in this model");
202 };
203 if matches!(geometry, ogeom_geom::Curve::Line(_)) {
205 return Ok(());
206 }
207 let placement = edge.transform(model.datums())?;
208 let at = |t: f64| -> OgeomResult<Point> { Ok(placement.apply(geometry.point_at(t, tol)?)) };
209 if let ogeom_geom::Curve::Circle(c) = geometry {
213 let frame = c.circle().frame();
214 let (x, y) = (
215 placement.apply_vector(frame.x().vector()),
216 placement.apply_vector(frame.y().vector()),
217 );
218 let tau = core::f64::consts::TAU;
219 for axis in AXES {
220 let theta = axis.dot(y).atan2(axis.dot(x));
221 for angle in [
222 theta,
223 theta + core::f64::consts::PI,
224 -theta,
225 core::f64::consts::PI - theta,
226 ] {
227 let turns = ((range.0 - angle) / tau).ceil();
228 let t = turns.mul_add(tau, angle);
229 if t <= range.1 {
230 points.push(at(t)?);
231 }
232 }
233 }
234 return Ok(());
235 }
236 const N: usize = 64;
237 #[allow(clippy::cast_precision_loss)]
238 let step = (range.1 - range.0) / N as f64;
239 let samples: Vec<(f64, Point)> = (0..=N)
240 .map(|i| {
241 #[allow(clippy::cast_precision_loss)]
242 let t = range.0 + step * i as f64;
243 at(t).map(|p| (t, p))
244 })
245 .collect::<OgeomResult<_>>()?;
246 for axis in AXES {
247 for sense in [1.0, -1.0] {
248 let score = |p: Point| p.to_vector().dot(axis) * sense;
249 let Some((best, _)) = samples
250 .iter()
251 .enumerate()
252 .max_by(|a, b| score(a.1.1).total_cmp(&score(b.1.1)))
253 else {
254 continue;
255 };
256 let lo = samples[best.saturating_sub(1)].0;
257 let hi = samples[(best + 1).min(N)].0;
258 points.push(samples[best].1);
259 if hi > lo {
260 let found = solve::minimize(
261 |t| at(t).map_or(f64::INFINITY, |p| -score(p)),
262 lo,
263 hi,
264 solve::Criteria::default(),
265 )?;
266 points.push(at(found.value)?);
267 }
268 }
269 }
270 Ok(())
271}
272
273fn face_extremes(
277 model: &Model,
278 face: &Shape,
279 tol: Tolerances,
280 points: &mut Vec<Point>,
281) -> OgeomResult<()> {
282 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
283 return Ok(());
284 };
285 let Some(surface) = model.geometry().surface(data.surface) else {
286 ogeom_bail!(Dangling, "surface is not in this model");
287 };
288 if ruled(surface) {
293 return Ok(());
294 }
295 let placement = face.transform(model.datums())?;
296 use ogeom_geom::Transformable as _;
300 let placed = surface.transformed(&placement, tol);
301 let extremes = placed
302 .as_ref()
303 .map_or([None; 6], |placed| closed_form_extremes(placed, tol));
304 if extremes.iter().all(Option::is_some) && crate::covers_whole_surface(model, face)? {
307 points.extend(extremes.iter().flatten().map(|e| e.0));
308 return Ok(());
309 }
310 let mesh = ogeom_mesh::triangulate_face(model, face, Deflection::default(), tol)?;
311 if mesh.positions.is_empty() {
312 return Ok(());
313 }
314 let chart: Vec<[Point2; 3]> = mesh
315 .triangles
316 .iter()
317 .map(|t| {
318 t.map(|i| {
319 let (u, v) = mesh.parameters[i as usize];
320 Point2::new(u, v)
321 })
322 })
323 .collect();
324 let inside = |p: Point2| chart.iter().any(|t| in_triangle(*t, p));
325 let span = chart.iter().flatten().fold(
326 (
327 f64::INFINITY,
328 f64::NEG_INFINITY,
329 f64::INFINITY,
330 f64::NEG_INFINITY,
331 ),
332 |b, p| (b.0.min(p.x), b.1.max(p.x), b.2.min(p.y), b.3.max(p.y)),
333 );
334 let lower = [span.0, span.2];
335 let upper = [span.1, span.3];
336 if !(upper[0] > lower[0] && upper[1] > lower[1]) {
337 return Ok(());
338 }
339 let mut sides: ogeom_core::FastMap<(u32, u32), usize> = ogeom_core::FastMap::default();
345 for t in &mesh.triangles {
346 for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
347 *sides.entry((a.min(b), a.max(b))).or_default() += 1;
348 }
349 }
350 let border: Vec<(Point2, Point2)> = sides
351 .into_iter()
352 .filter(|(_, n)| *n == 1)
353 .map(|((a, b), _)| {
354 let at = |i: u32| {
355 let (u, v) = mesh.parameters[i as usize];
356 Point2::new(u, v)
357 };
358 (at(a), at(b))
359 })
360 .collect();
361 let against = ((upper[0] - lower[0]).min(upper[1] - lower[1])) * 1e-6;
362 let on_border = |p: Point2| {
363 border
364 .iter()
365 .any(|(a, b)| distance_to_segment(p, *a, *b) <= against)
366 };
367 placed?;
369 let tau = core::f64::consts::TAU;
371 let inside_turned = |u: f64, v: f64, periodic_v: bool| {
372 let turns: &[f64] = &[0.0, 1.0, -1.0];
373 turns.iter().any(|&a| {
374 let shifts: &[f64] = if periodic_v { turns } else { &[0.0] };
375 shifts
376 .iter()
377 .any(|&b| inside(Point2::new(u + a * tau, v + b * tau)))
378 })
379 };
380 for (p, u, v, periodic_v) in extremes.into_iter().flatten() {
381 if inside_turned(u, v, periodic_v) {
382 points.push(p);
383 }
384 }
385 for axis in AXES {
386 for sense in [1.0, -1.0] {
387 let score = |p: Point| p.to_vector().dot(axis) * sense;
388 let Some(best) = (0..mesh.positions.len())
389 .max_by(|a, b| score(mesh.positions[*a]).total_cmp(&score(mesh.positions[*b])))
390 else {
391 continue;
392 };
393 points.push(mesh.positions[best]);
394 let (u, v) = mesh.parameters[best];
395 let refined = solve_on_face(
396 |uv: &[f64]| {
397 let p = Point2::new(uv[0], uv[1]);
398 if !inside(p) {
399 return f64::INFINITY;
400 }
401 surface
402 .point_at(uv[0], uv[1], tol)
403 .map_or(f64::INFINITY, |q| -score(placement.apply(q)))
404 },
405 [u, v],
406 lower,
407 upper,
408 )?;
409 if let Some([u, v]) = refined
410 && !on_border(Point2::new(u, v))
411 {
412 points.push(placement.apply(surface.point_at(u, v, tol)?));
413 }
414 }
415 }
416 Ok(())
417}
418
419fn closed_form_extremes(
423 placed: &ogeom_geom::SurfaceGeometry,
424 tol: Tolerances,
425) -> [Option<(Point, f64, f64, bool)>; 6] {
426 let mut found = [None; 6];
427 for (k, toward) in AXES
428 .iter()
429 .flat_map(|axis| [*axis, *axis * -1.0])
430 .enumerate()
431 {
432 found[k] = match placed {
433 ogeom_geom::SurfaceGeometry::Sphere(s) => {
434 let sphere = s.sphere();
435 let p = sphere.centre() + toward * sphere.radius();
436 ogeom_math::elementary::sphere_parameters(&sphere, p, tol)
437 .ok()
438 .map(|(u, v)| (p, u, v, false))
439 }
440 ogeom_geom::SurfaceGeometry::Torus(t) => {
441 let torus = t.torus();
442 let frame = torus.frame();
443 let z = frame.z().vector();
444 let along = toward.dot(z);
445 let across = toward - z * along;
446 let reach = across.magnitude();
447 let out = if reach > 1e-12 {
450 across / reach
451 } else {
452 frame.x().vector()
453 };
454 let lean = along.hypot(reach);
455 let tilt = (out * reach + z * along) / lean;
456 let p = frame.origin() + out * torus.major_radius() + tilt * torus.minor_radius();
457 ogeom_math::elementary::torus_parameters(&torus, p, tol)
458 .ok()
459 .map(|(u, v)| (p, u, v, true))
460 }
461 _ => None,
462 };
463 }
464 found
465}
466
467fn solve_on_face(
470 f: impl FnMut(&[f64]) -> f64,
471 start: [f64; 2],
472 lower: [f64; 2],
473 upper: [f64; 2],
474) -> OgeomResult<Option<[f64; 2]>> {
475 let step = ((upper[0] - lower[0]).min(upper[1] - lower[1]) * 1e-2).max(1e-9);
476 let found = ogeom_math::minimize_local(f, &start, &lower, &upper, step, 1e-15, 2000)?;
477 Ok(found
478 .value
479 .is_finite()
480 .then(|| [found.point[0], found.point[1]]))
481}
482
483fn distance_to_segment(p: Point2, a: Point2, b: Point2) -> f64 {
484 let (dx, dy) = (b.x - a.x, b.y - a.y);
485 let squared = dx * dx + dy * dy;
486 let t = if squared > 0.0 {
487 (((p.x - a.x) * dx + (p.y - a.y) * dy) / squared).clamp(0.0, 1.0)
488 } else {
489 0.0
490 };
491 (p.x - a.x - dx * t).hypot(p.y - a.y - dy * t)
492}
493
494fn in_triangle(t: [Point2; 3], p: Point2) -> bool {
495 let cross =
496 |a: Point2, b: Point2, c: Point2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
497 let (d1, d2, d3) = (
498 cross(t[0], t[1], p),
499 cross(t[1], t[2], p),
500 cross(t[2], t[0], p),
501 );
502 let eps = 1e-12;
503 let neg = d1 < -eps || d2 < -eps || d3 < -eps;
504 let pos = d1 > eps || d2 > eps || d3 > eps;
505 !(neg && pos)
506}