1use std::collections::{HashMap, HashSet};
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, Point2, Transform};
11use ogeom_topo::{
12 EdgeRepr, Location, Model, NodeData, PCurveId, Shape, ShapeType, SurfaceId, TShapeId,
13 explore_unique,
14};
15
16use crate::reshape::Reshape;
17
18pub fn unify_same_domain(
32 model: &mut Model,
33 shape: &Shape,
34 tol: Tolerances,
35) -> OgeomResult<(Built, usize)> {
36 let faces = explore_unique(model, shape, ShapeType::Face)?;
37
38 let mut carriers: Vec<Option<Carrier>> = Vec::with_capacity(faces.len());
42 for face in &faces {
43 carriers.push(carrier_of(model, face, tol)?);
44 }
45
46 let mut group: Vec<usize> = (0..faces.len()).collect();
48 fn root(group: &mut [usize], mut i: usize) -> usize {
49 while group[i] != i {
50 group[i] = group[group[i]];
51 i = group[i];
52 }
53 i
54 }
55 let mut edge_users: HashMap<TShapeId, Vec<usize>> = HashMap::new();
56 for (i, face) in faces.iter().enumerate() {
57 for edge in explore_unique(model, face, ShapeType::Edge)? {
58 edge_users.entry(edge.node()).or_default().push(i);
59 }
60 }
61 for users in edge_users.values() {
62 for pair in users.windows(2) {
63 let (a, b) = (pair[0], pair[1]);
64 if let (Some(ca), Some(cb)) = (carriers[a].as_ref(), carriers[b].as_ref())
67 && ca.world.normal().dot(cb.world.normal()) > 1.0 - tol.angular()
68 && ca.world.signed_distance_to(cb.world.frame().origin()).abs() <= tol.confusion()
69 {
70 let (ra, rb) = (root(&mut group, a), root(&mut group, b));
71 group[ra] = rb;
72 }
73 }
74 }
75
76 let mut clusters: HashMap<usize, Vec<usize>> = HashMap::new();
77 for i in 0..faces.len() {
78 let r = root(&mut group, i);
79 clusters.entry(r).or_default().push(i);
80 }
81
82 let mut reshape = Reshape::new();
83 for members in clusters.values() {
84 if members.len() < 2 {
85 continue;
86 }
87 let mut counts: HashMap<TShapeId, (usize, Shape)> = HashMap::new();
89 for &i in members {
90 for edge in explore_unique(model, &faces[i], ShapeType::Edge)? {
91 counts
92 .entry(edge.node())
93 .and_modify(|(n, _)| *n += 1)
94 .or_insert((1, edge));
95 }
96 }
97 let boundary: Vec<Shape> = {
98 let mut edges: Vec<(TShapeId, Shape)> = counts
99 .into_iter()
100 .filter(|(_, (n, _))| *n == 1)
101 .map(|(id, (_, e))| (id, e))
102 .collect();
103 edges.sort_by_key(|(id, _)| *id);
104 edges.into_iter().map(|(_, e)| e).collect()
105 };
106 if boundary.len() < 3 {
107 continue;
108 }
109 let keeper = members[0];
110 let surface_id = {
111 let Some(NodeData::Face(data)) = model.node(&faces[keeper]).map(|n| n.data().clone())
112 else {
113 continue;
114 };
115 data.surface
116 };
117 let Some(carrier) = carriers[keeper].clone() else {
120 continue;
121 };
122 for edge in &boundary {
123 ensure_planar_pcurve(model, edge, surface_id, &carrier, tol)?;
124 }
125 let wire = ogeom_algo::make_wire_unordered(model, &boundary, tol)?.shape;
126 let merged = {
127 let Some(NodeData::Face(data)) = model.node(&faces[keeper]).map(|n| n.data().clone())
128 else {
129 continue;
130 };
131 model.add_face(*data, std::slice::from_ref(&wire))?
132 };
133 reshape.replace(&faces[keeper], merged);
134 for &other in &members[1..] {
135 reshape.remove(&faces[other]);
136 }
137 }
138 if reshape.is_empty() {
139 let mut built = Built::from_nothing(shape.clone());
140 built.history.modify(shape, shape.clone());
141 return Ok((built, 0));
142 }
143 let unified = reshape.len();
144 Ok((reshape.apply(model, shape)?, unified))
145}
146
147#[derive(Debug, Clone)]
149struct Carrier {
150 world: Plane,
152 stored: Plane,
154 placement: Transform,
156}
157
158fn carrier_of(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<Option<Carrier>> {
160 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data().clone()) else {
161 return Ok(None);
162 };
163 let Some(SurfaceGeometry::Plane(p)) = model.geometry().surface(data.surface) else {
164 return Ok(None);
165 };
166 let stored = p.plane();
167 let placement = face.transform(model.datums())?;
168 if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 {
169 return Ok(None);
170 }
171 Ok(Some(Carrier {
172 world: stored.transformed(&placement, tol)?,
173 stored,
174 placement,
175 }))
176}
177
178fn ensure_planar_pcurve(
185 model: &mut Model,
186 edge: &Shape,
187 surface_id: SurfaceId,
188 carrier: &Carrier,
189 tol: Tolerances,
190) -> OgeomResult<()> {
191 let (curve, range, has) = {
192 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
193 ogeom_bail!(Construction, "edge holds no edge data");
194 };
195 let has = data.pcurve_for(surface_id, edge.location()).is_some();
196 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
197 ogeom_bail!(Construction, "an edge with no curve cannot be unified over");
198 };
199 (*curve, *range, has)
200 };
201 if has {
202 return Ok(());
203 }
204 let Some(geometry) = model.geometry().curve(curve).cloned() else {
205 ogeom_bail!(Construction, "edge refers to a curve not in this model");
206 };
207 let back = carrier.placement.inverse()?;
208 let flat = |p: ogeom_math::Point| {
209 let local = carrier.stored.frame().to_local(back.apply(p));
210 Point2::new(local.x, local.y)
211 };
212 let a = flat(geometry.point_at(range.0, tol)?);
213 let b = flat(geometry.point_at(range.1, tol)?);
214 ogeom_algo::attach_pcurve(
215 model,
216 edge,
217 chart_line(a, b, range, tol)?,
218 surface_id,
219 edge.location().clone(),
220 range,
221 )
222}
223
224fn chart_line(
230 a: Point2,
231 b: Point2,
232 range: (f64, f64),
233 tol: Tolerances,
234) -> OgeomResult<PlanarCurve> {
235 let span = range.1 - range.0;
236 let direction = Direction2::new((b - a) / span, tol)?;
237 let origin = a - direction.vector() * range.0;
238 Ok(
239 ogeom_geom::Line2d::over(ogeom_math::Axis2::new(origin, direction), range.0, range.1)?
240 .into(),
241 )
242}
243
244pub fn merge_edges(
258 model: &mut Model,
259 shape: &Shape,
260 tol: Tolerances,
261) -> OgeomResult<(Built, usize)> {
262 let mut current = shape.clone();
265 let mut steps: Vec<History> = Vec::new();
266 let mut merged = 0;
267 for _ in 0..8 {
268 let Some((step, joined)) = merge_pass(model, ¤t, tol)? else {
269 break;
270 };
271 current = step.shape;
272 steps.push(step.history);
273 merged += joined;
274 }
275 if steps.is_empty() {
276 let mut built = Built::from_nothing(shape.clone());
277 built.history.modify(shape, shape.clone());
278 return Ok((built, 0));
279 }
280 Ok((Built::new(current, History::chain(&steps)), merged))
281}
282
283fn merge_pass(
286 model: &mut Model,
287 shape: &Shape,
288 tol: Tolerances,
289) -> OgeomResult<Option<(Built, usize)>> {
290 let mut needed: HashMap<TShapeId, Vec<(SurfaceId, Location)>> = HashMap::new();
296 for face in explore_unique(model, shape, ShapeType::Face)? {
297 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data().clone()) else {
298 continue;
299 };
300 for edge in explore_unique(model, &face, ShapeType::Edge)? {
301 let slot = (data.surface, edge.location().clone());
302 let slots = needed.entry(edge.node()).or_default();
303 if !slots.contains(&slot) {
304 slots.push(slot);
305 }
306 }
307 }
308
309 let wires = explore_unique(model, shape, ShapeType::Wire)?;
310 let mut reshape = Reshape::new();
311 let mut merged_nodes: HashSet<TShapeId> = HashSet::new();
312 for wire in &wires {
313 let edges = model.ordered_children_of(wire)?;
314 if edges.len() < 2 {
315 continue;
316 }
317 let closed = is_closed_wire(model, &edges, tol);
320 let pairs = if closed { edges.len() } else { edges.len() - 1 };
321 let mut i = 0;
322 while i < pairs {
323 let (a, b) = (edges[i].clone(), edges[(i + 1) % edges.len()].clone());
324 if a.node() == b.node()
325 || merged_nodes.contains(&a.node())
326 || merged_nodes.contains(&b.node())
327 {
328 i += 1;
329 continue;
330 }
331 let mut slots: Vec<(SurfaceId, Location)> =
332 needed.get(&a.node()).cloned().unwrap_or_default();
333 for slot in needed.get(&b.node()).into_iter().flatten() {
334 if !slots.contains(slot) {
335 slots.push(slot.clone());
336 }
337 }
338 let Some(join) = joinable(model, &a, &b, &slots, tol)? else {
339 i += 1;
340 continue;
341 };
342 let joined = ogeom_algo::make_edge(model, join.curve, join.range, tol)?.shape;
343 for pcurve in join.pcurves {
344 ogeom_algo::attach_pcurve(
345 model,
346 &joined,
347 pcurve.curve,
348 pcurve.surface,
349 pcurve.location,
350 pcurve.range,
351 )?;
352 }
353 merged_nodes.insert(a.node());
354 merged_nodes.insert(b.node());
355 reshape.replace(&a, joined);
356 reshape.remove(&b);
357 i += 2;
358 }
359 }
360 if reshape.is_empty() {
361 return Ok(None);
362 }
363 let joined = reshape.len();
364 Ok(Some((reshape.apply(model, shape)?, joined)))
365}
366
367fn is_closed_wire(model: &Model, edges: &[Shape], tol: Tolerances) -> bool {
369 let ends = |e: &Shape| -> Option<(ogeom_math::Point, ogeom_math::Point)> {
370 let vertices = explore_unique(model, e, ShapeType::Vertex).ok()?;
371 let first = model.node(vertices.first()?)?.data().as_vertex()?.point;
372 let last = model.node(vertices.last()?)?.data().as_vertex()?.point;
373 Some((first, last))
374 };
375 let (Some(first), Some(last)) = (ends(&edges[0]), ends(&edges[edges.len() - 1])) else {
376 return false;
377 };
378 [first.0, first.1]
379 .iter()
380 .any(|p| p.distance(last.0) <= tol.confusion() || p.distance(last.1) <= tol.confusion())
381}
382
383struct Join {
385 curve: ogeom_geom::Curve,
387 range: (f64, f64),
389 pcurves: Vec<PcurveJoin>,
391}
392
393struct PcurveJoin {
395 surface: SurfaceId,
396 location: Location,
397 curve: PlanarCurve,
398 range: (f64, f64),
399}
400
401type CurveJoin = (ogeom_geom::Curve, (f64, f64), bool);
404
405fn joinable_curve(
408 model: &Model,
409 a: &Shape,
410 b: &Shape,
411 tol: Tolerances,
412) -> OgeomResult<Option<CurveJoin>> {
413 let read = |e: &Shape| -> Option<(ogeom_topo::CurveId, (f64, f64))> {
414 let data = model.node(e)?.data().as_edge()?;
415 let EdgeRepr::Curve3d { curve, range, .. } = data.curve3d()? else {
416 return None;
417 };
418 Some((*curve, *range))
419 };
420 let (Some((ca, ra)), Some((cb, rb))) = (read(a), read(b)) else {
421 return Ok(None);
422 };
423 if ca == cb {
424 let Some(geometry) = model.geometry().curve(ca).cloned() else {
425 return Ok(None);
426 };
427 if (ra.1 - rb.0).abs() <= tol.parametric() {
428 return Ok(Some((geometry, (ra.0, rb.1), true)));
429 }
430 if (rb.1 - ra.0).abs() <= tol.parametric() {
431 return Ok(Some((geometry, (rb.0, ra.1), false)));
432 }
433 return Ok(None);
434 }
435 let (Some(ga), Some(gb)) = (
437 model.geometry().curve(ca).cloned(),
438 model.geometry().curve(cb).cloned(),
439 ) else {
440 return Ok(None);
441 };
442 let (ogeom_geom::Curve::Line(la), ogeom_geom::Curve::Line(lb)) = (&ga, &gb) else {
443 return Ok(None);
444 };
445 if !la.axis().is_collinear(lb.axis(), tol) {
446 return Ok(None);
447 }
448 let (a0, a1) = (ga.point_at(ra.0, tol)?, ga.point_at(ra.1, tol)?);
449 let (b0, b1) = (gb.point_at(rb.0, tol)?, gb.point_at(rb.1, tol)?);
450 let fresh = |from: ogeom_math::Point,
451 to: ogeom_math::Point,
452 a_first: bool|
453 -> OgeomResult<Option<CurveJoin>> {
454 let segment = ogeom_geom::LineCurve::segment(from, to, tol)?;
455 let range = segment.domain();
456 Ok(Some((segment.into(), range, a_first)))
457 };
458 if a1.distance(b0) <= tol.confusion() {
459 return fresh(a0, b1, true);
460 }
461 if b1.distance(a0) <= tol.confusion() {
462 return fresh(b0, a1, false);
463 }
464 Ok(None)
465}
466
467fn joinable(
473 model: &Model,
474 a: &Shape,
475 b: &Shape,
476 slots: &[(SurfaceId, Location)],
477 tol: Tolerances,
478) -> OgeomResult<Option<Join>> {
479 let Some((curve, range, a_first)) = joinable_curve(model, a, b, tol)? else {
480 return Ok(None);
481 };
482 let (first, second) = if a_first { (a, b) } else { (b, a) };
483 let reprs = |e: &Shape| -> Vec<EdgeRepr> {
484 model
485 .node(e)
486 .and_then(|n| n.data().as_edge())
487 .map(|d| d.representations.to_vec())
488 .unwrap_or_default()
489 };
490 let (first_reprs, second_reprs) = (reprs(first), reprs(second));
491 if first_reprs
493 .iter()
494 .chain(&second_reprs)
495 .any(|r| matches!(r, EdgeRepr::Seam { .. }))
496 {
497 return Ok(None);
498 }
499 let parametric = |reprs: &[EdgeRepr]| -> Vec<(SurfaceId, Location, PCurveId, (f64, f64))> {
500 reprs
501 .iter()
502 .filter_map(|r| match r {
503 EdgeRepr::PCurve {
504 curve,
505 range,
506 surface,
507 location,
508 } => Some((*surface, location.clone(), *curve, *range)),
509 _ => None,
510 })
511 .collect()
512 };
513 let (ones, twos) = (parametric(&first_reprs), parametric(&second_reprs));
514 let mut pcurves = Vec::with_capacity(slots.len());
515 for (surface, location) in slots {
516 let (surface, location) = (*surface, location.clone());
517 let find = |from: &[(SurfaceId, Location, PCurveId, (f64, f64))]| {
518 from.iter()
519 .find(|(s, l, _, _)| *s == surface && *l == location)
520 .cloned()
521 };
522 let (Some((_, _, id_one, range_one)), Some((_, _, id_two, range_two))) =
523 (find(&ones), find(&twos))
524 else {
525 return Ok(None);
526 };
527 let (Some(one), Some(two)) = (
528 model.geometry().pcurve(id_one).cloned(),
529 model.geometry().pcurve(id_two).cloned(),
530 ) else {
531 return Ok(None);
532 };
533 let joined = if id_one == id_two && (range_one.1 - range_two.0).abs() <= tol.parametric() {
534 (one, (range_one.0, range_two.1))
535 } else {
536 let (PlanarCurve::Line(_), PlanarCurve::Line(_)) = (&one, &two) else {
539 return Ok(None);
540 };
541 let start = one.point_at(range_one.0, tol)?;
542 let end = two.point_at(range_two.1, tol)?;
543 if one
544 .point_at(range_one.1, tol)?
545 .distance(two.point_at(range_two.0, tol)?)
546 > tol.confusion()
547 {
548 return Ok(None);
549 }
550 (chart_line(start, end, range, tol)?, range)
551 };
552 pcurves.push(PcurveJoin {
553 surface,
554 location,
555 curve: joined.0,
556 range: joined.1,
557 });
558 }
559 for pcurve in &pcurves {
561 if !agrees(model, &curve, range, pcurve, tol)? {
562 return Ok(None);
563 }
564 }
565 Ok(Some(Join {
566 curve,
567 range,
568 pcurves,
569 }))
570}
571
572fn agrees(
575 model: &Model,
576 curve: &ogeom_geom::Curve,
577 range: (f64, f64),
578 pcurve: &PcurveJoin,
579 tol: Tolerances,
580) -> OgeomResult<bool> {
581 use ogeom_geom::Surface as _;
582 let Some(surface) = model.geometry().surface(pcurve.surface) else {
583 return Ok(false);
584 };
585 let placement = pcurve.location.composed(model.datums())?;
586 for k in 0..=6 {
587 let f = f64::from(k) / 6.0;
588 let t = (range.1 - range.0).mul_add(f, range.0);
589 let pt = (pcurve.range.1 - pcurve.range.0).mul_add(f, pcurve.range.0);
590 let (Ok(on_curve), Ok(chart)) = (curve.point_at(t, tol), pcurve.curve.point_at(pt, tol))
591 else {
592 return Ok(false);
593 };
594 let Ok(lifted) = surface.point_at(chart.x, chart.y, tol) else {
595 return Ok(false);
596 };
597 if placement.apply(lifted).distance(on_curve) > tol.confusion() {
598 return Ok(false);
599 }
600 }
601 Ok(true)
602}
603
604pub fn reduce_tolerances(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<usize> {
614 let mut shrunk = 0;
615 for edge in explore_unique(model, shape, ShapeType::Edge)? {
616 let measured = {
617 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
618 continue;
619 };
620 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
621 continue;
622 };
623 let Some(geometry) = model.geometry().curve(*curve) else {
624 continue;
625 };
626 let mut worst = 0.0f64;
629 let mut measurable = false;
630 for representation in &data.representations {
631 let EdgeRepr::PCurve {
632 curve: pc,
633 range: prange,
634 surface,
635 location,
636 } = representation
637 else {
638 continue;
639 };
640 let Some(pcurve) = model.geometry().pcurve(*pc) else {
641 continue;
642 };
643 let Some(surface_geometry) = model.geometry().surface(*surface) else {
644 continue;
645 };
646 use ogeom_geom::Surface as _;
647 for k in 0..=8 {
648 let t = range.0 + (range.1 - range.0) * f64::from(k) / 8.0;
649 let pt = prange.0 + (prange.1 - prange.0) * f64::from(k) / 8.0;
650 let (Ok(on_curve), Ok(chart)) =
651 (geometry.point_at(t, tol), pcurve.point_at(pt, tol))
652 else {
653 continue;
654 };
655 let Ok(lifted) = surface_geometry.point_at(chart.x, chart.y, tol) else {
656 continue;
657 };
658 let Ok(placement) = location.composed(model.datums()) else {
659 continue;
660 };
661 worst = worst.max(placement.apply(lifted).distance(on_curve));
662 measurable = true;
663 }
664 }
665 measurable.then_some(worst)
666 };
667 let Some(worst) = measured else { continue };
668 let target = ogeom_core::Tolerance::new(worst + tol.confusion())?;
669 if let Some(node) = model.node_mut(&edge)
670 && let NodeData::Edge(data) = node.data_mut()
671 && target.get() < data.tolerance.get()
672 {
673 data.tolerance = target;
674 shrunk += 1;
675 }
676 }
677 Ok(shrunk)
678}