1use crate::support::{chart_holds, distance_to_edge, edge_curve, face_mesh, same_occurrence};
19use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
20use ogeom_geom::{Curve3d as _, Surface as _, SurfaceGeometry, Transformable as _};
21use ogeom_math::{Point, Point2, Vector};
22use ogeom_topo::{Filter, Model, NodeData, Orientation, Shape, ShapeType, explore};
23
24const WALK_STATIONS: u32 = 256;
26
27struct Walk {
32 face: Shape,
33 surface: SurfaceGeometry,
34 from: Point,
35 step: Vector,
36 factor: f64,
37}
38
39impl Walk {
40 fn contact(&self, sigma: f64, tol: Tolerances) -> OgeomResult<(Point, Point2)> {
42 let found =
43 ogeom_algo::project_on_surface(&self.surface, self.from + self.step * sigma, 16, tol)?;
44 let (u, v) = found.parameters;
45 Ok((found.point, Point2::new(u, v)))
46 }
47}
48
49struct Reach {
53 setback: f64,
54 speed: f64,
55}
56
57fn walks(model: &Model, solid: &Shape, edge: &Shape, tol: Tolerances) -> OgeomResult<Vec<Walk>> {
71 let mut found = Vec::new();
72 let Ok((curve, range)) = edge_curve(model, edge, tol) else {
73 return Ok(found);
74 };
75 let mut sides: Vec<(Shape, bool)> = Vec::new();
76 for face in explore(model, solid, Filter::OfType(ShapeType::Face))? {
77 for e in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
78 if same_occurrence(model, &e, edge, tol) {
79 sides.push((face.clone(), e.orientation() == Orientation::Reversed));
80 break;
81 }
82 }
83 }
84 let [(face0, rev0), (face1, rev1)] = sides.as_slice() else {
85 return Ok(found);
86 };
87 let normal_at = |face: &Shape, p: Point| -> OgeomResult<Option<(SurfaceGeometry, Vector)>> {
88 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
89 return Ok(None);
90 };
91 let Some(surface) = model.geometry().surface(data.surface) else {
92 return Ok(None);
93 };
94 let placed = surface
95 .clone()
96 .transformed(&face.transform(model.datums())?, tol)?;
97 let (u, v) = ogeom_algo::project_on_surface(&placed, p, 16, tol)?.parameters;
98 let Ok(n) = placed.normal_at(u, v, tol) else {
99 return Ok(None);
100 };
101 let n = if face.orientation() == Orientation::Reversed {
102 -n.vector()
103 } else {
104 n.vector()
105 };
106 Ok(Some((placed, n)))
107 };
108 for fraction in [0.25, 0.5, 0.75] {
109 let t = range.0 + (range.1 - range.0) * fraction;
110 let p = curve.point_at(t, tol)?;
111 let d = curve.d1_at(t, tol)?;
112 let m = d.magnitude();
113 if m <= tol.confusion() {
114 continue;
115 }
116 let tangent = d / m;
117 let (Some((s0, n0)), Some((s1, n1))) = (normal_at(face0, p)?, normal_at(face1, p)?) else {
118 continue;
119 };
120 let turn = n0.dot(n1).clamp(-1.0, 1.0).acos();
121 if turn <= tol.angular() * 1e3 {
122 continue;
123 }
124 let factor = (turn * 0.5).tan();
125 let into = |face: &Shape, surface: &SurfaceGeometry, n: Vector, reversed: bool| {
128 let walk = if reversed { -tangent } else { tangent };
129 let d = n.cross(walk);
130 let l = d.magnitude();
131 if l <= 0.0 {
132 return Ok(None);
133 }
134 let d = d / l;
135 let lands = |step: Vector| -> OgeomResult<bool> {
136 let (u, v) = ogeom_algo::project_on_surface(surface, p + step, 16, tol)?.parameters;
137 chart_holds(model, face, Point2::new(u, v), tol)
138 };
139 let mut side: Option<Vector> = None;
144 for decade in 1..=5 {
145 let hair = tol.confusion() * 10_f64.powi(decade);
146 let here = match [lands(d * hair)?, lands(-d * hair)?] {
147 [true, _] => d,
148 [false, true] => -d,
149 [false, false] => continue,
150 };
151 if side.is_some_and(|s| s.dot(here) < 0.0) {
152 ogeom_bail!(
153 NotDone,
154 "the face beside the edge through {p:?} reads on different sides of \
155 it at the steps the check takes, which grow with the radius"
156 );
157 }
158 side = Some(here);
159 }
160 Ok(side)
161 };
162 let (Some(d0), Some(d1)) = (into(face0, &s0, n0, *rev0)?, into(face1, &s1, n1, *rev1)?)
163 else {
164 continue;
165 };
166 for (face, surface, step) in [(face0, s0, d0), (face1, s1, d1)] {
167 found.push(Walk {
168 face: face.clone(),
169 surface,
170 from: p,
171 step,
172 factor,
173 });
174 }
175 }
176 Ok(found)
177}
178
179fn reach(model: &Model, walk: &Walk, up: f64, tol: Tolerances) -> OgeomResult<Reach> {
188 let conf = tol.confusion();
189 let (p0, a) = walk.contact(0.0, tol)?;
190 let (pm, m) = walk.contact(up * 0.5, tol)?;
191 let (p1, b) = walk.contact(up, tol)?;
192 let quadric = matches!(
193 walk.surface,
194 SurfaceGeometry::Plane(_) | SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_)
195 );
196 let on = |p: Point, sigma: f64| p.distance(walk.from + walk.step * sigma) <= conf;
197 let mid = Point2::new(f64::midpoint(a.x, b.x), f64::midpoint(a.y, b.y));
198 let chart_span =
199 a.x.abs()
200 .max(a.y.abs())
201 .max(b.x.abs())
202 .max(b.y.abs())
203 .max(1.0);
204 if quadric
205 && on(p0, 0.0)
206 && on(pm, up * 0.5)
207 && on(p1, up)
208 && (m.x - mid.x).abs().max((m.y - mid.y).abs()) <= chart_span * 1e-12
209 {
210 let mesh = face_mesh(model, &walk.face, tol)?;
211 return Ok(Reach {
212 setback: up * straight_exit(&mesh, a, b),
213 speed: 1.0,
214 });
215 }
216 let holds = |sigma: f64| -> OgeomResult<bool> {
219 let target = walk.from + walk.step * sigma;
220 let (at, uv) = walk.contact(sigma, tol)?;
221 let Ok(n) = walk.surface.normal_at(uv.x, uv.y, tol) else {
222 return Ok(false);
223 };
224 let off = target - at;
225 let n = n.vector();
226 if (off - n * n.dot(off)).magnitude() > conf {
227 return Ok(false);
228 }
229 chart_holds(model, &walk.face, uv, tol)
230 };
231 let mut speed = 1.0_f64;
232 let mut last = (0.0, p0);
233 for k in 1..=WALK_STATIONS {
234 let sigma = up * f64::from(k) / f64::from(WALK_STATIONS);
235 if !holds(sigma)? {
236 let (mut lo, mut hi) = (last.0, sigma);
237 for _ in 0..48 {
238 let mid = f64::midpoint(lo, hi);
239 if holds(mid)? {
240 lo = mid;
241 } else {
242 hi = mid;
243 }
244 }
245 return Ok(Reach { setback: lo, speed });
246 }
247 let (at, _) = walk.contact(sigma, tol)?;
248 speed = speed.max(at.distance(last.1) / (sigma - last.0) * 2.0);
249 last = (sigma, at);
250 }
251 Ok(Reach { setback: up, speed })
252}
253
254fn straight_exit(mesh: &ogeom_topo::Triangulation, a: Point2, b: Point2) -> f64 {
260 let chart = |i: u32| {
261 let (u, v) = mesh.parameters[i as usize];
262 Point2::new(u, v)
263 };
264 let span = mesh
265 .parameters
266 .iter()
267 .fold(0.0_f64, |m, (u, v)| m.max(u.abs()).max(v.abs()))
268 .max(1.0);
269 let eps = span * 1e-9;
270 let (dx, dy) = (b.x - a.x, b.y - a.y);
271 let side = |p: Point2, q: Point2| {
273 let (ex, ey) = (q.x - p.x, q.y - p.y);
274 (ex * (a.y - p.y) - ey * (a.x - p.x), ex * dy - ey * dx)
275 };
276 let at_least = |(alpha, beta): (f64, f64), bound: f64, (lo, hi): (f64, f64)| {
278 let excess = alpha - bound;
279 if beta == 0.0 {
280 return if excess >= 0.0 { (lo, hi) } else { (1.0, 0.0) };
281 }
282 let root = -excess / beta;
283 if beta > 0.0 {
284 (lo.max(root), hi)
285 } else {
286 (lo, hi.min(root))
287 }
288 };
289 let mut spans: Vec<(f64, f64)> = Vec::new();
290 for t in &mesh.triangles {
291 let [p, q, r] = t.map(chart);
292 let sides = [side(p, q), side(q, r), side(r, p)];
293 let up = sides
296 .iter()
297 .fold((0.0, 1.0), |span, s| at_least(*s, -eps, span));
298 let down = sides
299 .iter()
300 .fold((0.0, 1.0), |span, s| at_least((-s.0, -s.1), -eps, span));
301 for span in [up, down] {
302 if span.0 <= span.1 {
303 spans.push(span);
304 }
305 }
306 }
307 spans.sort_by(|x, y| x.0.total_cmp(&y.0));
308 let mut reached: Option<f64> = None;
309 for (lo, hi) in spans {
310 match reached {
311 None if lo <= 0.0 => reached = Some(hi),
312 Some(r) if lo <= r => reached = Some(r.max(hi)),
313 _ => {}
314 }
315 }
316 reached.unwrap_or(0.0)
317}
318
319fn edge_largest(
323 model: &Model,
324 solid: &Shape,
325 edge: &Shape,
326 up_to: f64,
327 per: impl Fn(&Walk) -> f64,
328 tol: Tolerances,
329) -> OgeomResult<(f64, Vec<(Walk, Reach)>)> {
330 let conf = tol.confusion();
331 let mut largest = up_to;
332 let mut found = Vec::new();
333 for walk in walks(model, solid, edge, tol)? {
334 let rate = per(&walk);
335 if rate <= 0.0 {
336 continue;
337 }
338 let bounds = ogeom_algo::shape_bounds(model, &walk.face, tol)?;
339 let up = (up_to * rate).min(bounds.diagonal() + conf);
341 let reached = reach(model, &walk, up, tol)?;
342 if reached.setback < up || up < up_to * rate {
343 largest = largest.min((reached.setback - conf) / rate);
344 }
345 found.push((walk, reached));
346 }
347 Ok((largest, found))
348}
349
350pub(crate) fn legs_largest(
354 model: &Model,
355 solid: &Shape,
356 edge: &Shape,
357 up_to: f64,
358 per: impl Fn(&Shape) -> f64,
359 tol: Tolerances,
360) -> OgeomResult<f64> {
361 crate::support::keeping_face_meshes(|| {
362 Ok(edge_largest(model, solid, edge, up_to, |w| per(&w.face), tol)?.0)
363 })
364}
365
366pub fn largest_fillet_radius(
393 model: &Model,
394 solid: &Shape,
395 edge: &Shape,
396 up_to: f64,
397 tol: Tolerances,
398) -> OgeomResult<Option<f64>> {
399 largest_fillet_radius_all(model, solid, std::slice::from_ref(edge), up_to, tol)
400}
401
402pub fn largest_fillet_radius_all(
423 model: &Model,
424 solid: &Shape,
425 edges: &[Shape],
426 up_to: f64,
427 tol: Tolerances,
428) -> OgeomResult<Option<f64>> {
429 let conf = tol.confusion();
430 if !up_to.is_finite() || up_to <= conf {
431 ogeom_bail!(
432 Construction,
433 "a largest radius up to {up_to} asks for nothing"
434 );
435 }
436 if edges.is_empty() {
437 ogeom_bail!(Construction, "a request of no edges has no largest radius");
438 }
439 crate::support::keeping_face_meshes(|| -> OgeomResult<Option<f64>> {
440 let mut largest = up_to;
441 let mut seats = Vec::with_capacity(edges.len());
442 for edge in edges {
443 let (radius, walks) = edge_largest(model, solid, edge, up_to, |w| w.factor, tol)?;
444 largest = largest.min(radius);
445 seats.push(walks);
446 }
447 if largest <= conf {
448 return Ok(None);
449 }
450 let ends = |edge: &Shape| -> OgeomResult<Vec<Shape>> {
451 Ok(ogeom_algo::edge_vertices(model, edge)?
452 .map(|(a, b)| vec![a, b])
453 .unwrap_or_default())
454 };
455 for (i, first) in edges.iter().enumerate() {
456 for (j, second) in edges.iter().enumerate() {
457 if i == j {
458 continue;
459 }
460 let (own, other) = (ends(first)?, ends(second)?);
461 if own.iter().any(|v| other.iter().any(|o| o.is_same(v))) {
462 continue;
463 }
464 let Ok((curve, range)) = edge_curve(model, second, tol) else {
465 continue;
466 };
467 for (walk, reached) in &seats[i] {
468 let Some(strip) = seats[j]
471 .iter()
472 .filter(|(w, _)| w.face.is_same(&walk.face))
473 .map(|(w, _)| w.factor)
474 .reduce(f64::max)
475 else {
476 continue;
477 };
478 let rate = walk.factor * reached.speed + strip;
481 let gap = |r: f64| -> OgeomResult<f64> {
482 let (at, _) = walk.contact(r * walk.factor, tol)?;
483 Ok(distance_to_edge(&curve, range, at, tol)? - strip * r)
484 };
485 let mut r = 0.0_f64;
486 for _ in 0..100_000 {
487 if r >= largest {
488 break;
489 }
490 let g = gap(r)?;
491 if g <= conf {
494 largest = (r - conf / rate).max(0.0);
495 break;
496 }
497 r += g / rate;
498 }
499 largest = largest.min(r);
500 }
501 }
502 }
503 if largest <= conf {
504 return Ok(None);
505 }
506 if let Err(refusal) =
508 crate::fillet::request_fits(model, solid, edges, &vec![largest; edges.len()], tol)
509 {
510 ogeom_bail!(
511 NotDone,
512 "the largest radius read from the seats, {largest}, does not pass the \
513 request's check: {}",
514 refusal.error
515 );
516 }
517 Ok(Some(largest))
518 })
519}