1use crate::build::{attach_pcurve, attach_seam, make_edge_between, make_face_on};
19use crate::{Built, History, make_compound, make_shell, make_solid, make_vertex, make_wire};
20use ogeom_core::FastMap;
21use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
22use ogeom_geom::{Curve, Curve2d as _, Curve3d as _, Surface as _, SurfaceGeometry};
23use ogeom_math::{GeneralTransform, Point, Point2, Weighted};
24use ogeom_topo::{
25 EdgeRepr, Filter, Location, Model, NodeData, Orientation, Shape, ShapeType, TShapeId, explore,
26};
27
28type ConvertedEdge = (Shape, Curve, (f64, f64));
30
31type ChartWindow = ((f64, f64), (f64, f64));
33
34pub fn to_nurbs(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
53 rebuild(model, shape, None, Restate::Nurbs, tol)
54}
55
56pub fn to_nurbs_within(
72 model: &mut Model,
73 shape: &Shape,
74 tolerance: f64,
75 tol: Tolerances,
76) -> OgeomResult<Built> {
77 if !(tolerance.is_finite() && tolerance > 0.0) {
78 ogeom_bail!(Construction, "a tolerance of {tolerance} is not a distance");
79 }
80 let surface = |s: &SurfaceGeometry| -> OgeomResult<Option<(SurfaceGeometry, bool)>> {
81 if let Ok(exact) = s.to_bspline(tol) {
82 return Ok(Some((exact.into(), false)));
83 }
84 let fitted = s.fitted_bspline(tolerance, tol)?;
85 if !fitted.met {
86 ogeom_bail!(
87 NotDone,
88 "a surface fitted as a spline stays {} away",
89 fitted.error
90 );
91 }
92 let fitted: SurfaceGeometry = fitted.curve.into();
93 let turned = normals_oppose(s, &fitted, tol)?;
94 Ok(Some((fitted, turned)))
95 };
96 let curve = |c: &Curve, range: (f64, f64)| -> OgeomResult<Option<(Curve, (f64, f64))>> {
97 if let Ok(exact) = c.to_bspline_over(range, tol) {
98 let exact: Curve = exact.into();
99 let domain = exact.domain();
100 return Ok(Some((exact, domain)));
101 }
102 let fitted = c.fitted_bspline_over(range, tolerance, tol)?;
103 if !fitted.met {
104 ogeom_bail!(
105 NotDone,
106 "a curve fitted as a spline stays {} away",
107 fitted.error
108 );
109 }
110 Ok(Some((fitted.curve.into(), range)))
111 };
112 rebuild(model, shape, None, Restate::With(&surface, &curve), tol)
113}
114
115pub fn normals_oppose(
124 old: &SurfaceGeometry,
125 new: &SurfaceGeometry,
126 tol: Tolerances,
127) -> OgeomResult<bool> {
128 let ((u0, u1), (v0, v1)) = old.domain();
129 let (u, v) = (u0 + (u1 - u0) * 0.43, v0 + (v1 - v0) * 0.57);
131 let p = old.point_at(u, v, tol)?;
132 let n_old = old.normal_at(u, v, tol)?;
133 let at = crate::measure::project_on_surface(new, p, 16, tol)?.parameters;
134 let n_new = new.normal_at(at.0, at.1, tol)?;
135 Ok(n_old.vector().dot(n_new.vector()) < 0.0)
136}
137
138pub fn baked_shape(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
158 rebuild(model, shape, None, Restate::Keep, tol)
159}
160
161pub fn baked_where_placed(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Built> {
175 let mut placed = !shape.location().is_identity();
176 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
177 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data()) else {
178 ogeom_bail!(Dangling, "face is not in this model");
179 };
180 placed |= !face.location().is_identity() || !data.location.is_identity();
181 }
182 for edge in explore(model, shape, Filter::OfType(ShapeType::Edge))? {
183 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
184 ogeom_bail!(Dangling, "edge is not in this model");
185 };
186 placed |= !edge.location().is_identity()
187 || matches!(data.curve3d(), Some(EdgeRepr::Curve3d { location, .. }) if !location.is_identity());
188 }
189 if placed {
190 baked_shape(model, shape, tol)
191 } else {
192 Ok(Built::new(shape.clone(), History::identity()))
193 }
194}
195
196pub fn general_transformed_shape(
226 model: &mut Model,
227 shape: &Shape,
228 transform: &GeneralTransform,
229 tol: Tolerances,
230) -> OgeomResult<Built> {
231 if let Some(similarity) = transform.to_similarity(tol.angular()) {
232 return crate::place::transformed(model, shape, similarity);
233 }
234 rebuild(model, shape, Some(transform), Restate::Nurbs, tol)
235}
236
237pub type SurfaceRestatement<'a> =
240 &'a dyn Fn(&SurfaceGeometry) -> OgeomResult<Option<(SurfaceGeometry, bool)>>;
241
242pub type CurveRestatement<'a> =
245 &'a dyn Fn(&Curve, (f64, f64)) -> OgeomResult<Option<(Curve, (f64, f64))>>;
246
247pub fn restate_geometry(
260 model: &mut Model,
261 shape: &Shape,
262 surface: SurfaceRestatement<'_>,
263 curve: CurveRestatement<'_>,
264 tol: Tolerances,
265) -> OgeomResult<Built> {
266 rebuild(model, shape, None, Restate::With(surface, curve), tol)
267}
268
269#[derive(Clone, Copy)]
271enum Restate<'a> {
272 Keep,
274 Nurbs,
276 With(SurfaceRestatement<'a>, CurveRestatement<'a>),
278}
279
280fn rebuild(
282 model: &mut Model,
283 shape: &Shape,
284 affine: Option<&GeneralTransform>,
285 restate: Restate<'_>,
286 tol: Tolerances,
287) -> OgeomResult<Built> {
288 let mut state = Rebuild {
289 affine,
290 restate,
291 tol,
292 history: History::new(),
293 new_vertices: NewVertices::default(),
294 new_edges: FastMap::default(),
295 rederived: Vec::new(),
296 };
297 let rebuilt = state.shape(model, shape)?;
298 crate::pcurve_gap::state_pcurve_gaps_of(model, &state.rederived, tol)?;
302 Ok(Built::new(rebuilt, state.history))
303}
304
305struct Rebuild<'a> {
307 affine: Option<&'a GeneralTransform>,
308 restate: Restate<'a>,
309 tol: Tolerances,
310 history: History,
311 new_vertices: NewVertices,
313 new_edges: FastMap<(TShapeId, [u64; 3]), ConvertedEdge>,
315 rederived: Vec<Shape>,
318}
319
320impl Rebuild<'_> {
321 fn shape(&mut self, model: &mut Model, shape: &Shape) -> OgeomResult<Shape> {
325 let kind = model.kind_of(shape)?;
326 let rebuilt = match kind {
327 ShapeType::Face => return self.face(model, shape),
328 ShapeType::Shell | ShapeType::Solid | ShapeType::Compound => {
329 let mut children = Vec::new();
330 for child in model.ordered_children_of(shape)? {
331 children.push(self.shape(model, &child)?);
332 }
333 match kind {
334 ShapeType::Shell => make_shell(model, &children)?.shape,
335 ShapeType::Solid => make_solid(model, &children)?.shape,
336 _ => make_compound(model, &children)?.shape,
337 }
338 }
339 other => ogeom_bail!(
340 Construction,
341 "whole-shape conversion rebuilds solids, shells, faces and \
342 compounds of them; this is a {other:?}"
343 ),
344 };
345 self.history.modify(shape, rebuilt.clone());
346 Ok(rebuilt)
347 }
348
349 #[expect(
352 clippy::too_many_lines,
353 reason = "one walk over a face's wires, restating each edge in place"
354 )]
355 fn face(&mut self, model: &mut Model, face: &Shape) -> OgeomResult<Shape> {
356 let (affine, restate, tol) = (self.affine, self.restate, self.tol);
357 let map = |p: Point| affine.map_or(p, |t| t.apply(p));
358 let target = tol.confusion() * 1e2;
361 let face = face.clone();
362 let history = &mut self.history;
363 let new_vertices = &mut self.new_vertices;
364 let new_edges = &mut self.new_edges;
365 let rederived = &mut self.rederived;
366 let placement = face.transform(model.datums())?;
367 let (surface_id_old, old_surface) = {
368 let Some(node) = model.node(&face) else {
369 ogeom_bail!(Dangling, "face is not in this model");
370 };
371 let NodeData::Face(data) = node.data() else {
372 ogeom_bail!(Construction, "face node holds no face data");
373 };
374 let Some(surface) = model.geometry().surface(data.surface) else {
375 ogeom_bail!(Dangling, "face refers to a surface not in this model");
376 };
377 (data.surface, surface.clone())
378 };
379 use ogeom_geom::Transformable as _;
380 let placed = old_surface.transformed(&placement, tol)?;
381 let (placed, old_window) = bounded_to_face(
385 model,
386 &face,
387 surface_id_old,
388 &placed,
389 placement.scale_factor().abs(),
390 tol,
391 )?;
392 let mut flipped = false;
393 let mut turned_before_affine = None;
397 let patch_surface: SurfaceGeometry = match restate {
398 Restate::Nurbs => {
399 let mut patch = profile_held(&placed, old_window, tol).to_bspline(tol)?;
400 if let Some(t) = affine {
401 let unmapped: SurfaceGeometry = patch.clone().into();
402 turned_before_affine =
403 turned_by_rebuild(&old_surface, &placement, old_window, &unmapped, tol)
404 .map(|turned| turned != (t.linear.determinant() < 0.0));
405 patch = transformed_patch(&patch, t)?;
406 }
407 patch.into()
408 }
409 Restate::Keep => on_right_handed_frame(&placed, tol)?.unwrap_or_else(|| placed.clone()),
410 Restate::With(surface, _) => match surface(&placed)? {
411 Some((restated, flip)) => {
412 flipped = flip;
413 restated
414 }
415 None => placed.clone(),
416 },
417 };
418 let surface_id = model.geometry_mut().add_surface(patch_surface.clone());
419
420 let measured = if affine.is_none() {
435 turned_by_rebuild(&old_surface, &placement, old_window, &patch_surface, tol)
436 } else {
437 turned_before_affine
438 };
439 let reflecting = !face.location().preserves_handedness(model.datums())?;
440 let reflected = measured.unwrap_or(reflecting != flipped);
441 let walked_back = affine.is_none() && reflecting != reflected;
448
449 let mut rings = Vec::new();
450 let mut old_wires = Vec::new();
451 let mut corner_uv: FastMap<TShapeId, Point2> = FastMap::default();
452 for wire in model.ordered_children_of(&face.oriented(Orientation::Forward))? {
457 let mut ring = Vec::new();
458 let mut seams_done: Vec<TShapeId> = Vec::new();
459 for edge in model.ordered_children_of(&wire)? {
460 let edge_placement = edge.transform(model.datums())?;
461 let key = (edge.node(), placement_bits(&edge_placement));
462 let data = {
463 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
464 ogeom_bail!(Construction, "edge node holds no edge data");
465 };
466 data.clone()
467 };
468 if data.degenerate {
469 let Some(vertex) = model.children_of(&edge)?.first().cloned() else {
473 ogeom_bail!(Construction, "a degenerate edge has no vertex");
474 };
475 let at = {
476 let Some(v) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
477 ogeom_bail!(Construction, "vertex node holds no vertex data");
478 };
479 map(vertex.transform(model.datums())?.apply(v.point))
480 };
481 let reach = model
482 .node(&vertex)
483 .and_then(|n| n.data().as_vertex())
484 .map_or(0.0, |v| v.tolerance.get());
485 let new_vertex = new_vertices.at(model, vertex.node(), at, reach, tol);
486 let mut degenerate = ogeom_topo::EdgeData::new();
487 degenerate.degenerate = true;
488 let new_edge = model.add_edge(degenerate, &[new_vertex.clone(), new_vertex])?;
489 record(history, &edge, &new_edge);
490 record_vertices(model, &edge, &map, new_vertices, history, tol)?;
491 let row = degenerate_row(
492 model,
493 &data,
494 surface_id_old,
495 &edge,
496 &placed,
497 &patch_surface,
498 at,
499 walked_back,
500 tol,
501 )?;
502 attach_pcurve(
503 model,
504 &new_edge,
505 row.into(),
506 surface_id,
507 Location::identity(),
508 row.domain(),
509 )?;
510 ring.push(if edge.orientation() == Orientation::Reversed {
511 new_edge.reversed()
512 } else {
513 new_edge
514 });
515 continue;
516 }
517
518 let found = match new_edges.get(&key) {
523 Some(found) => Some(found.clone()),
524 None => {
525 let at = occurrence_points(model, &edge, &data, &map, tol)?;
526 let same = new_edges
527 .iter()
528 .filter(|((node, _), _)| *node == edge.node())
529 .find(|(_, (_, curve, range))| lies_at(curve, *range, at, tol))
530 .map(|(_, found)| found.clone());
531 if let Some(same) = &same {
532 new_edges.insert(key, same.clone());
533 }
534 same
535 }
536 };
537 let (new_edge, new_curve, new_range) = match found {
538 Some(found) => found,
539 None => {
540 let built =
541 convert_edge(model, &edge, &data, &map, restate, new_vertices, tol)?;
542 new_edges.insert(key, built.clone());
543 built
544 }
545 };
546 record(history, &edge, &new_edge);
547 record_vertices(model, &edge, &map, new_vertices, history, tol)?;
548
549 let old_repr = data.pcurve_for(surface_id_old, edge.location()).cloned();
553 let was_seam = matches!(old_repr, Some(EdgeRepr::Seam { .. }));
554 if was_seam {
555 if !seams_done.contains(&new_edge.node()) {
556 seams_done.push(new_edge.node());
557 let old_iso = match &old_repr {
559 Some(EdgeRepr::Seam { forward, range, .. }) => {
560 match model.geometry().pcurve(*forward) {
561 Some(side) => iso_direction(side, *range, tol)?,
562 None => None,
563 }
564 }
565 _ => None,
566 };
567 let exact =
568 match exact_seam_columns(&new_curve, new_range, &patch_surface, tol) {
569 Some(sides) => Some(sides),
570 None => match old_iso {
571 Some(row) => {
572 seam_lines(&new_curve, new_range, &patch_surface, row, tol)?
573 }
574 None => None,
575 },
576 };
577 let (forward, reversed) = match exact {
578 Some(sides) => sides,
579 None => {
580 seam_pcurves(&new_curve, new_range, &patch_surface, target, tol)?
581 }
582 };
583 let seam_range = forward.domain();
584 rederived.push(new_edge.clone());
585 attach_seam(
586 model,
587 &new_edge,
588 forward,
589 reversed,
590 surface_id,
591 Location::identity(),
592 seam_range,
593 )?;
594 }
595 } else {
596 let stated = model
603 .node(&new_edge)
604 .and_then(|n| n.data().as_edge())
605 .map_or(0.0, |d| d.tolerance.get());
606 let iso = exact_iso_pcurve(
607 model,
608 old_repr.as_ref(),
609 old_window,
610 &new_curve,
611 new_range,
612 &patch_surface,
613 (edge.orientation() == Orientation::Reversed) == walked_back,
614 tol,
615 )?;
616 let iso = match iso {
617 Some(iso)
618 if crate::pcurve_gap::pcurve_gap(
619 (&new_curve, new_range),
620 (&iso, iso.domain()),
621 &patch_surface,
622 stated,
623 tol,
624 )? <= target.max(stated) =>
625 {
626 Some(iso)
627 }
628 _ => None,
629 };
630 if let Some(iso) = iso {
631 let iso_range = iso.domain();
632 rederived.push(new_edge.clone());
633 attach_pcurve(
634 model,
635 &new_edge,
636 iso,
637 surface_id,
638 Location::identity(),
639 iso_range,
640 )?;
641 ring.push(if edge.orientation() == Orientation::Reversed {
642 new_edge.reversed()
643 } else {
644 new_edge
645 });
646 continue;
647 }
648 let uv_of = |model: &Model,
649 cache: &mut FastMap<TShapeId, Point2>,
650 vertex: &Shape|
651 -> OgeomResult<Point2> {
652 if let Some(&uv) = cache.get(&vertex.node()) {
653 return Ok(uv);
654 }
655 let Some(v) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
656 ogeom_bail!(Construction, "vertex node holds no vertex data");
657 };
658 let projection =
659 crate::measure::project_on_surface(&patch_surface, v.point, 24, tol)?;
660 let uv = Point2::new(projection.parameters.0, projection.parameters.1);
661 cache.insert(vertex.node(), uv);
662 Ok(uv)
663 };
664 let bounds = model.children_of(&new_edge)?;
665 let closed_edge =
670 bounds.len() < 2 || bounds[0].node() == bounds[bounds.len() - 1].node();
671 let ends = if closed_edge {
672 (None, None)
673 } else {
674 (
675 Some(uv_of(model, &mut corner_uv, &bounds[0])?),
676 Some(uv_of(model, &mut corner_uv, &bounds[bounds.len() - 1])?),
677 )
678 };
679 let derived = match ogeom_intersect::exact_pcurve_over(
684 &new_curve,
685 new_range,
686 &patch_surface,
687 tol,
688 ) {
689 Some(exact) => exact,
690 None => {
691 let fitted = fit_pcurve(
692 &new_curve,
693 new_range,
694 &patch_surface,
695 None,
696 ends,
697 target,
698 tol,
699 )?;
700 let widened = if let Some(node) = model.node_mut(&new_edge)
705 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
706 {
707 data.tolerance = data.tolerance.widen_to(target);
708 Some(data.tolerance)
709 } else {
710 None
711 };
712 if let Some(widened) = widened {
713 for vertex in model.ordered_children_of(&new_edge)? {
714 model.widen(&vertex, widened)?;
715 }
716 }
717 rederived.push(new_edge.clone());
718 fitted
719 }
720 };
721 attach_pcurve(
722 model,
723 &new_edge,
724 derived,
725 surface_id,
726 Location::identity(),
727 new_range,
728 )?;
729 }
730 ring.push(if edge.orientation() == Orientation::Reversed {
731 new_edge.reversed()
732 } else {
733 new_edge
734 });
735 }
736 rings.push(ring);
737 old_wires.push(wire);
738 }
739 let mut wires = Vec::with_capacity(rings.len());
740 for (ring, old) in rings.into_iter().zip(&old_wires) {
741 let ring: Vec<Shape> = if walked_back {
742 ring.iter().rev().map(Shape::reversed).collect()
743 } else {
744 ring
745 };
746 let wire = make_wire(model, &ring, tol)?.shape;
747 record(history, old, &wire);
748 wires.push(wire);
749 }
750 let built = make_face_on(model, surface_id, &wires, tol)?.shape;
751 let built = if (face.orientation() == Orientation::Reversed) != reflected {
752 built.reversed()
753 } else {
754 built
755 };
756 history.modify(&face, built.clone());
757 Ok(built)
758 }
759}
760
761fn on_right_handed_frame(
769 surface: &SurfaceGeometry,
770 tol: Tolerances,
771) -> OgeomResult<Option<SurfaceGeometry>> {
772 use ogeom_geom::{ConeSurface, CylinderSurface, PlaneSurface, SphereSurface, TorusSurface};
773 use ogeom_math::{Cone, Cylinder, Frame, Handedness, Plane, Sphere, Torus};
774 let twin = |frame: Frame| -> OgeomResult<Option<Frame>> {
775 if frame.handedness() == Handedness::Right {
776 return Ok(None);
777 }
778 Ok(Some(Frame::new(frame.origin(), frame.z(), frame.x(), tol)?))
779 };
780 let ((u0, u1), (v0, v1)) = surface.domain();
781 Ok(match surface {
782 SurfaceGeometry::Plane(p) => match twin(p.plane().frame())? {
783 Some(f) => Some(PlaneSurface::over(Plane::new(f), (u0, u1), (-v1, -v0))?.into()),
784 None => None,
785 },
786 SurfaceGeometry::Cylinder(c) => {
787 let cylinder = c.cylinder();
788 match twin(cylinder.frame())? {
789 Some(f) => Some(
790 CylinderSurface::new(Cylinder::new(f, cylinder.radius(), tol)?, (v0, v1))?
791 .into(),
792 ),
793 None => None,
794 }
795 }
796 SurfaceGeometry::Cone(c) => {
797 let cone = c.cone();
798 match twin(cone.frame())? {
799 Some(f) => Some(
800 ConeSurface::new(
801 Cone::new(f, cone.reference_radius(), cone.half_angle(), tol)?,
802 (v0, v1),
803 )?
804 .into(),
805 ),
806 None => None,
807 }
808 }
809 SurfaceGeometry::Sphere(s) => {
810 let sphere = s.sphere();
811 match twin(sphere.frame())? {
812 Some(f) => Some(SphereSurface::new(Sphere::new(f, sphere.radius(), tol)?).into()),
813 None => None,
814 }
815 }
816 SurfaceGeometry::Torus(t) => {
817 let torus = t.torus();
818 match twin(torus.frame())? {
819 Some(f) => Some(
820 TorusSurface::new(Torus::new(
821 f,
822 torus.major_radius(),
823 torus.minor_radius(),
824 tol,
825 )?)
826 .into(),
827 ),
828 None => None,
829 }
830 }
831 _ => None,
832 })
833}
834
835fn occurrence_points(
837 model: &Model,
838 edge: &Shape,
839 data: &ogeom_topo::EdgeData,
840 map: &dyn Fn(Point) -> Point,
841 tol: Tolerances,
842) -> OgeomResult<Option<[Point; 3]>> {
843 let Some(EdgeRepr::Curve3d {
844 curve,
845 range,
846 location,
847 }) = data.curve3d()
848 else {
849 return Ok(None);
850 };
851 let Some(geometry) = model.geometry().curve(*curve) else {
852 return Ok(None);
853 };
854 let placement = edge.transform(model.datums())? * location.composed(model.datums())?;
855 let mut out = [Point::ORIGIN; 3];
856 for (slot, t) in out
857 .iter_mut()
858 .zip([range.0, f64::midpoint(range.0, range.1), range.1])
859 {
860 *slot = map(placement.apply(geometry.point_at(t, tol)?));
861 }
862 Ok(Some(out))
863}
864
865fn lies_at(curve: &Curve, range: (f64, f64), at: Option<[Point; 3]>, tol: Tolerances) -> bool {
868 let Some([a, m, b]) = at else {
869 return false;
870 };
871 let near = |t: f64, p: Point| {
872 curve
873 .point_at(t, tol)
874 .is_ok_and(|q| q.distance(p) <= tol.confusion() * 1e2)
875 };
876 near(range.0, a) && near(f64::midpoint(range.0, range.1), m) && near(range.1, b)
877}
878
879fn convert_edge(
881 model: &mut Model,
882 edge: &Shape,
883 data: &ogeom_topo::EdgeData,
884 map: &dyn Fn(Point) -> Point,
885 restate: Restate<'_>,
886 vertices: &mut NewVertices,
887 tol: Tolerances,
888) -> OgeomResult<(Shape, Curve, (f64, f64))> {
889 let Some(EdgeRepr::Curve3d {
890 curve,
891 range,
892 location,
893 }) = data.curve3d()
894 else {
895 ogeom_bail!(Construction, "an edge with no curve cannot be converted");
896 };
897 let Some(geometry) = model.geometry().curve(*curve).cloned() else {
898 ogeom_bail!(Dangling, "curve is not in this model");
899 };
900 let placement = edge.transform(model.datums())? * location.composed(model.datums())?;
901 use ogeom_geom::Transformable as _;
902 let placed = geometry.transformed(&placement, tol)?;
903 let (from, to) = (geometry.domain(), placed.domain());
907 let range_on_placed = (
908 ogeom_geom::carried(range.0, from, to),
909 ogeom_geom::carried(range.1, from, to),
910 );
911 let (curve, new_range): (Curve, (f64, f64)) = if matches!(restate, Restate::Nurbs) {
912 let spline = placed.to_bspline_over(range_on_placed, tol)?;
913 let weighted: Vec<Weighted<Point>> = spline
916 .control_points()
917 .iter()
918 .map(|c| {
919 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
920 Weighted::new(map(p), c.weight, tol)
921 })
922 .collect::<OgeomResult<_>>()?;
923 let spline = ogeom_geom::BSplineCurve::rational(spline.knots().clone(), weighted)?;
924 let curve: Curve = spline.into();
925 let range = curve.domain();
926 (curve, range)
927 } else if let Restate::With(_, restated) = restate {
928 restated(&placed, range_on_placed)?.unwrap_or((placed, range_on_placed))
929 } else {
930 (placed, range_on_placed)
931 };
932 let old = model.children_of(edge)?;
936 if old.is_empty() {
937 ogeom_bail!(Construction, "an edge with no vertices cannot be converted");
938 }
939 let mut resolve = |model: &mut Model, occurrence: &Shape| -> OgeomResult<Shape> {
940 let Some(data) = model
941 .node(occurrence)
942 .and_then(|n| n.data().as_vertex().cloned())
943 else {
944 ogeom_bail!(Construction, "vertex node holds no vertex data");
945 };
946 let at = map(occurrence.transform(model.datums())?.apply(data.point));
950 let fresh = vertices.at(model, occurrence.node(), at, data.tolerance.get(), tol);
951 if data.tolerance.get() > tol.confusion() {
956 model.widen(&fresh, data.tolerance)?;
957 }
958 Ok(fresh)
959 };
960 let va = resolve(model, &old[0])?;
961 let vb = if old.len() == 1 {
962 va.clone()
963 } else {
964 resolve(model, &old[old.len() - 1])?
965 };
966 let built = make_edge_between(model, curve.clone(), new_range, &va, &vb, tol)?.shape;
967 if data.tolerance.get() > tol.confusion() {
968 model.widen(&built, data.tolerance)?;
969 }
970 Ok((built, curve, new_range))
971}
972
973#[allow(clippy::too_many_arguments)]
979fn fit_pcurve(
980 curve: &Curve,
981 range: (f64, f64),
982 surface: &SurfaceGeometry,
983 side: Option<(bool, f64)>,
984 ends: (Option<Point2>, Option<Point2>),
985 target: f64,
986 tol: Tolerances,
987) -> OgeomResult<ogeom_geom::PlanarCurve> {
988 use ogeom_geom::Curve2d as _;
989 let mut samples = 48;
990 loop {
991 let fitted = fit_pcurve_at(curve, range, surface, side, ends, target, samples, tol)?;
992 if samples >= 384 {
993 return Ok(fitted);
994 }
995 let mut held = true;
996 for i in 0..samples {
997 #[allow(clippy::cast_precision_loss)]
998 let t = range.0 + (range.1 - range.0) * (i as f64 + 0.5) / samples as f64;
999 let at = fitted.point_at(t, tol)?;
1000 let Ok(lifted) = surface.point_at(at.x, at.y, tol) else {
1003 continue;
1004 };
1005 if lifted.distance(curve.point_at(t, tol)?) > target {
1006 held = false;
1007 break;
1008 }
1009 }
1010 if held {
1011 return Ok(fitted);
1012 }
1013 samples *= 2;
1014 }
1015}
1016
1017#[allow(clippy::too_many_arguments)]
1019fn fit_pcurve_at(
1020 curve: &Curve,
1021 range: (f64, f64),
1022 surface: &SurfaceGeometry,
1023 side: Option<(bool, f64)>,
1024 ends: (Option<Point2>, Option<Point2>),
1025 target: f64,
1026 samples: usize,
1027 tol: Tolerances,
1028) -> OgeomResult<ogeom_geom::PlanarCurve> {
1029 let ((u0, u1), (v0, v1)) = surface.domain();
1030 let mut parameters = Vec::with_capacity(samples + 1);
1031 let mut trace = Vec::with_capacity(samples + 1);
1032 for i in 0..=samples {
1033 #[allow(clippy::cast_precision_loss)]
1034 let t = range.0 + (range.1 - range.0) * i as f64 / samples as f64;
1035 let p = curve.point_at(t, tol)?;
1036 let projection = crate::measure::project_on_surface(surface, p, 24, tol)?;
1037 let (mut u, mut v) = projection.parameters;
1038 match side {
1041 Some((true, bias))
1042 if (u - u0).abs() < (u1 - u0) * 0.25 || (u - u1).abs() < (u1 - u0) * 0.25 =>
1043 {
1044 u = bias;
1045 }
1046 Some((false, bias))
1047 if (v - v0).abs() < (v1 - v0) * 0.25 || (v - v1).abs() < (v1 - v0) * 0.25 =>
1048 {
1049 v = bias;
1050 }
1051 _ => {}
1052 }
1053 parameters.push(t);
1054 trace.push(Point2::new(u, v));
1055 }
1056 let on_pole: Vec<bool> = trace
1062 .iter()
1063 .map(|p| {
1064 surface
1065 .d1_at(p.x, p.y, tol)
1066 .is_ok_and(|(du, _)| du.magnitude() * (u1 - u0).abs() <= tol.confusion())
1067 })
1068 .collect();
1069 if on_pole.iter().any(|&p| p) && !on_pole.iter().all(|&p| p) {
1070 for i in 0..trace.len() {
1071 if !on_pole[i] {
1072 continue;
1073 }
1074 let beside = (1..trace.len())
1075 .flat_map(|k| [i.checked_sub(k), (i + k < trace.len()).then_some(i + k)])
1076 .flatten()
1077 .find(|&j| !on_pole[j]);
1078 if let Some(j) = beside {
1079 trace[i].x = trace[j].x;
1080 }
1081 }
1082 }
1083 let span = u1 - u0;
1088 for i in 1..trace.len() {
1089 let mut du = trace[i].x - trace[i - 1].x;
1090 while du > span * 0.5 {
1091 trace[i].x -= span;
1092 du = trace[i].x - trace[i - 1].x;
1093 }
1094 while du < -span * 0.5 {
1095 trace[i].x += span;
1096 du = trace[i].x - trace[i - 1].x;
1097 }
1098 }
1099 let beside = |corner: Point2, at: Point2| -> Point2 {
1106 if !span.is_finite() || span <= 0.0 {
1107 return corner;
1108 }
1109 let turns = ((at.x - corner.x) / span).round();
1110 Point2::new(turns.mul_add(span, corner.x), corner.y)
1111 };
1112 let at_pole = |k: usize| on_pole.get(k).copied().unwrap_or(false);
1115 let last_index = trace.len().saturating_sub(1);
1116 if let Some(start) = ends.0
1117 && let Some(first) = trace.first_mut()
1118 {
1119 *first = if at_pole(0) {
1120 Point2::new(first.x, start.y)
1121 } else {
1122 beside(start, *first)
1123 };
1124 }
1125 if let Some(end) = ends.1
1126 && let Some(last) = trace.last_mut()
1127 {
1128 *last = if at_pole(last_index) {
1129 Point2::new(last.x, end.y)
1130 } else {
1131 beside(end, *last)
1132 };
1133 }
1134 let mut stretch: f64 = 1.0;
1139 for p in &trace {
1140 let (du, dv) = surface.d1_at(p.x, p.y, tol)?;
1141 stretch = stretch.max(du.magnitude()).max(dv.magnitude());
1142 }
1143 let fitted =
1144 ogeom_geom::fit::fit_points_2d_at(¶meters, &trace, 3, target / (2.0 * stretch), tol)?;
1145 Ok(fitted.curve.into())
1146}
1147
1148#[allow(clippy::too_many_arguments)]
1157fn exact_iso_pcurve(
1158 model: &Model,
1159 old_repr: Option<&EdgeRepr>,
1160 _old_window: ChartWindow,
1161 curve: &Curve,
1162 range: (f64, f64),
1163 surface: &SurfaceGeometry,
1164 along: bool,
1165 tol: Tolerances,
1166) -> OgeomResult<Option<ogeom_geom::PlanarCurve>> {
1167 let Some(EdgeRepr::PCurve {
1168 curve: old_pc,
1169 range: old_range,
1170 ..
1171 }) = old_repr
1172 else {
1173 return Ok(None);
1174 };
1175 let Some(old_pcurve) = model.geometry().pcurve(*old_pc) else {
1176 return Ok(None);
1177 };
1178 let Some(row) = iso_direction(old_pcurve, *old_range, tol)? else {
1181 return Ok(None);
1182 };
1183 let ((nu0, nu1), (nv0, nv1)) = surface.domain();
1184 let start = curve.point_at(range.0, tol)?;
1185 let end = curve.point_at(range.1, tol)?;
1186 let mut a = foot(surface, start, tol)?;
1187 let mut b = foot(surface, end, tol)?;
1188 let closed_u = surface.is_periodic_u() || surface.is_closed_u(tol);
1189 let closed_v = surface.is_periodic_v() || surface.is_closed_v(tol);
1190 let inside = |t: f64| -> OgeomResult<Point2> { foot(surface, curve.point_at(t, tol)?, tol) };
1192 if row {
1193 let mut v = f64::midpoint(a.y, b.y);
1195 (a.x, b.x) = run_ends(
1196 (a.x, b.x),
1197 (nu0, nu1),
1198 closed_u,
1199 range,
1200 |t| Ok(inside(t)?.x),
1201 )?;
1202 if let Some(side) = beside_face(&[a.y, b.y], (nv0, nv1), closed_v, (b.x > a.x) == along) {
1204 v = side;
1205 }
1206 a.y = v;
1207 b.y = v;
1208 } else {
1209 let m = inside(f64::midpoint(range.0, range.1))?.x;
1214 let mut u = a.x.max(b.x).min(m).max(a.x.min(b.x));
1215 (a.y, b.y) = run_ends(
1216 (a.y, b.y),
1217 (nv0, nv1),
1218 closed_v,
1219 range,
1220 |t| Ok(inside(t)?.y),
1221 )?;
1222 if let Some(side) = beside_face(&[u], (nu0, nu1), closed_u, (b.y > a.y) != along) {
1224 u = side;
1225 }
1226 a.x = u;
1227 b.x = u;
1228 }
1229 Ok(Some(ogeom_geom::Line2d::segment(a, b, tol)?.into()))
1230}
1231
1232fn foot(surface: &SurfaceGeometry, p: Point, tol: Tolerances) -> OgeomResult<Point2> {
1239 use ogeom_geom::Surface as _;
1240 let (mut u, mut v) = crate::measure::project_on_surface(surface, p, 24, tol)?.parameters;
1241 for _ in 0..8 {
1242 let Ok(jet) = surface.jet_at(u, v, tol) else {
1243 break;
1244 };
1245 let gap = jet.point - p;
1246 let r = [gap.dot(jet.du), gap.dot(jet.dv)];
1247 let j = [
1248 [
1249 jet.du.dot(jet.du) + gap.dot(jet.d2u),
1250 jet.du.dot(jet.dv) + gap.dot(jet.duv),
1251 ],
1252 [
1253 jet.du.dot(jet.dv) + gap.dot(jet.duv),
1254 jet.dv.dot(jet.dv) + gap.dot(jet.d2v),
1255 ],
1256 ];
1257 let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
1258 if !det.is_finite() || det.abs() <= f64::EPSILON * j[0][0].abs() * j[1][1].abs() {
1261 break;
1262 }
1263 let (du, dv) = (
1264 (r[0] * j[1][1] - r[1] * j[0][1]) / det,
1265 (r[1] * j[0][0] - r[0] * j[1][0]) / det,
1266 );
1267 let (nu, nv) = (u - du, v - dv);
1268 let closer = surface
1269 .point_at(nu, nv, tol)
1270 .is_ok_and(|q| q.distance(p) <= jet.point.distance(p));
1271 if !closer {
1272 break;
1273 }
1274 (u, v) = (nu, nv);
1275 if du.hypot(dv) <= tol.parametric() {
1276 break;
1277 }
1278 }
1279 Ok(Point2::new(u, v))
1280}
1281
1282fn iso_direction(
1286 pcurve: &ogeom_geom::PlanarCurve,
1287 range: (f64, f64),
1288 tol: Tolerances,
1289) -> OgeomResult<Option<bool>> {
1290 const SAMPLES: u32 = 8;
1291 let mut points = Vec::with_capacity(SAMPLES as usize + 1);
1292 for i in 0..=SAMPLES {
1293 let t = range.0 + (range.1 - range.0) * f64::from(i) / f64::from(SAMPLES);
1294 points.push(pcurve.point_at(t, tol)?);
1295 }
1296 let first = points[0];
1297 let spread = |coordinate: fn(Point2) -> f64| {
1298 points
1299 .iter()
1300 .map(|p| (coordinate(*p) - coordinate(first)).abs())
1301 .fold(0.0, f64::max)
1302 };
1303 let (across_u, across_v) = (spread(|p| p.x), spread(|p| p.y));
1304 let still = tol.parametric();
1305 Ok(match (across_u <= still, across_v <= still) {
1306 (false, true) => Some(true),
1307 (true, false) => Some(false),
1308 _ => None,
1309 })
1310}
1311
1312fn run_ends(
1325 ends: (f64, f64),
1326 (lo, hi): (f64, f64),
1327 closed: bool,
1328 range: (f64, f64),
1329 at: impl Fn(f64) -> OgeomResult<f64>,
1330) -> OgeomResult<(f64, f64)> {
1331 let (mut from, mut to) = ends;
1332 let span = hi - lo;
1333 let on_closure = |x: f64| (x - lo).abs() < span * 1e-6 || (x - hi).abs() < span * 1e-6;
1334 let middle = f64::midpoint(lo, hi);
1335 let step = (range.1 - range.0) * 0.02;
1336 if closed && on_closure(from) {
1337 from = if at(range.0 + step)? < middle { lo } else { hi };
1338 }
1339 if closed && on_closure(to) {
1340 to = if at(range.1 - step)? < middle { lo } else { hi };
1341 }
1342 if (from - to).abs() < span * 1e-9 {
1343 (from, to) = (lo, hi);
1344 }
1345 Ok((from, to))
1346}
1347
1348fn beside_face(at: &[f64], (lo, hi): (f64, f64), closed: bool, face_above: bool) -> Option<f64> {
1358 let span = hi - lo;
1359 let on_closure = |x: &f64| (x - lo).abs() < span * 1e-6 || (x - hi).abs() < span * 1e-6;
1360 (closed && at.iter().any(on_closure)).then_some(if face_above { lo } else { hi })
1361}
1362
1363fn exact_seam_columns(
1369 curve: &Curve,
1370 range: (f64, f64),
1371 surface: &SurfaceGeometry,
1372 tol: Tolerances,
1373) -> Option<(ogeom_geom::PlanarCurve, ogeom_geom::PlanarCurve)> {
1374 use ogeom_geom::Surface as _;
1375 let ((u0, u1), (v0, v1)) = surface.domain();
1376 const N: usize = 8;
1377 #[allow(clippy::cast_precision_loss)]
1378 let params: Vec<f64> = (0..=N)
1379 .map(|i| range.0 + (range.1 - range.0) * i as f64 / N as f64)
1380 .collect();
1381 let mut chart: Vec<Point2> = Vec::with_capacity(N + 1);
1382 for &t in ¶ms {
1383 let p = curve.point_at(t, tol).ok()?;
1384 let at = crate::measure::project_on_surface(surface, p, 24, tol).ok()?;
1385 chart.push(Point2::new(at.parameters.0, at.parameters.1));
1386 }
1387 let on = |x: f64, a: f64, b: f64| {
1388 let eps = (b - a).abs() * 1e-6;
1389 (x - a).abs() <= eps || (x - b).abs() <= eps
1390 };
1391 let (columns, lo, hi, period) = if chart.iter().all(|p| on(p.x, u0, u1)) {
1395 let closed = surface.is_periodic_v() || surface.is_closed_v(tol);
1396 (true, u0, u1, closed.then_some(v1 - v0))
1397 } else if chart.iter().all(|p| on(p.y, v0, v1)) {
1398 let closed = surface.is_periodic_u() || surface.is_closed_u(tol);
1399 (false, v0, v1, closed.then_some(u1 - u0))
1400 } else {
1401 return None;
1402 };
1403 let mut run: Vec<f64> = chart
1404 .iter()
1405 .map(|p| if columns { p.y } else { p.x })
1406 .collect();
1407 if let Some(period) = period {
1408 for i in 1..run.len() {
1409 while run[i] - run[i - 1] > period * 0.5 {
1410 run[i] -= period;
1411 }
1412 while run[i] - run[i - 1] < -period * 0.5 {
1413 run[i] += period;
1414 }
1415 }
1416 let (w0, w1) = if columns { (v0, v1) } else { (u0, u1) };
1419 let eps = period * 1e-9;
1420 let low = run.iter().copied().fold(f64::INFINITY, f64::min);
1421 let high = run.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1422 let shift = if low < w0 - eps {
1423 ((w0 - eps - low) / period).ceil() * period
1424 } else if high > w1 + eps {
1425 -((high - w1 - eps) / period).ceil() * period
1426 } else {
1427 0.0
1428 };
1429 for r in &mut run {
1430 *r += shift;
1431 }
1432 }
1433 let slope = (run[N] - run[0]) / (range.1 - range.0);
1434 let span = (run[N] - run[0]).abs();
1435 if span <= tol.parametric() {
1436 return None;
1437 }
1438 let affine = params
1439 .iter()
1440 .zip(&run)
1441 .all(|(t, r)| (run[0] + slope * (t - range.0) - r).abs() <= span * 1e-7);
1442 if !affine {
1443 return None;
1444 }
1445 let side = |at: f64| -> Option<ogeom_geom::PlanarCurve> {
1446 let (a, b) = if columns {
1447 (Point2::new(at, run[0]), Point2::new(at, run[N]))
1448 } else {
1449 (Point2::new(run[0], at), Point2::new(run[N], at))
1450 };
1451 let knots =
1452 ogeom_math::KnotVector::new(vec![range.0, range.0, range.1, range.1], 1).ok()?;
1453 Some(
1454 ogeom_geom::BSpline2d::new(knots, vec![a, b], tol)
1455 .ok()?
1456 .into(),
1457 )
1458 };
1459 Some((side(hi)?, side(lo)?))
1460}
1461
1462fn seam_lines(
1469 curve: &Curve,
1470 range: (f64, f64),
1471 surface: &SurfaceGeometry,
1472 row: bool,
1473 tol: Tolerances,
1474) -> OgeomResult<Option<(ogeom_geom::PlanarCurve, ogeom_geom::PlanarCurve)>> {
1475 use ogeom_geom::Surface as _;
1476 let ((u0, u1), (v0, v1)) = surface.domain();
1477 let (across, along, closes_across, closes_along) = if row {
1478 (
1479 (v0, v1),
1480 (u0, u1),
1481 surface.is_periodic_v() || surface.is_closed_v(tol),
1482 surface.is_periodic_u() || surface.is_closed_u(tol),
1483 )
1484 } else {
1485 (
1486 (u0, u1),
1487 (v0, v1),
1488 surface.is_periodic_u() || surface.is_closed_u(tol),
1489 surface.is_periodic_v() || surface.is_closed_v(tol),
1490 )
1491 };
1492 if !closes_across {
1493 return Ok(None);
1494 }
1495 let middle = foot(
1499 surface,
1500 curve.point_at(f64::midpoint(range.0, range.1), tol)?,
1501 tol,
1502 )?;
1503 let fixed = if row { middle.y } else { middle.x };
1504 let reach = (across.1 - across.0).abs() * 1e-6;
1505 if (fixed - across.0).abs() > reach && (fixed - across.1).abs() > reach {
1506 return Ok(None);
1507 }
1508 let free = |t: f64| -> OgeomResult<f64> {
1509 let at = foot(surface, curve.point_at(t, tol)?, tol)?;
1510 Ok(if row { at.x } else { at.y })
1511 };
1512 let (a, b) = run_ends(
1513 (free(range.0)?, free(range.1)?),
1514 along,
1515 closes_along,
1516 range,
1517 free,
1518 )?;
1519 if (b - a).abs() <= tol.parametric() {
1520 return Ok(None);
1521 }
1522 let side = |at: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
1523 let (from, to) = if row {
1524 (Point2::new(a, at), Point2::new(b, at))
1525 } else {
1526 (Point2::new(at, a), Point2::new(at, b))
1527 };
1528 let knots = ogeom_math::KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
1529 Ok(ogeom_geom::BSpline2d::new(knots, vec![from, to], tol)?.into())
1530 };
1531 Ok(Some((side(across.1)?, side(across.0)?)))
1532}
1533
1534fn seam_pcurves(
1540 curve: &Curve,
1541 range: (f64, f64),
1542 surface: &SurfaceGeometry,
1543 target: f64,
1544 tol: Tolerances,
1545) -> OgeomResult<(ogeom_geom::PlanarCurve, ogeom_geom::PlanarCurve)> {
1546 use ogeom_geom::Surface as _;
1547 let ((u0, u1), (v0, v1)) = surface.domain();
1548 let middle = curve.point_at(f64::midpoint(range.0, range.1), tol)?;
1549 let (u, v) = crate::measure::project_on_surface(surface, middle, 24, tol)?.parameters;
1550 let off = |x: f64, a: f64, b: f64| (x - a).abs().min((x - b).abs()) / (b - a).abs();
1551 let on_rows =
1552 (surface.is_periodic_v() || surface.is_closed_v(tol)) && off(v, v0, v1) < off(u, u0, u1);
1553 let (column, lo, hi) = if on_rows {
1554 (false, v0, v1)
1555 } else {
1556 (true, u0, u1)
1557 };
1558 let forward = fit_pcurve(
1559 curve,
1560 range,
1561 surface,
1562 Some((column, hi)),
1563 (None, None),
1564 target,
1565 tol,
1566 )?;
1567 let reversed = fit_pcurve(
1568 curve,
1569 range,
1570 surface,
1571 Some((column, lo)),
1572 (None, None),
1573 target,
1574 tol,
1575 )?;
1576 Ok((forward, reversed))
1577}
1578
1579#[expect(
1584 clippy::too_many_arguments,
1585 reason = "the old and new charts of one edge"
1586)]
1587fn degenerate_row(
1588 model: &Model,
1589 data: &ogeom_topo::EdgeData,
1590 old_surface: ogeom_topo::SurfaceId,
1591 edge: &Shape,
1592 old: &SurfaceGeometry,
1593 new: &SurfaceGeometry,
1594 pole: Point,
1595 walked_back: bool,
1596 tol: Tolerances,
1597) -> OgeomResult<ogeom_geom::Line2d> {
1598 let Some(EdgeRepr::PCurve {
1601 curve,
1602 range: row_range,
1603 ..
1604 }) = data.pcurve_for(old_surface, edge.location())
1605 else {
1606 ogeom_bail!(Construction, "a degenerate edge has no pcurve to restate");
1607 };
1608 let row_range = *row_range;
1609 let Some(row) = model.geometry().pcurve(*curve) else {
1610 ogeom_bail!(Dangling, "pcurve is not in this model");
1611 };
1612 let (lo, hi) = row.domain();
1613 let at = row.point_at(f64::midpoint(lo, hi), tol)?;
1614 let v_old = at.y;
1615 let ((ua_old, ub_old), (va_old, vb_old)) = old.domain();
1616 let ((nu0, nu1), (nv0, nv1)) = new.domain();
1617 let v_new =
1622 pole_row(new, pole, tol).unwrap_or(if (v_old - va_old).abs() <= (v_old - vb_old).abs() {
1623 nv0
1624 } else {
1625 nv1
1626 });
1627 let (row_u0, row_u1) = (
1633 row.point_at(row_range.0, tol)?.x,
1634 row.point_at(row_range.1, tol)?.x,
1635 );
1636 let partial = (row_u1 - row_u0).abs() < (ub_old - ua_old).abs() - tol.parametric();
1637 let middle = f64::midpoint(va_old, vb_old);
1638 if partial && middle.is_finite() {
1639 let column = |t: f64| -> OgeomResult<f64> {
1640 let u = row.point_at(t, tol)?.x;
1641 let p = old.point_at(u, middle, tol)?;
1642 Ok(foot(new, p, tol)?.x)
1643 };
1644 let closed_u = new.is_periodic_u() || new.is_closed_u(tol);
1645 let (a, b) = run_ends(
1646 (column(row_range.0)?, column(row_range.1)?),
1647 (nu0, nu1),
1648 closed_u,
1649 row_range,
1650 column,
1651 )?;
1652 return ogeom_geom::Line2d::segment(Point2::new(a, v_new), Point2::new(b, v_new), tol);
1653 }
1654 let (from, to) = (Point2::new(nu0, v_new), Point2::new(nu1, v_new));
1655 if walked_back {
1656 ogeom_geom::Line2d::segment(to, from, tol)
1657 } else {
1658 ogeom_geom::Line2d::segment(from, to, tol)
1659 }
1660}
1661
1662fn pole_row(surface: &SurfaceGeometry, pole: Point, tol: Tolerances) -> Option<f64> {
1666 const SAMPLES: u32 = 256;
1667 let ((u0, u1), (v0, v1)) = surface.domain();
1668 let u = f64::midpoint(u0, u1);
1669 let gap = |v: f64| {
1670 surface
1671 .point_at(u, v, tol)
1672 .map_or(f64::INFINITY, |p| p.distance(pole))
1673 };
1674 let at = |k: u32| v0 + (v1 - v0) * f64::from(k) / f64::from(SAMPLES);
1675 let best = (0..=SAMPLES).min_by(|&a, &b| gap(at(a)).total_cmp(&gap(at(b))))?;
1676 let (mut lo, mut hi) = (at(best.saturating_sub(1)), at((best + 1).min(SAMPLES)));
1677 let golden = (5.0_f64.sqrt() - 1.0) / 2.0;
1678 for _ in 0..80 {
1679 let (a, b) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1680 if gap(a) <= gap(b) {
1681 hi = b;
1682 } else {
1683 lo = a;
1684 }
1685 }
1686 let v = f64::midpoint(lo, hi);
1687 (gap(v) <= tol.confusion()).then_some(v)
1688}
1689
1690fn turned_by_rebuild(
1694 old: &SurfaceGeometry,
1695 placement: &ogeom_math::Transform,
1696 window: ChartWindow,
1697 new: &SurfaceGeometry,
1698 tol: Tolerances,
1699) -> Option<bool> {
1700 let ((u0, u1), (v0, v1)) = window;
1701 if !(u0.is_finite() && u1.is_finite() && v0.is_finite() && v1.is_finite()) {
1702 return None;
1703 }
1704 let (u, v) = (u0 + (u1 - u0) * 0.43, v0 + (v1 - v0) * 0.57);
1706 let at = placement.apply(old.point_at(u, v, tol).ok()?);
1707 let was = placement.apply_vector(old.normal_at(u, v, tol).ok()?.vector());
1708 let foot = crate::measure::project_on_surface(new, at, 16, tol).ok()?;
1709 let (nu, nv) = foot.parameters;
1710 let now = new.normal_at(nu, nv, tol).ok()?.vector();
1711 Some(was.dot(now) < 0.0)
1712}
1713
1714fn profile_held(
1719 surface: &SurfaceGeometry,
1720 window: ChartWindow,
1721 tol: Tolerances,
1722) -> SurfaceGeometry {
1723 let SurfaceGeometry::Revolution(_) = surface else {
1724 return surface.clone();
1725 };
1726 let ((du0, du1), (dv0, dv1)) = surface.domain();
1727 let (_, (v0, v1)) = window;
1728 if !(v0.is_finite() && v1.is_finite()) || v1 - v0 <= tol.parametric() {
1729 return surface.clone();
1730 }
1731 ogeom_geom::TrimmedSurface::new(surface.clone(), (du0, du1), (v0.max(dv0), v1.min(dv1)), tol)
1732 .map_or_else(
1733 |_| surface.clone(),
1734 |t| SurfaceGeometry::Trimmed(Box::new(t)),
1735 )
1736}
1737
1738fn bounded_to_face(
1745 model: &Model,
1746 face: &Shape,
1747 surface_id: ogeom_topo::SurfaceId,
1748 placed: &SurfaceGeometry,
1749 stretch: f64,
1750 tol: Tolerances,
1751) -> OgeomResult<(SurfaceGeometry, ChartWindow)> {
1752 let (mut u0, mut u1) = (f64::INFINITY, f64::NEG_INFINITY);
1753 let (mut v0, mut v1) = (f64::INFINITY, f64::NEG_INFINITY);
1754 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
1755 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
1756 continue;
1757 };
1758 let mut widen = |pc: ogeom_topo::PCurveId, range: (f64, f64)| -> OgeomResult<()> {
1759 let Some(pcurve) = model.geometry().pcurve(pc) else {
1760 return Ok(());
1761 };
1762 for i in 0..=8 {
1763 #[allow(clippy::cast_precision_loss)]
1764 let t = range.0 + (range.1 - range.0) * f64::from(i) / 8.0;
1765 let p = pcurve.point_at(t, tol)?;
1766 u0 = u0.min(p.x);
1767 u1 = u1.max(p.x);
1768 v0 = v0.min(p.y);
1769 v1 = v1.max(p.y);
1770 }
1771 Ok(())
1772 };
1773 match data.pcurve_for(surface_id, edge.location()) {
1774 Some(EdgeRepr::PCurve { curve, range, .. }) => widen(*curve, *range)?,
1775 Some(EdgeRepr::Seam {
1776 forward,
1777 reversed,
1778 range,
1779 ..
1780 }) => {
1781 widen(*forward, *range)?;
1782 widen(*reversed, *range)?;
1783 }
1784 _ => {}
1785 }
1786 }
1787 if !u0.is_finite() || u1 - u0 <= tol.confusion() || v1 - v0 <= tol.confusion() {
1788 return Ok((placed.clone(), placed.domain()));
1789 }
1790 let (su, sv) = match placed {
1795 SurfaceGeometry::Plane(_) => (stretch, stretch),
1796 SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_) => (1.0, stretch),
1797 _ => (1.0, 1.0),
1798 };
1799 let (u0, u1, v0, v1) = (u0 * su, u1 * su, v0 * sv, v1 * sv);
1800 let margin = ((u1 - u0) + (v1 - v0)) * 0.05 + tol.confusion();
1801 let bounded: SurfaceGeometry = match placed {
1802 SurfaceGeometry::Plane(p) => ogeom_geom::PlaneSurface::over(
1803 p.plane(),
1804 (u0 - margin, u1 + margin),
1805 (v0 - margin, v1 + margin),
1806 )?
1807 .into(),
1808 SurfaceGeometry::Cylinder(c) => {
1809 ogeom_geom::CylinderSurface::new(c.cylinder(), (v0 - margin, v1 + margin))?.into()
1810 }
1811 SurfaceGeometry::Cone(c) => {
1812 let cone = c.cone();
1815 let slope = cone.half_angle().tan();
1816 let (mut low, mut high) = (v0 - margin, v1 + margin);
1817 if slope.abs() > tol.angular() {
1818 let apex = -cone.reference_radius() / slope;
1819 if v0 >= apex - tol.confusion() {
1820 low = low.max(apex);
1821 }
1822 if v1 <= apex + tol.confusion() {
1823 high = high.min(apex);
1824 }
1825 }
1826 ogeom_geom::ConeSurface::new(cone, (low, high))?.into()
1827 }
1828 SurfaceGeometry::Offset(o) if !finite(o.basis().domain()) => {
1831 let ((du0, du1), (dv0, dv1)) = placed.domain();
1832 ogeom_geom::TrimmedSurface::new(
1833 placed.clone(),
1834 ((u0 - margin).max(du0), (u1 + margin).min(du1)),
1835 ((v0 - margin).max(dv0), (v1 + margin).min(dv1)),
1836 tol,
1837 )
1838 .map_or_else(
1839 |_| placed.clone(),
1840 |t| SurfaceGeometry::Trimmed(Box::new(t)),
1841 )
1842 }
1843 SurfaceGeometry::Revolution(_) => {
1847 let ((du0, du1), _) = placed.domain();
1848 return Ok((placed.clone(), ((du0, du1), (v0, v1))));
1849 }
1850 other => other.clone(),
1851 };
1852 let window = bounded.domain();
1853 Ok((bounded, window))
1854}
1855
1856fn finite(domain: ChartWindow) -> bool {
1857 [domain.0.0, domain.0.1, domain.1.0, domain.1.1]
1858 .iter()
1859 .all(|x| x.is_finite() && x.abs() < 1e6)
1860}
1861
1862fn transformed_patch(
1864 patch: &ogeom_geom::BSplineSurface,
1865 transform: &GeneralTransform,
1866) -> OgeomResult<ogeom_geom::BSplineSurface> {
1867 let grid = patch.grid();
1868 let mapped = grid.map(|c| {
1869 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
1870 let moved = transform.apply(p);
1871 Weighted {
1872 scaled: Point::from_vector(moved.to_vector() * c.weight),
1873 weight: c.weight,
1874 }
1875 });
1876 ogeom_geom::BSplineSurface::rational(patch.u_knots().clone(), patch.v_knots().clone(), mapped)
1877}
1878
1879#[derive(Default)]
1886struct NewVertices(FastMap<TShapeId, Vec<(Point, Shape)>>);
1887
1888impl NewVertices {
1889 fn at(
1891 &mut self,
1892 model: &mut Model,
1893 node: TShapeId,
1894 at: Point,
1895 reach: f64,
1896 tol: Tolerances,
1897 ) -> Shape {
1898 if let Some(found) = self.find(node, at, reach, tol) {
1899 return found;
1900 }
1901 let fresh = make_vertex(model, at).shape;
1902 self.0.entry(node).or_default().push((at, fresh.clone()));
1903 fresh
1904 }
1905
1906 fn find(&self, node: TShapeId, at: Point, reach: f64, tol: Tolerances) -> Option<Shape> {
1908 let reach = reach.max(tol.confusion());
1909 self.0
1910 .get(&node)?
1911 .iter()
1912 .find(|(p, _)| p.distance(at) <= reach)
1913 .map(|(_, found)| found.clone())
1914 }
1915}
1916
1917fn record(history: &mut History, input: &Shape, image: &Shape) {
1920 if !history.modified(input).iter().any(|s| s.is_same(image)) {
1921 history.modify(input, image.clone());
1922 }
1923}
1924
1925fn record_vertices(
1928 model: &Model,
1929 edge: &Shape,
1930 map: &dyn Fn(Point) -> Point,
1931 vertices: &NewVertices,
1932 history: &mut History,
1933 tol: Tolerances,
1934) -> OgeomResult<()> {
1935 for vertex in model.children_of(edge)? {
1936 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
1937 ogeom_bail!(Construction, "vertex node holds no vertex data");
1938 };
1939 let at = map(vertex.transform(model.datums())?.apply(data.point));
1940 if let Some(twin) = vertices.find(vertex.node(), at, data.tolerance.get(), tol) {
1941 record(history, &vertex, &twin);
1942 }
1943 }
1944 Ok(())
1945}
1946
1947fn placement_bits(t: &ogeom_math::Transform) -> [u64; 3] {
1949 let p = t.apply(Point::new(0.123_456_789, 9.87, -3.21));
1950 point_bits(p)
1951}
1952
1953fn point_bits(p: Point) -> [u64; 3] {
1954 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
1955}
1956
1957#[cfg(test)]
1958#[allow(clippy::unwrap_used, clippy::expect_used)]
1959mod tests {
1960 use super::*;
1961 use crate::{make_box, make_cylinder, volume_properties};
1962 use ogeom_math::Frame;
1963 use ogeom_mesh::Deflection;
1964
1965 const T: Tolerances = Tolerances::millimetres();
1966
1967 fn fine() -> Deflection {
1968 Deflection {
1969 chord: 1e-3,
1970 ..Deflection::default()
1971 }
1972 }
1973
1974 #[test]
1978 fn a_baked_prism_walks_each_edge_once_each_way() {
1979 let mut model = Model::new();
1980 let block = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
1981 .unwrap()
1982 .shape;
1983 let face = ogeom_topo::explore_unique(&model, &block, ShapeType::Face).unwrap()[0].clone();
1984 let prism = crate::make_prism(&mut model, &face, ogeom_math::Vector::new(0.0, 0.0, 5.0), T)
1985 .unwrap()
1986 .shape;
1987 let baked = baked_shape(&mut model, &prism, T).unwrap().shape;
1988 assert_eq!(crate::check::edges_walked_one_way(&model, &baked), 0);
1989 }
1990
1991 #[test]
1992 fn a_box_converts_to_nurbs_and_measures_the_same() {
1993 let mut model = Model::new();
1994 let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
1995 let converted = to_nurbs(&mut model, &solid.shape, T).unwrap();
1996
1997 for face in explore(&model, &converted.shape, Filter::OfType(ShapeType::Face)).unwrap() {
1999 let NodeData::Face(data) = model.node(&face).unwrap().data() else {
2000 panic!("face data");
2001 };
2002 assert!(matches!(
2003 model.geometry().surface(data.surface),
2004 Some(SurfaceGeometry::BSpline(_))
2005 ));
2006 }
2007 let props = volume_properties(&model, &converted.shape, fine(), T).unwrap();
2008 assert!((props.mass - 24.0).abs() < 1e-6, "volume {}", props.mass);
2009 assert!(
2010 crate::check(&model, &converted.shape, T)
2011 .unwrap()
2012 .is_valid(),
2013 "{}",
2014 crate::check(&model, &converted.shape, T).unwrap()
2015 );
2016 assert_eq!(
2017 converted.history.modified(&solid.shape),
2018 std::slice::from_ref(&converted.shape)
2019 );
2020 }
2021
2022 #[test]
2023 fn a_cylinder_converts_seams_poles_and_all() {
2024 let mut model = Model::new();
2025 let solid = make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
2026 let converted = to_nurbs(&mut model, &solid.shape, T).unwrap();
2027 let exact = core::f64::consts::PI * 4.0 * 5.0;
2028 let props = volume_properties(&model, &converted.shape, fine(), T).unwrap();
2029 assert!(
2030 (props.mass - exact).abs() < exact * 5e-3,
2031 "volume {} against {exact}",
2032 props.mass
2033 );
2034 }
2035
2036 #[test]
2037 fn a_uniform_scale_is_a_placement_and_leaves_the_surfaces_analytic() {
2038 let mut model = Model::new();
2044 let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
2045 let scale = GeneralTransform::new(
2046 ogeom_math::Matrix3 {
2047 rows: [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]],
2048 },
2049 ogeom_math::Vector::ZERO,
2050 );
2051
2052 let scaled = general_transformed_shape(&mut model, &solid.shape, &scale, T).unwrap();
2053
2054 for face in explore(&model, &scaled.shape, Filter::OfType(ShapeType::Face)).unwrap() {
2055 let NodeData::Face(data) = model.node(&face).unwrap().data() else {
2056 panic!("face data");
2057 };
2058 assert!(
2059 matches!(
2060 model.geometry().surface(data.surface),
2061 Some(SurfaceGeometry::Plane(_))
2062 ),
2063 "a scaled box's face stopped being a plane"
2064 );
2065 }
2066 let props = volume_properties(&model, &scaled.shape, fine(), T).unwrap();
2067 assert!((props.mass - 192.0).abs() < 1e-6, "scaled {}", props.mass);
2068 }
2069
2070 #[test]
2071 fn a_shear_preserves_volume_and_a_stretch_scales_it() {
2072 let mut model = Model::new();
2073 let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
2074
2075 let shear = GeneralTransform::new(
2077 ogeom_math::Matrix3 {
2078 rows: [[1.0, 0.5, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2079 },
2080 ogeom_math::Vector::ZERO,
2081 );
2082 let sheared = general_transformed_shape(&mut model, &solid.shape, &shear, T).unwrap();
2083 let props = volume_properties(&model, &sheared.shape, fine(), T).unwrap();
2084 assert!((props.mass - 24.0).abs() < 1e-6, "sheared {}", props.mass);
2085
2086 let stretch = GeneralTransform::new(
2088 ogeom_math::Matrix3 {
2089 rows: [[2.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2090 },
2091 ogeom_math::Vector::ZERO,
2092 );
2093 let stretched = general_transformed_shape(&mut model, &solid.shape, &stretch, T).unwrap();
2094 let props = volume_properties(&model, &stretched.shape, fine(), T).unwrap();
2095 assert!((props.mass - 48.0).abs() < 1e-6, "stretched {}", props.mass);
2096 }
2097
2098 #[test]
2099 fn a_sheared_cylinder_still_encloses_its_volume() {
2100 let mut model = Model::new();
2103 let solid = make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
2104 let shear = GeneralTransform::new(
2105 ogeom_math::Matrix3 {
2106 rows: [[1.0, 0.0, 0.4], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2107 },
2108 ogeom_math::Vector::ZERO,
2109 );
2110 let sheared = general_transformed_shape(&mut model, &solid.shape, &shear, T).unwrap();
2111 let exact = core::f64::consts::PI * 4.0 * 5.0;
2112 let props = volume_properties(&model, &sheared.shape, fine(), T).unwrap();
2113 assert!(
2114 (props.mass - exact).abs() < exact * 5e-3,
2115 "sheared cylinder {} against {exact}",
2116 props.mass
2117 );
2118 }
2119
2120 #[test]
2126 fn every_sub_shape_traces_to_its_twin_under_an_affine_map() {
2127 let shear = GeneralTransform::new(
2128 ogeom_math::Matrix3 {
2129 rows: [[1.0, 0.0, 0.4], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2130 },
2131 ogeom_math::Vector::new(1.0, 2.0, 3.0),
2132 );
2133 let stretch = GeneralTransform::new(
2134 ogeom_math::Matrix3 {
2135 rows: [[2.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.5]],
2136 },
2137 ogeom_math::Vector::ZERO,
2138 );
2139 let mut model = Model::new();
2140 let block = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
2141 .unwrap()
2142 .shape;
2143 let drum = make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T)
2144 .unwrap()
2145 .shape;
2146 let ball = crate::make_sphere(&mut model, Frame::WORLD, 2.0, T)
2147 .unwrap()
2148 .shape;
2149 for solid in [&block, &drum, &ball] {
2150 for transform in [&shear, &stretch] {
2151 let built = general_transformed_shape(&mut model, solid, transform, T).unwrap();
2152 assert!(built.history.trace(solid)[0].is_same(&built.shape));
2153 for kind in [
2154 ShapeType::Face,
2155 ShapeType::Wire,
2156 ShapeType::Edge,
2157 ShapeType::Vertex,
2158 ] {
2159 let parts = ogeom_topo::explore_unique(&model, solid, kind).unwrap();
2160 let result = ogeom_topo::explore_unique(&model, &built.shape, kind).unwrap();
2161 assert_eq!(parts.len(), result.len(), "{kind:?}");
2162 for part in &parts {
2163 assert!(built.history.is_affected(part), "{kind:?}");
2164 let images = built.history.trace(part);
2165 assert_eq!(images.len(), 1, "{kind:?}");
2166 let found: Vec<&Shape> =
2167 result.iter().filter(|r| r.is_same(&images[0])).collect();
2168 assert_eq!(found.len(), 1, "{kind:?}");
2169 if kind == ShapeType::Vertex {
2170 let point = |v: &Shape| {
2171 let data = model.node(v).unwrap().data().as_vertex().unwrap();
2172 v.transform(model.datums()).unwrap().apply(data.point)
2173 };
2174 let want = transform.apply(point(part));
2175 assert!(point(found[0]).distance(want) < 1e-9);
2176 }
2177 }
2178 }
2179 }
2180 }
2181 }
2182}