1use ogeom_core::{FastMap, FastSet};
6
7use ogeom_algo::{Built, History};
8use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
9use ogeom_geom::{Curve2d as _, Curve3d as _, PlanarCurve, SurfaceGeometry};
10use ogeom_math::{Direction2, Plane, Point, Point2, Transform};
11use ogeom_topo::{
12 EdgeRepr, Location, Model, NodeData, Orientation, PCurveId, Shape, ShapeType, SurfaceId,
13 TShapeId, explore_unique,
14};
15
16use crate::reshape::Reshape;
17
18mod curved;
19
20pub fn unify_same_domain(
44 model: &mut Model,
45 shape: &Shape,
46 tol: Tolerances,
47) -> OgeomResult<(Built, usize)> {
48 unify(model, shape, None, tol)
49}
50
51pub fn unify_same_domain_around(
64 model: &mut Model,
65 shape: &Shape,
66 around: &[Shape],
67 tol: Tolerances,
68) -> OgeomResult<(Built, usize)> {
69 unify(model, shape, Some(around), tol)
70}
71
72pub fn same_domain(model: &Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<bool> {
81 Ok(
82 match (carrier_of(model, a, tol)?, carrier_of(model, b, tol)?) {
83 (Some(a), Some(b)) => same_carrier(&a, &b, tol),
84 _ => false,
85 },
86 )
87}
88
89fn unify(
93 model: &mut Model,
94 shape: &Shape,
95 around: Option<&[Shape]>,
96 tol: Tolerances,
97) -> OgeomResult<(Built, usize)> {
98 ogeom_algo::on_own_nodes_with(
99 model,
100 shape,
101 around.unwrap_or_default(),
102 |model, shape, parts| unify_own(model, shape, around.map(|_| parts), tol),
103 |(built, _)| &mut built.history,
104 )
105}
106
107fn unify_own(
108 model: &mut Model,
109 shape: &Shape,
110 around: Option<&[Shape]>,
111 tol: Tolerances,
112) -> OgeomResult<(Built, usize)> {
113 let faces = explore_unique(model, shape, ShapeType::Face)?;
114
115 let in_play: Vec<bool> = match around {
119 None => vec![true; faces.len()],
120 Some(seeds) => {
121 let mut edge_users: FastMap<TShapeId, Vec<usize>> = FastMap::default();
122 for (i, face) in faces.iter().enumerate() {
123 for edge in explore_unique(model, face, ShapeType::Edge)? {
124 edge_users.entry(edge.node()).or_default().push(i);
125 }
126 }
127 let mut reached = vec![false; faces.len()];
128 let mut carriers: FastMap<usize, Option<Carrier>> = FastMap::default();
129 let mut queue: Vec<usize> = Vec::new();
130 for (i, face) in faces.iter().enumerate() {
131 if seeds.iter().any(|s| s.node() == face.node()) {
132 reached[i] = true;
133 queue.push(i);
134 }
135 }
136 while let Some(i) = queue.pop() {
137 if let ogeom_core::collections::hash_map::Entry::Vacant(slot) = carriers.entry(i) {
138 slot.insert(carrier_of(model, &faces[i], tol)?);
139 }
140 for edge in explore_unique(model, &faces[i], ShapeType::Edge)? {
141 for &j in edge_users.get(&edge.node()).map_or(&[][..], Vec::as_slice) {
142 if reached[j] {
143 continue;
144 }
145 if let ogeom_core::collections::hash_map::Entry::Vacant(slot) =
146 carriers.entry(j)
147 {
148 slot.insert(carrier_of(model, &faces[j], tol)?);
149 }
150 if let (Some(Some(ci)), Some(Some(cj))) =
151 (carriers.get(&i), carriers.get(&j))
152 && same_carrier(ci, cj, tol)
153 {
154 reached[j] = true;
155 queue.push(j);
156 }
157 }
158 }
159 }
160 reached
161 }
162 };
163
164 let mut carriers: Vec<Option<Carrier>> = Vec::with_capacity(faces.len());
168 for (face, &playing) in faces.iter().zip(&in_play) {
169 carriers.push(if playing {
170 carrier_of(model, face, tol)?
171 } else {
172 None
173 });
174 }
175
176 let mut group: Vec<usize> = (0..faces.len()).collect();
178 fn root(group: &mut [usize], mut i: usize) -> usize {
179 while group[i] != i {
180 group[i] = group[group[i]];
181 i = group[i];
182 }
183 i
184 }
185 let mut edge_users: FastMap<TShapeId, Vec<usize>> = FastMap::default();
186 for (i, face) in faces.iter().enumerate() {
187 for edge in explore_unique(model, face, ShapeType::Edge)? {
188 edge_users.entry(edge.node()).or_default().push(i);
189 }
190 }
191 for users in edge_users.values() {
192 for pair in users.windows(2) {
193 let (a, b) = (pair[0], pair[1]);
194 if let (Some(ca), Some(cb)) = (carriers[a].as_ref(), carriers[b].as_ref())
197 && same_carrier(ca, cb, tol)
198 {
199 let (ra, rb) = (root(&mut group, a), root(&mut group, b));
200 group[ra] = rb;
201 }
202 }
203 }
204
205 let mut cluster_of_root: FastMap<usize, usize> = FastMap::default();
208 let mut clusters: Vec<Vec<usize>> = Vec::new();
209 for i in 0..faces.len() {
210 let r = root(&mut group, i);
211 let c = *cluster_of_root.entry(r).or_insert_with(|| {
212 clusters.push(Vec::new());
213 clusters.len() - 1
214 });
215 clusters[c].push(i);
216 }
217
218 let mut reshape = Reshape::new();
219 for members in &clusters {
220 if members.len() < 2 {
221 continue;
222 }
223 let mut tries = vec![members.clone()];
228 let mut k = 0;
229 while k < tries.len() {
230 let attempt = &tries[k];
231 k += 1;
232 match merge_cluster(model, &faces, attempt, &carriers, &edge_users, tol) {
233 Ok(Some(merged)) => {
234 reshape.replace(&faces[attempt[0]], merged);
235 for &other in &attempt[1..] {
236 reshape.remove(&faces[other]);
237 }
238 }
239 Ok(None) | Err(_) if k == 1 => {
242 let plain = plain_clusters(model, &faces, members, &carriers, tol)?;
243 if plain.as_slice() != [members.clone()] {
244 tries.extend(plain);
245 }
246 }
247 _ => {}
248 }
249 }
250 }
251 if reshape.is_empty() {
252 let mut built = Built::from_nothing(shape.clone());
253 built.history.modify(shape, shape.clone());
254 return Ok((built, 0));
255 }
256 let unified = reshape.len();
257 Ok((reshape.apply(model, shape)?, unified))
258}
259
260fn plain_clusters(
264 model: &Model,
265 faces: &[Shape],
266 members: &[usize],
267 carriers: &[Option<Carrier>],
268 tol: Tolerances,
269) -> OgeomResult<Vec<Vec<usize>>> {
270 let mut group: Vec<usize> = (0..members.len()).collect();
271 fn root(group: &mut [usize], mut i: usize) -> usize {
272 while group[i] != i {
273 group[i] = group[group[i]];
274 i = group[i];
275 }
276 i
277 }
278 let mut edge_users: FastMap<TShapeId, Vec<usize>> = FastMap::default();
279 for (k, &i) in members.iter().enumerate() {
280 for edge in explore_unique(model, &faces[i], ShapeType::Edge)? {
281 edge_users.entry(edge.node()).or_default().push(k);
282 }
283 }
284 let mut pairs: Vec<(usize, usize)> = edge_users
285 .values()
286 .flat_map(|users| users.windows(2).map(|w| (w[0], w[1])))
287 .collect();
288 pairs.sort_unstable();
289 for (a, b) in pairs {
290 if let (Some(ca), Some(cb)) = (&carriers[members[a]], &carriers[members[b]])
291 && same_plain_carrier(ca, cb, tol)
292 {
293 let (ra, rb) = (root(&mut group, a), root(&mut group, b));
294 group[ra] = rb;
295 }
296 }
297 let mut cluster_of_root: FastMap<usize, usize> = FastMap::default();
298 let mut clusters: Vec<Vec<usize>> = Vec::new();
299 for (k, &i) in members.iter().enumerate() {
300 let r = root(&mut group, k);
301 let c = *cluster_of_root.entry(r).or_insert_with(|| {
302 clusters.push(Vec::new());
303 clusters.len() - 1
304 });
305 clusters[c].push(i);
306 }
307 clusters.retain(|c| c.len() > 1);
308 Ok(clusters)
309}
310
311fn merge_cluster(
323 model: &mut Model,
324 faces: &[Shape],
325 members: &[usize],
326 carriers: &[Option<Carrier>],
327 edge_users: &FastMap<TShapeId, Vec<usize>>,
328 tol: Tolerances,
329) -> OgeomResult<Option<Shape>> {
330 let keeper = members[0];
331 let Some(carrier) = carriers[keeper].clone() else {
332 return Ok(None);
333 };
334 let Some(NodeData::Face(keeper_data)) = model.node(&faces[keeper]).map(|n| n.data().clone())
335 else {
336 return Ok(None);
337 };
338
339 let mut walks: Vec<(usize, Shape)> = Vec::new();
341 let mut runs: Vec<core::ops::Range<usize>> = Vec::new();
342 let mut uses: FastMap<TShapeId, Vec<usize>> = FastMap::default();
343 for &i in members {
344 let wires = model.children_of(&faces[i])?;
345 if wires.is_empty() {
346 return Ok(None);
347 }
348 for wire in wires {
349 let start = walks.len();
350 for edge in model.ordered_children_of(&wire)? {
351 uses.entry(edge.node()).or_default().push(walks.len());
352 walks.push((i, edge));
353 }
354 runs.push(start..walks.len());
355 }
356 }
357 let mut kept = vec![false; walks.len()];
358 for (node, at) in &uses {
359 let across = match at.as_slice() {
360 [_] => false,
361 [a, b] if walks[*a].0 == walks[*b].0 => false,
362 [a, b] => {
363 if walks[*a].1.orientation() == walks[*b].1.orientation()
364 || edge_users.get(node).map_or(0, Vec::len) != 2
365 {
366 return Ok(None);
367 }
368 true
369 }
370 _ => return Ok(None),
371 };
372 if !across {
373 for &k in at {
374 kept[k] = true;
375 }
376 }
377 }
378 let reversed = faces[keeper].orientation() == Orientation::Reversed;
379 let (rings, edits) = match &carrier {
380 Carrier::Flat(flat) => {
381 let Some(rings) =
382 flat_rings(model, faces, members, carriers, &walks, &kept, flat, tol)?
383 else {
384 return Ok(None);
385 };
386 (rings, None)
387 }
388 Carrier::Curved(_) => {
389 let mut on: Vec<&curved::Curved> = Vec::with_capacity(members.len());
390 for &i in members {
391 let Some(Carrier::Curved(c)) = &carriers[i] else {
392 return Ok(None);
393 };
394 on.push(c);
395 }
396 let cluster = curved::Members {
397 members,
398 carriers: &on,
399 wires: &runs,
400 };
401 let Some(merged) = curved::merge(model, &cluster, &walks, &kept, reversed, tol)? else {
402 return Ok(None);
403 };
404 (merged.rings, Some(merged.edits))
405 }
406 };
407
408 let mut wires = Vec::with_capacity(rings.len());
411 for ring in &rings {
412 let ring = ring.iter().map(|&w| walks[w].1.clone());
413 let ring: Vec<Shape> = if reversed {
414 ring.rev().map(|e| e.reversed()).collect()
415 } else {
416 ring.collect()
417 };
418 wires.push(ogeom_algo::make_wire(model, &ring, tol)?.shape);
419 }
420 let mut data = *keeper_data;
422 data.triangulation = None;
423 let face = model.add_face(data, &wires)?;
424 if let Some(edits) = edits {
427 edits.apply(model)?;
428 }
429 Ok(Some(face))
430}
431
432#[allow(clippy::too_many_arguments, reason = "the cluster's parts, read once")]
439fn flat_rings(
440 model: &mut Model,
441 faces: &[Shape],
442 members: &[usize],
443 carriers: &[Option<Carrier>],
444 walks: &[(usize, Shape)],
445 kept: &[bool],
446 flat: &Flat,
447 tol: Tolerances,
448) -> OgeomResult<Option<Vec<Vec<usize>>>> {
449 let Some(NodeData::Face(keeper_data)) =
450 model.node(&faces[members[0]]).map(|n| n.data().clone())
451 else {
452 return Ok(None);
453 };
454 let surface_id = keeper_data.surface;
455 let boundary: Vec<usize> = (0..walks.len()).filter(|&w| kept[w]).collect();
456 if boundary.is_empty() {
457 return Ok(None);
458 }
459 let mut seen: FastSet<TShapeId> = FastSet::default();
460 let outer_edges: Vec<Shape> = boundary
461 .iter()
462 .map(|&w| &walks[w].1)
463 .filter(|e| seen.insert(e.node()))
464 .cloned()
465 .collect();
466
467 let mut ends: Vec<(Shape, Shape)> = Vec::with_capacity(boundary.len());
469 for &w in &boundary {
470 let Some(pair) = ogeom_algo::edge_vertices(model, &walks[w].1)? else {
471 return Ok(None);
472 };
473 ends.push(pair);
474 }
475 let mut leaving: FastMap<TShapeId, usize> = FastMap::default();
476 for (k, (start, _)) in ends.iter().enumerate() {
477 if leaving.insert(start.node(), k).is_some() {
478 return Ok(None);
479 }
480 }
481 let mut placed = vec![false; boundary.len()];
482 let mut rings: Vec<Vec<usize>> = Vec::new();
483 for first in 0..boundary.len() {
484 if placed[first] {
485 continue;
486 }
487 let mut ring = Vec::new();
488 let mut k = first;
489 loop {
490 placed[k] = true;
491 ring.push(boundary[k]);
492 let Some(&next) = leaving.get(&ends[k].1.node()) else {
493 return Ok(None);
494 };
495 if next == first {
496 break;
497 }
498 if placed[next] {
499 return Ok(None);
500 }
501 k = next;
502 }
503 rings.push(ring);
504 }
505
506 let mut turns = Vec::with_capacity(rings.len());
508 for ring in &rings {
509 let edges: Vec<Shape> = ring.iter().map(|&w| walks[w].1.clone()).collect();
510 turns.push(ogeom_algo::edges_turn(model, &edges, flat.outward, tol)?);
511 }
512 let area = tol.confusion() * tol.confusion();
513 let mut outer = turns.iter().enumerate().filter(|(_, t)| **t > area);
514 let (Some((outer, _)), None) = (outer.next(), outer.next()) else {
515 return Ok(None);
516 };
517 if turns.iter().filter(|t| **t < -area).count() != rings.len() - 1 {
518 return Ok(None);
519 }
520 rings.swap(0, outer);
521
522 let mut charts: Vec<(SurfaceId, Flat)> = Vec::with_capacity(members.len());
526 for &i in &members[1..] {
527 if let (Some(NodeData::Face(data)), Some(Carrier::Flat(c))) =
528 (model.node(&faces[i]).map(|n| n.data()), &carriers[i])
529 {
530 charts.push((data.surface, c.clone()));
531 }
532 }
533 for edge in &outer_edges {
534 ensure_planar_pcurve(model, edge, surface_id, flat, &charts, tol)?;
535 }
536 widen_to_members(model, surface_id, flat, &charts, tol)?;
537 Ok(Some(rings))
538}
539
540fn widen_to_members(
545 model: &mut Model,
546 surface_id: SurfaceId,
547 carrier: &Flat,
548 charts: &[(SurfaceId, Flat)],
549 tol: Tolerances,
550) -> OgeomResult<()> {
551 use ogeom_geom::Surface as _;
552 let Some(SurfaceGeometry::Plane(kept)) = model.geometry().surface(surface_id).cloned() else {
553 return Ok(());
554 };
555 let ((mut ua, mut ub), (mut va, mut vb)) = kept.domain();
556 let was = ((ua, ub), (va, vb));
557 for (id, chart) in charts {
558 let Some(SurfaceGeometry::Plane(other)) = model.geometry().surface(*id) else {
559 continue;
560 };
561 let Some(map) = chart_to_chart(chart, carrier, tol)? else {
562 ogeom_bail!(
563 Construction,
564 "two faces on one plane do not share its chart"
565 );
566 };
567 let ((a, b), (c, d)) = other.domain();
568 for (u, v) in [(a, c), (a, d), (b, c), (b, d)] {
569 let at = map.apply(Point2::new(u, v));
570 (ua, ub) = (ua.min(at.x), ub.max(at.x));
571 (va, vb) = (va.min(at.y), vb.max(at.y));
572 }
573 }
574 if ((ua, ub), (va, vb)) != was
575 && let Some(held) = model.geometry_mut().surface_mut(surface_id)
576 {
577 *held = ogeom_geom::PlaneSurface::over(kept.plane(), (ua, ub), (va, vb))?.into();
578 }
579 Ok(())
580}
581
582#[derive(Debug, Clone)]
585enum Carrier {
586 Flat(Flat),
587 Curved(curved::Curved),
588}
589
590#[derive(Debug, Clone)]
592struct Flat {
593 world: Plane,
595 outward: ogeom_math::Vector,
600 stored: Plane,
602 placement: Transform,
604 centre: Point,
609 reach: f64,
610}
611
612fn same_carrier(a: &Carrier, b: &Carrier, tol: Tolerances) -> bool {
615 match (a, b) {
616 (Carrier::Flat(a), Carrier::Flat(b)) => {
617 let tilt = a
618 .world
619 .normal()
620 .vector()
621 .cross(b.world.normal().vector())
622 .magnitude();
623 a.outward.dot(b.outward) > 0.0
624 && a.world.signed_distance_to(b.centre).abs() + tilt * b.reach <= tol.confusion()
625 && b.world.signed_distance_to(a.centre).abs() + tilt * a.reach <= tol.confusion()
626 }
627 (Carrier::Curved(a), Carrier::Curved(b)) => curved::same_carrier(a, b, tol),
628 _ => false,
629 }
630}
631
632fn same_plain_carrier(a: &Carrier, b: &Carrier, tol: Tolerances) -> bool {
635 match (a, b) {
636 (Carrier::Curved(a), Carrier::Curved(b)) => curved::same_plain_carrier(a, b, tol),
637 _ => same_carrier(a, b, tol),
638 }
639}
640
641fn carrier_of(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<Option<Carrier>> {
643 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data().clone()) else {
644 return Ok(None);
645 };
646 let Some(SurfaceGeometry::Plane(p)) = model.geometry().surface(data.surface) else {
647 return Ok(curved::carrier_of(model, face, tol)?.map(Carrier::Curved));
648 };
649 let stored = p.plane();
650 let placement = face.transform(model.datums())?;
651 if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 {
652 return Ok(None);
653 }
654 let world = stored.transformed(&placement, tol)?;
655 let normal = world.normal().vector();
656 let bounds = ogeom_algo::face_bounds(model, face)?;
657 let (Some(low), Some(high)) = (bounds.low(), bounds.high()) else {
658 return Ok(None);
659 };
660 let mid = Point::new(
661 f64::midpoint(low.x, high.x),
662 f64::midpoint(low.y, high.y),
663 f64::midpoint(low.z, high.z),
664 );
665 let centre = mid - normal * world.signed_distance_to(mid);
666 let reach = [low.x, high.x]
667 .into_iter()
668 .flat_map(|x| [low.y, high.y].map(|y| (x, y)))
669 .flat_map(|(x, y)| [low.z, high.z].map(|z| Point::new(x, y, z)))
670 .map(|corner| corner.distance(centre))
671 .fold(0.0_f64, f64::max);
672 Ok(Some(Carrier::Flat(Flat {
673 outward: if face.orientation() == Orientation::Reversed {
674 -normal
675 } else {
676 normal
677 },
678 world,
679 stored,
680 placement,
681 centre,
682 reach,
683 })))
684}
685
686fn ensure_planar_pcurve(
698 model: &mut Model,
699 edge: &Shape,
700 surface_id: SurfaceId,
701 carrier: &Flat,
702 charts: &[(SurfaceId, Flat)],
703 tol: Tolerances,
704) -> OgeomResult<()> {
705 let (curve, range, has, sibling) = {
706 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
707 ogeom_bail!(Construction, "edge holds no edge data");
708 };
709 let has = data.pcurve_for(surface_id, edge.location()).is_some();
710 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
711 ogeom_bail!(Construction, "an edge with no curve cannot be unified over");
712 };
713 let sibling = charts.iter().find_map(|(surface, chart)| {
714 match data.pcurve_for(*surface, edge.location()) {
715 Some(EdgeRepr::PCurve { curve, range, .. }) => Some((*curve, *range, chart)),
716 _ => None,
717 }
718 });
719 (*curve, *range, has, sibling)
720 };
721 if has {
722 return Ok(());
723 }
724 if let Some((pcurve, prange, chart)) = sibling
725 && let Some(moved) = model.geometry().pcurve(pcurve).cloned()
726 && let Some(map) = chart_to_chart(chart, carrier, tol)?
727 {
728 return ogeom_algo::attach_pcurve(
729 model,
730 edge,
731 moved.transformed(&map, tol)?,
732 surface_id,
733 edge.location().clone(),
734 prange,
735 );
736 }
737 let Some(geometry) = model.geometry().curve(curve).cloned() else {
738 ogeom_bail!(Construction, "edge refers to a curve not in this model");
739 };
740 let back = carrier.placement.inverse()?;
741 let pcurve = if matches!(geometry, ogeom_geom::Curve::Line(_)) {
742 let flat = |p: ogeom_math::Point| {
743 let local = carrier.stored.frame().to_local(back.apply(p));
744 Point2::new(local.x, local.y)
745 };
746 let a = flat(geometry.point_at(range.0, tol)?);
747 let b = flat(geometry.point_at(range.1, tol)?);
748 chart_line(a, b, range, tol)?
749 } else {
750 let local = ogeom_geom::Transformable::transformed(&geometry, &back, tol)?;
751 let plane = SurfaceGeometry::Plane(ogeom_geom::PlaneSurface::new(carrier.stored));
752 ogeom_algo::pcurve_fit::fit_projected_pcurve(&local, range, &plane, tol)?.0
753 };
754 ogeom_algo::attach_pcurve(
755 model,
756 edge,
757 pcurve,
758 surface_id,
759 edge.location().clone(),
760 range,
761 )
762}
763
764fn chart_to_chart(
768 from: &Flat,
769 to: &Flat,
770 tol: Tolerances,
771) -> OgeomResult<Option<ogeom_math::Transform2>> {
772 use ogeom_math::{Point, Transform2, Vector2};
773 let into = to.placement.inverse()?;
774 let lift = |u: f64, v: f64| {
775 let world = from
776 .placement
777 .apply(from.stored.frame().to_world(Point::new(u, v, 0.0)));
778 to.stored.frame().to_local(into.apply(world))
779 };
780 let (o, x, y) = (lift(0.0, 0.0), lift(1.0, 0.0), lift(0.0, 1.0));
781 if [o, x, y].iter().any(|p| p.z.abs() > tol.confusion()) {
782 return Ok(None);
783 }
784 let ex = Vector2::new(x.x - o.x, x.y - o.y);
785 let ey = Vector2::new(y.x - o.x, y.y - o.y);
786 let turn = Transform2::rotation(Point2::ORIGIN, ex.y.atan2(ex.x));
787 let shift = Transform2::translation(Vector2::new(o.x, o.y));
788 let map = if ex.x.mul_add(ey.y, -(ex.y * ey.x)) > 0.0 {
789 shift * turn
790 } else {
791 let flip = Transform2::line_mirror(
792 Point2::ORIGIN,
793 Direction2::new(Vector2::new(0.0, 1.0), tol)?,
794 );
795 shift * turn * flip
796 };
797 let lands = map.apply(Point2::new(0.0, 1.0));
800 if lands.distance(Point2::new(y.x, y.y)) > tol.confusion() {
801 return Ok(None);
802 }
803 Ok(Some(map))
804}
805
806fn chart_line(
812 a: Point2,
813 b: Point2,
814 range: (f64, f64),
815 tol: Tolerances,
816) -> OgeomResult<PlanarCurve> {
817 let span = range.1 - range.0;
818 let direction = Direction2::new((b - a) / span, tol)?;
819 let origin = a - direction.vector() * range.0;
820 Ok(
821 ogeom_geom::Line2d::over(ogeom_math::Axis2::new(origin, direction), range.0, range.1)?
822 .into(),
823 )
824}
825
826pub fn merge_edges(
840 model: &mut Model,
841 shape: &Shape,
842 tol: Tolerances,
843) -> OgeomResult<(Built, usize)> {
844 let mut current = shape.clone();
847 let mut steps: Vec<History> = Vec::new();
848 let mut merged = 0;
849 for _ in 0..8 {
850 let Some((step, joined)) = merge_pass(model, ¤t, tol)? else {
851 break;
852 };
853 current = step.shape;
854 steps.push(step.history);
855 merged += joined;
856 }
857 if steps.is_empty() {
858 let mut built = Built::from_nothing(shape.clone());
859 built.history.modify(shape, shape.clone());
860 return Ok((built, 0));
861 }
862 Ok((Built::new(current, History::chain(&steps)), merged))
863}
864
865fn merge_pass(
868 model: &mut Model,
869 shape: &Shape,
870 tol: Tolerances,
871) -> OgeomResult<Option<(Built, usize)>> {
872 let mut needed: FastMap<TShapeId, Vec<(SurfaceId, Location)>> = FastMap::default();
878 for face in explore_unique(model, shape, ShapeType::Face)? {
879 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data().clone()) else {
880 continue;
881 };
882 for edge in explore_unique(model, &face, ShapeType::Edge)? {
883 let slot = (data.surface, edge.location().clone());
884 let slots = needed.entry(edge.node()).or_default();
885 if !slots.contains(&slot) {
886 slots.push(slot);
887 }
888 }
889 }
890
891 let wires = explore_unique(model, shape, ShapeType::Wire)?;
892 let mut reshape = Reshape::new();
893 let mut merged_nodes: FastSet<TShapeId> = FastSet::default();
894 for wire in &wires {
895 let edges = model.ordered_children_of(wire)?;
896 if edges.len() < 2 {
897 continue;
898 }
899 let closed = is_closed_wire(model, &edges, tol);
902 let pairs = if closed { edges.len() } else { edges.len() - 1 };
903 let mut i = 0;
904 while i < pairs {
905 let (a, b) = (edges[i].clone(), edges[(i + 1) % edges.len()].clone());
906 if a.node() == b.node()
907 || merged_nodes.contains(&a.node())
908 || merged_nodes.contains(&b.node())
909 {
910 i += 1;
911 continue;
912 }
913 let mut slots: Vec<(SurfaceId, Location)> =
914 needed.get(&a.node()).cloned().unwrap_or_default();
915 for slot in needed.get(&b.node()).into_iter().flatten() {
916 if !slots.contains(slot) {
917 slots.push(slot.clone());
918 }
919 }
920 let Some(join) = joinable(model, &a, &b, &slots, tol)? else {
921 i += 1;
922 continue;
923 };
924 let Some((from, to)) = join.ends.clone() else {
928 i += 1;
929 continue;
930 };
931 let Ok(joined) =
932 ogeom_algo::make_edge_between(model, join.curve, join.range, &from, &to, tol)
933 else {
934 i += 1;
935 continue;
936 };
937 let joined = joined.shape;
938 for pcurve in join.pcurves {
939 ogeom_algo::attach_pcurve(
940 model,
941 &joined,
942 pcurve.curve,
943 pcurve.surface,
944 pcurve.location,
945 pcurve.range,
946 )?;
947 }
948 merged_nodes.insert(a.node());
949 merged_nodes.insert(b.node());
950 reshape.replace(&a, joined);
951 reshape.remove(&b);
952 i += 2;
953 }
954 }
955 if reshape.is_empty() {
956 return Ok(None);
957 }
958 let joined = reshape.len();
959 Ok(Some((reshape.apply(model, shape)?, joined)))
960}
961
962fn is_closed_wire(model: &Model, edges: &[Shape], tol: Tolerances) -> bool {
964 let ends = |e: &Shape| -> Option<(ogeom_math::Point, ogeom_math::Point)> {
965 let vertices = explore_unique(model, e, ShapeType::Vertex).ok()?;
966 let first = model.node(vertices.first()?)?.data().as_vertex()?.point;
967 let last = model.node(vertices.last()?)?.data().as_vertex()?.point;
968 Some((first, last))
969 };
970 let (Some(first), Some(last)) = (ends(&edges[0]), ends(&edges[edges.len() - 1])) else {
971 return false;
972 };
973 [first.0, first.1]
974 .iter()
975 .any(|p| p.distance(last.0) <= tol.confusion() || p.distance(last.1) <= tol.confusion())
976}
977
978struct Join {
980 curve: ogeom_geom::Curve,
982 range: (f64, f64),
984 pcurves: Vec<PcurveJoin>,
986 ends: Option<(Shape, Shape)>,
989}
990
991struct PcurveJoin {
993 surface: SurfaceId,
994 location: Location,
995 curve: PlanarCurve,
996 range: (f64, f64),
997}
998
999type CurveJoin = (ogeom_geom::Curve, (f64, f64), bool);
1002
1003fn joinable_curve(
1006 model: &Model,
1007 a: &Shape,
1008 b: &Shape,
1009 tol: Tolerances,
1010) -> OgeomResult<Option<CurveJoin>> {
1011 let read = |e: &Shape| -> Option<(ogeom_topo::CurveId, (f64, f64))> {
1012 let data = model.node(e)?.data().as_edge()?;
1013 let EdgeRepr::Curve3d { curve, range, .. } = data.curve3d()? else {
1014 return None;
1015 };
1016 Some((*curve, *range))
1017 };
1018 let (Some((ca, ra)), Some((cb, rb))) = (read(a), read(b)) else {
1019 return Ok(None);
1020 };
1021 if ca == cb {
1022 let Some(geometry) = model.geometry().curve(ca).cloned() else {
1023 return Ok(None);
1024 };
1025 if (ra.1 - rb.0).abs() <= tol.parametric() {
1026 return Ok(Some((geometry, (ra.0, rb.1), true)));
1027 }
1028 if (rb.1 - ra.0).abs() <= tol.parametric() {
1029 return Ok(Some((geometry, (rb.0, ra.1), false)));
1030 }
1031 return Ok(None);
1032 }
1033 let (Some(ga), Some(gb)) = (
1035 model.geometry().curve(ca).cloned(),
1036 model.geometry().curve(cb).cloned(),
1037 ) else {
1038 return Ok(None);
1039 };
1040 let (ogeom_geom::Curve::Line(la), ogeom_geom::Curve::Line(lb)) = (&ga, &gb) else {
1041 return Ok(None);
1042 };
1043 if !la.axis().is_collinear(lb.axis(), tol) {
1044 return Ok(None);
1045 }
1046 let (a0, a1) = (ga.point_at(ra.0, tol)?, ga.point_at(ra.1, tol)?);
1047 let (b0, b1) = (gb.point_at(rb.0, tol)?, gb.point_at(rb.1, tol)?);
1048 let fresh = |from: ogeom_math::Point,
1049 to: ogeom_math::Point,
1050 a_first: bool|
1051 -> OgeomResult<Option<CurveJoin>> {
1052 let segment = ogeom_geom::LineCurve::segment(from, to, tol)?;
1053 let range = segment.domain();
1054 Ok(Some((segment.into(), range, a_first)))
1055 };
1056 if a1.distance(b0) <= tol.confusion() {
1057 return fresh(a0, b1, true);
1058 }
1059 if b1.distance(a0) <= tol.confusion() {
1060 return fresh(b0, a1, false);
1061 }
1062 Ok(None)
1063}
1064
1065fn joinable(
1071 model: &Model,
1072 a: &Shape,
1073 b: &Shape,
1074 slots: &[(SurfaceId, Location)],
1075 tol: Tolerances,
1076) -> OgeomResult<Option<Join>> {
1077 let Some((curve, range, a_first)) = joinable_curve(model, a, b, tol)? else {
1078 return Ok(None);
1079 };
1080 let (first, second) = if a_first { (a, b) } else { (b, a) };
1081 let reprs = |e: &Shape| -> Vec<EdgeRepr> {
1082 model
1083 .node(e)
1084 .and_then(|n| n.data().as_edge())
1085 .map(|d| d.representations.to_vec())
1086 .unwrap_or_default()
1087 };
1088 let (first_reprs, second_reprs) = (reprs(first), reprs(second));
1089 if first_reprs
1091 .iter()
1092 .chain(&second_reprs)
1093 .any(|r| matches!(r, EdgeRepr::Seam { .. }))
1094 {
1095 return Ok(None);
1096 }
1097 let parametric = |reprs: &[EdgeRepr]| -> Vec<(SurfaceId, Location, PCurveId, (f64, f64))> {
1098 reprs
1099 .iter()
1100 .filter_map(|r| match r {
1101 EdgeRepr::PCurve {
1102 curve,
1103 range,
1104 surface,
1105 location,
1106 } => Some((*surface, location.clone(), *curve, *range)),
1107 _ => None,
1108 })
1109 .collect()
1110 };
1111 let (ones, twos) = (parametric(&first_reprs), parametric(&second_reprs));
1112 let mut pcurves = Vec::with_capacity(slots.len());
1113 for (surface, location) in slots {
1114 let (surface, location) = (*surface, location.clone());
1115 let find = |from: &[(SurfaceId, Location, PCurveId, (f64, f64))]| {
1116 from.iter()
1117 .find(|(s, l, _, _)| *s == surface && *l == location)
1118 .cloned()
1119 };
1120 let (Some((_, _, id_one, range_one)), Some((_, _, id_two, range_two))) =
1121 (find(&ones), find(&twos))
1122 else {
1123 return Ok(None);
1124 };
1125 let (Some(one), Some(two)) = (
1126 model.geometry().pcurve(id_one).cloned(),
1127 model.geometry().pcurve(id_two).cloned(),
1128 ) else {
1129 return Ok(None);
1130 };
1131 let joined = if id_one == id_two && (range_one.1 - range_two.0).abs() <= tol.parametric() {
1132 (one, (range_one.0, range_two.1))
1133 } else {
1134 let (PlanarCurve::Line(_), PlanarCurve::Line(_)) = (&one, &two) else {
1137 return Ok(None);
1138 };
1139 let start = one.point_at(range_one.0, tol)?;
1140 let end = two.point_at(range_two.1, tol)?;
1141 if one
1142 .point_at(range_one.1, tol)?
1143 .distance(two.point_at(range_two.0, tol)?)
1144 > tol.confusion()
1145 {
1146 return Ok(None);
1147 }
1148 (chart_line(start, end, range, tol)?, range)
1149 };
1150 pcurves.push(PcurveJoin {
1151 surface,
1152 location,
1153 curve: joined.0,
1154 range: joined.1,
1155 });
1156 }
1157 for pcurve in &pcurves {
1159 if !agrees(model, &curve, range, pcurve, tol)? {
1160 return Ok(None);
1161 }
1162 }
1163 let bounds = |e: &Shape| {
1164 model.children_of(&Shape::new(
1165 e.node(),
1166 Location::identity(),
1167 Orientation::Forward,
1168 ))
1169 };
1170 let (starts, stops) = (bounds(first)?, bounds(second)?);
1171 let ends = starts.first().cloned().zip(stops.last().cloned());
1172 Ok(Some(Join {
1173 curve,
1174 range,
1175 pcurves,
1176 ends,
1177 }))
1178}
1179
1180fn agrees(
1183 model: &Model,
1184 curve: &ogeom_geom::Curve,
1185 range: (f64, f64),
1186 pcurve: &PcurveJoin,
1187 tol: Tolerances,
1188) -> OgeomResult<bool> {
1189 use ogeom_geom::Surface as _;
1190 let Some(surface) = model.geometry().surface(pcurve.surface) else {
1191 return Ok(false);
1192 };
1193 let placement = pcurve.location.composed(model.datums())?;
1194 for k in 0..=6 {
1195 let f = f64::from(k) / 6.0;
1196 let t = (range.1 - range.0).mul_add(f, range.0);
1197 let pt = (pcurve.range.1 - pcurve.range.0).mul_add(f, pcurve.range.0);
1198 let (Ok(on_curve), Ok(chart)) = (curve.point_at(t, tol), pcurve.curve.point_at(pt, tol))
1199 else {
1200 return Ok(false);
1201 };
1202 let Ok(lifted) = surface.point_at(chart.x, chart.y, tol) else {
1203 return Ok(false);
1204 };
1205 if placement.apply(lifted).distance(on_curve) > tol.confusion() {
1206 return Ok(false);
1207 }
1208 }
1209 Ok(true)
1210}
1211
1212pub fn reduce_tolerances(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<usize> {
1225 model.unshare(shape)?;
1226 let edges = explore_unique(model, shape, ShapeType::Edge)?;
1227 let measured = {
1230 let model = &*model;
1231 ogeom_core::parallel::map_ordered(&edges, |_, edge| measure_edge(model, edge, false, tol))
1232 };
1233 let mut shrunk = 0;
1234 for (edge, measured) in edges.iter().zip(measured) {
1235 let Some(worst) = measured else { continue };
1236 let target = ogeom_core::Tolerance::new(worst + tol.confusion())?;
1237 if let Some(node) = model.node_mut(edge)
1238 && let NodeData::Edge(data) = node.data_mut()
1239 && target.get() < data.tolerance.get()
1240 {
1241 data.tolerance = target;
1242 shrunk += 1;
1243 }
1244 }
1245 Ok(shrunk)
1246}
1247
1248pub(crate) fn measure_edge(
1253 model: &Model,
1254 edge: &Shape,
1255 paced: bool,
1256 tol: Tolerances,
1257) -> Option<f64> {
1258 let data = model.node(edge).and_then(|n| n.data().as_edge())?;
1259 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
1260 return None;
1261 };
1262 let geometry = model.geometry().curve(*curve)?;
1263 let mut worst = 0.0f64;
1267 let mut measurable = false;
1268 for representation in &data.representations {
1269 let (sides, prange, surface, location) = match representation {
1270 EdgeRepr::PCurve {
1271 curve,
1272 range,
1273 surface,
1274 location,
1275 } => ([Some(*curve), None], *range, *surface, location),
1276 EdgeRepr::Seam {
1277 forward,
1278 reversed,
1279 range,
1280 surface,
1281 location,
1282 } => (
1283 [Some(*forward), Some(*reversed)],
1284 *range,
1285 *surface,
1286 location,
1287 ),
1288 _ => continue,
1289 };
1290 let Some(surface_geometry) = model.geometry().surface(surface) else {
1291 continue;
1292 };
1293 let placed;
1294 let surface_geometry = if location.is_identity() {
1295 surface_geometry
1296 } else {
1297 use ogeom_geom::Transformable as _;
1298 let Ok(placement) = location.composed(model.datums()) else {
1299 continue;
1300 };
1301 let Ok(moved) = surface_geometry.transformed(&placement, tol) else {
1302 continue;
1303 };
1304 placed = moved;
1305 &placed
1306 };
1307 for pc in sides.into_iter().flatten() {
1308 let Some(pcurve) = model.geometry().pcurve(pc) else {
1309 continue;
1310 };
1311 let Ok(gap) = ogeom_algo::pcurve_fit::lifted_gap(
1312 (geometry, *range),
1313 (pcurve, prange),
1314 surface_geometry,
1315 paced || data.same_parameter(),
1316 tol,
1317 ) else {
1318 continue;
1319 };
1320 worst = worst.max(gap);
1321 measurable = true;
1322 }
1323 }
1324 measurable.then_some(worst)
1325}