1use ogeom_core::{FastMap, FastSet};
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, Filter, Location, Model, NodeData, Orientation, Shape, ShapeType,
29 SurfaceId, TShapeId, VertexData, explore, 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> {
230 ogeom_algo::on_own_nodes(model, shape, &[], |model, shape, _| {
231 to_bezier_own(model, shape, tol)
232 })
233}
234
235fn to_bezier_own(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
237 let nurbs = ogeom_algo::to_nurbs(model, shape, tol)?;
238 let divided = divide_by_continuity(model, &nurbs.shape, Continuity::CInfinity, tol)?;
239
240 let mut reshape = Reshape::new();
242 for edge in explore_unique(model, ÷d.shape, ShapeType::Edge)? {
243 let data = edge_data(model, &edge)?;
244 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
245 continue;
246 };
247 let Some(Curve::BSpline(spline)) = model.geometry().curve(curve).cloned() else {
248 continue;
249 };
250 if spline.knots().distinct().len() <= 2 {
251 continue;
252 }
253 let span = model
254 .geometry_mut()
255 .add_curve(Curve::BSpline(spline.segment(range, tol)?));
256 let mut fresh = data.clone();
257 for repr in &mut fresh.representations {
258 if let EdgeRepr::Curve3d { curve, .. } = repr {
259 *curve = span;
260 }
261 }
262 let bounds = model.children_of(&forward(&edge))?;
263 let rebuilt = model.add_edge(fresh, &bounds)?;
264 reshape.replace(&forward(&edge), rebuilt);
265 }
266 let spans = if reshape.is_empty() {
267 Built::new(divided.shape.clone(), History::identity())
268 } else {
269 reshape.apply(model, ÷d.shape)?
270 };
271
272 let mut reshape = Reshape::new();
275 for face in explore_unique(model, &spans.shape, ShapeType::Face)? {
276 let data = face_data(model, &face)?;
277 let Some(SurfaceGeometry::BSpline(spline)) =
278 model.geometry().surface(data.surface).cloned()
279 else {
280 continue;
281 };
282 if spline.u_knots().distinct().len() <= 2 && spline.v_knots().distinct().len() <= 2 {
283 continue;
284 }
285 let rings = rings(model, &forward(&face), data.surface, tol)?;
286 let ((u0, u1), (v0, v1)) = chart_bounds(&rings, tol);
287 let ((du0, du1), (dv0, dv1)) = spline.domain();
288 let snap = |x: f64, knots: &[(f64, usize)]| {
291 knots
292 .iter()
293 .map(|&(k, _)| k)
294 .find(|k| (k - x).abs() <= tol.parametric())
295 .unwrap_or(x)
296 };
297 let (us, vs) = (spline.u_knots().distinct(), spline.v_knots().distinct());
298 let (u0, u1) = (snap(u0, &us), snap(u1, &us));
299 let (v0, v1) = (snap(v0, &vs), snap(v1, &vs));
300 let patch = spline.segment((u0.max(du0), u1.min(du1)), (v0.max(dv0), v1.min(dv1)), tol)?;
301 let id = model
302 .geometry_mut()
303 .add_surface(SurfaceGeometry::BSpline(patch));
304 let walked = explore(model, &forward(&face), Filter::OfType(ShapeType::Edge))?;
309 let outside = |p: Point2| {
310 (u0 - p.x)
311 .max(p.x - u1)
312 .max(0.0)
313 .hypot((v0 - p.y).max(p.y - v1).max(0.0))
314 };
315 for edge in explore_unique(model, &face, ShapeType::Edge)? {
316 let repr = edge_data(model, &edge)?
317 .pcurve_for(data.surface, edge.location())
318 .cloned();
319 let Some(mut repr) = repr else { continue };
320 let once = walked.iter().filter(|e| e.node() == edge.node()).count() == 1;
321 if let (
322 true,
323 EdgeRepr::Seam {
324 forward: ahead,
325 reversed: back,
326 range,
327 location,
328 ..
329 },
330 ) = (once, &repr)
331 {
332 let middle = |id| -> OgeomResult<Point2> {
333 let Some(pcurve) = model.geometry().pcurve(id) else {
334 ogeom_bail!(Dangling, "pcurve is not in this model");
335 };
336 pcurve.point_at(0.5 * (range.0 + range.1), tol)
337 };
338 let curve = if outside(middle(*back)?) < outside(middle(*ahead)?) {
339 *back
340 } else {
341 *ahead
342 };
343 repr = EdgeRepr::PCurve {
344 curve,
345 range: *range,
346 surface: data.surface,
347 location: location.clone(),
348 };
349 }
350 match &mut repr {
351 EdgeRepr::PCurve { surface, .. } | EdgeRepr::Seam { surface, .. } => *surface = id,
352 _ => continue,
353 }
354 let Some(node) = model.node_mut(&edge) else {
355 ogeom_bail!(Dangling, "edge is not in this model");
356 };
357 if let NodeData::Edge(e) = node.data_mut() {
358 let agreed = e.same_parameter();
359 e.add(repr);
360 e.assert_same_parameter(agreed);
361 }
362 }
363 let mut fresh = data.clone();
364 fresh.surface = id;
365 fresh.triangulation = None;
366 let wires = model.children_of(&forward(&face))?;
367 let rebuilt = model.add_face(fresh, &wires)?;
368 reshape.replace(&forward(&face), rebuilt);
369 }
370 let patches = if reshape.is_empty() {
371 Built::new(spans.shape.clone(), History::identity())
372 } else {
373 reshape.apply(model, &spans.shape)?
374 };
375 Ok(Built::new(
376 patches.shape,
377 nurbs
378 .history
379 .then(÷d.history)
380 .then(&spans.history)
381 .then(&patches.history),
382 ))
383}
384
385pub(crate) type Bounds = ((f64, f64), (f64, f64));
388
389fn divide_faces(
393 model: &mut Model,
394 shape: &Shape,
395 tol: Tolerances,
396 mut lines: impl FnMut(&Model, &Shape, &SurfaceGeometry, Bounds) -> OgeomResult<Vec<IsoLine>>,
397) -> OgeomResult<Built> {
398 let mut current = Built::new(shape.clone(), History::identity());
399 let mut settled: FastSet<TShapeId> = FastSet::default();
400 'rounds: for _ in 0..100_000 {
401 for face in explore_unique(model, ¤t.shape, ShapeType::Face)? {
402 if settled.contains(&face.node()) {
403 continue;
404 }
405 let data = face_data(model, &face)?;
406 if data.natural_restriction {
407 match bounded_natural(model, &face, tol)? {
410 Some(bounded) => {
411 let mut reshape = Reshape::new();
412 reshape.replace(&forward(&face), bounded);
413 let next = reshape.apply(model, ¤t.shape)?;
414 current = Built::new(next.shape, current.history.then(&next.history));
415 continue 'rounds;
416 }
417 None => {
418 settled.insert(face.node());
419 continue;
420 }
421 }
422 }
423 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
424 ogeom_bail!(Dangling, "surface is not in this model");
425 };
426 let rings = rings(model, &forward(&face), data.surface, tol)?;
427 let bounds = chart_bounds(&rings, tol);
428 for line in lines(&*model, &face, &surface, bounds)? {
429 let (lo, hi) = match line {
430 IsoLine::U(_) => bounds.0,
431 IsoLine::V(_) => bounds.1,
432 };
433 let margin = (hi - lo).abs() * 1e-7 + tol.parametric();
434 if line.at() <= lo + margin || line.at() >= hi - margin {
435 continue;
436 }
437 if let Some(reshape) = cut_face(model, &face, line, tol)? {
438 let next = reshape.apply(model, ¤t.shape)?;
439 current = Built::new(next.shape, current.history.then(&next.history));
440 continue 'rounds;
441 }
442 }
443 settled.insert(face.node());
444 }
445 return Ok(current);
446 }
447 ogeom_bail!(Construction, "the division did not settle");
448}
449
450pub(crate) fn bounded_natural(
455 model: &mut Model,
456 face: &Shape,
457 tol: Tolerances,
458) -> OgeomResult<Option<Shape>> {
459 let face_fwd = forward(face);
460 let data = face_data(model, &face_fwd)?;
461 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
462 ogeom_bail!(Dangling, "surface is not in this model");
463 };
464 let ((u0, u1), (v0, v1)) = surface.domain();
465 if ![u0, u1, v0, v1]
466 .iter()
467 .all(|x| x.is_finite() && x.abs() < 1e6)
468 {
469 return Ok(None);
470 }
471 let (pu, pv) = (surface.is_periodic_u(), surface.is_periodic_v());
472 let corner = |u: f64, v: f64| surface.point_at(u, v, tol);
473 let sides = [
476 (IsoLine::V(v0), u0, u1),
477 (IsoLine::U(u1), v0, v1),
478 (IsoLine::V(v1), u1, u0),
479 (IsoLine::U(u0), v1, v0),
480 ];
481 let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
483 let mut vertices: Vec<Shape> = Vec::with_capacity(4);
484 for (i, (u, v)) in corners.iter().enumerate() {
485 let p = corner(*u, *v)?;
486 let found = (0..i).find(|j| {
487 let (a, b) = corners[*j];
488 corner(a, b).is_ok_and(|q| q.distance(p) <= tol.confusion())
489 });
490 vertices.push(match found {
491 Some(j) => vertices[j].clone(),
492 None => model.add_vertex(VertexData::new(p)),
493 });
494 }
495 let chart_line = |line: IsoLine, a: f64, b: f64, t: (f64, f64)| -> OgeomResult<PlanarCurve> {
496 let knots = KnotVector::new(vec![t.0, t.0, t.1, t.1], 1)?;
497 let (p, q) = (line.point(a), line.point(b));
498 let (p, q) = if t.0 <= t.1 { (p, q) } else { (q, p) };
499 Ok(PlanarCurve::BSpline(BSpline2d::new(
500 knots,
501 vec![p, q],
502 tol,
503 )?))
504 };
505 let mut ring: Vec<Shape> = Vec::with_capacity(4);
507 let mut seam_u: Option<Shape> = None;
508 let mut seam_v: Option<Shape> = None;
509 for (index, (line, a, b)) in sides.into_iter().enumerate() {
510 let (from, to) = (vertices[index].clone(), vertices[(index + 1) % 4].clone());
511 let periodic_pair = match line {
512 IsoLine::U(_) => pu,
513 IsoLine::V(_) => pv,
514 };
515 if periodic_pair {
518 let held = match line {
519 IsoLine::U(_) => &seam_u,
520 IsoLine::V(_) => &seam_v,
521 };
522 if let Some(seam) = held {
523 ring.push(seam.reversed());
524 continue;
525 }
526 }
527 let (lo, hi) = (a.min(b), a.max(b));
528 let mid = line.point(0.5 * (lo + hi));
529 let pinched = corner(line.point(lo).x, line.point(lo).y)?.distance(corner(mid.x, mid.y)?)
530 <= tol.confusion()
531 && corner(line.point(hi).x, line.point(hi).y)?.distance(corner(mid.x, mid.y)?)
532 <= tol.confusion();
533 let edge = if pinched {
534 let mut edge_data = EdgeData::new();
535 edge_data.degenerate = true;
536 let trim = chart_line(line, lo, hi, (lo, hi))?;
537 let id = model.geometry_mut().add_pcurve(trim);
538 edge_data.add(EdgeRepr::PCurve {
539 curve: id,
540 surface: data.surface,
541 location: Location::identity(),
542 range: (lo, hi),
543 });
544 let pole = model.add_edge(edge_data, &[from.clone(), from.clone()])?;
545 if a <= b { pole } else { pole.reversed() }
547 } else {
548 let Some((curve, scale, offset)) =
549 iso_curve(&surface, line, &[0.5 * (lo + hi)], (lo, hi), tol)?
550 else {
551 return Ok(None);
552 };
553 let (t_lo, t_hi) = (scale * lo + offset, scale * hi + offset);
554 let (range, rising) = if t_lo <= t_hi {
555 ((t_lo, t_hi), true)
556 } else {
557 ((t_hi, t_lo), false)
558 };
559 let curve_id = model.geometry_mut().add_curve(curve);
560 let mut edge_data = EdgeData::on_curve(curve_id, Location::identity(), range);
561 let (start_v, end_v) = {
563 let low_end = if a <= b { &from } else { &to };
564 let high_end = if a <= b { &to } else { &from };
565 if rising {
566 (low_end.clone(), high_end.clone())
567 } else {
568 (high_end.clone(), low_end.clone())
569 }
570 };
571 let pcurve_at = |line: IsoLine| -> OgeomResult<PlanarCurve> {
572 let (p, q) = if rising {
573 (line.point(lo), line.point(hi))
574 } else {
575 (line.point(hi), line.point(lo))
576 };
577 let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
578 Ok(PlanarCurve::BSpline(BSpline2d::new(
579 knots,
580 vec![p, q],
581 tol,
582 )?))
583 };
584 if periodic_pair {
585 let other = match line {
587 IsoLine::U(c) => IsoLine::U(if (c - u1).abs() < (c - u0).abs() {
588 u0
589 } else {
590 u1
591 }),
592 IsoLine::V(c) => IsoLine::V(if (c - v1).abs() < (c - v0).abs() {
593 v0
594 } else {
595 v1
596 }),
597 };
598 let here = model.geometry_mut().add_pcurve(pcurve_at(line)?);
599 let there = model.geometry_mut().add_pcurve(pcurve_at(other)?);
600 edge_data.add(EdgeRepr::Seam {
601 forward: here,
602 reversed: there,
603 surface: data.surface,
604 location: Location::identity(),
605 range,
606 });
607 } else {
608 let id = model.geometry_mut().add_pcurve(pcurve_at(line)?);
609 edge_data.add(EdgeRepr::PCurve {
610 curve: id,
611 surface: data.surface,
612 location: Location::identity(),
613 range,
614 });
615 }
616 edge_data.assert_same_parameter(true);
617 let edge = model.add_edge(edge_data, &[start_v, end_v])?;
618 let walked_with_curve = (a <= b) == rising;
619 let occurrence = if walked_with_curve {
620 edge.clone()
621 } else {
622 edge.reversed()
623 };
624 if periodic_pair {
625 match line {
626 IsoLine::U(_) => seam_u = Some(occurrence.clone()),
627 IsoLine::V(_) => seam_v = Some(occurrence.clone()),
628 }
629 }
630 ring.push(occurrence);
631 continue;
632 };
633 ring.push(edge);
634 }
635 let wire = model.add_wire(&ring)?;
636 let mut fresh = data.clone();
637 fresh.natural_restriction = false;
638 fresh.triangulation = None;
639 let bounded = model.add_face(fresh, &[wire])?;
640 Ok(Some(bounded))
641}
642
643fn divide_edges(
646 model: &mut Model,
647 shape: &Shape,
648 order: usize,
649 tol: Tolerances,
650) -> OgeomResult<Built> {
651 let mut reshape = Reshape::new();
652 for edge in explore_unique(model, shape, ShapeType::Edge)? {
653 let edge = forward(&edge);
654 let data = edge_data(model, &edge)?;
655 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
656 continue;
657 };
658 let Some(geometry) = model.geometry().curve(curve) else {
659 ogeom_bail!(Dangling, "curve is not in this model");
660 };
661 let margin = (range.1 - range.0) * 1e-9 + tol.parametric();
662 let at: Vec<f64> = curve_knots(geometry)
663 .map(|knots| weak_knots(&knots, order))
664 .unwrap_or_default()
665 .into_iter()
666 .filter(|t| *t > range.0 + margin && *t < range.1 - margin)
667 .collect();
668 if at.is_empty() {
669 continue;
670 }
671 let fractions: Vec<f64> = at
672 .iter()
673 .map(|t| (t - range.0) / (range.1 - range.0))
674 .collect();
675 let (pieces, _) = split_edge(model, &edge, &fractions, None, tol)?;
676 reshape.split(&edge, pieces);
677 }
678 if reshape.is_empty() {
679 return Ok(Built::new(shape.clone(), History::identity()));
680 }
681 reshape.apply(model, shape)
682}
683
684pub(crate) fn placed_baked(
688 model: &mut Model,
689 shape: &Shape,
690 tol: Tolerances,
691) -> OgeomResult<Built> {
692 let mut placed = false;
693 for kind in [ShapeType::Edge, ShapeType::Face] {
694 for s in explore_unique(model, shape, kind)? {
695 placed |= !s.location().is_identity();
696 }
697 }
698 for face in explore_unique(model, shape, ShapeType::Face)? {
699 placed |= !face_data(model, &face)?.location.is_identity();
700 }
701 if placed {
702 ogeom_algo::baked_shape(model, shape, tol)
703 } else {
704 Ok(Built::new(shape.clone(), History::identity()))
705 }
706}
707
708fn curve_knots(curve: &Curve) -> Option<KnotVector> {
711 match curve {
712 Curve::BSpline(b) => Some(b.knots().clone()),
713 Curve::Trimmed(t) => curve_knots(t.basis()),
714 _ => None,
715 }
716}
717
718fn weak_knots(knots: &KnotVector, order: usize) -> Vec<f64> {
721 let (a, b) = knots.domain();
722 let p = knots.degree();
723 knots
724 .distinct()
725 .into_iter()
726 .filter(|(value, m)| *value > a && *value < b && p.saturating_sub(*m) < order)
727 .map(|(value, _)| value)
728 .collect()
729}
730
731fn knot_lines(surface: &SurfaceGeometry, order: usize) -> Vec<IsoLine> {
734 match surface {
735 SurfaceGeometry::BSpline(b) => weak_knots(b.u_knots(), order)
736 .into_iter()
737 .map(IsoLine::U)
738 .chain(weak_knots(b.v_knots(), order).into_iter().map(IsoLine::V))
739 .collect(),
740 SurfaceGeometry::Extrusion(e) => curve_knots(e.curve())
741 .map(|k| weak_knots(&k, order).into_iter().map(IsoLine::U).collect())
742 .unwrap_or_default(),
743 SurfaceGeometry::Revolution(r) => curve_knots(r.curve())
744 .map(|k| weak_knots(&k, order).into_iter().map(IsoLine::V).collect())
745 .unwrap_or_default(),
746 SurfaceGeometry::Trimmed(t) => knot_lines(t.basis(), order),
747 SurfaceGeometry::Offset(o) => knot_lines(o.basis(), order.saturating_add(1)),
749 _ => Vec::new(),
750 }
751}
752
753const fn angular(surface: &SurfaceGeometry) -> (bool, bool) {
755 match surface {
756 SurfaceGeometry::Cylinder(_)
757 | SurfaceGeometry::Cone(_)
758 | SurfaceGeometry::Revolution(_) => (true, false),
759 SurfaceGeometry::Sphere(_) | SurfaceGeometry::Torus(_) => (true, true),
760 _ => (false, false),
761 }
762}
763
764#[derive(Debug, Clone)]
768pub(crate) struct Occurrence {
769 pub(crate) edge: Shape,
770 pcurve: PlanarCurve,
771 range: (f64, f64),
772}
773
774impl Occurrence {
775 pub(crate) fn reversed(&self) -> bool {
776 self.edge.orientation() == Orientation::Reversed
777 }
778
779 pub(crate) fn at(&self, s: f64, tol: Tolerances) -> OgeomResult<Point2> {
781 let s = if self.reversed() { 1.0 - s } else { s };
782 self.pcurve
783 .point_at(self.range.0 + (self.range.1 - self.range.0) * s, tol)
784 }
785
786 pub(crate) fn polyline(&self, samples: usize, tol: Tolerances) -> OgeomResult<Vec<Point2>> {
787 (0..=samples)
788 .map(|i| {
789 #[allow(clippy::cast_precision_loss)]
790 let s = i as f64 / samples as f64;
791 self.at(s, tol)
792 })
793 .collect()
794 }
795}
796
797type Trims = (PlanarCurve, Option<PlanarCurve>, (f64, f64));
799
800type Outer = (Vec<Shape>, Vec<Point2>, Vec<Vec<Shape>>);
802
803pub(crate) fn rings(
806 model: &Model,
807 face: &Shape,
808 surface: SurfaceId,
809 tol: Tolerances,
810) -> OgeomResult<Vec<Vec<Occurrence>>> {
811 let mut out = Vec::new();
812 for wire in model.ordered_children_of(face)? {
813 let edges = model.ordered_children_of(&wire)?;
814 let mut choices: Vec<Option<Trims>> = Vec::new();
815 for edge in &edges {
816 let data = edge_data(model, edge)?;
817 let pcurve =
818 |id| -> OgeomResult<PlanarCurve> {
819 model.geometry().pcurve(id).cloned().ok_or_else(|| {
820 ogeom_core::ogeom_err!(Dangling, "pcurve is not in this model")
821 })
822 };
823 choices.push(match data.pcurve_for(surface, edge.location()) {
824 Some(EdgeRepr::PCurve { curve, range, .. }) => {
825 Some((pcurve(*curve)?, None, *range))
826 }
827 Some(EdgeRepr::Seam {
828 forward,
829 reversed,
830 range,
831 ..
832 }) => Some((pcurve(*forward)?, Some(pcurve(*reversed)?), *range)),
833 _ => None,
834 });
835 }
836 let first_plain = choices
842 .iter()
843 .position(|c| c.as_ref().is_some_and(|c| c.1.is_none()));
844 let start = first_plain.unwrap_or(0);
845 let walk = |flip: bool| -> OgeomResult<(Vec<Occurrence>, f64)> {
846 let n = edges.len();
847 let mut ring: Vec<Option<Occurrence>> = vec![None; n];
848 let mut last: Option<Point2> = None;
849 let mut gap = 0.0;
850 for k in 0..n {
851 let i = (start + k) % n;
852 let Some((a, b, range)) = choices[i].clone() else {
853 ogeom_bail!(Construction, "an edge has no trim on this face");
854 };
855 let mut occurrence = Occurrence {
856 edge: edges[i].clone(),
857 pcurve: a,
858 range,
859 };
860 if let Some(b) = b {
861 let other = Occurrence {
862 pcurve: b,
863 ..occurrence.clone()
864 };
865 let take_other = match last {
866 Some(last) => {
867 other.at(0.0, tol)?.distance(last)
868 < occurrence.at(0.0, tol)?.distance(last)
869 }
870 None => occurrence.reversed() != flip,
871 };
872 if take_other {
873 occurrence = other;
874 }
875 }
876 if let Some(last) = last {
877 gap += occurrence.at(0.0, tol)?.distance(last);
878 }
879 last = Some(occurrence.at(1.0, tol)?);
880 ring[i] = Some(occurrence);
881 }
882 let ring: Vec<Occurrence> = ring.into_iter().flatten().collect();
883 if let (Some(first), Some(last)) = (ring.first(), last) {
884 gap += first.at(0.0, tol)?.distance(last);
885 }
886 Ok((ring, gap))
887 };
888 let (mut ring, gap) = walk(false)?;
889 if first_plain.is_none() && gap > tol.parametric() {
890 let (flipped, flipped_gap) = walk(true)?;
891 if flipped_gap < gap {
892 ring = flipped;
893 }
894 }
895 out.push(ring);
896 }
897 Ok(out)
898}
899
900pub(crate) fn chart_bounds(rings: &[Vec<Occurrence>], tol: Tolerances) -> Bounds {
901 let mut b = (
902 (f64::INFINITY, f64::NEG_INFINITY),
903 (f64::INFINITY, f64::NEG_INFINITY),
904 );
905 for o in rings.iter().flatten() {
906 for i in 0..=32 {
907 if let Ok(p) = o.at(f64::from(i) / 32.0, tol) {
908 b.0 = (b.0.0.min(p.x), b.0.1.max(p.x));
909 b.1 = (b.1.0.min(p.y), b.1.1.max(p.y));
910 }
911 }
912 }
913 b
914}
915
916#[derive(Debug, Clone, Copy)]
918enum Meeting {
919 Vertex(TShapeId),
921 Inside(TShapeId, u64),
923}
924
925#[allow(clippy::too_many_lines)]
928fn cut_face(
929 model: &mut Model,
930 face: &Shape,
931 line: IsoLine,
932 tol: Tolerances,
933) -> OgeomResult<Option<Reshape>> {
934 let face_fwd = forward(face);
935 let data = face_data(model, &face_fwd)?;
936 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
937 ogeom_bail!(Dangling, "surface is not in this model");
938 };
939 let rings = rings(model, &face_fwd, data.surface, tol)?;
940 let bounds = chart_bounds(&rings, tol);
941 let span = (bounds.0.1 - bounds.0.0).max(bounds.1.1 - bounds.1.0);
942 let eps = span * 1e-9 + tol.parametric();
943 let c = line.at();
944
945 let mut cuts: FastMap<TShapeId, Vec<f64>> = FastMap::default();
948 let mut meetings: Vec<(f64, Meeting)> = Vec::new();
949 for occurrence in rings.iter().flatten() {
950 let f = |s: f64| -> OgeomResult<f64> { Ok(line.fixed(occurrence.at(s, tol)?) - c) };
951 const N: usize = 64;
952 #[allow(clippy::cast_precision_loss)]
953 let values: Vec<f64> = (0..=N)
954 .map(|i| f(i as f64 / N as f64))
955 .collect::<OgeomResult<_>>()?;
956 if values.iter().all(|v| v.abs() <= eps) {
957 ogeom_bail!(Construction, "an edge of the face runs along {line:?}");
958 }
959 let end = occurrence.at(1.0, tol)?;
960 if (line.fixed(end) - c).abs() <= eps {
961 let Some((_, v)) = edge_vertices(model, &occurrence.edge)? else {
962 continue;
963 };
964 meetings.push((line.free(end), Meeting::Vertex(v.node())));
965 }
966 for i in 0..N {
967 let (a, b) = (values[i], values[i + 1]);
968 if a.abs() <= eps || b.abs() <= eps || a.signum() == b.signum() {
969 if i + 1 < N
972 && b.abs() <= eps
973 && a.abs() > eps
974 && values[i + 2].abs() > eps
975 && a.signum() != values[i + 2].signum()
976 {
977 #[allow(clippy::cast_precision_loss)]
978 let s = (i + 1) as f64 / N as f64;
979 record(
980 model,
981 &surface,
982 occurrence,
983 s,
984 line,
985 &mut cuts,
986 &mut meetings,
987 tol,
988 )?;
989 }
990 continue;
991 }
992 #[allow(clippy::cast_precision_loss)]
993 let (mut lo, mut hi) = (i as f64 / N as f64, (i + 1) as f64 / N as f64);
994 let mut f_lo = a;
995 for _ in 0..80 {
996 let mid = 0.5 * (lo + hi);
997 let f_mid = f(mid)?;
998 if f_mid.signum() == f_lo.signum() {
999 (lo, f_lo) = (mid, f_mid);
1000 } else {
1001 hi = mid;
1002 }
1003 }
1004 record(
1005 model,
1006 &surface,
1007 occurrence,
1008 0.5 * (lo + hi),
1009 line,
1010 &mut cuts,
1011 &mut meetings,
1012 tol,
1013 )?;
1014 }
1015 }
1016 meetings.sort_by(|a, b| a.0.total_cmp(&b.0));
1017 meetings.dedup_by(|b, a| {
1018 (a.0 - b.0).abs() <= eps
1019 && match (a.1, b.1) {
1020 (Meeting::Vertex(x), Meeting::Vertex(y)) => x == y,
1021 (Meeting::Inside(x, s), Meeting::Inside(y, t)) => x == y && s == t,
1022 _ => false,
1023 }
1024 });
1025
1026 let outlines: Vec<Vec<Point2>> = rings
1028 .iter()
1029 .map(|ring| -> OgeomResult<Vec<Point2>> {
1030 let mut out = Vec::new();
1031 for o in ring {
1032 out.extend(o.polyline(32, tol)?);
1033 }
1034 Ok(out)
1035 })
1036 .collect::<OgeomResult<_>>()?;
1037 let mut stretches: Vec<(usize, usize)> = Vec::new();
1038 for i in 0..meetings.len().saturating_sub(1) {
1039 let (w0, w1) = (meetings[i].0, meetings[i + 1].0);
1040 if w1 - w0 <= eps * 10.0 {
1041 continue;
1042 }
1043 if inside(&outlines, line.point(0.5 * (w0 + w1))) {
1044 stretches.push((i, i + 1));
1045 }
1046 }
1047 if stretches.is_empty() {
1048 return Ok(None);
1049 }
1050 let outer_sign = outlines
1051 .iter()
1052 .map(|o| signed_area(o))
1053 .max_by(|a, b| a.abs().total_cmp(&b.abs()))
1054 .unwrap_or(1.0)
1055 .signum();
1056
1057 let mut reshape = Reshape::new();
1059 let mut pieces_of: FastMap<TShapeId, Vec<(Shape, f64, f64)>> = FastMap::default();
1060 let mut vertex_at: FastMap<(TShapeId, u64), Shape> = FastMap::default();
1061 let mut vertex_of_node: FastMap<TShapeId, Shape> = FastMap::default();
1062 for v in explore_unique(model, &face_fwd, ShapeType::Vertex)? {
1063 vertex_of_node.insert(v.node(), v);
1064 }
1065 for occurrence in rings.iter().flatten() {
1066 let edge = forward(&occurrence.edge);
1067 if pieces_of.contains_key(&edge.node()) {
1068 continue;
1069 }
1070 let Some(fractions) = cuts.get(&edge.node()) else {
1071 continue;
1072 };
1073 let mut fractions = fractions.clone();
1074 fractions.sort_by(f64::total_cmp);
1075 fractions.dedup_by(|b, a| (*a - *b).abs() <= 1e-12);
1076 let (pieces, vertices) = split_edge(model, &edge, &fractions, Some(data.surface), tol)?;
1077 for (s, v) in fractions.iter().zip(&vertices) {
1078 vertex_at.insert((edge.node(), s.to_bits()), v.clone());
1079 }
1080 let mut bounds = vec![0.0];
1081 bounds.extend(&fractions);
1082 bounds.push(1.0);
1083 pieces_of.insert(
1084 edge.node(),
1085 pieces
1086 .iter()
1087 .zip(bounds.windows(2))
1088 .map(|(p, w)| (p.clone(), w[0], w[1]))
1089 .collect(),
1090 );
1091 reshape.split(&edge, pieces);
1092 }
1093 let meeting_vertex = |m: Meeting| -> OgeomResult<Shape> {
1094 match m {
1095 Meeting::Vertex(node) => vertex_of_node
1096 .get(&node)
1097 .cloned()
1098 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a meeting vertex is lost")),
1099 Meeting::Inside(edge, s) => vertex_at
1100 .get(&(edge, s))
1101 .cloned()
1102 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a cut vertex is lost")),
1103 }
1104 };
1105
1106 let mut sides: [Vec<Item>; 2] = [Vec::new(), Vec::new()];
1108 for occurrence in rings.iter().flatten() {
1109 let edge = forward(&occurrence.edge);
1110 let parts: Vec<(Shape, f64, f64)> = match pieces_of.get(&edge.node()) {
1111 Some(parts) => parts.clone(),
1112 None => vec![(edge.clone(), 0.0, 1.0)],
1113 };
1114 let mut ordered: Vec<(Shape, f64, f64)> = if occurrence.reversed() {
1115 parts
1116 .into_iter()
1117 .rev()
1118 .map(|(p, a, b)| (p.reversed(), 1.0 - b, 1.0 - a))
1119 .collect()
1120 } else {
1121 parts
1122 };
1123 for (piece, a, b) in ordered.drain(..) {
1124 let sub = |s: f64| occurrence.at(a + (b - a) * s, tol);
1125 let polyline: Vec<Point2> = (0..=16)
1126 .map(|i| sub(f64::from(i) / 16.0))
1127 .collect::<OgeomResult<_>>()?;
1128 let side = usize::from(line.fixed(polyline[8]) > c);
1129 let Some((from, to)) = edge_vertices(model, &piece)? else {
1130 ogeom_bail!(Construction, "a boundary piece has no vertices");
1131 };
1132 sides[side].push(Item {
1133 edge: piece,
1134 from: from.node(),
1135 to: to.node(),
1136 polyline,
1137 });
1138 }
1139 }
1140
1141 let probes: Vec<f64> = stretches
1143 .iter()
1144 .map(|&(i, j)| 0.5 * (meetings[i].0 + meetings[j].0))
1145 .collect();
1146 let span = (
1147 meetings.first().map_or(0.0, |m| m.0),
1148 meetings.last().map_or(0.0, |m| m.0),
1149 );
1150 let Some((curve, scale, offset)) = iso_curve(&surface, line, &probes, span, tol)? else {
1151 ogeom_bail!(
1152 Construction,
1153 "the surface has no closed-form iso-curve to cut along"
1154 );
1155 };
1156 for (i, j) in stretches {
1157 let (w0, w1) = (meetings[i].0, meetings[j].0);
1158 let (a, b) = (
1159 meeting_vertex(meetings[i].1)?,
1160 meeting_vertex(meetings[j].1)?,
1161 );
1162 let (t0, t1) = (scale * w0 + offset, scale * w1 + offset);
1163 let rising = t1 > t0;
1164 let (range, (start, end), (p0, p1)) = if rising {
1165 ((t0, t1), (&a, &b), (line.point(w0), line.point(w1)))
1166 } else {
1167 ((t1, t0), (&b, &a), (line.point(w1), line.point(w0)))
1168 };
1169 let curve_id = model.geometry_mut().add_curve(curve.clone());
1170 let mut edge_data = EdgeData::on_curve(curve_id, Location::identity(), range);
1171 let trim = BSpline2d::new(
1172 KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?,
1173 vec![p0, p1],
1174 tol,
1175 )?;
1176 let trim_id = model.geometry_mut().add_pcurve(PlanarCurve::BSpline(trim));
1177 edge_data.add(EdgeRepr::PCurve {
1178 curve: trim_id,
1179 surface: data.surface,
1180 location: Location::identity(),
1181 range,
1182 });
1183 edge_data.assert_same_parameter(true);
1184 let reach = [(start, range.0), (end, range.1)]
1187 .iter()
1188 .map(|(v, t)| -> OgeomResult<f64> {
1189 Ok(vertex_point(model, v)?.distance(curve.point_at(*t, tol)?))
1190 })
1191 .collect::<OgeomResult<Vec<f64>>>()?
1192 .into_iter()
1193 .fold(0.0_f64, f64::max);
1194 let edge = model.add_edge(edge_data, &[start.clone(), end.clone()])?;
1195 model.widen(&edge, ogeom_core::Tolerance::new(reach + tol.confusion())?)?;
1196 let polyline: Vec<Point2> = (0..=16)
1197 .map(|k| {
1198 let w = w0 + (w1 - w0) * f64::from(k) / 16.0;
1199 line.point(w)
1200 })
1201 .collect();
1202 let left = match line {
1206 IsoLine::U(_) => 0,
1207 IsoLine::V(_) => 1,
1208 };
1209 let up_side = if outer_sign >= 0.0 { left } else { 1 - left };
1210 let up = if rising {
1211 edge.clone()
1212 } else {
1213 edge.reversed()
1214 };
1215 let down = up.reversed();
1216 sides[up_side].push(Item {
1217 edge: up,
1218 from: a.node(),
1219 to: b.node(),
1220 polyline: polyline.clone(),
1221 });
1222 sides[1 - up_side].push(Item {
1223 edge: down,
1224 from: b.node(),
1225 to: a.node(),
1226 polyline: polyline.into_iter().rev().collect(),
1227 });
1228 }
1229
1230 let mut faces: Vec<Shape> = Vec::new();
1232 for items in sides {
1233 let loops = chain(items, eps.max(tol.parametric() * 1e3))?;
1234 let mut outers: Vec<Outer> = Vec::new();
1235 let mut holes: Vec<(Vec<Shape>, Vec<Point2>)> = Vec::new();
1236 for (edges, outline) in loops {
1237 if signed_area(&outline) * outer_sign > 0.0 {
1238 outers.push((edges, outline, Vec::new()));
1239 } else {
1240 holes.push((edges, outline));
1241 }
1242 }
1243 for (edges, outline) in holes {
1244 let probe = outline[outline.len() / 2];
1245 let Some(host) = outers
1246 .iter_mut()
1247 .filter(|o| inside(std::slice::from_ref(&o.1), probe))
1248 .min_by(|a, b| signed_area(&a.1).abs().total_cmp(&signed_area(&b.1).abs()))
1249 else {
1250 ogeom_bail!(Construction, "a hole of the cut face lies in no piece");
1251 };
1252 host.2.push(edges);
1253 }
1254 for (edges, _, hole_edges) in outers {
1255 let mut wires = vec![model.add_wire(&edges)?];
1256 for h in hole_edges {
1257 wires.push(model.add_wire(&h)?);
1258 }
1259 let mut fresh = data.clone();
1260 fresh.triangulation = None;
1261 fresh.natural_restriction = false;
1262 faces.push(model.add_face(fresh, &wires)?);
1263 }
1264 }
1265 if faces.len() < 2 {
1266 return Ok(None);
1267 }
1268 reshape.split(&face_fwd, faces);
1269 Ok(Some(reshape))
1270}
1271
1272#[allow(clippy::too_many_arguments)]
1279fn record(
1280 model: &Model,
1281 surface: &SurfaceGeometry,
1282 occurrence: &Occurrence,
1283 s: f64,
1284 line: IsoLine,
1285 cuts: &mut FastMap<TShapeId, Vec<f64>>,
1286 meetings: &mut Vec<(f64, Meeting)>,
1287 tol: Tolerances,
1288) -> OgeomResult<()> {
1289 let at = occurrence.at(s, tol)?;
1290 let degenerate = model
1291 .node(&occurrence.edge)
1292 .and_then(|n| n.data().as_edge())
1293 .is_none_or(|d| d.degenerate);
1294 if !degenerate && let Some((first, last)) = edge_vertices(model, &occurrence.edge)? {
1295 let lifted = surface.point_at(at.x, at.y, tol)?;
1296 for (end, vertex) in [(0.0, first), (1.0, last)] {
1297 let held = model
1298 .node(&vertex)
1299 .and_then(|n| n.data().as_vertex())
1300 .map_or(0.0, |d| d.tolerance.get());
1301 if vertex_point(model, &vertex)?.distance(lifted) <= held + tol.confusion() {
1302 let corner = occurrence.at(end, tol)?;
1303 meetings.push((line.free(corner), Meeting::Vertex(vertex.node())));
1304 return Ok(());
1305 }
1306 }
1307 }
1308 let forward_s = if occurrence.reversed() { 1.0 - s } else { s };
1311 let forward_s = (forward_s * 1e12).round() / 1e12;
1312 let node = occurrence.edge.node();
1313 cuts.entry(node).or_default().push(forward_s);
1314 meetings.push((line.free(at), Meeting::Inside(node, forward_s.to_bits())));
1315 Ok(())
1316}
1317
1318#[derive(Debug, Clone)]
1320struct Item {
1321 edge: Shape,
1322 from: TShapeId,
1323 to: TShapeId,
1324 polyline: Vec<Point2>,
1325}
1326
1327fn chain(mut items: Vec<Item>, snap: f64) -> OgeomResult<Vec<(Vec<Shape>, Vec<Point2>)>> {
1330 let mut loops = Vec::new();
1331 while let Some(first) = items.pop() {
1332 let start_node = first.from;
1333 let start_at = first.polyline[0];
1334 let mut here = *first.polyline.last().unwrap_or(&start_at);
1335 let mut at_node = first.to;
1336 let mut edges = vec![first.edge];
1337 let mut outline = first.polyline;
1338 loop {
1339 if at_node == start_node && here.distance(start_at) <= snap {
1340 break;
1341 }
1342 let next = items
1343 .iter()
1344 .enumerate()
1345 .filter(|(_, it)| it.from == at_node)
1346 .min_by(|a, b| {
1347 a.1.polyline[0]
1348 .distance(here)
1349 .total_cmp(&b.1.polyline[0].distance(here))
1350 })
1351 .map(|(i, _)| i);
1352 let Some(i) = next else {
1353 ogeom_bail!(Construction, "a piece of the cut face does not close");
1354 };
1355 let it = items.swap_remove(i);
1356 here = *it.polyline.last().unwrap_or(&here);
1357 at_node = it.to;
1358 edges.push(it.edge);
1359 outline.extend(it.polyline);
1360 }
1361 loops.push((edges, outline));
1362 }
1363 Ok(loops)
1364}
1365
1366pub(crate) fn split_edge(
1377 model: &mut Model,
1378 edge: &Shape,
1379 fractions: &[f64],
1380 along: Option<SurfaceId>,
1381 tol: Tolerances,
1382) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
1383 let data = edge_data(model, edge)?;
1384 let Some((start, end)) = edge_vertices(model, edge)? else {
1385 ogeom_bail!(Construction, "an edge with no vertices cannot be cut");
1386 };
1387 let measured =
1388 along.and_then(
1389 |surface| match data.pcurve_for(surface, &Location::identity()) {
1390 Some(
1391 EdgeRepr::PCurve { curve, range, .. }
1392 | EdgeRepr::Seam {
1393 forward: curve,
1394 range,
1395 ..
1396 },
1397 ) => Some((surface, *curve, *range)),
1398 _ => None,
1399 },
1400 );
1401 let curve = match data.curve3d() {
1402 Some(EdgeRepr::Curve3d { curve, range, .. }) if !data.degenerate => {
1403 let Some(geometry) = model.geometry().curve(*curve) else {
1404 ogeom_bail!(Dangling, "curve is not in this model");
1405 };
1406 Some((geometry.clone(), *range))
1407 }
1408 _ => None,
1409 };
1410 let same = |range: (f64, f64)| {
1413 let reference = match (measured, &curve) {
1414 (Some((_, _, r)), _) => Some(r),
1415 (None, Some((_, r))) => Some(*r),
1416 (None, None) => None,
1417 };
1418 data.same_parameter() && reference == Some(range)
1419 };
1420 let points: Vec<Option<Point>> = fractions
1421 .iter()
1422 .map(|&s| -> OgeomResult<Option<Point>> {
1423 Ok(match (measured, &curve) {
1424 (Some((surface, pcurve, range)), _) => {
1425 let (Some(surface), Some(pcurve)) = (
1426 model.geometry().surface(surface),
1427 model.geometry().pcurve(pcurve),
1428 ) else {
1429 ogeom_bail!(Dangling, "an edge names geometry not in this model");
1430 };
1431 let uv = pcurve.point_at(range.0 + (range.1 - range.0) * s, tol)?;
1432 Some(surface.point_at(uv.x, uv.y, tol)?)
1433 }
1434 (None, Some((c, range))) => {
1435 Some(c.point_at(range.0 + (range.1 - range.0) * s, tol)?)
1436 }
1437 (None, None) => None,
1438 })
1439 })
1440 .collect::<OgeomResult<_>>()?;
1441 let mut own: Vec<Vec<f64>> = Vec::with_capacity(data.representations.len());
1443 for repr in &data.representations {
1444 let cut = match (repr, &curve) {
1445 (EdgeRepr::Curve3d { range, .. }, Some((c, _)))
1446 if measured.is_some() && !same(*range) =>
1447 {
1448 passing(fractions, &points, |s| {
1449 c.point_at(range.0 + (range.1 - range.0) * s, tol).ok()
1450 })?
1451 }
1452 (
1453 EdgeRepr::PCurve {
1454 curve: id,
1455 range,
1456 surface,
1457 location,
1458 }
1459 | EdgeRepr::Seam {
1460 forward: id,
1461 range,
1462 surface,
1463 location,
1464 ..
1465 },
1466 _,
1467 ) => {
1468 let measuring = measured.is_some_and(|(on, _, _)| on == *surface);
1469 if measuring || same(*range) || !location.is_identity() {
1470 fractions.to_vec()
1471 } else {
1472 let (Some(surface), Some(pcurve)) = (
1473 model.geometry().surface(*surface),
1474 model.geometry().pcurve(*id),
1475 ) else {
1476 ogeom_bail!(Dangling, "an edge names geometry not in this model");
1477 };
1478 passing(fractions, &points, |s| {
1479 let uv = pcurve
1480 .point_at(range.0 + (range.1 - range.0) * s, tol)
1481 .ok()?;
1482 surface.point_at(uv.x, uv.y, tol).ok()
1483 })?
1484 }
1485 }
1486 _ => fractions.to_vec(),
1487 };
1488 own.push(cut);
1489 }
1490 let curve_cut = data
1492 .representations
1493 .iter()
1494 .position(|r| matches!(r, EdgeRepr::Curve3d { .. }))
1495 .map_or_else(|| fractions.to_vec(), |k| own[k].clone());
1496 let mut vertices = Vec::with_capacity(fractions.len());
1497 for &s in &curve_cut {
1498 let vertex = match &curve {
1499 Some((c, range)) => {
1500 let p = c.point_at(range.0 + (range.1 - range.0) * s, tol)?;
1501 model.add_vertex(VertexData::with_tolerance(p, data.tolerance.get())?)
1502 }
1503 _ => start.clone(),
1505 };
1506 vertices.push(vertex);
1507 }
1508 let mut ends = vec![start];
1509 ends.extend(vertices.iter().cloned());
1510 ends.push(end);
1511 let mut pieces = Vec::with_capacity(fractions.len() + 1);
1512 for k in 0..=fractions.len() {
1513 let mut piece = data.clone();
1514 for (repr, cut) in piece.representations.iter_mut().zip(&own) {
1515 let (a, b) = (
1516 if k == 0 { 0.0 } else { cut[k - 1] },
1517 cut.get(k).copied().unwrap_or(1.0),
1518 );
1519 match repr {
1520 EdgeRepr::Curve3d { range, .. }
1521 | EdgeRepr::PCurve { range, .. }
1522 | EdgeRepr::Seam { range, .. } => {
1523 *range = (
1524 range.0 + (range.1 - range.0) * a,
1525 range.0 + (range.1 - range.0) * b,
1526 );
1527 }
1528 _ => {}
1529 }
1530 }
1531 piece.representations.retain(|r| {
1532 matches!(
1533 r,
1534 EdgeRepr::Curve3d { .. } | EdgeRepr::PCurve { .. } | EdgeRepr::Seam { .. }
1535 )
1536 });
1537 pieces.push(model.add_edge(piece, &[ends[k].clone(), ends[k + 1].clone()])?);
1538 }
1539 Ok((pieces, vertices))
1540}
1541
1542fn passing(
1547 fractions: &[f64],
1548 points: &[Option<Point>],
1549 at: impl Fn(f64) -> Option<Point>,
1550) -> OgeomResult<Vec<f64>> {
1551 const SAMPLES: u32 = 64;
1552 let mut out = Vec::with_capacity(fractions.len());
1553 for (&s, p) in fractions.iter().zip(points) {
1554 let Some(p) = p else {
1555 out.push(s);
1556 continue;
1557 };
1558 let off = |t: f64| at(t).map_or(f64::INFINITY, |q| q.distance(*p));
1559 let nearest = (0..=SAMPLES)
1560 .map(|i| (i, off(f64::from(i) / f64::from(SAMPLES))))
1561 .min_by(|a, b| a.1.total_cmp(&b.1));
1562 let Some((i, _)) = nearest.filter(|(_, d)| d.is_finite()) else {
1563 out.push(s);
1564 continue;
1565 };
1566 let (lo, hi) = (
1567 f64::from(i.saturating_sub(1)) / f64::from(SAMPLES),
1568 f64::from((i + 1).min(SAMPLES)) / f64::from(SAMPLES),
1569 );
1570 out.push(ogeom_math::solve::minimize(off, lo, hi, ogeom_math::Criteria::default())?.value);
1571 }
1572 Ok(out)
1573}
1574
1575fn iso_curve(
1578 surface: &SurfaceGeometry,
1579 line: IsoLine,
1580 probes: &[f64],
1581 span: (f64, f64),
1582 tol: Tolerances,
1583) -> OgeomResult<Option<(Curve, f64, f64)>> {
1584 let c = line.at();
1585 let circle = |centre: Point, z: Direction, x: Point, radius: f64| -> OgeomResult<Curve> {
1586 let x = Direction::new(x - centre, tol)?;
1587 let frame = Frame::new(centre, z, x, tol)?;
1588 Ok(CircleCurve::new(Circle::new(frame, radius, tol)?).into())
1589 };
1590 let straight = |at: Point, along: ogeom_math::Vector| -> OgeomResult<(Curve, f64)> {
1591 let length = along.magnitude();
1592 let direction = Direction::new(along, tol)?;
1593 Ok((
1594 LineCurve::new(Axis {
1595 location: at,
1596 direction,
1597 })
1598 .into(),
1599 length,
1600 ))
1601 };
1602 let found: (Curve, f64, f64) = match (surface, line) {
1603 (SurfaceGeometry::BSpline(b), IsoLine::U(_)) => {
1604 (Curve::BSpline(b.iso_u_curve(c, tol)?), 1.0, 0.0)
1605 }
1606 (SurfaceGeometry::BSpline(b), IsoLine::V(_)) => {
1607 (Curve::BSpline(b.iso_v_curve(c, tol)?), 1.0, 0.0)
1608 }
1609 (
1610 SurfaceGeometry::Plane(_) | SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_),
1611 IsoLine::U(_),
1612 )
1613 | (SurfaceGeometry::Plane(_) | SurfaceGeometry::Extrusion(_), _) => {
1614 let (lo, hi) = if span.0.is_finite() && span.1.is_finite() && span.1 > span.0 {
1619 span
1620 } else {
1621 (0.0, 1.0)
1622 };
1623 let at = surface.point_at(line.point(lo).x, line.point(lo).y, tol)?;
1624 let next = surface.point_at(line.point(hi).x, line.point(hi).y, tol)?;
1625 match (surface, line) {
1626 (SurfaceGeometry::Extrusion(e), IsoLine::V(_)) => {
1627 let shift = Transform::translation(e.direction().vector() * c);
1628 (e.curve().transformed(&shift, tol)?, 1.0, 0.0)
1629 }
1630 _ => {
1631 let (curve, length) = straight(at, next - at)?;
1632 let speed = length / (hi - lo);
1633 (curve, speed, -speed * lo)
1634 }
1635 }
1636 }
1637 (
1638 SurfaceGeometry::Cylinder(_)
1639 | SurfaceGeometry::Cone(_)
1640 | SurfaceGeometry::Sphere(_)
1641 | SurfaceGeometry::Torus(_)
1642 | SurfaceGeometry::Revolution(_),
1643 IsoLine::V(_),
1644 ) => {
1645 let axis = match surface {
1647 SurfaceGeometry::Cylinder(s) => frame_axis(s.cylinder().frame()),
1648 SurfaceGeometry::Cone(s) => frame_axis(s.cone().frame()),
1649 SurfaceGeometry::Sphere(s) => frame_axis(s.sphere().frame()),
1650 SurfaceGeometry::Torus(s) => frame_axis(s.torus().frame()),
1651 SurfaceGeometry::Revolution(s) => s.axis(),
1652 _ => return Ok(None),
1653 };
1654 let start = surface.point_at(0.0, c, tol)?;
1655 let centre = axis.project(start);
1656 let radius = start.distance(centre);
1657 if radius <= tol.confusion() {
1658 return Ok(None);
1659 }
1660 (circle(centre, axis.direction, start, radius)?, 1.0, 0.0)
1661 }
1662 (SurfaceGeometry::Sphere(_) | SurfaceGeometry::Torus(_), IsoLine::U(_)) => {
1663 let at0 = surface.point_at(c, 0.0, tol)?;
1665 let quarter = surface.point_at(c, core::f64::consts::FRAC_PI_2, tol)?;
1666 let opposite = surface.point_at(c, -core::f64::consts::FRAC_PI_2, tol)?;
1667 let centre = Point::from_vector((quarter.to_vector() + opposite.to_vector()) * 0.5);
1668 let radius = at0.distance(centre);
1669 let z = Direction::new((at0 - centre).cross(quarter - centre), tol)?;
1670 (circle(centre, z, at0, radius)?, 1.0, 0.0)
1671 }
1672 (SurfaceGeometry::Revolution(r), IsoLine::U(_)) => {
1673 let turn = Transform::rotation(r.axis(), c);
1674 (r.curve().transformed(&turn, tol)?, 1.0, 0.0)
1675 }
1676 (SurfaceGeometry::Trimmed(t), _) => return iso_curve(t.basis(), line, probes, span, tol),
1677 _ => {
1683 const N: usize = 128;
1684 let (lo, hi) = span;
1685 if hi <= lo || !hi.is_finite() || !lo.is_finite() {
1686 return Ok(None);
1687 }
1688 #[allow(clippy::cast_precision_loss)]
1689 let params: Vec<f64> = (0..=N)
1690 .map(|i| lo + (hi - lo) * i as f64 / N as f64)
1691 .collect();
1692 let fitted = ogeom_geom::fit::fit_curve_sampled(
1693 |w| {
1694 let on = line.point(w);
1695 surface.point_at(on.x, on.y, tol)
1696 },
1697 ¶ms,
1698 false,
1699 3,
1700 tol.confusion(),
1701 tol,
1702 )?;
1703 if !fitted.met {
1704 return Ok(None);
1705 }
1706 (Curve::BSpline(fitted.curve), 1.0, 0.0)
1707 }
1708 };
1709 let (curve, scale, offset) = &found;
1712 for &w in probes {
1713 let on = line.point(w);
1714 let expected = surface.point_at(on.x, on.y, tol)?;
1715 let got = curve.point_at(scale * w + offset, tol)?;
1716 if expected.distance(got) > tol.confusion() {
1717 return Ok(None);
1718 }
1719 }
1720 Ok(Some(found))
1721}
1722
1723fn frame_axis(frame: Frame) -> Axis {
1724 Axis {
1725 location: frame.origin(),
1726 direction: frame.z(),
1727 }
1728}
1729
1730pub(crate) fn forward(shape: &Shape) -> Shape {
1733 if shape.orientation() == Orientation::Reversed {
1734 shape.reversed()
1735 } else {
1736 shape.clone()
1737 }
1738}
1739
1740pub(crate) fn edge_data(model: &Model, edge: &Shape) -> OgeomResult<EdgeData> {
1741 match model.node(edge).map(ogeom_topo::TShape::data) {
1742 Some(NodeData::Edge(data)) => Ok((**data).clone()),
1743 Some(_) => ogeom_bail!(Construction, "expected an edge"),
1744 None => ogeom_bail!(Dangling, "edge is not in this model"),
1745 }
1746}
1747
1748pub(crate) fn face_data(model: &Model, face: &Shape) -> OgeomResult<ogeom_topo::FaceData> {
1749 match model.node(face).map(ogeom_topo::TShape::data) {
1750 Some(NodeData::Face(data)) => Ok((**data).clone()),
1751 Some(_) => ogeom_bail!(Construction, "expected a face"),
1752 None => ogeom_bail!(Dangling, "face is not in this model"),
1753 }
1754}
1755
1756pub(crate) fn vertex_point(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
1757 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
1758 ogeom_bail!(Construction, "a vertex holds no point");
1759 };
1760 Ok(data.point)
1761}
1762
1763pub(crate) fn signed_area(ring: &[Point2]) -> f64 {
1769 let n = ring.len();
1770 let Some(&anchor) = ring.first() else {
1771 return 0.0;
1772 };
1773 (0..n)
1774 .map(|i| (ring[i] - anchor).cross(ring[(i + 1) % n] - anchor))
1775 .sum::<f64>()
1776 * 0.5
1777}
1778
1779pub(crate) fn inside(rings: &[Vec<Point2>], p: Point2) -> bool {
1781 let mut odd = false;
1782 for ring in rings {
1783 let n = ring.len();
1784 for i in 0..n {
1785 let (a, b) = (ring[i], ring[(i + 1) % n]);
1786 if (a.y > p.y) != (b.y > p.y) {
1787 let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1788 if x > p.x {
1789 odd = !odd;
1790 }
1791 }
1792 }
1793 }
1794 odd
1795}
1796
1797#[cfg(test)]
1798#[allow(clippy::unwrap_used, clippy::expect_used)]
1799mod tests {
1800 use super::*;
1801 use ogeom_geom::{BSplineSurface, OffsetSurface};
1802 use ogeom_math::ControlGrid;
1803
1804 const T: Tolerances = Tolerances::millimetres();
1805
1806 #[test]
1809 fn a_ring_far_out_in_its_chart_encloses_its_own_area() {
1810 for o in [0.0, 1e6, 1e7, 2e7] {
1811 let (x, y) = (o, -0.7 * o);
1812 let ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 0.01), (0.0, 0.01)]
1813 .map(|(dx, dy)| Point2::new(x + dx, y + dy));
1814 let area = signed_area(&ring);
1815 assert!((area - 0.01).abs() < 1e-9, "o {o:e}: area {area}");
1816 }
1817 }
1818
1819 #[test]
1826 fn an_iso_curve_without_a_closed_form_keeps_to_the_surface_between_its_samples() {
1827 const ROWS: u32 = 401;
1828 let mut control = Vec::new();
1829 for i in 0..ROWS {
1830 let x = f64::from(i) / f64::from(ROWS - 1);
1831 let ridge = 0.05 * (-((x - 0.503) / 0.006).powi(2)).exp();
1832 control.extend([Point::new(x, 0.0, ridge), Point::new(x, 1.0, ridge)]);
1833 }
1834 let mut u_knots = vec![0.0; 4];
1835 u_knots.extend((1..ROWS - 3).map(|k| f64::from(k) / f64::from(ROWS - 3)));
1836 u_knots.extend([1.0; 4]);
1837 let patch = BSplineSurface::new(
1838 KnotVector::new(u_knots, 3).unwrap(),
1839 KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1).unwrap(),
1840 &ControlGrid::new(control, ROWS as usize, 2).unwrap(),
1841 T,
1842 )
1843 .unwrap();
1844 let surface =
1845 SurfaceGeometry::Offset(Box::new(OffsetSurface::new(patch.into(), 5e-4).unwrap()));
1846 let line = IsoLine::V(0.5);
1847 let (curve, scale, offset) = iso_curve(&surface, line, &[0.5], (0.0, 1.0), T)
1848 .unwrap()
1849 .expect("the ridge is followed within the confusion distance");
1850 let mut worst = 0.0_f64;
1851 for k in 0..=20_000 {
1852 let w = f64::from(k) / 20_000.0;
1853 let on = line.point(w);
1854 let expected = surface.point_at(on.x, on.y, T).unwrap();
1855 let got = curve.point_at(scale * w + offset, T).unwrap();
1856 worst = worst.max(expected.distance(got));
1857 }
1858 assert!(
1859 worst <= 2.0 * T.confusion(),
1860 "the iso-curve leaves the surface by {worst:.3e}"
1861 );
1862 }
1863}