1use std::collections::{HashMap, HashSet};
18
19use ogeom_algo::{Built, History, edge_vertices, surface_properties};
20use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
21use ogeom_geom::{
22 BSpline2d, CircleCurve, Continuity, Curve, Curve2d as _, Curve3d as _, LineCurve, PlanarCurve,
23 Surface as _, SurfaceGeometry, Transformable as _,
24};
25use ogeom_math::{Axis, Circle, Direction, Frame, KnotVector, Point, Point2, Transform};
26use ogeom_mesh::Deflection;
27use ogeom_topo::{
28 EdgeData, EdgeRepr, Location, Model, NodeData, Orientation, Shape, ShapeType, SurfaceId,
29 TShapeId, VertexData, explore_unique,
30};
31
32use crate::Reshape;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum IsoLine {
37 U(f64),
39 V(f64),
41}
42
43impl IsoLine {
44 const fn at(self) -> f64 {
45 match self {
46 Self::U(c) | Self::V(c) => c,
47 }
48 }
49
50 const fn fixed(self, p: Point2) -> f64 {
52 match self {
53 Self::U(_) => p.x,
54 Self::V(_) => p.y,
55 }
56 }
57
58 const fn free(self, p: Point2) -> f64 {
60 match self {
61 Self::U(_) => p.y,
62 Self::V(_) => p.x,
63 }
64 }
65
66 const fn point(self, free: f64) -> Point2 {
67 match self {
68 Self::U(c) => Point2::new(c, free),
69 Self::V(c) => Point2::new(free, c),
70 }
71 }
72}
73
74pub fn divide_face(
84 model: &mut Model,
85 shape: &Shape,
86 face: &Shape,
87 line: IsoLine,
88 tol: Tolerances,
89) -> OgeomResult<Built> {
90 let Some(reshape) = cut_face(model, face, line, tol)? else {
91 ogeom_bail!(Construction, "the line {line:?} does not cross the face");
92 };
93 reshape.apply(model, shape)
94}
95
96pub fn divide_by_continuity(
108 model: &mut Model,
109 shape: &Shape,
110 at_least: Continuity,
111 tol: Tolerances,
112) -> OgeomResult<Built> {
113 let order = match at_least {
114 Continuity::C0 => return Ok(Built::new(shape.clone(), History::identity())),
115 Continuity::G1 | Continuity::C1 => 1,
116 Continuity::G2 | Continuity::C2 => 2,
117 Continuity::CInfinity => usize::MAX,
118 };
119 let start = placed_baked(model, shape, tol)?;
120 let edges = divide_edges(model, &start.shape, order, tol)?;
121 let faces = divide_faces(model, &edges.shape, tol, |_, _, surface, _| {
122 Ok(knot_lines(surface, order))
123 })?;
124 Ok(Built::new(
125 faces.shape,
126 start.history.then(&edges.history).then(&faces.history),
127 ))
128}
129
130pub fn divide_by_angle(
140 model: &mut Model,
141 shape: &Shape,
142 max_angle: f64,
143 tol: Tolerances,
144) -> OgeomResult<Built> {
145 if !max_angle.is_finite() || max_angle <= tol.angular() {
146 ogeom_bail!(Construction, "{max_angle} is not an angle to divide by");
147 }
148 let start = placed_baked(model, shape, tol)?;
149 let faces = divide_faces(model, &start.shape, tol, |_, _, surface, bounds| {
150 let (u_turns, v_turns) = angular(surface);
151 let mut lines = Vec::new();
152 for (turns, (lo, hi), iso) in [
153 (u_turns, bounds.0, IsoLine::U as fn(f64) -> IsoLine),
154 (v_turns, bounds.1, IsoLine::V),
155 ] {
156 let span = hi - lo;
157 if turns && span > max_angle * (1.0 + 1e-9) {
158 let pieces = (span / max_angle - 1e-6).ceil();
159 lines.push(iso(lo + span / pieces));
160 }
161 }
162 Ok(lines)
163 })?;
164 Ok(Built::new(faces.shape, start.history.then(&faces.history)))
165}
166
167pub fn divide_by_area(
175 model: &mut Model,
176 shape: &Shape,
177 max_area: f64,
178 tol: Tolerances,
179) -> OgeomResult<Built> {
180 if !max_area.is_finite() || max_area <= tol.confusion() * tol.confusion() {
181 ogeom_bail!(Construction, "{max_area} is not an area to divide by");
182 }
183 let start = placed_baked(model, shape, tol)?;
184 let faces = divide_faces(model, &start.shape, tol, |model, face, surface, bounds| {
185 let area = surface_properties(model, face, Deflection::default(), tol)?.mass;
186 if area <= max_area {
187 return Ok(Vec::new());
188 }
189 let ((u0, u1), (v0, v1)) = bounds;
190 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
191 let length = |from: (f64, f64), to: (f64, f64)| -> OgeomResult<f64> {
194 let mut total = 0.0;
195 let mut last = surface.point_at(from.0, from.1, tol)?;
196 for i in 1..=16 {
197 let s = f64::from(i) / 16.0;
198 let p = surface.point_at(
199 from.0 + (to.0 - from.0) * s,
200 from.1 + (to.1 - from.1) * s,
201 tol,
202 )?;
203 total += p.distance(last);
204 last = p;
205 }
206 Ok(total)
207 };
208 let across_u = length((u0, vm), (u1, vm))?;
209 let across_v = length((um, v0), (um, v1))?;
210 Ok(if across_u >= across_v {
211 vec![IsoLine::U(um), IsoLine::V(vm)]
212 } else {
213 vec![IsoLine::V(vm), IsoLine::U(um)]
214 })
215 })?;
216 Ok(Built::new(faces.shape, start.history.then(&faces.history)))
217}
218
219pub fn to_bezier(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
227 let nurbs = ogeom_algo::to_nurbs(model, shape, tol)?;
228 let divided = divide_by_continuity(model, &nurbs.shape, Continuity::CInfinity, tol)?;
229
230 let mut reshape = Reshape::new();
232 for edge in explore_unique(model, ÷d.shape, ShapeType::Edge)? {
233 let data = edge_data(model, &edge)?;
234 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
235 continue;
236 };
237 let Some(Curve::BSpline(spline)) = model.geometry().curve(curve).cloned() else {
238 continue;
239 };
240 if spline.knots().distinct().len() <= 2 {
241 continue;
242 }
243 let span = model
244 .geometry_mut()
245 .add_curve(Curve::BSpline(spline.segment(range, tol)?));
246 let mut fresh = data.clone();
247 for repr in &mut fresh.representations {
248 if let EdgeRepr::Curve3d { curve, .. } = repr {
249 *curve = span;
250 }
251 }
252 let bounds = model.children_of(&forward(&edge))?;
253 let rebuilt = model.add_edge(fresh, &bounds)?;
254 reshape.replace(&forward(&edge), rebuilt);
255 }
256 let spans = if reshape.is_empty() {
257 Built::new(divided.shape.clone(), History::identity())
258 } else {
259 reshape.apply(model, ÷d.shape)?
260 };
261
262 let mut reshape = Reshape::new();
265 for face in explore_unique(model, &spans.shape, ShapeType::Face)? {
266 let data = face_data(model, &face)?;
267 let Some(SurfaceGeometry::BSpline(spline)) =
268 model.geometry().surface(data.surface).cloned()
269 else {
270 continue;
271 };
272 if spline.u_knots().distinct().len() <= 2 && spline.v_knots().distinct().len() <= 2 {
273 continue;
274 }
275 let rings = rings(model, &forward(&face), data.surface, tol)?;
276 let ((u0, u1), (v0, v1)) = chart_bounds(&rings, tol);
277 let ((du0, du1), (dv0, dv1)) = spline.domain();
278 let patch = spline.segment((u0.max(du0), u1.min(du1)), (v0.max(dv0), v1.min(dv1)), tol)?;
279 let id = model
280 .geometry_mut()
281 .add_surface(SurfaceGeometry::BSpline(patch));
282 for edge in explore_unique(model, &face, ShapeType::Edge)? {
283 let repr = edge_data(model, &edge)?
284 .pcurve_for(data.surface, edge.location())
285 .cloned();
286 let Some(mut repr) = repr else { continue };
287 match &mut repr {
288 EdgeRepr::PCurve { surface, .. } | EdgeRepr::Seam { surface, .. } => *surface = id,
289 _ => continue,
290 }
291 let Some(node) = model.node_mut(&edge) else {
292 ogeom_bail!(Dangling, "edge is not in this model");
293 };
294 if let NodeData::Edge(e) = node.data_mut() {
295 let agreed = e.same_parameter();
296 e.add(repr);
297 e.assert_same_parameter(agreed);
298 }
299 }
300 let mut fresh = data.clone();
301 fresh.surface = id;
302 fresh.triangulation = None;
303 let wires = model.children_of(&forward(&face))?;
304 let rebuilt = model.add_face(fresh, &wires)?;
305 reshape.replace(&forward(&face), rebuilt);
306 }
307 let patches = if reshape.is_empty() {
308 Built::new(spans.shape.clone(), History::identity())
309 } else {
310 reshape.apply(model, &spans.shape)?
311 };
312 Ok(Built::new(
313 patches.shape,
314 nurbs
315 .history
316 .then(÷d.history)
317 .then(&spans.history)
318 .then(&patches.history),
319 ))
320}
321
322type Bounds = ((f64, f64), (f64, f64));
325
326fn divide_faces(
330 model: &mut Model,
331 shape: &Shape,
332 tol: Tolerances,
333 mut lines: impl FnMut(&Model, &Shape, &SurfaceGeometry, Bounds) -> OgeomResult<Vec<IsoLine>>,
334) -> OgeomResult<Built> {
335 let mut current = Built::new(shape.clone(), History::identity());
336 let mut settled: HashSet<TShapeId> = HashSet::new();
337 'rounds: for _ in 0..100_000 {
338 for face in explore_unique(model, ¤t.shape, ShapeType::Face)? {
339 if settled.contains(&face.node()) {
340 continue;
341 }
342 let data = face_data(model, &face)?;
343 if data.natural_restriction {
344 match bounded_natural(model, &face, tol)? {
347 Some(bounded) => {
348 let mut reshape = Reshape::new();
349 reshape.replace(&forward(&face), bounded);
350 let next = reshape.apply(model, ¤t.shape)?;
351 current = Built::new(next.shape, current.history.then(&next.history));
352 continue 'rounds;
353 }
354 None => {
355 settled.insert(face.node());
356 continue;
357 }
358 }
359 }
360 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
361 ogeom_bail!(Dangling, "surface is not in this model");
362 };
363 let rings = rings(model, &forward(&face), data.surface, tol)?;
364 let bounds = chart_bounds(&rings, tol);
365 for line in lines(&*model, &face, &surface, bounds)? {
366 let (lo, hi) = match line {
367 IsoLine::U(_) => bounds.0,
368 IsoLine::V(_) => bounds.1,
369 };
370 let margin = (hi - lo).abs() * 1e-7 + tol.parametric();
371 if line.at() <= lo + margin || line.at() >= hi - margin {
372 continue;
373 }
374 if let Some(reshape) = cut_face(model, &face, line, tol)? {
375 let next = reshape.apply(model, ¤t.shape)?;
376 current = Built::new(next.shape, current.history.then(&next.history));
377 continue 'rounds;
378 }
379 }
380 settled.insert(face.node());
381 }
382 return Ok(current);
383 }
384 ogeom_bail!(Construction, "the division did not settle");
385}
386
387fn bounded_natural(model: &mut Model, face: &Shape, tol: Tolerances) -> OgeomResult<Option<Shape>> {
392 let face_fwd = forward(face);
393 let data = face_data(model, &face_fwd)?;
394 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
395 ogeom_bail!(Dangling, "surface is not in this model");
396 };
397 let ((u0, u1), (v0, v1)) = surface.domain();
398 if ![u0, u1, v0, v1]
399 .iter()
400 .all(|x| x.is_finite() && x.abs() < 1e6)
401 {
402 return Ok(None);
403 }
404 let (pu, pv) = (surface.is_periodic_u(), surface.is_periodic_v());
405 let corner = |u: f64, v: f64| surface.point_at(u, v, tol);
406 let sides = [
409 (IsoLine::V(v0), u0, u1),
410 (IsoLine::U(u1), v0, v1),
411 (IsoLine::V(v1), u1, u0),
412 (IsoLine::U(u0), v1, v0),
413 ];
414 let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
416 let mut vertices: Vec<Shape> = Vec::with_capacity(4);
417 for (i, (u, v)) in corners.iter().enumerate() {
418 let p = corner(*u, *v)?;
419 let found = (0..i).find(|j| {
420 let (a, b) = corners[*j];
421 corner(a, b).is_ok_and(|q| q.distance(p) <= tol.confusion())
422 });
423 vertices.push(match found {
424 Some(j) => vertices[j].clone(),
425 None => model.add_vertex(VertexData::new(p)),
426 });
427 }
428 let chart_line = |line: IsoLine, a: f64, b: f64, t: (f64, f64)| -> OgeomResult<PlanarCurve> {
429 let knots = KnotVector::new(vec![t.0, t.0, t.1, t.1], 1)?;
430 let (p, q) = (line.point(a), line.point(b));
431 let (p, q) = if t.0 <= t.1 { (p, q) } else { (q, p) };
432 Ok(PlanarCurve::BSpline(BSpline2d::new(
433 knots,
434 vec![p, q],
435 tol,
436 )?))
437 };
438 let mut ring: Vec<Shape> = Vec::with_capacity(4);
440 let mut seam_u: Option<Shape> = None;
441 let mut seam_v: Option<Shape> = None;
442 for (index, (line, a, b)) in sides.into_iter().enumerate() {
443 let (from, to) = (vertices[index].clone(), vertices[(index + 1) % 4].clone());
444 let periodic_pair = match line {
445 IsoLine::U(_) => pu,
446 IsoLine::V(_) => pv,
447 };
448 if periodic_pair {
451 let held = match line {
452 IsoLine::U(_) => &seam_u,
453 IsoLine::V(_) => &seam_v,
454 };
455 if let Some(seam) = held {
456 ring.push(seam.reversed());
457 continue;
458 }
459 }
460 let (lo, hi) = (a.min(b), a.max(b));
461 let mid = line.point(0.5 * (lo + hi));
462 let pinched = corner(line.point(lo).x, line.point(lo).y)?.distance(corner(mid.x, mid.y)?)
463 <= tol.confusion()
464 && corner(line.point(hi).x, line.point(hi).y)?.distance(corner(mid.x, mid.y)?)
465 <= tol.confusion();
466 let edge = if pinched {
467 let mut edge_data = EdgeData::new();
468 edge_data.degenerate = true;
469 let trim = chart_line(line, lo, hi, (lo, hi))?;
470 let id = model.geometry_mut().add_pcurve(trim);
471 edge_data.add(EdgeRepr::PCurve {
472 curve: id,
473 surface: data.surface,
474 location: Location::identity(),
475 range: (lo, hi),
476 });
477 let pole = model.add_edge(edge_data, &[from.clone(), from.clone()])?;
478 if a <= b { pole } else { pole.reversed() }
480 } else {
481 let Some((curve, scale, offset)) =
482 iso_curve(&surface, line, &[0.5 * (lo + hi)], (lo, hi), tol)?
483 else {
484 return Ok(None);
485 };
486 let (t_lo, t_hi) = (scale * lo + offset, scale * hi + offset);
487 let (range, rising) = if t_lo <= t_hi {
488 ((t_lo, t_hi), true)
489 } else {
490 ((t_hi, t_lo), false)
491 };
492 let curve_id = model.geometry_mut().add_curve(curve);
493 let mut edge_data = EdgeData::on_curve(curve_id, Location::identity(), range);
494 let (start_v, end_v) = {
496 let low_end = if a <= b { &from } else { &to };
497 let high_end = if a <= b { &to } else { &from };
498 if rising {
499 (low_end.clone(), high_end.clone())
500 } else {
501 (high_end.clone(), low_end.clone())
502 }
503 };
504 let pcurve_at = |line: IsoLine| -> OgeomResult<PlanarCurve> {
505 let (p, q) = if rising {
506 (line.point(lo), line.point(hi))
507 } else {
508 (line.point(hi), line.point(lo))
509 };
510 let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
511 Ok(PlanarCurve::BSpline(BSpline2d::new(
512 knots,
513 vec![p, q],
514 tol,
515 )?))
516 };
517 if periodic_pair {
518 let other = match line {
520 IsoLine::U(c) => IsoLine::U(if (c - u1).abs() < (c - u0).abs() {
521 u0
522 } else {
523 u1
524 }),
525 IsoLine::V(c) => IsoLine::V(if (c - v1).abs() < (c - v0).abs() {
526 v0
527 } else {
528 v1
529 }),
530 };
531 let here = model.geometry_mut().add_pcurve(pcurve_at(line)?);
532 let there = model.geometry_mut().add_pcurve(pcurve_at(other)?);
533 edge_data.add(EdgeRepr::Seam {
534 forward: here,
535 reversed: there,
536 surface: data.surface,
537 location: Location::identity(),
538 range,
539 });
540 } else {
541 let id = model.geometry_mut().add_pcurve(pcurve_at(line)?);
542 edge_data.add(EdgeRepr::PCurve {
543 curve: id,
544 surface: data.surface,
545 location: Location::identity(),
546 range,
547 });
548 }
549 edge_data.assert_same_parameter(true);
550 let edge = model.add_edge(edge_data, &[start_v, end_v])?;
551 let walked_with_curve = (a <= b) == rising;
552 let occurrence = if walked_with_curve {
553 edge.clone()
554 } else {
555 edge.reversed()
556 };
557 if periodic_pair {
558 match line {
559 IsoLine::U(_) => seam_u = Some(occurrence.clone()),
560 IsoLine::V(_) => seam_v = Some(occurrence.clone()),
561 }
562 }
563 ring.push(occurrence);
564 continue;
565 };
566 ring.push(edge);
567 }
568 let wire = model.add_wire(&ring)?;
569 let mut fresh = data.clone();
570 fresh.natural_restriction = false;
571 fresh.triangulation = None;
572 let bounded = model.add_face(fresh, &[wire])?;
573 Ok(Some(bounded))
574}
575
576fn divide_edges(
579 model: &mut Model,
580 shape: &Shape,
581 order: usize,
582 tol: Tolerances,
583) -> OgeomResult<Built> {
584 let mut reshape = Reshape::new();
585 for edge in explore_unique(model, shape, ShapeType::Edge)? {
586 let edge = forward(&edge);
587 let data = edge_data(model, &edge)?;
588 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
589 continue;
590 };
591 let Some(geometry) = model.geometry().curve(curve) else {
592 ogeom_bail!(Dangling, "curve is not in this model");
593 };
594 let margin = (range.1 - range.0) * 1e-9 + tol.parametric();
595 let at: Vec<f64> = curve_knots(geometry)
596 .map(|knots| weak_knots(&knots, order))
597 .unwrap_or_default()
598 .into_iter()
599 .filter(|t| *t > range.0 + margin && *t < range.1 - margin)
600 .collect();
601 if at.is_empty() {
602 continue;
603 }
604 let fractions: Vec<f64> = at
605 .iter()
606 .map(|t| (t - range.0) / (range.1 - range.0))
607 .collect();
608 let (pieces, _) = split_edge(model, &edge, &fractions, tol)?;
609 reshape.split(&edge, pieces);
610 }
611 if reshape.is_empty() {
612 return Ok(Built::new(shape.clone(), History::identity()));
613 }
614 reshape.apply(model, shape)
615}
616
617fn placed_baked(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
621 let mut placed = false;
622 for kind in [ShapeType::Edge, ShapeType::Face] {
623 for s in explore_unique(model, shape, kind)? {
624 placed |= !s.location().is_identity();
625 }
626 }
627 for face in explore_unique(model, shape, ShapeType::Face)? {
628 placed |= !face_data(model, &face)?.location.is_identity();
629 }
630 if placed {
631 ogeom_algo::baked_shape(model, shape, tol)
632 } else {
633 Ok(Built::new(shape.clone(), History::identity()))
634 }
635}
636
637fn curve_knots(curve: &Curve) -> Option<KnotVector> {
640 match curve {
641 Curve::BSpline(b) => Some(b.knots().clone()),
642 Curve::Trimmed(t) => curve_knots(t.basis()),
643 _ => None,
644 }
645}
646
647fn weak_knots(knots: &KnotVector, order: usize) -> Vec<f64> {
650 let (a, b) = knots.domain();
651 let p = knots.degree();
652 knots
653 .distinct()
654 .into_iter()
655 .filter(|(value, m)| *value > a && *value < b && p.saturating_sub(*m) < order)
656 .map(|(value, _)| value)
657 .collect()
658}
659
660fn knot_lines(surface: &SurfaceGeometry, order: usize) -> Vec<IsoLine> {
663 match surface {
664 SurfaceGeometry::BSpline(b) => weak_knots(b.u_knots(), order)
665 .into_iter()
666 .map(IsoLine::U)
667 .chain(weak_knots(b.v_knots(), order).into_iter().map(IsoLine::V))
668 .collect(),
669 SurfaceGeometry::Extrusion(e) => curve_knots(e.curve())
670 .map(|k| weak_knots(&k, order).into_iter().map(IsoLine::U).collect())
671 .unwrap_or_default(),
672 SurfaceGeometry::Revolution(r) => curve_knots(r.curve())
673 .map(|k| weak_knots(&k, order).into_iter().map(IsoLine::V).collect())
674 .unwrap_or_default(),
675 SurfaceGeometry::Trimmed(t) => knot_lines(t.basis(), order),
676 SurfaceGeometry::Offset(o) => knot_lines(o.basis(), order.saturating_add(1)),
678 _ => Vec::new(),
679 }
680}
681
682const fn angular(surface: &SurfaceGeometry) -> (bool, bool) {
684 match surface {
685 SurfaceGeometry::Cylinder(_)
686 | SurfaceGeometry::Cone(_)
687 | SurfaceGeometry::Revolution(_) => (true, false),
688 SurfaceGeometry::Sphere(_) | SurfaceGeometry::Torus(_) => (true, true),
689 _ => (false, false),
690 }
691}
692
693#[derive(Debug, Clone)]
697struct Occurrence {
698 edge: Shape,
699 pcurve: PlanarCurve,
700 range: (f64, f64),
701}
702
703impl Occurrence {
704 fn reversed(&self) -> bool {
705 self.edge.orientation() == Orientation::Reversed
706 }
707
708 fn at(&self, s: f64, tol: Tolerances) -> OgeomResult<Point2> {
710 let s = if self.reversed() { 1.0 - s } else { s };
711 self.pcurve
712 .point_at(self.range.0 + (self.range.1 - self.range.0) * s, tol)
713 }
714
715 fn polyline(&self, samples: usize, tol: Tolerances) -> OgeomResult<Vec<Point2>> {
716 (0..=samples)
717 .map(|i| {
718 #[allow(clippy::cast_precision_loss)]
719 let s = i as f64 / samples as f64;
720 self.at(s, tol)
721 })
722 .collect()
723 }
724}
725
726type Trims = (PlanarCurve, Option<PlanarCurve>, (f64, f64));
728
729type Outer = (Vec<Shape>, Vec<Point2>, Vec<Vec<Shape>>);
731
732fn rings(
735 model: &Model,
736 face: &Shape,
737 surface: SurfaceId,
738 tol: Tolerances,
739) -> OgeomResult<Vec<Vec<Occurrence>>> {
740 let mut out = Vec::new();
741 for wire in model.ordered_children_of(face)? {
742 let edges = model.ordered_children_of(&wire)?;
743 let mut choices: Vec<Option<Trims>> = Vec::new();
744 for edge in &edges {
745 let data = edge_data(model, edge)?;
746 let pcurve =
747 |id| -> OgeomResult<PlanarCurve> {
748 model.geometry().pcurve(id).cloned().ok_or_else(|| {
749 ogeom_core::ogeom_err!(Dangling, "pcurve is not in this model")
750 })
751 };
752 choices.push(match data.pcurve_for(surface, edge.location()) {
753 Some(EdgeRepr::PCurve { curve, range, .. }) => {
754 Some((pcurve(*curve)?, None, *range))
755 }
756 Some(EdgeRepr::Seam {
757 forward,
758 reversed,
759 range,
760 ..
761 }) => Some((pcurve(*forward)?, Some(pcurve(*reversed)?), *range)),
762 _ => None,
763 });
764 }
765 let first_plain = choices
769 .iter()
770 .position(|c| c.as_ref().is_some_and(|c| c.1.is_none()))
771 .unwrap_or(0);
772 let n = edges.len();
773 let mut ring: Vec<Option<Occurrence>> = vec![None; n];
774 let mut last: Option<Point2> = None;
775 for k in 0..n {
776 let i = (first_plain + k) % n;
777 let Some((a, b, range)) = choices[i].clone() else {
778 ogeom_bail!(Construction, "an edge has no trim on this face");
779 };
780 let mut occurrence = Occurrence {
781 edge: edges[i].clone(),
782 pcurve: a,
783 range,
784 };
785 if let Some(b) = b {
786 let other = Occurrence {
787 pcurve: b,
788 ..occurrence.clone()
789 };
790 let take_other = match last {
791 Some(last) => {
792 other.at(0.0, tol)?.distance(last) < occurrence.at(0.0, tol)?.distance(last)
793 }
794 None => occurrence.reversed(),
795 };
796 if take_other {
797 occurrence = other;
798 }
799 }
800 last = Some(occurrence.at(1.0, tol)?);
801 ring[i] = Some(occurrence);
802 }
803 out.push(ring.into_iter().flatten().collect());
804 }
805 Ok(out)
806}
807
808fn chart_bounds(rings: &[Vec<Occurrence>], tol: Tolerances) -> Bounds {
809 let mut b = (
810 (f64::INFINITY, f64::NEG_INFINITY),
811 (f64::INFINITY, f64::NEG_INFINITY),
812 );
813 for o in rings.iter().flatten() {
814 for i in 0..=32 {
815 if let Ok(p) = o.at(f64::from(i) / 32.0, tol) {
816 b.0 = (b.0.0.min(p.x), b.0.1.max(p.x));
817 b.1 = (b.1.0.min(p.y), b.1.1.max(p.y));
818 }
819 }
820 }
821 b
822}
823
824#[derive(Debug, Clone, Copy)]
826enum Meeting {
827 Vertex(TShapeId),
829 Inside(TShapeId, u64),
831}
832
833#[allow(clippy::too_many_lines)]
836fn cut_face(
837 model: &mut Model,
838 face: &Shape,
839 line: IsoLine,
840 tol: Tolerances,
841) -> OgeomResult<Option<Reshape>> {
842 let face_fwd = forward(face);
843 let data = face_data(model, &face_fwd)?;
844 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
845 ogeom_bail!(Dangling, "surface is not in this model");
846 };
847 let rings = rings(model, &face_fwd, data.surface, tol)?;
848 let bounds = chart_bounds(&rings, tol);
849 let span = (bounds.0.1 - bounds.0.0).max(bounds.1.1 - bounds.1.0);
850 let eps = span * 1e-9 + tol.parametric();
851 let c = line.at();
852
853 let mut cuts: HashMap<TShapeId, Vec<f64>> = HashMap::new();
856 let mut meetings: Vec<(f64, Meeting)> = Vec::new();
857 for occurrence in rings.iter().flatten() {
858 let f = |s: f64| -> OgeomResult<f64> { Ok(line.fixed(occurrence.at(s, tol)?) - c) };
859 const N: usize = 64;
860 #[allow(clippy::cast_precision_loss)]
861 let values: Vec<f64> = (0..=N)
862 .map(|i| f(i as f64 / N as f64))
863 .collect::<OgeomResult<_>>()?;
864 if values.iter().all(|v| v.abs() <= eps) {
865 ogeom_bail!(Construction, "an edge of the face runs along {line:?}");
866 }
867 let end = occurrence.at(1.0, tol)?;
868 if (line.fixed(end) - c).abs() <= eps {
869 let Some((_, v)) = edge_vertices(model, &occurrence.edge)? else {
870 continue;
871 };
872 meetings.push((line.free(end), Meeting::Vertex(v.node())));
873 }
874 for i in 0..N {
875 let (a, b) = (values[i], values[i + 1]);
876 if a.abs() <= eps || b.abs() <= eps || a.signum() == b.signum() {
877 if i + 1 < N
880 && b.abs() <= eps
881 && a.abs() > eps
882 && values[i + 2].abs() > eps
883 && a.signum() != values[i + 2].signum()
884 {
885 #[allow(clippy::cast_precision_loss)]
886 let s = (i + 1) as f64 / N as f64;
887 record(occurrence, s, line, &mut cuts, &mut meetings, tol)?;
888 }
889 continue;
890 }
891 #[allow(clippy::cast_precision_loss)]
892 let (mut lo, mut hi) = (i as f64 / N as f64, (i + 1) as f64 / N as f64);
893 let mut f_lo = a;
894 for _ in 0..80 {
895 let mid = 0.5 * (lo + hi);
896 let f_mid = f(mid)?;
897 if f_mid.signum() == f_lo.signum() {
898 (lo, f_lo) = (mid, f_mid);
899 } else {
900 hi = mid;
901 }
902 }
903 record(
904 occurrence,
905 0.5 * (lo + hi),
906 line,
907 &mut cuts,
908 &mut meetings,
909 tol,
910 )?;
911 }
912 }
913 meetings.sort_by(|a, b| a.0.total_cmp(&b.0));
914 meetings.dedup_by(|b, a| {
915 (a.0 - b.0).abs() <= eps
916 && match (a.1, b.1) {
917 (Meeting::Vertex(x), Meeting::Vertex(y)) => x == y,
918 (Meeting::Inside(x, s), Meeting::Inside(y, t)) => x == y && s == t,
919 _ => false,
920 }
921 });
922
923 let outlines: Vec<Vec<Point2>> = rings
925 .iter()
926 .map(|ring| -> OgeomResult<Vec<Point2>> {
927 let mut out = Vec::new();
928 for o in ring {
929 out.extend(o.polyline(32, tol)?);
930 }
931 Ok(out)
932 })
933 .collect::<OgeomResult<_>>()?;
934 let mut stretches: Vec<(usize, usize)> = Vec::new();
935 for i in 0..meetings.len().saturating_sub(1) {
936 let (w0, w1) = (meetings[i].0, meetings[i + 1].0);
937 if w1 - w0 <= eps * 10.0 {
938 continue;
939 }
940 if inside(&outlines, line.point(0.5 * (w0 + w1))) {
941 stretches.push((i, i + 1));
942 }
943 }
944 if stretches.is_empty() {
945 return Ok(None);
946 }
947 let outer_sign = outlines
948 .iter()
949 .map(|o| signed_area(o))
950 .max_by(|a, b| a.abs().total_cmp(&b.abs()))
951 .unwrap_or(1.0)
952 .signum();
953
954 let mut reshape = Reshape::new();
956 let mut pieces_of: HashMap<TShapeId, Vec<(Shape, f64, f64)>> = HashMap::new();
957 let mut vertex_at: HashMap<(TShapeId, u64), Shape> = HashMap::new();
958 let mut vertex_of_node: HashMap<TShapeId, Shape> = HashMap::new();
959 for v in explore_unique(model, &face_fwd, ShapeType::Vertex)? {
960 vertex_of_node.insert(v.node(), v);
961 }
962 for occurrence in rings.iter().flatten() {
963 let edge = forward(&occurrence.edge);
964 if pieces_of.contains_key(&edge.node()) {
965 continue;
966 }
967 let Some(fractions) = cuts.get(&edge.node()) else {
968 continue;
969 };
970 let mut fractions = fractions.clone();
971 fractions.sort_by(f64::total_cmp);
972 fractions.dedup_by(|b, a| (*a - *b).abs() <= 1e-12);
973 let (pieces, vertices) = split_edge(model, &edge, &fractions, tol)?;
974 for (s, v) in fractions.iter().zip(&vertices) {
975 vertex_at.insert((edge.node(), s.to_bits()), v.clone());
976 }
977 let mut bounds = vec![0.0];
978 bounds.extend(&fractions);
979 bounds.push(1.0);
980 pieces_of.insert(
981 edge.node(),
982 pieces
983 .iter()
984 .zip(bounds.windows(2))
985 .map(|(p, w)| (p.clone(), w[0], w[1]))
986 .collect(),
987 );
988 reshape.split(&edge, pieces);
989 }
990 let meeting_vertex = |m: Meeting| -> OgeomResult<Shape> {
991 match m {
992 Meeting::Vertex(node) => vertex_of_node
993 .get(&node)
994 .cloned()
995 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a meeting vertex is lost")),
996 Meeting::Inside(edge, s) => vertex_at
997 .get(&(edge, s))
998 .cloned()
999 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a cut vertex is lost")),
1000 }
1001 };
1002
1003 let mut sides: [Vec<Item>; 2] = [Vec::new(), Vec::new()];
1005 for occurrence in rings.iter().flatten() {
1006 let edge = forward(&occurrence.edge);
1007 let parts: Vec<(Shape, f64, f64)> = match pieces_of.get(&edge.node()) {
1008 Some(parts) => parts.clone(),
1009 None => vec![(edge.clone(), 0.0, 1.0)],
1010 };
1011 let mut ordered: Vec<(Shape, f64, f64)> = if occurrence.reversed() {
1012 parts
1013 .into_iter()
1014 .rev()
1015 .map(|(p, a, b)| (p.reversed(), 1.0 - b, 1.0 - a))
1016 .collect()
1017 } else {
1018 parts
1019 };
1020 for (piece, a, b) in ordered.drain(..) {
1021 let sub = |s: f64| occurrence.at(a + (b - a) * s, tol);
1022 let polyline: Vec<Point2> = (0..=16)
1023 .map(|i| sub(f64::from(i) / 16.0))
1024 .collect::<OgeomResult<_>>()?;
1025 let side = usize::from(line.fixed(polyline[8]) > c);
1026 let Some((from, to)) = edge_vertices(model, &piece)? else {
1027 ogeom_bail!(Construction, "a boundary piece has no vertices");
1028 };
1029 sides[side].push(Item {
1030 edge: piece,
1031 from: from.node(),
1032 to: to.node(),
1033 polyline,
1034 });
1035 }
1036 }
1037
1038 let probes: Vec<f64> = stretches
1040 .iter()
1041 .map(|&(i, j)| 0.5 * (meetings[i].0 + meetings[j].0))
1042 .collect();
1043 let span = (
1044 meetings.first().map_or(0.0, |m| m.0),
1045 meetings.last().map_or(0.0, |m| m.0),
1046 );
1047 let Some((curve, scale, offset)) = iso_curve(&surface, line, &probes, span, tol)? else {
1048 ogeom_bail!(
1049 Construction,
1050 "the surface has no closed-form iso-curve to cut along"
1051 );
1052 };
1053 for (i, j) in stretches {
1054 let (w0, w1) = (meetings[i].0, meetings[j].0);
1055 let (a, b) = (
1056 meeting_vertex(meetings[i].1)?,
1057 meeting_vertex(meetings[j].1)?,
1058 );
1059 let (t0, t1) = (scale * w0 + offset, scale * w1 + offset);
1060 let rising = t1 > t0;
1061 let (range, (start, end), (p0, p1)) = if rising {
1062 ((t0, t1), (&a, &b), (line.point(w0), line.point(w1)))
1063 } else {
1064 ((t1, t0), (&b, &a), (line.point(w1), line.point(w0)))
1065 };
1066 let curve_id = model.geometry_mut().add_curve(curve.clone());
1067 let mut edge_data = EdgeData::on_curve(curve_id, Location::identity(), range);
1068 let trim = BSpline2d::new(
1069 KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?,
1070 vec![p0, p1],
1071 tol,
1072 )?;
1073 let trim_id = model.geometry_mut().add_pcurve(PlanarCurve::BSpline(trim));
1074 edge_data.add(EdgeRepr::PCurve {
1075 curve: trim_id,
1076 surface: data.surface,
1077 location: Location::identity(),
1078 range,
1079 });
1080 edge_data.assert_same_parameter(true);
1081 let reach = [(start, range.0), (end, range.1)]
1084 .iter()
1085 .map(|(v, t)| -> OgeomResult<f64> {
1086 Ok(vertex_point(model, v)?.distance(curve.point_at(*t, tol)?))
1087 })
1088 .collect::<OgeomResult<Vec<f64>>>()?
1089 .into_iter()
1090 .fold(0.0_f64, f64::max);
1091 edge_data.widen(ogeom_core::Tolerance::new(reach + tol.confusion())?);
1092 let edge = model.add_edge(edge_data, &[start.clone(), end.clone()])?;
1093 let polyline: Vec<Point2> = (0..=16)
1094 .map(|k| {
1095 let w = w0 + (w1 - w0) * f64::from(k) / 16.0;
1096 line.point(w)
1097 })
1098 .collect();
1099 let left = match line {
1103 IsoLine::U(_) => 0,
1104 IsoLine::V(_) => 1,
1105 };
1106 let up_side = if outer_sign >= 0.0 { left } else { 1 - left };
1107 let up = if rising {
1108 edge.clone()
1109 } else {
1110 edge.reversed()
1111 };
1112 let down = up.reversed();
1113 sides[up_side].push(Item {
1114 edge: up,
1115 from: a.node(),
1116 to: b.node(),
1117 polyline: polyline.clone(),
1118 });
1119 sides[1 - up_side].push(Item {
1120 edge: down,
1121 from: b.node(),
1122 to: a.node(),
1123 polyline: polyline.into_iter().rev().collect(),
1124 });
1125 }
1126
1127 let mut faces: Vec<Shape> = Vec::new();
1129 for items in sides {
1130 let loops = chain(items, eps.max(tol.parametric() * 1e3))?;
1131 let mut outers: Vec<Outer> = Vec::new();
1132 let mut holes: Vec<(Vec<Shape>, Vec<Point2>)> = Vec::new();
1133 for (edges, outline) in loops {
1134 if signed_area(&outline) * outer_sign > 0.0 {
1135 outers.push((edges, outline, Vec::new()));
1136 } else {
1137 holes.push((edges, outline));
1138 }
1139 }
1140 for (edges, outline) in holes {
1141 let probe = outline[outline.len() / 2];
1142 let Some(host) = outers
1143 .iter_mut()
1144 .filter(|o| inside(std::slice::from_ref(&o.1), probe))
1145 .min_by(|a, b| signed_area(&a.1).abs().total_cmp(&signed_area(&b.1).abs()))
1146 else {
1147 ogeom_bail!(Construction, "a hole of the cut face lies in no piece");
1148 };
1149 host.2.push(edges);
1150 }
1151 for (edges, _, hole_edges) in outers {
1152 let mut wires = vec![model.add_wire(&edges)?];
1153 for h in hole_edges {
1154 wires.push(model.add_wire(&h)?);
1155 }
1156 let mut fresh = data.clone();
1157 fresh.triangulation = None;
1158 fresh.natural_restriction = false;
1159 faces.push(model.add_face(fresh, &wires)?);
1160 }
1161 }
1162 if faces.len() < 2 {
1163 return Ok(None);
1164 }
1165 reshape.split(&face_fwd, faces);
1166 Ok(Some(reshape))
1167}
1168
1169fn record(
1171 occurrence: &Occurrence,
1172 s: f64,
1173 line: IsoLine,
1174 cuts: &mut HashMap<TShapeId, Vec<f64>>,
1175 meetings: &mut Vec<(f64, Meeting)>,
1176 tol: Tolerances,
1177) -> OgeomResult<()> {
1178 let at = occurrence.at(s, tol)?;
1179 let forward_s = if occurrence.reversed() { 1.0 - s } else { s };
1182 let forward_s = (forward_s * 1e12).round() / 1e12;
1183 let node = occurrence.edge.node();
1184 cuts.entry(node).or_default().push(forward_s);
1185 meetings.push((line.free(at), Meeting::Inside(node, forward_s.to_bits())));
1186 Ok(())
1187}
1188
1189#[derive(Debug, Clone)]
1191struct Item {
1192 edge: Shape,
1193 from: TShapeId,
1194 to: TShapeId,
1195 polyline: Vec<Point2>,
1196}
1197
1198fn chain(mut items: Vec<Item>, snap: f64) -> OgeomResult<Vec<(Vec<Shape>, Vec<Point2>)>> {
1201 let mut loops = Vec::new();
1202 while let Some(first) = items.pop() {
1203 let start_node = first.from;
1204 let start_at = first.polyline[0];
1205 let mut here = *first.polyline.last().unwrap_or(&start_at);
1206 let mut at_node = first.to;
1207 let mut edges = vec![first.edge];
1208 let mut outline = first.polyline;
1209 loop {
1210 if at_node == start_node && here.distance(start_at) <= snap {
1211 break;
1212 }
1213 let next = items
1214 .iter()
1215 .enumerate()
1216 .filter(|(_, it)| it.from == at_node)
1217 .min_by(|a, b| {
1218 a.1.polyline[0]
1219 .distance(here)
1220 .total_cmp(&b.1.polyline[0].distance(here))
1221 })
1222 .map(|(i, _)| i);
1223 let Some(i) = next else {
1224 ogeom_bail!(Construction, "a piece of the cut face does not close");
1225 };
1226 let it = items.swap_remove(i);
1227 here = *it.polyline.last().unwrap_or(&here);
1228 at_node = it.to;
1229 edges.push(it.edge);
1230 outline.extend(it.polyline);
1231 }
1232 loops.push((edges, outline));
1233 }
1234 Ok(loops)
1235}
1236
1237fn split_edge(
1241 model: &mut Model,
1242 edge: &Shape,
1243 fractions: &[f64],
1244 tol: Tolerances,
1245) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
1246 let data = edge_data(model, edge)?;
1247 let Some((start, end)) = edge_vertices(model, edge)? else {
1248 ogeom_bail!(Construction, "an edge with no vertices cannot be cut");
1249 };
1250 let mut vertices = Vec::with_capacity(fractions.len());
1251 for &s in fractions {
1252 let vertex = match data.curve3d() {
1253 Some(EdgeRepr::Curve3d { curve, range, .. }) if !data.degenerate => {
1254 let Some(curve) = model.geometry().curve(*curve) else {
1255 ogeom_bail!(Dangling, "curve is not in this model");
1256 };
1257 let p = curve.point_at(range.0 + (range.1 - range.0) * s, tol)?;
1258 model.add_vertex(VertexData::with_tolerance(p, data.tolerance.get())?)
1259 }
1260 _ => start.clone(),
1262 };
1263 vertices.push(vertex);
1264 }
1265 let mut bounds = vec![0.0];
1266 bounds.extend(fractions);
1267 bounds.push(1.0);
1268 let mut ends = vec![start];
1269 ends.extend(vertices.iter().cloned());
1270 ends.push(end);
1271 let mut pieces = Vec::with_capacity(bounds.len() - 1);
1272 for k in 0..bounds.len() - 1 {
1273 let (a, b) = (bounds[k], bounds[k + 1]);
1274 let sub = |range: (f64, f64)| {
1275 (
1276 range.0 + (range.1 - range.0) * a,
1277 range.0 + (range.1 - range.0) * b,
1278 )
1279 };
1280 let mut piece = data.clone();
1281 piece.representations.retain(|r| {
1282 matches!(
1283 r,
1284 EdgeRepr::Curve3d { .. } | EdgeRepr::PCurve { .. } | EdgeRepr::Seam { .. }
1285 )
1286 });
1287 for repr in &mut piece.representations {
1288 match repr {
1289 EdgeRepr::Curve3d { range, .. }
1290 | EdgeRepr::PCurve { range, .. }
1291 | EdgeRepr::Seam { range, .. } => *range = sub(*range),
1292 _ => {}
1293 }
1294 }
1295 pieces.push(model.add_edge(piece, &[ends[k].clone(), ends[k + 1].clone()])?);
1296 }
1297 Ok((pieces, vertices))
1298}
1299
1300fn iso_curve(
1303 surface: &SurfaceGeometry,
1304 line: IsoLine,
1305 probes: &[f64],
1306 span: (f64, f64),
1307 tol: Tolerances,
1308) -> OgeomResult<Option<(Curve, f64, f64)>> {
1309 let c = line.at();
1310 let circle = |centre: Point, z: Direction, x: Point, radius: f64| -> OgeomResult<Curve> {
1311 let x = Direction::new(x - centre, tol)?;
1312 let frame = Frame::new(centre, z, x, tol)?;
1313 Ok(CircleCurve::new(Circle::new(frame, radius, tol)?).into())
1314 };
1315 let straight = |at: Point, along: ogeom_math::Vector| -> OgeomResult<(Curve, f64)> {
1316 let length = along.magnitude();
1317 let direction = Direction::new(along, tol)?;
1318 Ok((
1319 LineCurve::new(Axis {
1320 location: at,
1321 direction,
1322 })
1323 .into(),
1324 length,
1325 ))
1326 };
1327 let found: (Curve, f64, f64) = match (surface, line) {
1328 (SurfaceGeometry::BSpline(b), IsoLine::U(_)) => {
1329 (Curve::BSpline(b.iso_u_curve(c, tol)?), 1.0, 0.0)
1330 }
1331 (SurfaceGeometry::BSpline(b), IsoLine::V(_)) => {
1332 (Curve::BSpline(b.iso_v_curve(c, tol)?), 1.0, 0.0)
1333 }
1334 (
1335 SurfaceGeometry::Plane(_) | SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_),
1336 IsoLine::U(_),
1337 )
1338 | (SurfaceGeometry::Plane(_) | SurfaceGeometry::Extrusion(_), _) => {
1339 let at = surface.point_at(line.point(0.0).x, line.point(0.0).y, tol)?;
1342 let next = surface.point_at(line.point(1.0).x, line.point(1.0).y, tol)?;
1343 match (surface, line) {
1344 (SurfaceGeometry::Extrusion(e), IsoLine::V(_)) => {
1345 let shift = Transform::translation(e.direction().vector() * c);
1346 (e.curve().transformed(&shift, tol)?, 1.0, 0.0)
1347 }
1348 _ => {
1349 let (curve, speed) = straight(at, next - at)?;
1350 (curve, speed, 0.0)
1351 }
1352 }
1353 }
1354 (
1355 SurfaceGeometry::Cylinder(_)
1356 | SurfaceGeometry::Cone(_)
1357 | SurfaceGeometry::Sphere(_)
1358 | SurfaceGeometry::Torus(_)
1359 | SurfaceGeometry::Revolution(_),
1360 IsoLine::V(_),
1361 ) => {
1362 let axis = match surface {
1364 SurfaceGeometry::Cylinder(s) => frame_axis(s.cylinder().frame()),
1365 SurfaceGeometry::Cone(s) => frame_axis(s.cone().frame()),
1366 SurfaceGeometry::Sphere(s) => frame_axis(s.sphere().frame()),
1367 SurfaceGeometry::Torus(s) => frame_axis(s.torus().frame()),
1368 SurfaceGeometry::Revolution(s) => s.axis(),
1369 _ => return Ok(None),
1370 };
1371 let start = surface.point_at(0.0, c, tol)?;
1372 let centre = axis.project(start);
1373 let radius = start.distance(centre);
1374 if radius <= tol.confusion() {
1375 return Ok(None);
1376 }
1377 (circle(centre, axis.direction, start, radius)?, 1.0, 0.0)
1378 }
1379 (SurfaceGeometry::Sphere(_) | SurfaceGeometry::Torus(_), IsoLine::U(_)) => {
1380 let at0 = surface.point_at(c, 0.0, tol)?;
1382 let quarter = surface.point_at(c, core::f64::consts::FRAC_PI_2, tol)?;
1383 let opposite = surface.point_at(c, -core::f64::consts::FRAC_PI_2, tol)?;
1384 let centre = Point::from_vector((quarter.to_vector() + opposite.to_vector()) * 0.5);
1385 let radius = at0.distance(centre);
1386 let z = Direction::new((at0 - centre).cross(quarter - centre), tol)?;
1387 (circle(centre, z, at0, radius)?, 1.0, 0.0)
1388 }
1389 (SurfaceGeometry::Revolution(r), IsoLine::U(_)) => {
1390 let turn = Transform::rotation(r.axis(), c);
1391 (r.curve().transformed(&turn, tol)?, 1.0, 0.0)
1392 }
1393 (SurfaceGeometry::Trimmed(t), _) => return iso_curve(t.basis(), line, probes, span, tol),
1394 _ => {
1398 const N: usize = 128;
1399 let (lo, hi) = span;
1400 if hi <= lo || !hi.is_finite() || !lo.is_finite() {
1401 return Ok(None);
1402 }
1403 #[allow(clippy::cast_precision_loss)]
1404 let params: Vec<f64> = (0..=N)
1405 .map(|i| lo + (hi - lo) * i as f64 / N as f64)
1406 .collect();
1407 let points: Vec<Point> = params
1408 .iter()
1409 .map(|w| {
1410 let on = line.point(*w);
1411 surface.point_at(on.x, on.y, tol)
1412 })
1413 .collect::<OgeomResult<_>>()?;
1414 let fitted = ogeom_geom::fit::fit_points_at(¶ms, &points, 3, tol.confusion(), tol)?;
1415 if !fitted.met {
1416 return Ok(None);
1417 }
1418 (Curve::BSpline(fitted.curve), 1.0, 0.0)
1419 }
1420 };
1421 let (curve, scale, offset) = &found;
1424 for &w in probes {
1425 let on = line.point(w);
1426 let expected = surface.point_at(on.x, on.y, tol)?;
1427 let got = curve.point_at(scale * w + offset, tol)?;
1428 if expected.distance(got) > tol.confusion() {
1429 return Ok(None);
1430 }
1431 }
1432 Ok(Some(found))
1433}
1434
1435fn frame_axis(frame: Frame) -> Axis {
1436 Axis {
1437 location: frame.origin(),
1438 direction: frame.z(),
1439 }
1440}
1441
1442fn forward(shape: &Shape) -> Shape {
1445 if shape.orientation() == Orientation::Reversed {
1446 shape.reversed()
1447 } else {
1448 shape.clone()
1449 }
1450}
1451
1452fn edge_data(model: &Model, edge: &Shape) -> OgeomResult<EdgeData> {
1453 match model.node(edge).map(ogeom_topo::TShape::data) {
1454 Some(NodeData::Edge(data)) => Ok((**data).clone()),
1455 Some(_) => ogeom_bail!(Construction, "expected an edge"),
1456 None => ogeom_bail!(Dangling, "edge is not in this model"),
1457 }
1458}
1459
1460fn face_data(model: &Model, face: &Shape) -> OgeomResult<ogeom_topo::FaceData> {
1461 match model.node(face).map(ogeom_topo::TShape::data) {
1462 Some(NodeData::Face(data)) => Ok((**data).clone()),
1463 Some(_) => ogeom_bail!(Construction, "expected a face"),
1464 None => ogeom_bail!(Dangling, "face is not in this model"),
1465 }
1466}
1467
1468fn vertex_point(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
1469 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
1470 ogeom_bail!(Construction, "a vertex holds no point");
1471 };
1472 Ok(data.point)
1473}
1474
1475fn signed_area(ring: &[Point2]) -> f64 {
1476 let n = ring.len();
1477 (0..n)
1478 .map(|i| {
1479 let (a, b) = (ring[i], ring[(i + 1) % n]);
1480 a.x * b.y - b.x * a.y
1481 })
1482 .sum::<f64>()
1483 * 0.5
1484}
1485
1486fn inside(rings: &[Vec<Point2>], p: Point2) -> bool {
1488 let mut odd = false;
1489 for ring in rings {
1490 let n = ring.len();
1491 for i in 0..n {
1492 let (a, b) = (ring[i], ring[(i + 1) % n]);
1493 if (a.y > p.y) != (b.y > p.y) {
1494 let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1495 if x > p.x {
1496 odd = !odd;
1497 }
1498 }
1499 }
1500 }
1501 odd
1502}