1use ogeom_core::{FastMap, OgeomResult, Tolerances};
39use ogeom_geom::{Curve, Curve2d as _, Surface as _, SurfaceGeometry};
40use ogeom_math::{Point2, Transform};
41use ogeom_topo::{
42 EdgeRepr, Model, Orientation, SameKey, Shape, ShapeType, SurfaceId, TShapeId, explore_unique,
43};
44
45use crate::build::edge_vertices;
46use crate::pcurve_gap::{nearest_point_on_stretch, stretches_apart};
47
48#[derive(Debug, Clone, PartialEq)]
51pub struct OutAndBack {
52 pub face: Shape,
54 pub wire: usize,
56 pub edges: [usize; 2],
60 pub notch: Option<Notch>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct Notch {
67 pub longer: usize,
69 pub parameter: f64,
72}
73
74pub fn edge_uses(model: &Model, shape: &Shape) -> OgeomResult<FastMap<TShapeId, usize>> {
86 let mut uses: FastMap<TShapeId, usize> = FastMap::default();
87 for (edge, n) in placed_edge_uses(model, shape)? {
88 *uses.entry(edge.0.node()).or_default() += n;
89 }
90 Ok(uses)
91}
92
93pub fn placed_edge_uses(model: &Model, shape: &Shape) -> OgeomResult<FastMap<SameKey, usize>> {
101 let mut uses: FastMap<SameKey, usize> = FastMap::default();
102 for face in explore_unique(model, shape, ShapeType::Face)? {
103 for wire in model.children_of(&face)? {
104 for edge in model.children_of(&wire)? {
105 *uses.entry(SameKey(edge)).or_default() += 1;
106 }
107 }
108 }
109 Ok(uses)
110}
111
112pub fn out_and_back(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<OutAndBack>> {
123 let uses = placed_edge_uses(model, shape)?;
124 let mut found = Vec::new();
125 for face in explore_unique(model, shape, ShapeType::Face)? {
126 let Some(surface_id) = model
127 .node(&face)
128 .and_then(|n| n.data().as_face())
129 .map(|d| d.surface)
130 else {
131 continue;
132 };
133 let Some(surface) = model.geometry().surface(surface_id) else {
134 continue;
135 };
136 let stored = face.oriented(Orientation::Forward);
137 for (w, wire) in model.ordered_children_of(&stored)?.into_iter().enumerate() {
138 let walked = model.ordered_children_of(&wire.oriented(Orientation::Forward))?;
139 if walked.len() < 2 {
140 continue;
141 }
142 for i in 0..walked.len() {
143 let j = (i + 1) % walked.len();
144 if walked.len() == 2 && j == 0 {
145 continue;
148 }
149 let (out, back) = (&walked[i], &walked[j]);
150 if out.node() == back.node() {
151 continue;
152 }
153 let notch = if is_tail(model, (surface_id, surface), out, back, &uses, tol)? {
154 None
155 } else if let Some(notch) =
156 notch_of(model, (surface_id, surface), out, back, &uses, tol)?
157 {
158 Some(notch)
159 } else {
160 continue;
161 };
162 found.push(OutAndBack {
163 face: face.clone(),
164 wire: w,
165 edges: [i, j],
166 notch,
167 });
168 }
169 }
170 }
171 Ok(found)
172}
173
174pub fn is_tail(
183 model: &Model,
184 (surface_id, surface): (SurfaceId, &SurfaceGeometry),
185 out: &Shape,
186 back: &Shape,
187 uses: &FastMap<SameKey, usize>,
188 tol: Tolerances,
189) -> OgeomResult<bool> {
190 let Some((out_data, back_data)) = alone(model, out, back, uses) else {
191 return Ok(false);
192 };
193 let (Some((a, b)), Some((c, d))) = (edge_vertices(model, out)?, edge_vertices(model, back)?)
194 else {
195 return Ok(false);
196 };
197 if a.node() == b.node() || c.node() != b.node() || d.node() != a.node() {
198 return Ok(false);
199 }
200 let (Some(out_chart), Some(back_chart)) = (
201 chart_ends(model, surface_id, out, tol)?,
202 chart_ends(model, surface_id, back, tol)?,
203 ) else {
204 return Ok(false);
205 };
206 if !within_a_period(surface, out_chart.0, back_chart.1, tol)
207 || !within_a_period(surface, out_chart.1, back_chart.0, tol)
208 {
209 return Ok(false);
210 }
211 if out.node() == back.node() {
212 return Ok(true);
213 }
214 let reach = out_data.tolerance.get().max(back_data.tolerance.get());
215 Ok(crate::edges_apart(model, out, back, tol)?.is_some_and(|apart| apart <= reach))
216}
217
218pub fn notch_of(
226 model: &Model,
227 (surface_id, surface): (SurfaceId, &SurfaceGeometry),
228 out: &Shape,
229 back: &Shape,
230 uses: &FastMap<SameKey, usize>,
231 tol: Tolerances,
232) -> OgeomResult<Option<Notch>> {
233 let Some((out_data, back_data)) = alone(model, out, back, uses) else {
234 return Ok(None);
235 };
236 if out.node() == back.node() {
237 return Ok(None);
238 }
239 let (Some((a, b)), Some((c, d))) = (edge_vertices(model, out)?, edge_vertices(model, back)?)
240 else {
241 return Ok(None);
242 };
243 if a.node() == b.node() || c.node() != b.node() || d.node() == c.node() || d.node() == a.node()
244 {
245 return Ok(None);
246 }
247 let (Some(out_chart), Some(back_chart)) = (
248 chart_ends(model, surface_id, out, tol)?,
249 chart_ends(model, surface_id, back, tol)?,
250 ) else {
251 return Ok(None);
252 };
253 if !within_a_period(surface, out_chart.1, back_chart.0, tol) {
254 return Ok(None);
255 }
256 let (Some(out_curve), Some(back_curve)) =
257 (placed_curve(model, out)?, placed_curve(model, back)?)
258 else {
259 return Ok(None);
260 };
261 let reach = out_data.tolerance.get().max(back_data.tolerance.get());
262 let tip_at_end = |edge: &Shape, walked_into: bool| {
265 (edge.orientation() == Orientation::Reversed) != walked_into
266 };
267 for (longer, (long_curve, short_curve, far, tip_high)) in [
268 (out_curve, back_curve, &d, tip_at_end(out, true)),
269 (back_curve, out_curve, &a, tip_at_end(back, false)),
270 ]
271 .into_iter()
272 .enumerate()
273 {
274 let Some(vertex) = model.node(far).and_then(|n| n.data().as_vertex()) else {
275 continue;
276 };
277 let placed = far.transform(model.datums())?.apply(vertex.point);
278 let (curve, range, placement) = long_curve;
279 let into = placement.inverse()?;
280 let (t, off) = nearest_point_on_stretch(curve, range, into.apply(placed), tol)?;
281 let off = off * placement.scale_factor().abs();
282 if off > reach.max(vertex.tolerance.get()) {
283 continue;
284 }
285 let (lo, hi) = (range.0.min(range.1), range.0.max(range.1));
286 if !(t > lo && t < hi) {
287 continue;
288 }
289 let stretch = if tip_high { (t, range.1) } else { (range.0, t) };
290 if stretches_apart((curve, stretch, placement), short_curve, tol)? <= reach {
291 return Ok(Some(Notch {
292 longer,
293 parameter: t,
294 }));
295 }
296 }
297 Ok(None)
298}
299
300fn alone<'m>(
304 model: &'m Model,
305 out: &Shape,
306 back: &Shape,
307 uses: &FastMap<SameKey, usize>,
308) -> Option<(&'m ogeom_topo::EdgeData, &'m ogeom_topo::EdgeData)> {
309 let data = |edge: &Shape| model.node(edge).and_then(|n| n.data().as_edge());
310 let (out_data, back_data) = (data(out)?, data(back)?);
311 if out_data.degenerate || back_data.degenerate {
312 return None;
313 }
314 let used = |edge: &Shape| uses.get(&SameKey(edge.clone())).copied().unwrap_or(0);
315 let alone = if out.node() == back.node() {
316 used(out) == 2 && out.orientation() != back.orientation()
317 } else {
318 used(out) == 1 && used(back) == 1
319 };
320 alone.then_some((out_data, back_data))
321}
322
323type PlacedCurve<'m> = (&'m Curve, (f64, f64), Transform);
326
327fn placed_curve<'m>(model: &'m Model, edge: &Shape) -> OgeomResult<Option<PlacedCurve<'m>>> {
329 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
330 return Ok(None);
331 };
332 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
333 return Ok(None);
334 };
335 let Some(curve) = model.geometry().curve(*curve) else {
336 return Ok(None);
337 };
338 Ok(Some((curve, *range, edge.transform(model.datums())?)))
339}
340
341fn chart_ends(
344 model: &Model,
345 surface: SurfaceId,
346 edge: &Shape,
347 tol: Tolerances,
348) -> OgeomResult<Option<(Point2, Point2)>> {
349 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
350 return Ok(None);
351 };
352 let Some(EdgeRepr::PCurve {
353 curve,
354 range,
355 location,
356 ..
357 }) = data.pcurve_for(surface, edge.location())
358 else {
359 return Ok(None);
360 };
361 let plain = data
364 .representations
365 .iter()
366 .filter(|r| {
367 matches!(r, EdgeRepr::PCurve { .. })
368 && r.surface() == Some(surface)
369 && r.location() == Some(location)
370 })
371 .count();
372 if plain > 1 {
373 return Ok(None);
374 }
375 let Some(pcurve) = model.geometry().pcurve(*curve) else {
376 return Ok(None);
377 };
378 let (start, end) = (
379 pcurve.point_at(range.0, tol)?,
380 pcurve.point_at(range.1, tol)?,
381 );
382 Ok(Some(if edge.orientation() == Orientation::Reversed {
383 (end, start)
384 } else {
385 (start, end)
386 }))
387}
388
389fn within_a_period(surface: &SurfaceGeometry, p: Point2, q: Point2, tol: Tolerances) -> bool {
392 let ((u0, u1), (v0, v1)) = surface.domain();
393 let closes = [
394 surface.is_periodic_u() || surface.is_closed_u(tol),
395 surface.is_periodic_v() || surface.is_closed_v(tol),
396 ];
397 [
398 (closes[0], u1 - u0, (p.x - q.x).abs()),
399 (closes[1], v1 - v0, (p.y - q.y).abs()),
400 ]
401 .into_iter()
402 .all(|(closes, period, apart)| !closes || !period.is_finite() || 2.0 * apart < period)
403}