1use ogeom_core::{OgeomResult, Tolerances};
21use ogeom_geom::{Curve2d as _, Line2d, Surface as _, SurfaceGeometry};
22use ogeom_math::{Point2, Vector2};
23use ogeom_topo::{EdgeData, EdgeRepr, Location, Model, Shape};
24
25use crate::build::edge_vertices;
26
27const MOST_POLES: usize = 4;
30
31pub fn pole_crossings(
60 model: &mut Model,
61 face: &Shape,
62 tol: Tolerances,
63) -> OgeomResult<Vec<(Shape, Shape)>> {
64 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
65 ogeom_core::ogeom_bail!(Dangling, "face is not in this model");
66 };
67 let surface_id = data.surface;
68 let Some(surface) = model.geometry().surface(surface_id).cloned() else {
69 ogeom_core::ogeom_bail!(Dangling, "face names a surface not in this model");
70 };
71 let mut rebuilt = Vec::new();
72 let stored = face.oriented(ogeom_topo::Orientation::Forward);
73 for (w, wire) in model.ordered_children_of(&stored)?.into_iter().enumerate() {
74 let edges = model.ordered_children_of(&wire)?;
75 let Some(spans) = pole_spans(model, &surface, surface_id, &edges, w == 0, tol)? else {
76 continue;
77 };
78 let mut ring = Vec::with_capacity(edges.len() + spans.len());
79 let mut spans = spans.into_iter().peekable();
80 for (i, edge) in edges.iter().enumerate() {
81 ring.push(edge.clone());
82 if let Some(pole) = spans.next_if(|pole| pole.after == i) {
83 ring.push(degenerate_edge(model, &pole, surface_id, tol)?);
84 }
85 }
86 let new = model.add_wire(&ring)?;
87 rebuilt.push((wire, new));
88 }
89 Ok(rebuilt)
90}
91
92struct Pole {
95 after: usize,
96 vertex: Shape,
97 from: Point2,
98 to: Point2,
99}
100
101type Walked = Vec<Point2>;
104
105fn pole_spans(
109 model: &Model,
110 surface: &SurfaceGeometry,
111 surface_id: ogeom_topo::SurfaceId,
112 edges: &[Shape],
113 outer: bool,
114 tol: Tolerances,
115) -> OgeomResult<Option<Vec<Pole>>> {
116 let n = edges.len();
117 let mut walked: Vec<Walked> = Vec::with_capacity(n);
118 for edge in edges {
119 let Some(points) = walk(model, surface_id, edge, tol)? else {
120 return Ok(None);
121 };
122 walked.push(points);
123 }
124 let ((u0, u1), (v0, v1)) = surface.domain();
125 let periods = [
126 (surface.is_periodic_u() || surface.is_closed_u(tol)).then_some(u1 - u0),
127 (surface.is_periodic_v() || surface.is_closed_v(tol)).then_some(v1 - v0),
128 ];
129 let mut shifts = vec![Vector2::new(0.0, 0.0); n];
131 let mut poles: Vec<(usize, Shape, usize, f64)> = Vec::new();
132 for i in 0..n {
133 let j = (i + 1) % n;
134 let (end, start) = (walked[i][walked[i].len() - 1], walked[j][0]);
135 let gap = start - end;
136 if gap.x.abs() <= tol.parametric() && gap.y.abs() <= tol.parametric() {
137 continue;
138 }
139 let axis = if gap.y.abs() <= tol.parametric() {
140 Some(0)
141 } else if gap.x.abs() <= tol.parametric() {
142 Some(1)
143 } else {
144 None
145 };
146 if let Some(axis) = axis
147 && let Some(vertex) =
148 collapsed_row(model, surface, &edges[i], &edges[j], end, axis, tol)?
149 {
150 poles.push((i, vertex, axis, if axis == 0 { gap.x } else { gap.y }));
151 continue;
152 }
153 let whole = |value: f64, period: Option<f64>| {
154 value.abs() <= tol.parametric()
155 || period
156 .is_some_and(|p| (value - (value / p).round() * p).abs() <= tol.parametric())
157 };
158 if whole(gap.x, periods[0]) && whole(gap.y, periods[1]) {
159 shifts[i] = gap;
160 continue;
161 }
162 return Ok(None);
163 }
164 if poles.is_empty() || poles.len() > MOST_POLES {
165 return Ok(None);
166 }
167 let shortest: Vec<f64> = poles
170 .iter()
171 .map(|(_, _, axis, gap)| periods[*axis].map_or(*gap, |p| gap - (gap / p).round() * p))
172 .collect();
173 let turns: Vec<&[i32]> = poles
174 .iter()
175 .map(|(_, _, axis, _)| -> &[i32] {
176 if periods[*axis].is_some() {
177 &[-1, 0, 1]
178 } else {
179 &[0]
180 }
181 })
182 .collect();
183 let mut solutions: Vec<Vec<f64>> = Vec::new();
184 let mut choice = vec![0usize; poles.len()];
185 loop {
186 let crossing: Vec<f64> = (0..poles.len())
187 .map(|k| {
188 let p = periods[poles[k].2].unwrap_or(0.0);
189 shortest[k] + f64::from(turns[k][choice[k]]) * p
190 })
191 .collect();
192 if let Some(area) = unrolled_area(&walked, &shifts, &poles, &crossing, tol)
193 && (if outer { area } else { -area }) > tol.parametric() * tol.parametric()
194 {
195 solutions.push(crossing);
196 }
197 let mut k = 0;
199 loop {
200 if k == poles.len() {
201 break;
202 }
203 choice[k] += 1;
204 if choice[k] < turns[k].len() {
205 break;
206 }
207 choice[k] = 0;
208 k += 1;
209 }
210 if k == poles.len() {
211 break;
212 }
213 }
214 let [crossing] = &solutions[..] else {
215 return Ok(None);
216 };
217 if crossing
218 .iter()
219 .zip(&shortest)
220 .all(|(c, s)| (c - s).abs() <= tol.parametric())
221 {
222 return Ok(None);
223 }
224 Ok(Some(
225 poles
226 .into_iter()
227 .zip(crossing)
228 .map(|((i, vertex, axis, _), span)| {
229 let from = walked[i][walked[i].len() - 1];
230 let step = if axis == 0 {
231 Vector2::new(*span, 0.0)
232 } else {
233 Vector2::new(0.0, *span)
234 };
235 Pole {
236 after: i,
237 vertex,
238 from,
239 to: from + step,
240 }
241 })
242 .collect(),
243 ))
244}
245
246fn unrolled_area(
251 walked: &[Walked],
252 shifts: &[Vector2],
253 poles: &[(usize, Shape, usize, f64)],
254 crossing: &[f64],
255 tol: Tolerances,
256) -> Option<f64> {
257 let mut offset = Vector2::new(0.0, 0.0);
258 let mut path: Vec<Point2> = Vec::new();
259 for (i, points) in walked.iter().enumerate() {
260 path.extend(points.iter().map(|p| *p + offset));
261 let end = points[points.len() - 1] + offset;
263 let stored_next = walked[(i + 1) % walked.len()][0];
264 let next = match poles.iter().position(|(at, ..)| *at == i) {
265 Some(k) => {
266 let step = if poles[k].2 == 0 {
267 Vector2::new(crossing[k], 0.0)
268 } else {
269 Vector2::new(0.0, crossing[k])
270 };
271 end + step
272 }
273 None => end + (stored_next - points[points.len() - 1] - shifts[i]),
274 };
275 offset = next - stored_next;
276 }
277 if offset.x.abs() > tol.parametric() || offset.y.abs() > tol.parametric() {
278 return None;
279 }
280 let mut twice = 0.0;
281 for k in 0..path.len() {
282 let (a, b) = (path[k], path[(k + 1) % path.len()]);
283 twice += a.x.mul_add(b.y, -(b.x * a.y));
284 }
285 Some(twice / 2.0)
286}
287
288fn collapsed_row(
291 model: &Model,
292 surface: &SurfaceGeometry,
293 edge: &Shape,
294 next: &Shape,
295 at: Point2,
296 axis: usize,
297 tol: Tolerances,
298) -> OgeomResult<Option<Shape>> {
299 const PIECES: u32 = 16;
301 let (Some((_, end)), Some((start, _))) =
302 (edge_vertices(model, edge)?, edge_vertices(model, next)?)
303 else {
304 return Ok(None);
305 };
306 if !end.is_same(&start) || !end.location().is_identity() {
307 return Ok(None);
308 }
309 let Some(vertex) = model.node(&end).and_then(|n| n.data().as_vertex()) else {
310 return Ok(None);
311 };
312 let reach = vertex.tolerance.get().max(tol.confusion());
313 let ((u0, u1), (v0, v1)) = surface.domain();
314 let (lo, hi) = if axis == 0 { (u0, u1) } else { (v0, v1) };
315 if !(lo.is_finite() && hi.is_finite()) {
316 return Ok(None);
317 }
318 for k in 0..=PIECES {
319 let t = (hi - lo).mul_add(f64::from(k) / f64::from(PIECES), lo);
320 let uv = if axis == 0 {
321 Point2::new(t, at.y)
322 } else {
323 Point2::new(at.x, t)
324 };
325 let Ok(lifted) = surface.point_at(uv.x, uv.y, tol) else {
326 return Ok(None);
327 };
328 if lifted.distance(vertex.point) > reach {
329 return Ok(None);
330 }
331 }
332 Ok(Some(end))
333}
334
335fn walk(
339 model: &Model,
340 surface_id: ogeom_topo::SurfaceId,
341 edge: &Shape,
342 tol: Tolerances,
343) -> OgeomResult<Option<Walked>> {
344 const PIECES: u32 = 16;
346 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
347 return Ok(None);
348 };
349 let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
350 let (id, range) = match data.pcurve_for(surface_id, edge.location()) {
351 Some(EdgeRepr::PCurve {
352 curve,
353 range,
354 location,
355 ..
356 }) if location.is_identity() => (*curve, *range),
357 Some(EdgeRepr::Seam {
358 forward,
359 reversed: back,
360 range,
361 location,
362 ..
363 }) if location.is_identity() => (if reversed { *back } else { *forward }, *range),
364 _ => return Ok(None),
365 };
366 let Some(pcurve) = model.geometry().pcurve(id) else {
367 return Ok(None);
368 };
369 let mut points = Vec::with_capacity(PIECES as usize + 1);
370 for k in 0..=PIECES {
371 let f = f64::from(k) / f64::from(PIECES);
372 let f = if reversed { 1.0 - f } else { f };
373 points.push(pcurve.point_at((range.1 - range.0).mul_add(f, range.0), tol)?);
374 }
375 Ok(Some(points))
376}
377
378fn degenerate_edge(
381 model: &mut Model,
382 pole: &Pole,
383 surface: ogeom_topo::SurfaceId,
384 tol: Tolerances,
385) -> OgeomResult<Shape> {
386 let mut data = EdgeData::new();
387 data.degenerate = true;
388 let edge = model.add_edge(data, &[pole.vertex.clone(), pole.vertex.clone()])?;
389 crate::build::attach_pcurve(
390 model,
391 &edge,
392 Line2d::segment(pole.from, pole.to, tol)?.into(),
393 surface,
394 Location::identity(),
395 (0.0, pole.from.distance(pole.to)),
396 )?;
397 Ok(edge)
398}