1use geop_core_geometry::{
38 intersection::{Intersections, crossing_points, curve_curve_intersect},
39 meet::{Offset, meet, tangent_groups},
40 nurb_curve::{NurbCurve, NurbCurve3D, true_point_fractions},
41 nurb_surface::NurbSurface3D,
42 search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
43};
44use geop_core_math::{geop_error::WithContext, with_context};
45use geop_core_math::{
46 geop_error::{GeopError, GeopResult},
47 scalars::Scalar,
48 solvers::refine::{Refine, Refined, refine},
49 vector::{Vector2, Vector3},
50};
51use geop_core_topology::{
52 Curve2, Curve3, Sense,
53 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec, FacesAround},
54 contains::face::{PointClassification, pcurves_contain},
55};
56use geop_core_topology::{FaceId, SolidId};
57use geop_ops_model::{BodyNames, NamedModel, Namer};
58
59use crate::shell::{pcurve, surface_missed};
60
61const FIRST_LEGS: usize = 8;
64const MOST_LEGS: usize = 256;
65
66const TRACE_DRIFT: f64 = 1e-5;
70
71const SEED: u64 = 0x2EB0_0000_0000_0001;
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum Smooth {
79 Refuse,
80 Follow,
81}
82
83pub fn replace_faces(
99 part: &mut NamedModel,
100 namer: &Namer,
101 faces: &[FaceId],
102 smooth: Smooth,
103 mut surface: impl FnMut(&str, &NurbSurface3D) -> GeopResult<NurbSurface3D>,
104) -> GeopResult<SolidId> {
105 let ctx = with_context!("replace_faces({}, faces={faces:?})", namer.root());
106 let Some(&first) = faces.first() else {
107 return Err(GeopError::new("pick the faces")).with_context(ctx);
108 };
109 let solid = part
110 .solid_of_face(first, "only faces of a solid can be moved")
111 .with_context(ctx)?;
112 let model = part.topology();
113 let all = model.solid_faces(solid).with_context(ctx)?;
114 if let Some(&f) = faces.iter().find(|f| !all.contains(f)) {
115 return Err(GeopError::new(format!(
116 "face {} is not of the same solid as face {}: pick the faces of one solid at a time",
117 part.name(f)?,
118 part.name(first)?
119 )))
120 .with_context(ctx);
121 }
122 let (mut spec, sources) = model.body_spec(&all, true).with_context(ctx)?;
123 let names = BodyNames {
124 solid: Some(namer.root()),
125 ..part.source_names(&sources)?
126 };
127 let mut replaced: Vec<bool> = sources.faces.iter().map(|f| faces.contains(f)).collect();
128 while let Some((f, g, e)) = tangent_to_kept(&spec, &replaced).with_context(ctx)? {
129 if smooth == Smooth::Follow {
130 replaced[g] = true;
131 continue;
132 }
133 return Err(GeopError::new(format!(
134 "face {} meets face {} tangentially along edge {} — a fillet or a smooth blend: moving a face next to one, without it, is not supported",
135 names.faces[f], names.faces[g], names.edges[e]
136 )))
137 .with_context(ctx);
138 }
139 for (f, face) in spec.faces.iter_mut().enumerate() {
140 if replaced[f] {
141 let roomy = room(face.surface.clone()).with_context(ctx)?;
142 face.surface = surface(&names.faces[f], &roomy).with_context(ctx)?;
143 }
144 }
145 let reseat = vec![false; spec.vertices.len()];
146 let result = rebuilt(&spec, &names, &replaced, &reseat).with_context(ctx)?;
147 part.replace_with_solid(&[solid.into()], result, names)
148 .with_context(ctx)
149}
150
151pub fn rebuilt(
162 spec: &BodySpec,
163 names: &BodyNames,
164 replaced: &[bool],
165 reseat: &[bool],
166) -> GeopResult<BodySpec> {
167 Rebuild::new(spec, names, replaced, reseat)?.result()
168}
169
170pub(crate) struct Placed<'a>(Rebuild<'a>);
174
175impl<'a> Placed<'a> {
176 pub(crate) fn new(
178 spec: &'a BodySpec,
179 names: &'a BodyNames,
180 replaced: &'a [bool],
181 reseat: &[bool],
182 ) -> GeopResult<Self> {
183 Rebuild::new(spec, names, replaced, reseat).map(Self)
184 }
185
186 pub(crate) fn turned(&self) -> Vec<usize> {
194 let rebuild = &self.0;
195 (rebuild.spec.edges.iter().enumerate())
196 .filter(|(_, edge)| {
197 edge.start != edge.end && (rebuild.moved[edge.start] || rebuild.moved[edge.end])
198 })
199 .filter(|(_, edge)| {
200 let was = rebuild.spec.vertices[edge.end].sub(&rebuild.spec.vertices[edge.start]);
201 let now = rebuild.points[edge.end].sub(&rebuild.points[edge.start]);
202 now.prod_dot(&was).definitely_less(Scalar::ZERO)
203 })
204 .map(|(e, _)| e)
205 .collect()
206 }
207
208 pub(crate) fn rebuilt(self) -> GeopResult<BodySpec> {
210 self.0.result()
211 }
212}
213
214pub fn tangent_to_kept(
221 spec: &BodySpec,
222 replaced: &[bool],
223) -> GeopResult<Option<(usize, usize, usize)>> {
224 for (e, uses) in spec.edge_uses().iter().enumerate() {
225 let (f, g) = match uses.as_slice() {
226 [(f, ..), (g, ..)] if replaced[*f] && !replaced[*g] => (*f, *g),
227 [(g, ..), (f, ..)] if replaced[*f] && !replaced[*g] => (*f, *g),
228 _ => continue,
229 };
230 if smooth_along(spec, &spec.edges[e], f, g)? {
231 return Ok(Some((f, g, e)));
232 }
233 }
234 Ok(None)
235}
236
237pub(crate) fn smooth_along(
240 spec: &BodySpec,
241 edge: &EdgeSpec,
242 f: usize,
243 g: usize,
244) -> GeopResult<bool> {
245 let t = edge.curve.mid_parameter()?;
246 let point = edge.curve.evaluate(t)?;
247 let normal = |f: usize| {
248 spec.faces[f]
249 .surface
250 .normal_at(&point, MAX_NODES, min_subdivision_size())
251 };
252 Ok(normal(f)?
253 .prod_cross(&normal(g)?)
254 .norm_sq()
255 .could_be_equal(Scalar::ZERO))
256}
257
258struct Rebuild<'a> {
260 spec: &'a BodySpec,
261 names: &'a BodyNames,
262 replaced: &'a [bool],
263 surfaces: Vec<NurbSurface3D>,
266 touched: Vec<bool>,
268 seams: Vec<Option<Curve3>>,
271 moved: Vec<bool>,
273 points: Vec<Vector3>,
274 curves: Vec<Option<Curve3>>,
276}
277
278impl<'a> Rebuild<'a> {
279 fn new(
280 spec: &'a BodySpec,
281 names: &'a BodyNames,
282 replaced: &'a [bool],
283 reseat: &[bool],
284 ) -> GeopResult<Self> {
285 let around = spec.vertex_faces()?;
286 let moved: Vec<bool> = (around.iter().enumerate())
287 .map(|(v, faces)| reseat[v] || faces.iter().any(|&(f, _)| replaced[f]))
288 .collect();
289 let touched: Vec<bool> = (0..spec.faces.len())
290 .map(|f| {
291 replaced[f]
292 || (around.iter().enumerate())
293 .any(|(v, faces)| moved[v] && faces.iter().any(|&(g, _)| g == f))
294 })
295 .collect();
296 let surfaces = (spec.faces.iter().zip(&touched).zip(replaced))
300 .map(|((face, &touched), &replaced)| {
301 if touched && !replaced {
302 room(face.surface.clone())
303 } else {
304 Ok(face.surface.clone())
305 }
306 })
307 .collect::<GeopResult<Vec<_>>>()?;
308 let mut rebuild = Self {
309 spec,
310 names,
311 replaced,
312 surfaces,
313 touched,
314 seams: Vec::new(),
315 moved,
316 points: spec.vertices.clone(),
317 curves: Vec::new(),
318 };
319 let along: Vec<Vec<(usize, Sense, &Curve2)>> = spec.edge_uses();
320 rebuild.seams = (0..spec.edges.len())
321 .map(|e| {
322 rebuild
323 .seam(e, &along[e])
324 .map_err(|err| err.with_context(format!("edge {}", names.edges[e])))
325 })
326 .collect::<GeopResult<_>>()?;
327 for (v, faces) in around.iter().enumerate() {
328 if rebuild.moved[v] {
329 rebuild.points[v] = rebuild
330 .vertex(v, faces)
331 .map_err(|e| e.with_context(format!("moving vertex {}", names.vertices[v])))?;
332 }
333 }
334 Ok(rebuild)
335 }
336
337 fn vertex(&self, v: usize, faces: &FacesAround) -> GeopResult<Vector3> {
340 let seed = self.spec.vertices[v];
341 let mut pieces = Vec::with_capacity(faces.len());
342 for &(f, uv) in faces {
343 let surface = &self.surfaces[f];
344 let at_pole = surface.pole_at(uv[0], uv[1]).is_some();
347 let (u, w) = if self.replaced[f] && !at_pole {
348 let (u, w) = surface
349 .project_in_domain(seed, uv[0].sharpen(), uv[1].sharpen(), PROJECT_ITERATIONS)
350 .map_err(|e| {
351 e.with_context(format!(
352 "projecting {seed:?} onto the new surface of face {} from {uv:?}",
353 self.names.faces[f]
354 ))
355 })?;
356 (u.sharpen(), w.sharpen())
357 } else {
358 (uv[0], uv[1])
359 };
360 pieces.push((surface, (u, w)));
361 }
362 let poles: Vec<_> = (pieces.iter())
372 .map(|(s, (u, w))| s.pole_at(*u, *w))
373 .collect();
374 if let Some(pole) = poles.iter().flatten().next()
375 && poles
376 .iter()
377 .all(|p| p.is_some_and(|p| p.could_be_equal(pole)))
378 {
379 return Ok(*pole);
380 }
381 let groups = tangent_groups(&pieces)
382 .map_err(|e| e.with_context(format!("grouping its faces' surfaces at {pieces:?}")))?;
383 let on_border = (self.spec.edge_uses().iter().zip(&self.spec.edges))
387 .any(|(uses, edge)| uses.len() == 1 && (edge.start == v || edge.end == v));
388 let fewer = |determined: GeopResult<Vector3>| match determined {
389 Err(_) if on_border => nearest(seed, &pieces, &groups),
390 determined => determined,
391 };
392 match groups.len() {
393 0 | 1 => fewer(self.within(faces)),
394 2 => fewer(self.on_seam(v, faces, &pieces, &groups)),
395 n => nearest(seed, &pieces, &groups).map_err(|e| {
399 let names: Vec<&str> = faces.iter().map(|&(f, _)| self.names.faces[f].as_str()).collect();
400 GeopError::new(format!(
401 "the {n} smooth surfaces of faces {names:?} around it do not meet in one point: it would open into an edge, which is not supported ({})",
402 e.root_message()
403 ))
404 }),
405 }
406 .map_err(|e| e.with_context(format!("where its {} smooth surfaces meet", groups.len())))
407 }
408
409 fn within(&self, faces: &FacesAround) -> GeopResult<Vector3> {
415 let mut point: Option<Vector3> = None;
416 for &(f, uv) in faces {
417 let here = self.surfaces[f].evaluate(uv[0], uv[1])?;
418 point = Some(match point {
419 None => here,
420 Some(p) if p.could_be_equal(&here) => {
421 Vector3::from_array(std::array::from_fn(|k| p[k].intersect(here[k])))
422 }
423 Some(p) => {
424 return Err(GeopError::new(format!(
425 "it lies within one smooth surface, whose pieces now put it at {p:?} and at {here:?}: where it goes is not determined"
426 )));
427 }
428 });
429 }
430 point.ok_or_else(|| GeopError::new("no face meets there"))
431 }
432
433 fn on_seam(
438 &self,
439 v: usize,
440 faces: &FacesAround,
441 pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
442 groups: &[Vec<usize>],
443 ) -> GeopResult<Vector3> {
444 let group_of = |f: usize| {
445 let i = faces.iter().position(|&(g, _)| g == f)?;
446 groups.iter().position(|group| group.contains(&i))
447 };
448 let uses = self.spec.edge_uses();
449 let seams: Vec<(usize, &Curve3, usize)> = (self.spec.edges.iter().enumerate())
450 .filter(|(_, edge)| edge.start == v || edge.end == v)
451 .filter_map(|(e, _)| {
452 let curve = self.seams[e].as_ref()?;
453 let [(f, ..), (g, ..)] = uses[e].as_slice() else {
454 return None;
455 };
456 let group = group_of(*f)?;
457 (group_of(*g) == Some(group)).then_some((e, curve, group))
458 })
459 .collect();
460 let seed = self.spec.vertices[v];
461 let mut found: Option<(usize, Vector3)> = None;
465 for &(e, curve, group) in &seams {
466 let point = self.crossing(curve, &groups[1 - group], pieces, seed)?;
467 match found {
468 Some((first, at)) if !at.could_be_equal(&point) => {
469 return Err(GeopError::new(format!(
470 "it ends the seams {} and {}, which no longer meet: they cross the other surface at {at:?} and at {point:?} — taking them apart is not supported",
471 self.names.edges[first], self.names.edges[e]
472 )));
473 }
474 Some(_) => {}
475 None => found = Some((e, point)),
476 }
477 }
478 found.map(|(_, p)| p).ok_or_else(|| {
479 GeopError::new(
480 "only two smooth surfaces meet there, and no edge from it runs within one of them: where it goes is not determined",
481 )
482 })
483 }
484
485 fn crossing(
488 &self,
489 curve: &Curve3,
490 other: &[usize],
491 pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
492 seed: Vector3,
493 ) -> GeopResult<Vector3> {
494 let mut best: Option<(Vector3, f64)> = None;
495 for &i in other {
496 let (surface, _) = pieces[i];
497 let points = crossing_points(curve, surface).map_err(|e| {
498 e.with_context(format!("the seam from it crossing surface {i}, moved"))
499 })?;
500 for (point, _) in points {
501 let distance = point.sub(&seed).norm_sq().to_f64();
502 if best.is_none_or(|(_, d)| distance < d) {
503 best = Some((point, distance));
504 }
505 }
506 }
507 best.map(|(p, _)| p).ok_or_else(|| {
508 GeopError::new(format!(
509 "the seam from it, carried on, does not cross the moved surface: the face around {seed:?} no longer reaches it"
510 ))
511 })
512 }
513
514 fn border(
520 &self,
521 edge: &EdgeSpec,
522 (f, sense, pcurve): (usize, Sense, &Curve2),
523 ) -> GeopResult<Curve3> {
524 let (start, end) = (self.points[edge.start], self.points[edge.end]);
525 let surface = &self.surfaces[f];
526 let mut curve = None;
527 if edge.curve.degree == 1 && edge.curve.control_points.len() == 2 {
528 let line = NurbCurve3D::line(start, end)?;
529 if surface_missed(&line, [f], &self.surfaces)?.is_none() {
530 curve = Some(line);
531 }
532 }
533 let curve = match curve {
534 Some(line) => line,
535 None => {
536 let along = match sense {
543 Sense::Forward => pcurve.clone(),
544 Sense::Reversed => pcurve.reverse(),
545 };
546 let (t0, t1) = edge.curve.domain();
547 let (p0, p1) = along.domain();
548 let at = |frac: Scalar| -> GeopResult<Vector3> {
549 let point = edge
550 .curve
551 .evaluate(Scalar::interpolate(t0, t1, frac).sharpen())?;
552 let seed = along.evaluate(Scalar::interpolate(p0, p1, frac).sharpen())?;
553 let (u, v) = surface.project_in_domain(
554 point,
555 seed[0].sharpen(),
556 seed[1].sharpen(),
557 PROJECT_ITERATIONS,
558 )?;
559 surface.evaluate(u, v)
560 };
561 fitted(at, (start, end), |_| Ok(()))
562 .map_err(|e| e.with_context("fitting it on its face's new surface"))?
563 }
564 };
565 if let Some((_, middle)) = surface_missed(&curve, [f], &self.surfaces)? {
566 return Err(GeopError::new(format!(
567 "its new curve from {start:?} to {end:?} is not on face {} at {middle:?}",
568 self.names.faces[f]
569 )));
570 }
571 Ok(curve)
572 }
573
574 fn curve(&self, e: usize, uses: &[(usize, Sense, &Curve2)]) -> GeopResult<Option<Curve3>> {
576 let edge = &self.spec.edges[e];
577 let changed = self.moved[edge.start]
578 || self.moved[edge.end]
579 || uses.iter().any(|&(f, ..)| self.replaced[f]);
580 if !changed {
581 return Ok(None);
582 }
583 let (start, end) = (self.points[edge.start], self.points[edge.end]);
584 if edge.start != edge.end && start.could_be_equal(&end) {
585 return Err(GeopError::new(format!(
586 "moved this far, it shrinks to a point at {start:?}: a face it bounds would vanish"
587 )));
588 }
589 if let [border] = uses {
590 return self.border(edge, *border).map(Some);
591 }
592 let [(f, f_sense, f_pcurve), (g, g_sense, g_pcurve)] = uses else {
593 return Err(GeopError::new(format!(
594 "it bounds {} faces, not one or two: moving it is not supported",
595 uses.len()
596 )));
597 };
598 let faces = [*f, *g];
599 let straight = (edge.curve.degree == 1 && edge.curve.control_points.len() == 2)
602 .then(|| NurbCurve3D::line(start, end))
603 .transpose()?
604 .filter(|line| {
605 matches!(surface_missed(line, faces, &self.surfaces), Ok(None))
606 && turns_around(&edge.curve, &[start, end]).is_ok()
607 });
608 let curve = match (&self.seams[e], straight) {
609 (Some(seam), straight) => match (restricted(seam, start, end), straight) {
614 (Ok(along), _) => along,
615 (Err(_), Some(line)) => line,
616 (Err(err), None) => {
617 return Err(err.with_context(format!(
618 "it is a seam between faces {} and {}, pieces of one smooth surface",
619 self.names.faces[*f], self.names.faces[*g]
620 )));
621 }
622 },
623 (None, Some(line)) => line,
624 (None, None) => {
625 let uses = [(*f, f_sense, *f_pcurve), (*g, g_sense, *g_pcurve)];
626 match self.on_iso_line(edge, start, end, uses)? {
627 Some(iso) => iso,
628 None => self.traced(&edge.curve, start, end, uses)?,
629 }
630 }
631 };
632 if let Some((face, middle)) = surface_missed(&curve, faces, &self.surfaces)? {
633 return Err(GeopError::new(format!(
634 "its new curve from {start:?} to {end:?} is not on face {} at {middle:?}",
635 self.names.faces[face]
636 )));
637 }
638 Ok(Some(curve))
639 }
640
641 fn on_iso_line(
647 &self,
648 edge: &EdgeSpec,
649 start: Vector3,
650 end: Vector3,
651 uses: [(usize, &Sense, &Curve2); 2],
652 ) -> GeopResult<Option<Curve3>> {
653 let faces = uses.map(|(f, ..)| f);
654 for (f, sense, pcurve) in uses {
655 let along = match sense {
656 Sense::Forward => pcurve.clone(),
657 Sense::Reversed => pcurve.reverse(),
658 };
659 let Some(line) = iso_line(&self.surfaces[f], &along)? else {
660 continue;
661 };
662 let Ok(curve) = restricted(&line, start, end) else {
663 continue;
664 };
665 if surface_missed(&curve, faces, &self.surfaces)?.is_none()
666 && turns_around(&edge.curve, &[start, end]).is_ok()
667 {
668 return Ok(Some(curve));
669 }
670 }
671 Ok(None)
672 }
673
674 fn seam(&self, e: usize, uses: &[(usize, Sense, &Curve2)]) -> GeopResult<Option<Curve3>> {
682 let [(f, f_sense, f_pcurve), (g, _, g_pcurve)] = uses else {
683 return Ok(None);
684 };
685 if !self.touched[*f] && !self.touched[*g] {
686 return Ok(None);
687 }
688 let normal = |f: usize, pcurve: &Curve2| -> GeopResult<Vector3> {
689 let uv = pcurve.evaluate(pcurve.mid_parameter()?)?;
690 self.surfaces[f].normal(uv[0], uv[1])
691 };
692 let smooth = normal(*f, f_pcurve)?
693 .prod_cross(&normal(*g, g_pcurve)?)
694 .norm_sq()
695 .could_be_equal(Scalar::ZERO);
696 if !smooth {
697 return Ok(None);
698 }
699 match (self.replaced[*f], self.replaced[*g]) {
700 (false, false) => Ok(Some(carried_on(&self.spec.edges[e].curve, |_| Ok(true))?)),
701 (true, true) => {
702 let along = match f_sense {
703 Sense::Forward => (*f_pcurve).clone(),
704 Sense::Reversed => f_pcurve.reverse(),
705 };
706 let surface = &self.surfaces[*f];
707 if let Some(line) = iso_line(surface, &along)? {
708 return Ok(Some(line));
709 }
710 let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
719 let pcurve = carried_on(&along, |longer| {
720 let (a, b) = longer.ends()?;
721 for p in [a, b] {
722 let inside = !p[0].definitely_less(u0)
723 && !p[0].definitely_greater(u1)
724 && !p[1].definitely_less(v0)
725 && !p[1].definitely_greater(v1);
726 if !inside {
727 return Ok(false);
728 }
729 let point = surface.evaluate(p[0], p[1])?;
730 if (0..3).any(|k| point[k].width().to_f64() > TRACE_DRIFT) {
731 return Ok(false);
732 }
733 }
734 Ok(true)
735 })?;
736 let (t0, t1) = pcurve.domain();
737 let at = |frac: Scalar| -> GeopResult<Vector3> {
738 let uv = pcurve.evaluate(Scalar::interpolate(t0, t1, frac).sharpen())?;
739 surface.evaluate(uv[0], uv[1])
740 };
741 let ends = (at(Scalar::ZERO)?, at(Scalar::ONE)?);
742 Ok(Some(fitted(at, ends, |_| Ok(())).map_err(|err| {
743 err.with_context("fitting the seam on its new surface")
744 })?))
745 }
746 _ => Err(GeopError::new(format!(
747 "faces {} and {} are pieces of one smooth surface, and only one of them is put on another surface: not supported",
748 self.names.faces[*f], self.names.faces[*g]
749 ))),
750 }
751 }
752
753 fn traced(
758 &self,
759 guide: &Curve3,
760 start: Vector3,
761 end: Vector3,
762 uses: [(usize, &Sense, &Curve2); 2],
763 ) -> GeopResult<Curve3> {
764 let (t0, t1) = guide.domain();
765 let (a, b) = guide.ends()?;
766 let (shift0, shift1) = (start.sub(&a), end.sub(&b));
767 let on_both = |frac: Scalar| -> GeopResult<Vector3> {
771 let t = Scalar::interpolate(t0, t1, frac).sharpen();
772 let seed = guide
773 .evaluate(t)?
774 .add(&shift0.prod_scalar(Scalar::ONE.sub(frac)))
775 .add(&shift1.prod_scalar(frac))
776 .sharpen();
777 let normal = guide.tangent(t)?.normalize()?.sharpen();
778 let mut offsets = Vec::with_capacity(2);
779 for (f, sense, pcurve) in uses {
780 let (p0, p1) = pcurve.domain();
781 let along = match sense {
782 Sense::Forward => frac,
783 Sense::Reversed => Scalar::ONE.sub(frac),
784 };
785 let uv = pcurve.evaluate(Scalar::interpolate(p0, p1, along).sharpen())?;
786 offsets.push(Offset::new(
787 &self.surfaces[f],
788 (uv[0].sharpen(), uv[1].sharpen()),
789 Scalar::ZERO,
790 ));
791 }
792 meet(seed, &mut offsets, &[(normal, seed)])
793 };
794 fitted(on_both, (start, end), |points| turns_around(guide, points))
795 .map_err(|e| e.with_context("tracing it where its faces meet"))
796 }
797
798 fn result(mut self) -> GeopResult<BodySpec> {
801 let spec = self.spec;
802 let along = spec.edge_uses();
803 let curves = (0..spec.edges.len())
804 .map(|e| {
805 self.curve(e, &along[e])
806 .map_err(|err| err.with_context(format!("edge {}", self.names.edges[e])))
807 })
808 .collect::<GeopResult<_>>()?;
809 self.curves = curves;
810 let edges = (spec.edges.iter().zip(&self.curves))
811 .map(|(edge, curve)| EdgeSpec {
812 curve: curve.clone().unwrap_or_else(|| edge.curve.clone()),
813 start: edge.start,
814 end: edge.end,
815 })
816 .collect();
817 let faces = (spec.faces.iter().enumerate())
818 .map(|(f, face)| {
819 let lp = |coedges: &Vec<CoedgeSpec>| self.face_loop(f, coedges);
820 let rebuilt_face = FaceSpec {
821 surface: self.surfaces[f].clone(),
822 outer: lp(&face.outer)?,
823 holes: face.holes.iter().map(lp).collect::<GeopResult<_>>()?,
824 };
825 for hole in &rebuilt_face.holes {
829 if !hole_inside(&rebuilt_face.surface, &rebuilt_face.outer, hole)? {
830 return Err(GeopError::new(format!(
831 "face {} runs into its own hole: the sides of a wall would cross",
832 self.names.faces[f]
833 )));
834 }
835 }
836 Ok(rebuilt_face)
837 })
838 .collect::<GeopResult<_>>()?;
839 Ok(BodySpec {
840 vertices: self.points.clone(),
841 edges,
842 faces,
843 shells: spec.shells.clone(),
844 solid: spec.solid,
845 })
846 }
847
848 fn face_loop(&self, f: usize, coedges: &[CoedgeSpec]) -> GeopResult<Vec<CoedgeSpec>> {
852 let surface = &self.surfaces[f];
853 let replaced = self.replaced[f];
854 let at_pole = |c: &CoedgeSpec| -> GeopResult<bool> {
859 let CoedgeOn::Vertex(v) = c.on else {
860 return Ok(true);
861 };
862 let end = c.pcurve.ends()?.1;
863 Ok(surface
864 .pole_at(end[0], end[1])
865 .is_some_and(|pole| pole.could_be_equal(&self.points[v])))
866 };
867 let mut kept = Vec::with_capacity(coedges.len());
868 for c in coedges {
869 if at_pole(c)? {
870 kept.push(c.clone());
871 }
872 }
873 let coedges = &kept[..];
874 let mut junctions = Vec::with_capacity(coedges.len());
875 for c in coedges {
876 let (_, end) = self.spec.ends(c.on)?;
877 let old = c.pcurve.ends()?.1;
878 let kept = |old: Vector2| -> GeopResult<bool> {
879 Ok(surface
880 .evaluate(old[0], old[1])?
881 .could_be_equal(&self.points[end]))
882 };
883 junctions.push(if (self.moved[end] || replaced) && !kept(old)? {
884 let (u, v) = surface.project_in_domain(
885 self.points[end],
886 old[0].sharpen(),
887 old[1].sharpen(),
888 PROJECT_ITERATIONS,
889 )?;
890 Vector2::from_array([u, v])
891 } else {
892 old
893 });
894 }
895 let n = coedges.len();
896 let mut out = Vec::with_capacity(n);
897 for (k, c) in coedges.iter().enumerate() {
898 let changed = match c.on {
899 CoedgeOn::Edge(e, _) => self.curves[e].is_some(),
900 CoedgeOn::Vertex(v) => self.moved[v],
901 };
902 let pcurve = if !changed && !replaced {
903 c.pcurve.clone()
904 } else {
905 let (start, end) = (junctions[(k + n - 1) % n], junctions[k]);
906 let curve = match c.on {
907 CoedgeOn::Edge(e, sense) => Some(
908 sense.orient(self.curves[e].as_ref().unwrap_or(&self.spec.edges[e].curve)),
909 ),
910 CoedgeOn::Vertex(_) => None,
911 };
912 pcurve(surface, &c.pcurve, curve.as_ref(), start, end).map_err(|e| {
913 e.with_context(format!("the pcurve on face {}", self.names.faces[f]))
914 })?
915 };
916 out.push(CoedgeSpec { on: c.on, pcurve });
917 }
918 Ok(out)
919 }
920}
921
922fn iso_line(surface: &NurbSurface3D, along: &Curve2) -> GeopResult<Option<Curve3>> {
928 if along.degree != 1 || along.control_points.len() != 2 {
929 return Ok(None);
930 }
931 let end = |i: usize| -> GeopResult<Vector2> {
934 let p = along.control_points[i];
935 if p[2].is_exactly(Scalar::ONE) {
936 Ok(Vector2::from_array([p[0], p[1]]))
937 } else {
938 p.dehomogenized()
939 }
940 };
941 let (a, b) = (end(0)?, end(1)?);
942 let Some(k) = (0..2).find(|&k| a[k].is_exactly(b[k]) && a[k].is_sharp()) else {
943 return Ok(None);
944 };
945 let u_fixed = k == 0;
946 let at = a[k];
947 let (lo, hi) = if u_fixed {
948 surface.domain_u()
949 } else {
950 surface.domain_v()
951 };
952 let ends = at.is_exactly(lo) || at.is_exactly(hi);
953 let inside = at.definitely_greater(lo) && at.definitely_less(hi);
954 if !(ends || inside) {
955 return Ok(None);
956 }
957 let row = surface.iso_curve(u_fixed, at)?;
958 Ok(if b[1 - k].definitely_greater(a[1 - k]) {
959 Some(row)
960 } else if b[1 - k].definitely_less(a[1 - k]) {
961 Some(row.reverse())
962 } else {
963 None
964 })
965}
966
967fn turns_around(guide: &Curve3, points: &[Vector3]) -> GeopResult<()> {
971 let (t0, t1) = guide.domain();
972 let n = points.len() - 1;
973 for (k, pair) in points.windows(2).enumerate() {
974 let frac = Scalar::from_ratio(2 * k as i64 + 1, 2 * n as i64)?;
975 let tangent = guide.tangent(Scalar::interpolate(t0, t1, frac).sharpen())?;
976 if !pair[1]
977 .sub(&pair[0])
978 .prod_dot(&tangent)
979 .definitely_greater(Scalar::ZERO)
980 {
981 return Err(GeopError::new(format!(
982 "moved this far, it turns around between {:?} and {:?}: a face it bounds would vanish, or two would cross",
983 pair[0], pair[1]
984 )));
985 }
986 }
987 Ok(())
988}
989
990pub fn room(surface: NurbSurface3D) -> GeopResult<NurbSurface3D> {
1004 let mut surface = surface;
1005 let straight = |s: &NurbSurface3D, along_u| -> GeopResult<bool> {
1009 Ok(straight_extended(s, along_u, [None, None])?.is_some())
1010 };
1011 for along_u in [true, false] {
1012 if straight(&surface, along_u)? {
1013 let mut halved = [None, None];
1014 for (side, at_end) in [false, true].into_iter().enumerate() {
1015 if side_is_pole(&surface, along_u, at_end)? {
1016 continue;
1017 }
1018 for k in 0..4 {
1019 let mut one = [None, None];
1020 one[side] = Some(k);
1021 if let Some(longer) = straight_extended(&surface, along_u, one)?
1022 && longer.has_normal_everywhere()?
1023 {
1024 halved[side] = Some(k);
1025 break;
1026 }
1027 }
1028 }
1029 if let Some(longer) = straight_extended(&surface, along_u, halved)? {
1030 surface = longer;
1031 }
1032 }
1033 }
1034 for along_u in [true, false] {
1035 if straight(&surface, along_u)? {
1036 continue;
1037 }
1038 for at_end in [false, true] {
1039 if side_is_pole(&surface, along_u, at_end)? {
1040 continue;
1041 }
1042 let breaks = if along_u {
1046 surface.breakpoints_u()
1047 } else {
1048 surface.breakpoints_v()
1049 };
1050 let n = breaks.len();
1051 let mut by = if at_end {
1052 breaks[n - 1].sub(breaks[n - 2])
1053 } else {
1054 breaks[1].sub(breaks[0])
1055 };
1056 for _ in 0..4 {
1057 if let Ok(longer) = surface.extended(along_u, at_end, by.sharpen())
1058 && longer.has_normal_everywhere()?
1059 {
1060 surface = longer;
1061 break;
1062 }
1063 by = by.div(Scalar::TWO)?;
1064 }
1065 }
1066 }
1067 Ok(surface)
1068}
1069
1070fn straight_extended(
1078 surface: &NurbSurface3D,
1079 along_u: bool,
1080 halved: [Option<u32>; 2],
1081) -> GeopResult<Option<NurbSurface3D>> {
1082 let (degree, num, other) = if along_u {
1083 (surface.degree_u, surface.num_u, surface.num_v)
1084 } else {
1085 (surface.degree_v, surface.num_v, surface.num_u)
1086 };
1087 if degree != 1 || num != 2 {
1088 return Ok(None);
1089 }
1090 let index = |row: usize, j: usize| {
1091 if along_u {
1092 row * surface.num_v + j
1093 } else {
1094 j * surface.num_v + row
1095 }
1096 };
1097 let cps = &surface.control_points;
1098 if !(0..other).all(|j| cps[index(0, j)][3].could_be_equal(cps[index(1, j)][3])) {
1099 return Ok(None);
1100 }
1101 let knots = if along_u {
1102 &surface.knot_vector_u
1103 } else {
1104 &surface.knot_vector_v
1105 };
1106 let (mut lo, mut hi) = (knots[1], knots[2]);
1107 let width = hi.sub(lo);
1108 let mut control_points = cps.clone();
1109 for (end, k) in halved.into_iter().enumerate() {
1110 let Some(k) = k else {
1111 continue;
1112 };
1113 let fraction = Scalar::from_ratio(1, 1 << k)?;
1114 for j in 0..other {
1115 let (a, b) = (cps[index(1 - end, j)], cps[index(end, j)]);
1116 control_points[index(end, j)] = if k == 0 {
1118 b.prod_scalar(Scalar::TWO).sub(&a)
1119 } else {
1120 b.add(&b.sub(&a).prod_scalar(fraction))
1121 };
1122 }
1123 let by = if k == 0 { width } else { width.mul(fraction) };
1124 if end == 1 {
1125 hi = hi.add(by);
1126 } else {
1127 lo = lo.sub(by);
1128 }
1129 }
1130 let knots = vec![lo, lo, hi, hi];
1131 Ok(Some(if along_u {
1132 NurbSurface3D::try_new(
1133 1,
1134 surface.degree_v,
1135 control_points,
1136 knots,
1137 surface.knot_vector_v.clone(),
1138 )?
1139 } else {
1140 NurbSurface3D::try_new(
1141 surface.degree_u,
1142 1,
1143 control_points,
1144 surface.knot_vector_u.clone(),
1145 knots,
1146 )?
1147 }))
1148}
1149
1150fn side_is_pole(surface: &NurbSurface3D, along_u: bool, at_end: bool) -> GeopResult<bool> {
1154 let (nu, nv) = (surface.num_u, surface.num_v);
1155 let row: Vec<usize> = if along_u {
1156 let i = if at_end { nu - 1 } else { 0 };
1157 (0..nv).map(|j| i * nv + j).collect()
1158 } else {
1159 let j = if at_end { nv - 1 } else { 0 };
1160 (0..nu).map(|i| i * nv + j).collect()
1161 };
1162 let first = surface.control_points[row[0]].dehomogenized()?;
1163 for &k in &row[1..] {
1164 if !surface.control_points[k]
1165 .dehomogenized()?
1166 .could_be_equal(&first)
1167 {
1168 return Ok(false);
1169 }
1170 }
1171 Ok(true)
1172}
1173
1174fn nearest(
1179 seed: Vector3,
1180 pieces: &[(&NurbSurface3D, (Scalar, Scalar))],
1181 groups: &[Vec<usize>],
1182) -> GeopResult<Vector3> {
1183 let mut offsets: Vec<Offset> = groups
1184 .iter()
1185 .map(|group| Offset {
1186 pieces: group.iter().map(|&i| pieces[i]).collect(),
1187 offset: Scalar::ZERO,
1188 })
1189 .collect();
1190 meet(seed.sharpen(), &mut offsets, &[])
1191}
1192
1193pub(crate) fn along(curve: &Curve3, start: Vector3, end: Vector3) -> GeopResult<Curve3> {
1196 restricted(&carried_on(curve, |_| Ok(true))?, start, end)
1197}
1198
1199fn restricted(longer: &Curve3, start: Vector3, end: Vector3) -> GeopResult<Curve3> {
1202 let at = |p: &Vector3| -> GeopResult<Scalar> {
1203 let (t, _) = longer.closest_point(p)?;
1204 if !longer.evaluate(t)?.could_be_equal(p) {
1205 return Err(GeopError::new(format!(
1206 "the seam, carried on, does not pass through its moved end {p:?}"
1207 )));
1208 }
1209 Ok(t)
1210 };
1211 let (s, t) = (at(&start)?, at(&end)?);
1212 if !t.definitely_greater(s) {
1213 return Err(GeopError::new(format!(
1214 "its moved ends {start:?} and {end:?} lie the other way round along it: a face it bounds has vanished"
1215 )));
1216 }
1217 longer.sub_curve(s, t)
1218}
1219
1220fn fitted(
1226 at: impl Fn(Scalar) -> GeopResult<Vector3>,
1227 (start, end): (Vector3, Vector3),
1228 check: impl Fn(&[Vector3]) -> GeopResult<()>,
1229) -> GeopResult<Curve3> {
1230 let fraction = |i: usize, n: usize| Scalar::from_ratio(i as i64, n as i64);
1231 let refined = refine(FIRST_LEGS, usize::BITS as usize, |legs| -> GeopResult<_> {
1232 let mut points = vec![start];
1233 for i in 1..legs {
1234 points.push(at(fraction(i, legs)?)?);
1235 }
1236 points.push(end);
1237 check(&points)?;
1238 let mut between = Vec::with_capacity(legs);
1239 for i in 0..legs {
1240 let mut inside = Vec::new();
1241 for &(p, q) in true_point_fractions(i, legs) {
1242 let frac = fraction(i, legs)?.add(Scalar::from_ratio(p, q * legs as i64)?);
1243 inside.push(at(frac)?);
1244 }
1245 between.push(inside);
1246 }
1247 let curve = NurbCurve3D::interpolate_enclosing(&points, &between, 3)?;
1248 let wide = curve
1249 .control_points
1250 .iter()
1251 .any(|p| (0..3).any(|k| p[k].width().to_f64() > TRACE_DRIFT));
1252 if wide && 2 * legs <= MOST_LEGS {
1253 return Ok(Refine::Finer(2 * legs));
1254 }
1255 Ok(Refine::Done(curve))
1256 })?;
1257 match refined {
1258 Refined::Done(curve) => Ok(curve),
1259 Refined::Exhausted(legs) => Err(GeopError::new(format!(
1260 "its legs were split past every cap, at {legs}"
1261 ))),
1262 }
1263}
1264
1265fn carried_on<const D: usize>(
1271 curve: &NurbCurve<D>,
1272 within: impl Fn(&NurbCurve<D>) -> GeopResult<bool>,
1273) -> GeopResult<NurbCurve<D>> {
1274 let (t0, t1) = curve.domain();
1275 let width = t1.sub(t0);
1276 let mut out = curve.clone();
1277 for at_end in [false, true] {
1278 let mut by = width;
1279 for _ in 0..4 {
1280 if let Ok(longer) = out.extended(at_end, by.sharpen())
1281 && within(&longer)?
1282 {
1283 out = longer;
1284 break;
1285 }
1286 by = by.div(Scalar::TWO)?;
1287 }
1288 }
1289 Ok(out)
1290}
1291
1292fn hole_inside(
1296 surface: &NurbSurface3D,
1297 outer: &[CoedgeSpec],
1298 hole: &[CoedgeSpec],
1299) -> GeopResult<bool> {
1300 for h in hole {
1301 for o in outer {
1302 match curve_curve_intersect(&h.pcurve, &o.pcurve, 1, MAX_NODES, min_subdivision_size())?
1303 {
1304 Intersections::Found(found) if found.is_empty() => {}
1305 _ => return Ok(false),
1306 }
1307 }
1308 }
1309 let first = &hole[0].pcurve;
1310 let point = first.evaluate(first.mid_parameter()?)?;
1311 let pcurves: Vec<_> = outer.iter().map(|c| &c.pcurve).collect();
1312 Ok(pcurves_contain(
1313 surface,
1314 &pcurves,
1315 point,
1316 MAX_NODES,
1317 min_subdivision_size(),
1318 SEED,
1319 )? == PointClassification::Inside)
1320}
1321
1322#[cfg(test)]
1323mod tests;