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, Shape, ShapeType, explore_unique};
15
16const AXES: [Vector; 3] = [Vector::X, Vector::Y, Vector::Z];
17
18pub fn tight_bounds(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Aabb> {
28 let mut points: Vec<Point> = Vec::new();
29
30 for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
31 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
32 continue;
33 };
34 points.push(vertex.transform(model.datums())?.apply(data.point));
35 }
36
37 for edge in explore_unique(model, shape, ShapeType::Edge)? {
40 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
41 continue;
42 };
43 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
44 continue;
45 };
46 let Some(geometry) = model.geometry().curve(*curve) else {
47 ogeom_bail!(Dangling, "curve is not in this model");
48 };
49 let placement = edge.transform(model.datums())?;
50 let at = |t: f64| -> OgeomResult<Point> { Ok(placement.apply(geometry.point_at(t, tol)?)) };
51 const N: usize = 64;
52 #[allow(clippy::cast_precision_loss)]
53 let step = (range.1 - range.0) / N as f64;
54 let samples: Vec<(f64, Point)> = (0..=N)
55 .map(|i| {
56 #[allow(clippy::cast_precision_loss)]
57 let t = range.0 + step * i as f64;
58 at(t).map(|p| (t, p))
59 })
60 .collect::<OgeomResult<_>>()?;
61 for axis in AXES {
62 for sense in [1.0, -1.0] {
63 let score = |p: Point| p.to_vector().dot(axis) * sense;
64 let Some((best, _)) = samples
65 .iter()
66 .enumerate()
67 .max_by(|a, b| score(a.1.1).total_cmp(&score(b.1.1)))
68 else {
69 continue;
70 };
71 let lo = samples[best.saturating_sub(1)].0;
72 let hi = samples[(best + 1).min(N)].0;
73 points.push(samples[best].1);
74 if hi > lo {
75 let found = solve::minimize(
76 |t| at(t).map_or(f64::INFINITY, |p| -score(p)),
77 lo,
78 hi,
79 solve::Criteria::default(),
80 )?;
81 points.push(at(found.value)?);
82 }
83 }
84 }
85 }
86
87 for face in explore_unique(model, shape, ShapeType::Face)? {
91 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data()) else {
92 continue;
93 };
94 let Some(surface) = model.geometry().surface(data.surface) else {
95 ogeom_bail!(Dangling, "surface is not in this model");
96 };
97 let placement = face.transform(model.datums())?;
98 let mesh = ogeom_mesh::triangulate_face(model, &face, Deflection::default(), tol)?;
99 if mesh.positions.is_empty() {
100 continue;
101 }
102 let chart: Vec<[Point2; 3]> = mesh
103 .triangles
104 .iter()
105 .map(|t| {
106 t.map(|i| {
107 let (u, v) = mesh.parameters[i as usize];
108 Point2::new(u, v)
109 })
110 })
111 .collect();
112 let inside = |p: Point2| chart.iter().any(|t| in_triangle(*t, p));
113 let span = chart.iter().flatten().fold(
114 (
115 f64::INFINITY,
116 f64::NEG_INFINITY,
117 f64::INFINITY,
118 f64::NEG_INFINITY,
119 ),
120 |b, p| (b.0.min(p.x), b.1.max(p.x), b.2.min(p.y), b.3.max(p.y)),
121 );
122 let lower = [span.0, span.2];
123 let upper = [span.1, span.3];
124 if !(upper[0] > lower[0] && upper[1] > lower[1]) {
125 continue;
126 }
127 for axis in AXES {
128 for sense in [1.0, -1.0] {
129 let score = |p: Point| p.to_vector().dot(axis) * sense;
130 let Some(best) = (0..mesh.positions.len())
131 .max_by(|a, b| score(mesh.positions[*a]).total_cmp(&score(mesh.positions[*b])))
132 else {
133 continue;
134 };
135 points.push(mesh.positions[best]);
136 let (u, v) = mesh.parameters[best];
137 let refined = solve_on_face(
138 |uv: &[f64]| {
139 let p = Point2::new(uv[0], uv[1]);
140 if !inside(p) {
141 return f64::INFINITY;
142 }
143 surface
144 .point_at(uv[0], uv[1], tol)
145 .map_or(f64::INFINITY, |q| -score(placement.apply(q)))
146 },
147 [u, v],
148 lower,
149 upper,
150 )?;
151 if let Some([u, v]) = refined {
152 points.push(placement.apply(surface.point_at(u, v, tol)?));
153 }
154 }
155 }
156 }
157
158 if points.is_empty() {
159 ogeom_bail!(Construction, "the shape holds nothing to bound");
160 }
161 Ok(Aabb::of_points(&points))
162}
163
164fn solve_on_face(
167 f: impl FnMut(&[f64]) -> f64,
168 start: [f64; 2],
169 lower: [f64; 2],
170 upper: [f64; 2],
171) -> OgeomResult<Option<[f64; 2]>> {
172 let step = ((upper[0] - lower[0]).min(upper[1] - lower[1]) * 1e-2).max(1e-9);
173 let found = ogeom_math::minimize_local(f, &start, &lower, &upper, step, 1e-15, 2000)?;
174 Ok(found
175 .value
176 .is_finite()
177 .then(|| [found.point[0], found.point[1]]))
178}
179
180fn in_triangle(t: [Point2; 3], p: Point2) -> bool {
181 let cross =
182 |a: Point2, b: Point2, c: Point2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
183 let (d1, d2, d3) = (
184 cross(t[0], t[1], p),
185 cross(t[1], t[2], p),
186 cross(t[2], t[0], p),
187 );
188 let eps = 1e-12;
189 let neg = d1 < -eps || d2 < -eps || d3 < -eps;
190 let pos = d1 > eps || d2 > eps || d3 > eps;
191 !(neg && pos)
192}