1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
35use ogeom_geom::{Curve2d as _, PlanarCurve, Surface as _};
36use ogeom_math::{Point, Point2, Vector2, solve};
37use ogeom_topo::{EdgeRepr, Model, Orientation, Shape, TShapeId};
38
39const SEGMENTS: u32 = 32;
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct ChartCrossing {
46 pub edges: [(usize, usize); 2],
50 pub parameters: [f64; 2],
52 pub apart: [i64; 2],
57 pub at: Point,
59}
60
61struct Walked<'m> {
63 wire: usize,
64 edge: usize,
65 node: TShapeId,
66 pcurve: &'m PlanarCurve,
67 range: (f64, f64),
68 shift: Vector2,
71 vertices: Vec<(Point, f64)>,
73 tolerance: f64,
74 drawn: bool,
76 reversed: bool,
78 ends: Option<(TShapeId, TShapeId)>,
80 samples: Vec<(f64, Point2)>,
82}
83
84pub fn chart_crossings(
94 model: &Model,
95 face: &Shape,
96 tol: Tolerances,
97) -> OgeomResult<Vec<ChartCrossing>> {
98 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
99 ogeom_bail!(Dangling, "face is not in this model");
100 };
101 let Some(surface) = model.geometry().surface(data.surface) else {
102 ogeom_bail!(Dangling, "face names a surface not in this model");
103 };
104 let ((u0, u1), (v0, v1)) = surface.domain();
105 let periods = [
106 (surface.is_periodic_u() || surface.is_closed_u(tol))
107 .then_some(u1 - u0)
108 .filter(|p| *p > 0.0 && p.is_finite()),
109 (surface.is_periodic_v() || surface.is_closed_v(tol))
110 .then_some(v1 - v0)
111 .filter(|p| *p > 0.0 && p.is_finite()),
112 ];
113 let placement = face.transform(model.datums())?;
114 let stored = face.oriented(Orientation::Forward);
115
116 let mut walked: Vec<Walked<'_>> = Vec::new();
117 for (w, wire) in model.ordered_children_of(&stored)?.into_iter().enumerate() {
118 let mut last: Option<Point2> = None;
119 for (i, edge) in model.ordered_children_of(&wire)?.into_iter().enumerate() {
120 let Some(edge_data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
121 return Ok(Vec::new());
122 };
123 let reversed = edge.orientation() == Orientation::Reversed;
124 let (id, range) = match edge_data.pcurve_for(data.surface, edge.location()) {
125 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
126 Some(EdgeRepr::Seam {
127 forward,
128 reversed: back,
129 range,
130 ..
131 }) => (if reversed { *back } else { *forward }, *range),
132 _ => return Ok(Vec::new()),
133 };
134 let Some(pcurve) = model.geometry().pcurve(id) else {
135 ogeom_bail!(Dangling, "an edge names geometry not in this model");
136 };
137 let (first, end) = {
138 let (a, b) = (
139 pcurve.point_at(range.0, tol)?,
140 pcurve.point_at(range.1, tol)?,
141 );
142 if reversed { (b, a) } else { (a, b) }
143 };
144 let mut shift = Vector2::new(0.0, 0.0);
147 if let Some(last) = last {
148 for (axis, period) in periods.iter().enumerate() {
149 let Some(period) = period else { continue };
150 let (to, from) = if axis == 0 {
151 (last.x, first.x)
152 } else {
153 (last.y, first.y)
154 };
155 let whole = ((to - from) / period).round() * period;
156 if axis == 0 {
157 shift.x = whole;
158 } else {
159 shift.y = whole;
160 }
161 }
162 }
163 last = Some(end + shift);
164 let mut vertices = Vec::with_capacity(2);
165 for v in model.children_of(&edge)? {
166 let Some(vd) = model.node(&v).and_then(|n| n.data().as_vertex()) else {
167 continue;
168 };
169 vertices.push((
170 v.transform(model.datums())?.apply(vd.point),
171 vd.tolerance.get(),
172 ));
173 }
174 let pieces = if matches!(pcurve, PlanarCurve::Line(_)) {
175 1
176 } else {
177 SEGMENTS
178 };
179 let samples = if edge_data.degenerate {
180 Vec::new()
181 } else {
182 (0..=pieces)
183 .map(|k| {
184 let t = range.0 + (range.1 - range.0) * f64::from(k) / f64::from(pieces);
185 pcurve.point_at(t, tol).map(|p| (t, p + shift))
186 })
187 .collect::<OgeomResult<_>>()?
188 };
189 let ends =
190 crate::build::edge_vertices(model, &edge)?.map(|(a, b)| (a.node(), b.node()));
191 walked.push(Walked {
192 wire: w,
193 edge: i,
194 node: edge.node(),
195 reversed,
196 ends,
197 pcurve,
198 range,
199 shift,
200 vertices,
201 tolerance: edge_data.tolerance.get(),
202 drawn: !edge_data.degenerate,
203 samples,
204 });
205 }
206 }
207
208 let mut moves: Vec<[i64; 2]> = vec![[0, 0]];
210 for (axis, period) in periods.iter().enumerate() {
211 if period.is_some() {
212 let mut more = Vec::new();
213 for m in &moves {
214 for k in [-1, 1] {
215 let mut moved = *m;
216 moved[axis] = k;
217 more.push(moved);
218 }
219 }
220 moves.extend(more);
221 }
222 }
223 let offset = |m: [i64; 2]| {
224 #[allow(clippy::cast_precision_loss, reason = "one period either way")]
225 Vector2::new(
226 periods[0].map_or(0.0, |p| p * m[0] as f64),
227 periods[1].map_or(0.0, |p| p * m[1] as f64),
228 )
229 };
230 let boxes: Vec<(Point2, Point2)> = walked.iter().map(|e| bounds(&e.samples)).collect();
231
232 let mut found: Vec<ChartCrossing> = Vec::new();
233 for a in 0..walked.len() {
234 if !walked[a].drawn {
235 continue;
236 }
237 for b in a + 1..walked.len() {
238 if !walked[b].drawn || walked[a].node == walked[b].node {
239 continue;
240 }
241 for &m in &moves {
242 let by = offset(m);
243 let (lo, hi) = (boxes[b].0 + by, boxes[b].1 + by);
244 if lo.x > boxes[a].1.x
245 || hi.x < boxes[a].0.x
246 || lo.y > boxes[a].1.y
247 || hi.y < boxes[a].0.y
248 {
249 continue;
250 }
251 found.extend(pair_crossings(
252 &walked[a], &walked[b], by, m, surface, &placement, tol,
253 )?);
254 }
255 }
256 }
257 Ok(found)
258}
259
260fn bounds(samples: &[(f64, Point2)]) -> (Point2, Point2) {
262 let mut lo = Point2::new(f64::INFINITY, f64::INFINITY);
263 let mut hi = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
264 for (_, p) in samples {
265 lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
266 hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
267 }
268 (lo, hi)
269}
270
271fn pair_crossings(
275 a: &Walked<'_>,
276 b: &Walked<'_>,
277 by: Vector2,
278 apart: [i64; 2],
279 surface: &ogeom_geom::SurfaceGeometry,
280 placement: &ogeom_math::Transform,
281 tol: Tolerances,
282) -> OgeomResult<Vec<ChartCrossing>> {
283 let mut found: Vec<(Point2, ChartCrossing)> = Vec::new();
284 for i in 1..a.samples.len() {
285 let (pa, qa) = (a.samples[i - 1], a.samples[i]);
286 for j in 1..b.samples.len() {
287 let (pb, qb) = (b.samples[j - 1], b.samples[j]);
288 let Some((sa, sb)) = segments_cross(pa.1, qa.1, pb.1 + by, qb.1 + by) else {
289 continue;
290 };
291 let seed = [pa.0 + (qa.0 - pa.0) * sa, pb.0 + (qb.0 - pb.0) * sb];
292 let Some([ta, tb]) = polish(a, b, by, seed, tol) else {
293 continue;
294 };
295 let uv = a.pcurve.point_at(ta, tol)?;
296 if found
298 .iter()
299 .any(|(at, _)| at.distance(uv) <= tol.confusion())
300 {
301 continue;
302 }
303 let (Ok((point, du, dv)), Ok(da), Ok(db)) = (
304 surface.point_d1_at(uv.x, uv.y, tol),
305 a.pcurve.d1_at(ta, tol),
306 b.pcurve.d1_at(tb, tol),
307 ) else {
308 continue;
309 };
310 let at = placement.apply(point);
311 let (ta3, tb3) = (du * da.x + dv * da.y, du * db.x + dv * db.y);
314 let (la, lb) = (ta3.magnitude(), tb3.magnitude());
315 if la <= 0.0
316 || lb <= 0.0
317 || ta3.cross(tb3).magnitude() <= crate::self_cross::CROSSING_SINE * la * lb
318 {
319 continue;
320 }
321 let widest = a.tolerance.max(b.tolerance).max(tol.approximation());
323 let near = |e: &Walked<'_>| {
324 e.vertices
325 .iter()
326 .any(|(v, t)| at.distance(*v) <= crate::check::CHART_GAP_SLACK * widest.max(*t))
327 };
328 if near(a) && near(b) {
329 continue;
330 }
331 if apart == [0, 0] && sliver(a, ta, b, tb, surface, tol)? {
332 continue;
333 }
334 found.push((
335 uv,
336 ChartCrossing {
337 edges: [(a.wire, a.edge), (b.wire, b.edge)],
338 parameters: [ta, tb],
339 apart,
340 at,
341 },
342 ));
343 }
344 }
345 Ok(found.into_iter().map(|(_, crossing)| crossing).collect())
346}
347
348fn sliver(
353 a: &Walked<'_>,
354 ta: f64,
355 b: &Walked<'_>,
356 tb: f64,
357 surface: &ogeom_geom::SurfaceGeometry,
358 tol: Tolerances,
359) -> OgeomResult<bool> {
360 const SAMPLES: u32 = 32;
361 let (Some(ea), Some(eb)) = (a.ends, b.ends) else {
362 return Ok(false);
363 };
364 let ((into, t_into), (out, t_out)) = if ea.1 == eb.0 {
367 ((a, ta), (b, tb))
368 } else if eb.1 == ea.0 {
369 ((b, tb), (a, ta))
370 } else {
371 return Ok(false);
372 };
373 if into.wire != out.wire {
374 return Ok(false);
375 }
376 let walked_end = |e: &Walked<'_>, last: bool| {
377 if last != e.reversed {
378 e.range.1
379 } else {
380 e.range.0
381 }
382 };
383 let stretch = |e: &Walked<'_>, from: f64, to: f64| -> OgeomResult<Vec<Point>> {
384 (0..=SAMPLES)
385 .map(|k| {
386 let uv = e
387 .pcurve
388 .point_at(from + (to - from) * f64::from(k) / f64::from(SAMPLES), tol)?;
389 surface.point_at(uv.x, uv.y, tol)
390 })
391 .collect()
392 };
393 let first = stretch(into, t_into, walked_end(into, true))?;
394 let second = stretch(out, walked_end(out, false), t_out)?;
395 let width = 2.0 * (into.tolerance + out.tolerance);
396 let within = |points: &[Point], line: &[Point]| {
397 points.iter().all(|p| {
398 line.windows(2)
399 .any(|s| distance_to_segment(*p, s[0], s[1]) <= width)
400 })
401 };
402 Ok(within(&first, &second) && within(&second, &first))
403}
404
405fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
407 let along = b - a;
408 let length = along.dot(along);
409 if length <= 0.0 {
410 return p.distance(a);
411 }
412 let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
413 p.distance(a + along * t)
414}
415
416fn segments_cross(p: Point2, q: Point2, r: Point2, s: Point2) -> Option<(f64, f64)> {
419 let (d, e) = (q - p, s - r);
420 let denominator = d.cross(e);
421 if denominator == 0.0 {
422 return None;
423 }
424 let w = r - p;
425 let (t, u) = (w.cross(e) / denominator, w.cross(d) / denominator);
426 ((0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u)).then_some((t, u))
427}
428
429fn polish(
433 a: &Walked<'_>,
434 b: &Walked<'_>,
435 by: Vector2,
436 seed: [f64; 2],
437 tol: Tolerances,
438) -> Option<[f64; 2]> {
439 let clamp = |t: f64, (lo, hi): (f64, f64)| t.max(lo.min(hi)).min(lo.max(hi));
440 let system = |x: &[f64; 2]| {
441 let (ta, tb) = (clamp(x[0], a.range), clamp(x[1], b.range));
442 let pa = a.pcurve.point_at(ta, tol).unwrap_or(Point2::ORIGIN) + a.shift;
443 let pb = b.pcurve.point_at(tb, tol).unwrap_or(Point2::ORIGIN) + b.shift + by;
444 let da = a.pcurve.d1_at(ta, tol).unwrap_or(Vector2::new(0.0, 0.0));
445 let db = b.pcurve.d1_at(tb, tol).unwrap_or(Vector2::new(0.0, 0.0));
446 ([pa.x - pb.x, pa.y - pb.y], [[da.x, -db.x], [da.y, -db.y]])
447 };
448 let criteria = solve::Criteria {
449 residual: tol.confusion() * 0.01,
450 step: tol.parametric(),
451 max_iterations: 40,
452 };
453 let (x, _, _, _) = solve::newton_system_fixed(system, seed, criteria).ok()?;
454 let (ta, tb) = (clamp(x[0], a.range), clamp(x[1], b.range));
455 let pa = a.pcurve.point_at(ta, tol).ok()? + a.shift;
456 let pb = b.pcurve.point_at(tb, tol).ok()? + b.shift + by;
457 (pa.distance(pb) <= tol.confusion()).then_some([ta, tb])
458}