1use std::collections::HashMap;
43
44use serde::{Deserialize, Serialize};
45
46use geop_core_geometry::{
47 nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, loop_signed_area},
48 nurb_surface::NurbSurface3D,
49};
50use geop_core_math::{
51 geop_error::{GeopError, GeopResult, WithContext},
52 plain::{P3, add, cross, distance, dot, norm, normalize, scale, sub, unwrap_angle},
53 scalars::Scalar,
54 solvers::{
55 bisect::{Bracket, bisect},
56 iterate::{Repeat, Step, iterate},
57 linear::solve_symmetric2,
58 refine::{Refine, Refined, refine},
59 repeat::{self, repeat},
60 walk::{Walked, walk},
61 },
62 vector::{Vector2, Vector3},
63};
64use geop_core_topology::{
65 Sense,
66 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
67 contains::face::{PointClassification, pcurves_contain},
68};
69
70use super::{
71 geometry::{Frame, Profile, Revolved, Scope, Shape, SurfaceDef, SurfaceKind, parameter_of},
72 midpoints::{CurveMidpoints, SurfaceMidpoints},
73 reader::{Reader, unsupported},
74 structure::Item,
75};
76
77#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83pub struct ImportedBody {
84 pub label: String,
86 pub spec: BodySpec,
87 pub vertex_names: Vec<Vec<String>>,
88 pub edge_names: Vec<Vec<String>>,
89 pub face_names: Vec<Vec<String>>,
90 pub healed: Healing,
93}
94
95#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
98pub struct Healing {
99 pub vertices: Vec<(String, f64)>,
101 pub edges: Vec<(String, f64)>,
103 pub faces: Vec<String>,
105 pub uncertainty: f64,
107}
108
109impl Healing {
110 pub fn is_empty(&self) -> bool {
111 self.vertices.is_empty() && self.edges.is_empty() && self.faces.is_empty()
112 }
113
114 pub fn furthest(&self) -> f64 {
116 self.vertices
117 .iter()
118 .chain(&self.edges)
119 .map(|m| m.1)
120 .fold(0.0, f64::max)
121 }
122
123 pub fn summary(&self) -> String {
125 let mut out = format!(
126 "its faces disagree with each other by up to {:.1e} mm (the file states {:e} mm): {} vertices and {} edges were moved onto them",
127 self.furthest(),
128 self.uncertainty,
129 self.vertices.len(),
130 self.edges.len()
131 );
132 if !self.faces.is_empty() {
133 out += &format!(
134 ", and {} faces rebuilt from their edges: {}",
135 self.faces.len(),
136 self.faces.join(", ")
137 );
138 }
139 out
140 }
141
142 pub fn details(&self) -> Vec<String> {
144 let line = |what: &str, moved: &[(String, f64)]| {
145 let names: Vec<String> = moved
146 .iter()
147 .map(|(name, by)| format!("{name} by {by:.1e}"))
148 .collect();
149 format!("{what}: {}", names.join(", "))
150 };
151 let mut out = Vec::new();
152 if !self.vertices.is_empty() {
153 out.push(line("vertices put where their faces meet", &self.vertices));
154 }
155 if !self.edges.is_empty() {
156 out.push(line("edges rebuilt where their faces meet", &self.edges));
157 }
158 if !self.faces.is_empty() {
159 out.push(format!(
160 "faces rebuilt from their edges, their surfaces contradicting their neighbours': {}",
161 self.faces.join(", ")
162 ));
163 }
164 out
165 }
166}
167
168const MAX_NODES: usize = 5000;
172const MIN_SUBDIVISION_SIZE: f64 = 1e-4;
173const NORMALIZE_STEPS: usize = 1_000_000;
177
178const LOOP_SAMPLES: usize = 16;
182
183const SPHERE_AXES: usize = 256;
186
187struct Vertex {
188 point: Vector3,
191 origin: Vector3,
195 name: Vec<String>,
196 id: Option<u64>,
198}
199
200impl Vertex {
201 fn label(&self) -> String {
203 label(&self.name, self.id)
204 }
205}
206
207fn loop_points(edges: &[Edge], lp: &[Use]) -> Vec<P3> {
211 let mut points = Vec::new();
212 for &(e, forward) in lp {
213 let curve = CurveMidpoints::of(&edges[e].curve);
214 let (lo, hi) = curve.domain();
215 for i in 0..LOOP_SAMPLES {
216 let f = i as f64 / LOOP_SAMPLES as f64;
217 let f = if forward { f } else { 1.0 - f };
218 points.push(curve.point(lo + (hi - lo) * f));
219 }
220 }
221 points
222}
223
224struct Edge {
225 curve: NurbCurve3D,
226 start: usize,
227 end: usize,
228 name: Vec<String>,
229 alive: bool,
231 id: Option<u64>,
233 shape: Option<Shape>,
236}
237
238impl Edge {
239 fn label(&self) -> String {
241 label(&self.name, self.id)
242 }
243
244 fn ends(&self, forward: bool) -> (usize, usize) {
247 if forward {
248 (self.start, self.end)
249 } else {
250 (self.end, self.start)
251 }
252 }
253}
254
255fn label(name: &[String], id: Option<u64>) -> String {
257 match id {
258 Some(id) => format!("{} (#{id})", name.join(",")),
259 None => name.join(","),
260 }
261}
262
263type Use = (usize, bool);
265
266struct Face {
267 surface: SurfaceDef,
268 same_sense: bool,
271 loops: Vec<Vec<Use>>,
274 outer: Option<usize>,
276 patch: Option<[f64; 4]>,
279 shell: usize,
280 name: Vec<String>,
281 id: u64,
283}
284
285impl Face {
286 fn outward_is_natural(&self) -> bool {
289 self.same_sense != self.surface.flipped
290 }
291}
292
293struct Builder<'r> {
294 reader: Reader<'r>,
295 scope: Scope,
296 vertices: Vec<Vertex>,
297 vertex_index: HashMap<u64, usize>,
298 edges: Vec<Edge>,
299 edge_index: HashMap<u64, usize>,
300 faces: Vec<Face>,
301 surfaces: HashMap<u64, SurfaceDef>,
302 shells: usize,
303}
304
305fn s3(p: P3) -> Vector3 {
306 Vector3::from_array(p.map(Scalar::from_f64))
307}
308
309fn name(prefix: &str, k: usize) -> Vec<String> {
310 vec![format!("{prefix}{k}")]
311}
312
313pub fn read_body(reader: Reader<'_>, item: &Item) -> GeopResult<ImportedBody> {
315 let mut builder = Builder {
316 reader,
317 scope: item.scope,
318 vertices: Vec::new(),
319 vertex_index: HashMap::new(),
320 edges: Vec::new(),
321 edge_index: HashMap::new(),
322 faces: Vec::new(),
323 surfaces: HashMap::new(),
324 shells: 0,
325 };
326 let instance = reader.instance(item.id)?;
327 let solid = !instance.is("SHELL_BASED_SURFACE_MODEL");
328 if instance.is("MANIFOLD_SOLID_BREP") {
329 let args = reader.args(item.id, "MANIFOLD_SOLID_BREP")?;
330 builder.shell(args.reference(1)?, false)?;
331 } else if instance.is("BREP_WITH_VOIDS") {
332 let args = reader.args(item.id, "BREP_WITH_VOIDS")?;
333 builder.shell(args.reference(1)?, false)?;
334 for void in args.references(2)? {
335 let v = reader.args(void, "ORIENTED_CLOSED_SHELL")?;
336 builder.shell(v.reference(2)?, !v.logical(3)?)?;
337 }
338 } else {
339 let args = reader.args(item.id, "SHELL_BASED_SURFACE_MODEL")?;
340 for shell in args.references(1)? {
341 let s = reader.instance(shell)?;
342 if s.is("ORIENTED_OPEN_SHELL") || s.is("ORIENTED_CLOSED_SHELL") {
343 let (_, o) =
344 reader.args_of_any(shell, &["ORIENTED_OPEN_SHELL", "ORIENTED_CLOSED_SHELL"])?;
345 builder.shell(o.reference(2)?, !o.logical(3)?)?;
346 } else {
347 builder.shell(shell, false)?;
348 }
349 }
350 }
351 builder.normalize()?;
352 builder.spec(solid, item.label.clone())
353}
354
355impl Builder<'_> {
356 fn shell(&mut self, id: u64, reversed: bool) -> GeopResult<()> {
357 let (_, args) = self
358 .reader
359 .args_of_any(id, &["CLOSED_SHELL", "OPEN_SHELL"])?;
360 let shell = self.shells;
361 self.shells += 1;
362 for face in args.references(1)? {
363 self.face(face, shell, reversed)
364 .map_err(|e| e.with_context(format!("reading the face #{face}")))?;
365 }
366 Ok(())
367 }
368
369 fn vertex(&mut self, id: u64) -> GeopResult<usize> {
370 if let Some(&v) = self.vertex_index.get(&id) {
371 return Ok(v);
372 }
373 let args = self.reader.args(id, "VERTEX_POINT")?;
374 let point = self.reader.point(&self.scope, args.reference(1)?)?;
375 let v = self.vertices.len();
376 self.vertices.push(Vertex {
377 point: s3(point),
378 origin: s3(point),
379 name: name("v", self.vertex_index.len()),
380 id: Some(id),
381 });
382 self.vertex_index.insert(id, v);
383 Ok(v)
384 }
385
386 fn edge(&mut self, id: u64) -> GeopResult<usize> {
387 if let Some(&e) = self.edge_index.get(&id) {
388 return Ok(e);
389 }
390 let ctx = |e: GeopError| e.with_context(format!("reading the edge #{id}"));
391 let args = self.reader.args(id, "EDGE_CURVE").map_err(ctx)?;
392 let start = self.vertex(args.reference(1)?).map_err(ctx)?;
393 let end = self.vertex(args.reference(2)?).map_err(ctx)?;
394 let (curve, reversed) = self
395 .reader
396 .curve(&self.scope, args.reference(3)?)
397 .map_err(ctx)?;
398 let shape = curve.shape();
399 let along = args.logical(4).map_err(ctx)? != reversed;
400 let (a, b) = (
401 self.vertices[start].point.to_f64_array(),
402 self.vertices[end].point.to_f64_array(),
403 );
404 let closed = start == end;
405 let curve = if along {
406 curve.edge(a, b, closed, self.scope.uncertainty)
407 } else {
408 curve
409 .edge(b, a, closed, self.scope.uncertainty)
410 .map(|c| c.reverse())
411 }
412 .map_err(ctx)?;
413 let e = self.edges.len();
414 self.edges.push(Edge {
415 curve,
416 start,
417 end,
418 name: name("e", self.edge_index.len()),
419 alive: true,
420 id: Some(id),
421 shape,
422 });
423 self.edge_index.insert(id, e);
424 Ok(e)
425 }
426
427 fn oriented_edge(&mut self, id: u64) -> GeopResult<Use> {
430 let instance = self.reader.instance(id)?;
431 if instance.is("EDGE_CURVE") {
432 return Ok((self.edge(id)?, true));
433 }
434 let args = self.reader.args(id, "ORIENTED_EDGE")?;
435 let (edge, forward) = self.oriented_edge(args.reference(3)?)?;
436 Ok((edge, forward == args.logical(4)?))
437 }
438
439 fn face(&mut self, id: u64, shell: usize, reversed: bool) -> GeopResult<()> {
440 let (_, args) = self
441 .reader
442 .args_of_any(id, &["ADVANCED_FACE", "FACE_SURFACE"])?;
443 let surface_id = args.reference(2)?;
444 let surface = match self.surfaces.get(&surface_id) {
445 Some(s) => s.clone(),
446 None => {
447 let s = self.reader.surface(&self.scope, surface_id)?;
448 self.surfaces.insert(surface_id, s.clone());
449 s
450 }
451 };
452 let mut loops = Vec::new();
453 for bound in args.references(1)? {
454 let (_, b) = self
455 .reader
456 .args_of_any(bound, &["FACE_OUTER_BOUND", "FACE_BOUND"])?;
457 let lp = b.reference(1)?;
458 let forward = b.logical(2)? != reversed;
459 let lp_instance = self.reader.instance(lp)?;
460 if lp_instance.is("VERTEX_LOOP") {
461 continue;
464 }
465 if !lp_instance.is("EDGE_LOOP") {
466 return Err(unsupported(
467 lp,
468 lp_instance,
469 "a face bound that is not a loop of edges",
470 ));
471 }
472 let mut coedges = Vec::new();
473 for oriented in self.reader.args(lp, "EDGE_LOOP")?.references(1)? {
474 coedges.push(self.oriented_edge(oriented)?);
475 }
476 if !forward {
477 coedges = coedges.into_iter().rev().map(|(e, f)| (e, !f)).collect();
478 }
479 if coedges.is_empty() {
480 continue;
481 }
482 loops.push(coedges);
483 }
484 let surface = match surface.revolved() {
485 Some(revolved) if matches!(revolved.profile, Profile::Sphere { .. }) => SurfaceDef {
486 kind: SurfaceKind::Revolved(self.sphere_axis(revolved, &loops)?),
487 ..surface
488 },
489 _ => surface,
490 };
491 let surface = match surface.revolved() {
495 Some(revolved) => {
496 let mut best: Option<f64> = None;
497 for &u in loops.iter().flatten() {
498 let p = self.vertices[self.ends(u).0].point.to_f64_array();
499 if let Some(m) = revolved.past_axis(p)
500 && best.is_none_or(|b| m.abs() > b.abs())
501 {
502 best = Some(m);
503 }
504 }
505 match best {
506 Some(m) if m > 0.0 => SurfaceDef {
507 kind: SurfaceKind::Revolved(revolved.mirrored()),
508 flipped: !surface.flipped,
509 },
510 _ => surface,
511 }
512 }
513 None => surface,
514 };
515 let same_sense = args.logical(3)? != reversed;
516 let index = self.faces.len();
517 self.faces.push(Face {
518 surface,
519 same_sense,
520 loops,
521 outer: None,
522 patch: None,
523 shell,
524 name: name("f", index),
525 id,
526 });
527 Ok(())
528 }
529
530 fn sphere_axis(&self, sphere: &Revolved, loops: &[Vec<Use>]) -> GeopResult<Revolved> {
539 let frame = &sphere.frame;
540 let poles = [frame.point([0.0, 0.0, 1.0]), frame.point([0.0, 0.0, -1.0])];
541 let Profile::Sphere { radius } = sphere.profile else {
542 unreachable!("a sphere's profile");
543 };
544 let poles = poles.map(|p| add(frame.origin, scale(sub(p, frame.origin), radius)));
545 let mut through = false;
546 for &(e, _) in loops.iter().flatten() {
547 let edge = &self.edges[e];
548 let ends = [edge.start, edge.end].map(|v| self.vertices[v].origin.to_f64_array());
549 for pole in poles {
550 if ends
551 .iter()
552 .any(|&end| distance(end, pole) <= sphere.on_axis)
553 {
554 continue;
555 }
556 let t = parameter_of(&edge.curve, pole)?;
557 if distance(edge.curve.evaluate(t)?.to_f64_array(), pole) <= sphere.on_axis {
558 through = true;
559 }
560 }
561 }
562 if !through {
563 return Ok(sphere.clone());
564 }
565 let mut points = Vec::new();
566 for &u in loops.iter().flatten() {
567 points.extend(
568 self.samples(u, LOOP_SAMPLES)?
569 .into_iter()
570 .filter_map(|(_, p)| normalize(sub(p, frame.origin))),
571 );
572 }
573 let nearest = |axis: P3| {
577 points
578 .iter()
579 .map(|p| dot(*p, axis).abs())
580 .fold(0.0, f64::max)
581 };
582 let golden = std::f64::consts::PI * (3.0 - 5f64.sqrt());
583 let axis = (0..SPHERE_AXES)
584 .map(|k| {
585 let z = (k as f64 + 0.5) / SPHERE_AXES as f64;
586 let r = (1.0 - z * z).sqrt();
587 let (s, c) = (golden * k as f64).sin_cos();
588 [r * c, r * s, z]
589 })
590 .min_by(|a, b| nearest(*a).total_cmp(&nearest(*b)))
591 .expect("candidates");
592 Ok(Revolved {
593 frame: Frame::new(frame.origin, axis, frame.x)?,
594 ..sphere.clone()
595 })
596 }
597
598 fn ends(&self, (e, forward): Use) -> (usize, usize) {
600 self.edges[e].ends(forward)
601 }
602
603 fn samples(&self, (e, forward): Use, n: usize) -> GeopResult<Vec<(f64, P3)>> {
606 let midpoints = CurveMidpoints::of(&self.edges[e].curve);
609 let (lo, hi) = midpoints.domain();
610 let mut out = Vec::with_capacity(n + 1);
611 for i in 0..=n {
612 let f = i as f64 / n as f64;
613 let f = if forward { f } else { 1.0 - f };
614 let t = lo + (hi - lo) * f;
615 out.push((t, midpoints.point(t)));
616 }
617 Ok(out)
618 }
619
620 fn loop_points(&self, lp: &[Use]) -> GeopResult<Vec<P3>> {
623 Ok(loop_points(&self.edges, lp))
624 }
625
626 fn split_edge(&mut self, e: usize, ts: &[Scalar]) -> GeopResult<Vec<usize>> {
630 let edge = &self.edges[e];
631 let (start, end, base, id, shape) =
632 (edge.start, edge.end, edge.name.clone(), edge.id, edge.shape);
633 let curve = edge.curve.clone();
634 let (lo, hi) = curve.domain();
635 let mut bounds = vec![lo];
636 bounds.extend_from_slice(ts);
637 bounds.push(hi);
638 let mut new_vertices = Vec::new();
639 for (k, &t) in ts.iter().enumerate() {
640 let v = self.vertices.len();
641 let mut name = base.clone();
642 name.push(format!("c{k}"));
643 self.vertices.push(Vertex {
644 point: curve.evaluate(t)?,
645 origin: curve.evaluate(t)?,
646 name,
647 id: None,
648 });
649 new_vertices.push(v);
650 }
651 let mut chain = vec![start];
652 chain.extend(&new_vertices);
653 chain.push(end);
654 let mut pieces = Vec::new();
655 for k in 0..bounds.len() - 1 {
656 let piece = curve.sub_curve(bounds[k], bounds[k + 1])?;
657 let mut name = base.clone();
658 name.push(format!("p{k}"));
659 pieces.push(self.edges.len());
660 self.edges.push(Edge {
661 curve: piece,
662 start: chain[k],
663 end: chain[k + 1],
664 name,
665 alive: true,
666 id,
667 shape,
668 });
669 }
670 self.edges[e].alive = false;
671 for face in &mut self.faces {
672 for lp in &mut face.loops {
673 let mut out = Vec::with_capacity(lp.len() + pieces.len());
674 for &(edge, forward) in lp.iter() {
675 if edge != e {
676 out.push((edge, forward));
677 } else if forward {
678 out.extend(pieces.iter().map(|&p| (p, true)));
679 } else {
680 out.extend(pieces.iter().rev().map(|&p| (p, false)));
681 }
682 }
683 *lp = out;
684 }
685 }
686 Ok(new_vertices)
687 }
688
689 fn normalize(&mut self) -> GeopResult<()> {
692 self.join_coincident_vertices();
693 let mut f = 0;
696 repeat(NORMALIZE_STEPS, || -> GeopResult<_> {
697 if f >= self.faces.len() {
698 return Ok(repeat::Step::Done(()));
699 }
700 let face = &self.faces[f];
701 let id = face.id;
702 let revolved = face.surface.revolved().cloned();
703 let had_seam = self
704 .drop_doubled_edges(f)
705 .map_err(|e| e.with_context(format!("taking the seams out of the face #{id}")))?;
706 let wraps = match &revolved {
707 Some(revolved) => {
708 had_seam
709 || self.faces[f].loops.is_empty()
710 || self.windings(f, revolved)?.iter().any(|&w| w != 0)
711 }
712 None => false,
713 };
714 if let (true, Some(revolved)) = (wraps, &revolved) {
715 let round_axis = self.windings(f, revolved)?.iter().any(|&w| w != 0);
716 if revolved.v_is_angle() && !round_axis {
717 if self.tube_windings(f, revolved)?.iter().any(|&w| w != 0) {
718 self.cut_tube_wrapping_face(f, revolved).map_err(|e| {
719 e.with_context(format!(
720 "cutting the face #{id}, which goes round its torus' tube, into pieces along parallels"
721 ))
722 })?;
723 } else {
724 self.cut_whole_torus(f, revolved).map_err(|e| {
725 e.with_context(format!(
726 "cutting the face #{id}, a whole torus, into bands round its axis"
727 ))
728 })?;
729 }
730 } else {
731 self.cut_wrapping_face(f).map_err(|e| {
732 e.with_context(format!(
733 "cutting the face #{id}, which wraps around its axis, into sectors"
734 ))
735 })?;
736 }
737 return Ok(repeat::Step::Again);
739 }
740 if let Some(revolved) = &revolved
741 && self.turns_more_than_once(f, revolved)?
742 {
743 self.cut_helical_face(f, revolved).map_err(|e| {
744 e.with_context(format!(
745 "cutting the face #{id}, which turns about its axis more than once without going round it, along meridians"
746 ))
747 })?;
748 return Ok(repeat::Step::Again);
749 }
750 f += 1;
751 Ok(repeat::Step::Again)
752 })?
753 .done("cutting the faces into the shape geop needs")?;
754 for e in 0..self.edges.len() {
756 let edge = &self.edges[e];
757 if edge.alive && edge.start == edge.end {
758 let mid = edge.curve.mid_parameter()?;
759 self.split_edge(e, &[mid])?;
760 }
761 }
762 Ok(())
763 }
764
765 fn join_coincident_vertices(&mut self) {
775 let tolerance = self.scope.uncertainty;
776 let near = |vertices: &[Vertex], a: usize, b: usize| {
777 distance(
778 vertices[a].origin.to_f64_array(),
779 vertices[b].origin.to_f64_array(),
780 ) <= tolerance
781 };
782 for f in 0..self.faces.len() {
783 for l in 0..self.faces[f].loops.len() {
784 let n = self.faces[f].loops[l].len();
785 for k in 0..n {
786 let lp = &self.faces[f].loops[l];
787 let (a, b) = (lp[k], lp[(k + 1) % n]);
788 let end = self.edges[a.0].ends(a.1).1;
789 let start = self.edges[b.0].ends(b.1).0;
790 if end != start && near(&self.vertices, end, start) {
791 self.merge_vertex(end, start);
792 }
793 }
794 }
795 }
796 for e in 0..self.edges.len() {
797 let edge = &self.edges[e];
798 if !edge.alive || edge.start == edge.end || !near(&self.vertices, edge.start, edge.end)
799 {
800 continue;
801 }
802 let at = self.vertices[edge.start].origin.to_f64_array();
803 let short = (edge.curve.control_points.iter()).all(|c| {
804 c.dehomogenized()
805 .is_ok_and(|p| distance(p.to_f64_array(), at) <= tolerance)
806 });
807 if !short {
808 continue;
809 }
810 let (start, end) = (edge.start, edge.end);
811 self.edges[e].alive = false;
812 self.merge_vertex(start, end);
813 for face in &mut self.faces {
814 for lp in &mut face.loops {
815 lp.retain(|&(x, _)| x != e);
816 }
817 }
818 }
819 }
820
821 fn merge_vertex(&mut self, keep: usize, gone: usize) {
824 let point = self.vertices[gone].point;
825 self.vertices[keep].point = self.vertices[keep].point.union(&point);
826 for edge in &mut self.edges {
827 if edge.start == gone {
828 edge.start = keep;
829 }
830 if edge.end == gone {
831 edge.end = keep;
832 }
833 }
834 }
835
836 fn drop_doubled_edges(&mut self, f: usize) -> GeopResult<bool> {
843 let mut count: HashMap<usize, usize> = HashMap::new();
844 for lp in &self.faces[f].loops {
845 for &(e, _) in lp {
846 *count.entry(e).or_default() += 1;
847 }
848 }
849 let used_elsewhere = |e: usize| {
854 (self.faces.iter().enumerate())
855 .any(|(g, face)| g != f && face.loops.iter().flatten().any(|&(x, _)| x == e))
856 };
857 let mut doubled: Vec<usize> = count
858 .into_iter()
859 .filter(|&(e, n)| n == 2 && !used_elsewhere(e))
860 .map(|(e, _)| e)
861 .collect();
862 doubled.sort();
863 for &e in &doubled {
864 let loops = std::mem::take(&mut self.faces[f].loops);
865 let mut out = Vec::new();
866 for lp in loops {
867 let uses: Vec<usize> = (0..lp.len()).filter(|&k| lp[k].0 == e).collect();
868 match uses.as_slice() {
869 [] => out.push(lp),
870 &[i, j] => {
871 if lp[i].1 == lp[j].1 {
872 return Err(GeopError::new(format!(
873 "it runs along the edge {} twice the same way",
874 self.edges[e].label()
875 )));
876 }
877 let inner: Vec<Use> = lp[i + 1..j].to_vec();
878 let outer: Vec<Use> = lp[j + 1..].iter().chain(&lp[..i]).copied().collect();
879 out.extend([inner, outer].into_iter().filter(|l| !l.is_empty()));
880 }
881 [_] if lp.len() == 1 => {}
885 _ => {
886 return Err(GeopError::new(format!(
887 "the edge {} is used twice, by different loops of it",
888 self.edges[e].label()
889 )));
890 }
891 }
892 }
893 self.faces[f].loops = out;
894 self.edges[e].alive = false;
895 }
896 for lp in &self.faces[f].loops {
897 let (start, _) = self.ends(lp[0]);
898 let (_, end) = self.ends(*lp.last().expect("not empty"));
899 if start != end {
900 return Err(GeopError::new(
901 "taking them out leaves a loop that does not close",
902 ));
903 }
904 }
905 Ok(!doubled.is_empty())
906 }
907
908 fn tube_windings(&self, f: usize, revolved: &Revolved) -> GeopResult<Vec<i64>> {
911 self.faces[f]
912 .loops
913 .iter()
914 .map(|lp| {
915 Ok(winding(&unwrap_tube(
916 revolved,
917 &self.loop_points(lp)?,
918 true,
919 )))
920 })
921 .collect()
922 }
923
924 fn windings(&self, f: usize, revolved: &Revolved) -> GeopResult<Vec<i64>> {
927 self.faces[f]
928 .loops
929 .iter()
930 .map(|lp| {
931 let ccw = self.faces[f].outward_is_natural();
932 Ok(winding(&unwrap(
933 revolved,
934 &self.loop_points(lp)?,
935 true,
936 ccw,
937 )))
938 })
939 .collect()
940 }
941
942 fn cut_wrapping_face(&mut self, f: usize) -> GeopResult<()> {
951 let revolved = self.faces[f]
952 .surface
953 .revolved()
954 .expect("only a surface of revolution wraps")
955 .clone();
956 let outward_natural = self.faces[f].outward_is_natural();
957 let mut rings: Vec<usize> = Vec::new();
958 let mut bottom = None;
959 let mut top = None;
960 let mut holes = Vec::new();
961 let mut summary = Vec::new();
963 for (k, lp) in self.faces[f].loops.iter().enumerate() {
964 let points = self.loop_points(lp)?;
965 let angles = unwrap(&revolved, &points, true, outward_natural);
966 let mean_v =
967 points.iter().map(|&p| revolved.chart(p).1).sum::<f64>() / points.len() as f64;
968 summary.push((winding(&angles), mean_v, lp.len()));
969 match winding(&angles) {
970 0 => holes.push(k),
971 w @ (1 | -1) => {
972 let above = (w > 0) == outward_natural;
976 let slot = if above { &mut bottom } else { &mut top };
977 if slot.replace(k).is_some() {
978 return Err(GeopError::new(format!(
979 "it has more than one loop round its axis with the face {} it",
980 if above { "above" } else { "below" }
981 )));
982 }
983 rings.push(k);
984 }
985 w => {
986 return Err(GeopError::new(format!(
987 "a loop of it goes round its axis {w} times"
988 )));
989 }
990 }
991 }
992 let v_start = if revolved.v_is_angle() {
997 let (Some(b), Some(t)) = (bottom, top) else {
998 return Err(GeopError::new(format!(
999 "it goes round its torus' axis without a loop round the axis on either side of it (its loops' windings, mean profile parameters and lengths: {summary:?})"
1000 )));
1001 };
1002 let mean = |k: usize| -> GeopResult<f64> {
1003 let v = unwrap_tube(
1004 &revolved,
1005 &self.loop_points(&self.faces[f].loops[k])?,
1006 false,
1007 );
1008 Ok(v.iter().sum::<f64>() / v.len().max(1) as f64)
1009 };
1010 let (b, t) = (mean(b)?, mean(t)?);
1011 t + (b - t).rem_euclid(std::f64::consts::TAU) / 2.0
1012 } else {
1013 f64::NEG_INFINITY
1014 };
1015 let v_of = |p: P3| {
1016 let v = revolved.chart(p).1;
1017 if v_start.is_finite() {
1018 v_start + (v - v_start).rem_euclid(std::f64::consts::TAU)
1019 } else {
1020 v
1021 }
1022 };
1023 let ring_v = |builder: &Self, k: usize| -> GeopResult<f64> {
1024 let points = builder.loop_points(&builder.faces[f].loops[k])?;
1025 Ok(points.iter().map(|&p| v_of(p)).sum::<f64>() / points.len() as f64)
1026 };
1027 let bottom_v = bottom.map(|k| ring_v(self, k)).transpose()?;
1028 let top_v = top.map(|k| ring_v(self, k)).transpose()?;
1029 let poles = revolved.poles();
1030 let bottom_pole = match bottom {
1032 Some(_) => None,
1033 None => Some(
1034 poles
1035 .iter()
1036 .copied()
1037 .filter(|&p| top_v.is_none_or(|t| p < t))
1038 .reduce(if top_v.is_some() { f64::max } else { f64::min })
1040 .ok_or_else(|| GeopError::new(format!("it has nothing closing it off below: no loop round its axis and no pole (its loops' windings, mean profile parameters and lengths: {summary:?}, the face's outward normal natural: {outward_natural})")))?,
1041 ),
1042 };
1043 let top_pole = match top {
1044 Some(_) => None,
1045 None => Some(
1046 poles
1047 .iter()
1048 .copied()
1049 .filter(|&p| bottom_v.or(bottom_pole).is_none_or(|b| p > b))
1051 .reduce(f64::min)
1052 .ok_or_else(|| GeopError::new(format!("it has nothing closing it off above: no loop round its axis and no pole (its loops' windings, mean profile parameters and lengths: {summary:?}, the face's outward normal natural: {outward_natural})")))?,
1053 ),
1054 };
1055 if let (Some(b), Some(t)) = (bottom_v.or(bottom_pole), top_v.or(top_pole))
1056 && b >= t
1057 {
1058 return Err(GeopError::new(format!(
1059 "its bottom {b} is not below its top {t} (its loops' windings, mean profile parameters and lengths: {summary:?})"
1060 )));
1061 }
1062
1063 let mut blocked: Vec<(f64, f64)> = Vec::new();
1065 for (k, lp) in self.faces[f].loops.iter().enumerate() {
1066 let points = self.loop_points(lp)?;
1067 if holes.contains(&k) {
1068 let angles = unwrap(&revolved, &points, false, outward_natural);
1069 let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1070 let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1071 blocked.push((lo, hi));
1072 }
1073 for &u in lp {
1074 let (start, _) = self.ends(u);
1075 if let (Some(a), _) = revolved.chart(self.vertices[start].point.to_f64_array()) {
1076 blocked.push((a, a));
1077 }
1078 }
1079 }
1080 let cuts = choose_cuts(&blocked);
1081
1082 let ring_vertices = self.split_rings(f, &rings, &cuts, &|p| revolved.chart(p).0)?;
1083
1084 let point_at = |builder: &Self,
1086 side: Option<usize>,
1087 pole: Option<f64>,
1088 c: usize|
1089 -> (P3, f64, Option<usize>) {
1090 match side {
1091 Some(ring) => {
1092 let v = ring_vertices[&ring][c];
1093 let p = builder.vertices[v].point.to_f64_array();
1094 (p, v_of(p), Some(v))
1095 }
1096 None => {
1097 let pole = pole.expect("a side without a ring has a pole");
1098 (revolved.profile_point(pole), pole, None)
1099 }
1100 }
1101 };
1102 let pole_vertex = |builder: &mut Self, pole: Option<f64>, which: &str| -> Option<usize> {
1103 pole.map(|v| {
1104 let p = revolved.profile_point(v);
1105 let existing = builder
1107 .vertices
1108 .iter()
1109 .position(|x| distance(x.point.to_f64_array(), p) <= builder.scope.uncertainty);
1110 existing.unwrap_or_else(|| {
1111 builder.vertices.push(Vertex {
1112 point: s3(p),
1113 origin: s3(p),
1114 name: vec![builder.faces[f].name[0].clone(), which.to_string()],
1115 id: None,
1116 });
1117 builder.vertices.len() - 1
1118 })
1119 })
1120 };
1121 let bottom_pole_vertex = pole_vertex(self, bottom_pole, "south");
1122 let top_pole_vertex = pole_vertex(self, top_pole, "north");
1123 let mut meridians = Vec::new();
1124 for (c, &cut) in cuts.iter().enumerate() {
1125 let (pb, vb, b) = point_at(self, bottom, bottom_pole, c);
1126 let (pt, vt, t) = point_at(self, top, top_pole, c);
1127 let start = b.or(bottom_pole_vertex).expect("a bottom");
1128 let end = t.or(top_pole_vertex).expect("a top");
1129 let curve = self.meridian(
1130 &revolved,
1131 cut,
1132 (pb, vb, bottom_pole.is_some()),
1133 (pt, vt, top_pole.is_some()),
1134 )?;
1135 let mut name = self.faces[f].name.clone();
1136 name.push(format!("m{c}"));
1137 meridians.push(self.edges.len());
1138 self.edges.push(Edge {
1139 curve,
1140 start,
1141 end,
1142 name,
1143 alive: true,
1144 id: None,
1145 shape: Some(revolved.iso()),
1146 });
1147 }
1148
1149 let n = cuts.len();
1151 let face_loops = self.faces[f].loops.clone();
1152 let ring_run = |builder: &Self,
1153 ring: usize,
1154 from: usize,
1155 to: usize,
1156 up: bool|
1157 -> GeopResult<Vec<Use>> {
1158 let lp = &face_loops[ring];
1162 let angles = unwrap(&revolved, &builder.loop_points(lp)?, true, outward_natural);
1163 let along = (winding(&angles) > 0) == up;
1164 builder.run_between(
1165 lp,
1166 ring_vertices[&ring][from],
1167 ring_vertices[&ring][to],
1168 along,
1169 )
1170 };
1171 let face = &self.faces[f];
1172 let (surface, same_sense, shell, base_name) = (
1173 face.surface.clone(),
1174 face.same_sense,
1175 face.shell,
1176 face.name.clone(),
1177 );
1178 let mut hole_sector = Vec::new();
1180 for &h in &holes {
1181 let points = self.loop_points(&face_loops[h])?;
1182 let a = unwrap(&revolved, &points, false, outward_natural);
1183 let mid = a.iter().sum::<f64>() / a.len() as f64;
1184 let sector = (0..n)
1185 .find(|&s| {
1186 let lo = cuts[s];
1187 let hi = if s + 1 < n {
1188 cuts[s + 1]
1189 } else {
1190 cuts[0] + std::f64::consts::TAU
1191 };
1192 lo + (mid - lo).rem_euclid(std::f64::consts::TAU) < hi
1193 })
1194 .expect("the sectors cover every angle");
1195 hole_sector.push((h, sector));
1196 }
1197 let v_range_of = |builder: &Self, loops: &[usize]| -> GeopResult<(f64, f64)> {
1198 let mut lo = f64::INFINITY;
1199 let mut hi = f64::NEG_INFINITY;
1200 for &k in loops {
1201 for p in builder.loop_points(&face_loops[k])? {
1202 let v = v_of(p);
1203 lo = lo.min(v);
1204 hi = hi.max(v);
1205 }
1206 }
1207 Ok((lo, hi))
1208 };
1209 for s in 0..n {
1210 let next = (s + 1) % n;
1211 let lo = cuts[s];
1212 let hi = if s + 1 < n {
1213 cuts[s + 1]
1214 } else {
1215 cuts[0] + std::f64::consts::TAU
1216 };
1217 let mut lp: Vec<Use> = Vec::new();
1218 if let Some(ring) = bottom {
1219 lp.extend(ring_run(self, ring, s, next, true)?);
1220 }
1221 lp.push((meridians[next], true));
1222 if let Some(ring) = top {
1223 lp.extend(ring_run(self, ring, next, s, false)?);
1224 }
1225 lp.push((meridians[s], false));
1226 if !outward_natural {
1227 lp = lp.into_iter().rev().map(|(e, fw)| (e, !fw)).collect();
1228 }
1229 let mut loops = vec![lp];
1230 let mut members: Vec<usize> = bottom.into_iter().chain(top).collect();
1231 for &(h, sector) in &hole_sector {
1232 if sector == s {
1233 loops.push(face_loops[h].clone());
1234 members.push(h);
1235 }
1236 }
1237 let (v_lo, v_hi) = v_range_of(self, &members)?;
1238 let v0 = bottom_pole.unwrap_or(v_lo);
1239 let v1 = top_pole.unwrap_or(v_hi);
1240 if revolved.v_is_angle() && v1 - v0 >= 0.99 * std::f64::consts::TAU {
1244 return Err(GeopError::new(
1245 "it goes all the way round its torus' tube as well as its axis, as a whole torus does: not supported",
1246 ));
1247 }
1248 let (v0, v1) = widen_v(&revolved, v0, v1, bottom_pole.is_some(), top_pole.is_some());
1249 let mut name = base_name.clone();
1250 name.push(format!("q{s}"));
1251 self.faces.push(Face {
1252 surface: surface.clone(),
1253 same_sense,
1254 loops,
1255 outer: Some(0),
1256 patch: Some([lo, hi, v0, v1]),
1257 shell,
1258 name,
1259 id: self.faces[f].id,
1260 });
1261 }
1262 self.faces.remove(f);
1263 Ok(())
1264 }
1265
1266 fn cut_tube_wrapping_face(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1275 use std::f64::consts::TAU;
1276 let outward_natural = self.faces[f].outward_is_natural();
1277 let face_loops = self.faces[f].loops.clone();
1278 let tube = |p: P3| revolved.chart(p).1;
1279 let mut left = None;
1280 let mut right = None;
1281 let mut holes = Vec::new();
1282 let mut windings = Vec::new();
1283 for (k, lp) in face_loops.iter().enumerate() {
1284 let w = winding(&unwrap_tube(revolved, &self.loop_points(lp)?, true));
1285 windings.push(w);
1286 match w {
1287 0 => holes.push(k),
1288 1 | -1 => {
1289 let is_left = (w < 0) == outward_natural;
1294 let slot = if is_left { &mut left } else { &mut right };
1295 if slot.replace(k).is_some() {
1296 return Err(GeopError::new(format!(
1297 "it has more than one loop round its tube with the face at {} angles about the axis (the loops' windings round the tube: {windings:?})",
1298 if is_left { "larger" } else { "smaller" }
1299 )));
1300 }
1301 }
1302 w => {
1303 return Err(GeopError::new(format!(
1304 "a loop of it goes round its tube {w} times"
1305 )));
1306 }
1307 }
1308 }
1309 let (Some(left), Some(right)) = (left, right) else {
1310 return Err(GeopError::new(format!(
1311 "it is not bounded by two loops round its tube, one at either end (the loops' windings round the tube: {windings:?}, the face's outward normal natural: {outward_natural})"
1312 )));
1313 };
1314 let mean_angle = |builder: &Self, k: usize| -> GeopResult<f64> {
1318 let angles = unwrap(
1319 revolved,
1320 &builder.loop_points(&face_loops[k])?,
1321 false,
1322 outward_natural,
1323 );
1324 Ok(angles.iter().sum::<f64>() / angles.len().max(1) as f64)
1325 };
1326 let (a_left, a_right) = (mean_angle(self, left)?, mean_angle(self, right)?);
1327 let u_start = a_right + (a_left - a_right).rem_euclid(TAU) / 2.0;
1328 let u_of = |p: P3| -> GeopResult<f64> {
1329 let a = revolved
1330 .chart(p)
1331 .0
1332 .ok_or_else(|| GeopError::new(format!("a point of it, {p:?}, is on its axis")))?;
1333 Ok(u_start + (a - u_start).rem_euclid(TAU))
1334 };
1335
1336 let mut blocked: Vec<(f64, f64)> = Vec::new();
1338 for (k, lp) in face_loops.iter().enumerate() {
1339 if holes.contains(&k) {
1340 let angles = unwrap_tube(revolved, &self.loop_points(lp)?, false);
1341 let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1342 let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1343 blocked.push((lo, hi));
1344 }
1345 for &u in lp {
1346 let a = tube(self.vertices[self.ends(u).0].point.to_f64_array());
1347 blocked.push((a, a));
1348 }
1349 }
1350 let cuts = choose_cuts(&blocked);
1351 let ring_vertices = self.split_rings(f, &[left, right], &cuts, &|p| Some(tube(p)))?;
1352 let face_loops = self.faces[f].loops.clone();
1354
1355 let mut parallels = Vec::new();
1357 for (c, &cut) in cuts.iter().enumerate() {
1358 let (a, b) = (ring_vertices[&left][c], ring_vertices[&right][c]);
1359 let from = u_of(self.vertices[a].point.to_f64_array())?;
1360 let to = u_of(self.vertices[b].point.to_f64_array())?;
1361 if from >= to {
1362 return Err(GeopError::new(format!(
1363 "its rings round the tube cross at the angle {cut} round it: the left one at the angle {from} about the axis, the right one at {to}"
1364 )));
1365 }
1366 let curve = revolved.parallel(cut, from, to)?;
1367 let mut name = self.faces[f].name.clone();
1368 name.push(format!("m{c}"));
1369 parallels.push(self.edges.len());
1370 self.edges.push(Edge {
1371 curve,
1372 start: a,
1373 end: b,
1374 name,
1375 alive: true,
1376 id: None,
1377 shape: Some(revolved.iso()),
1378 });
1379 }
1380
1381 let n = cuts.len();
1386 let upper = |s: usize| {
1387 if s + 1 < n {
1388 cuts[s + 1]
1389 } else {
1390 cuts[0] + TAU
1391 }
1392 };
1393 let mut hole_piece = Vec::new();
1394 for &h in &holes {
1395 let a = unwrap_tube(revolved, &self.loop_points(&face_loops[h])?, false);
1396 let mid = a.iter().sum::<f64>() / a.len() as f64;
1397 let piece = (0..n)
1398 .find(|&s| cuts[s] + (mid - cuts[s]).rem_euclid(TAU) < upper(s))
1399 .expect("the pieces cover every angle");
1400 hole_piece.push((h, piece));
1401 }
1402 let (w_left, w_right) = (windings[left], windings[right]);
1403 let face = &self.faces[f];
1404 let (surface, same_sense, shell, base_name, id) = (
1405 face.surface.clone(),
1406 face.same_sense,
1407 face.shell,
1408 face.name.clone(),
1409 face.id,
1410 );
1411 for s in 0..n {
1412 let next = (s + 1) % n;
1413 let mut lp = vec![(parallels[s], true)];
1414 lp.extend(self.run_between(
1415 &face_loops[right],
1416 ring_vertices[&right][s],
1417 ring_vertices[&right][next],
1418 w_right > 0,
1419 )?);
1420 lp.push((parallels[next], false));
1421 lp.extend(self.run_between(
1422 &face_loops[left],
1423 ring_vertices[&left][next],
1424 ring_vertices[&left][s],
1425 w_left < 0,
1426 )?);
1427 let mut loops = vec![lp];
1428 for &(h, piece) in &hole_piece {
1429 if piece == s {
1430 loops.push(face_loops[h].clone());
1431 }
1432 }
1433 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1435 for lp in &loops {
1436 for p in self.loop_points(lp)? {
1437 let u = u_of(p)?;
1438 lo = lo.min(u);
1439 hi = hi.max(u);
1440 }
1441 }
1442 let span = hi - lo;
1443 let margin = (0.02 * span + 1e-3).min((TAU - span) / 4.0);
1444 if !outward_natural {
1445 loops[0] = loops[0].iter().rev().map(|&(e, fw)| (e, !fw)).collect();
1446 }
1447 let mut name = base_name.clone();
1448 name.push(format!("q{s}"));
1449 self.faces.push(Face {
1450 surface: surface.clone(),
1451 same_sense,
1452 loops,
1453 outer: Some(0),
1454 patch: Some([lo - margin, hi + margin, cuts[s], upper(s)]),
1455 shell,
1456 name,
1457 id,
1458 });
1459 }
1460 self.faces.remove(f);
1461 Ok(())
1462 }
1463
1464 fn cut_whole_torus(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1469 use std::f64::consts::TAU;
1470 let outward_natural = self.faces[f].outward_is_natural();
1471 let holes = self.faces[f].loops.clone();
1472 let mut round_tube: Vec<(f64, f64)> = Vec::new();
1475 let mut round_axis: Vec<(f64, f64)> = Vec::new();
1476 for lp in &holes {
1477 let points = self.loop_points(lp)?;
1478 let range = |angles: Vec<f64>| {
1479 let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1480 let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1481 (lo, hi)
1482 };
1483 round_tube.push(range(unwrap_tube(revolved, &points, false)));
1484 round_axis.push(range(unwrap(revolved, &points, false, outward_natural)));
1485 }
1486 let cuts = choose_cuts(&round_tube);
1487 let start = choose_cuts(&round_axis)[0];
1488 let base_name = self.faces[f].name.clone();
1489 let mut parallels = Vec::new();
1490 for (c, &cut) in cuts.iter().enumerate() {
1491 let curve = revolved.parallel(cut, start, start)?;
1492 let point = curve.evaluate(curve.domain().0)?;
1493 let mut name = base_name.clone();
1494 name.push(format!("m{c}"));
1495 let v = self.vertices.len();
1496 self.vertices.push(Vertex {
1497 point,
1498 origin: point,
1499 name: [name.clone(), vec!["v".to_string()]].concat(),
1500 id: None,
1501 });
1502 parallels.push(self.edges.len());
1503 self.edges.push(Edge {
1504 curve,
1505 start: v,
1506 end: v,
1507 name,
1508 alive: true,
1509 id: None,
1510 shape: Some(revolved.iso()),
1511 });
1512 }
1513 let n = cuts.len();
1514 let upper = |s: usize| {
1515 if s + 1 < n {
1516 cuts[s + 1]
1517 } else {
1518 cuts[0] + TAU
1519 }
1520 };
1521 let face = &self.faces[f];
1522 let (surface, same_sense, shell, id) =
1523 (face.surface.clone(), face.same_sense, face.shell, face.id);
1524 let mut bands: Vec<Vec<Vec<Use>>> = (0..n)
1525 .map(|s| {
1526 let next = (s + 1) % n;
1530 vec![
1531 vec![(parallels[s], outward_natural)],
1532 vec![(parallels[next], !outward_natural)],
1533 ]
1534 })
1535 .collect();
1536 for lp in holes {
1537 let a = unwrap_tube(revolved, &self.loop_points(&lp)?, false);
1538 let mid = a.iter().sum::<f64>() / a.len() as f64;
1539 let band = (0..n)
1540 .find(|&s| cuts[s] + (mid - cuts[s]).rem_euclid(TAU) < upper(s))
1541 .expect("the bands cover every angle");
1542 bands[band].push(lp);
1543 }
1544 for (s, loops) in bands.into_iter().enumerate() {
1545 let mut name = base_name.clone();
1546 name.push(format!("b{s}"));
1547 self.faces.push(Face {
1548 surface: surface.clone(),
1549 same_sense,
1550 loops,
1551 outer: None,
1552 patch: None,
1553 shell,
1554 name,
1555 id,
1556 });
1557 }
1558 self.faces.remove(f);
1559 Ok(())
1560 }
1561
1562 fn turns_more_than_once(&self, f: usize, revolved: &Revolved) -> GeopResult<bool> {
1567 let natural = self.faces[f].outward_is_natural();
1568 for lp in &self.faces[f].loops {
1569 let angles = unwrap(revolved, &self.loop_points(lp)?, false, natural);
1570 let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1571 let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1572 if hi - lo >= std::f64::consts::TAU {
1573 return Ok(true);
1574 }
1575 }
1576 Ok(false)
1577 }
1578
1579 fn cut_helical_face(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1591 use std::f64::consts::PI;
1592 if self.faces[f].loops.len() != 1 {
1593 return Err(GeopError::new(format!(
1594 "it has {} loops: only a strip bounded by one loop is cut",
1595 self.faces[f].loops.len()
1596 )));
1597 }
1598 if revolved.v_is_angle() {
1599 return Err(GeopError::new(
1600 "it lies on a torus: only strips on cylinders, cones and other surfaces of revolution with a profile from end to end are cut",
1601 ));
1602 }
1603 let lp = self.faces[f].loops[0].clone();
1604 let mut samples: Vec<(usize, f64, f64, f64)> = Vec::new(); let mut prev: Option<f64> = None;
1607 for (k, &u) in lp.iter().enumerate() {
1608 for (t, p) in self.samples(u, LOOP_SAMPLES)? {
1609 let (Some(a), v) = revolved.chart(p) else {
1610 return Err(GeopError::new(format!(
1611 "its loop runs through its axis at {p:?}"
1612 )));
1613 };
1614 let a = prev.map_or(a, |r| unwrap_angle(a, r));
1615 prev = Some(a);
1616 samples.push((k, t, a, v));
1617 }
1618 }
1619 let lo = samples.iter().map(|s| s.2).fold(f64::INFINITY, f64::min);
1620 let hi = samples
1621 .iter()
1622 .map(|s| s.2)
1623 .fold(f64::NEG_INFINITY, f64::max);
1624 let corners: Vec<f64> = samples
1626 .iter()
1627 .enumerate()
1628 .filter(|&(i, s)| i == 0 || samples[i - 1].0 != s.0)
1629 .map(|(_, s)| s.2)
1630 .collect();
1631 let clearance = |offset: f64| {
1632 corners
1633 .iter()
1634 .map(|&a| {
1635 let r = (a - offset).rem_euclid(PI);
1636 r.min(PI - r)
1637 })
1638 .fold(f64::INFINITY, f64::min)
1639 };
1640 let offset = (0..720)
1641 .map(|i| lo + PI * i as f64 / 720.0)
1642 .max_by(|a, b| clearance(*a).total_cmp(&clearance(*b)))
1643 .expect("candidates");
1644 let cuts: Vec<f64> = (1..)
1645 .map(|k| offset + PI * k as f64)
1646 .take_while(|&a| a < hi)
1647 .collect();
1648
1649 let mut crossings: Vec<(usize, f64, usize, f64)> = Vec::new(); for w in samples.windows(2) {
1653 let ((k0, t0, a0, _), (k1, t1, a1, _)) = (w[0], w[1]);
1654 if k0 != k1 {
1655 continue;
1656 }
1657 let u = lp[k0];
1658 for (c, &cut) in cuts.iter().enumerate() {
1659 if (a0 < cut) == (a1 < cut) {
1661 continue;
1662 }
1663 let angle_at = |t: f64| -> GeopResult<f64> {
1666 let p = super::geometry::point_at(&self.edges[u.0].curve, t)?;
1667 let a = revolved
1668 .chart(p)
1669 .0
1670 .ok_or_else(|| GeopError::new("its loop runs through its axis"))?;
1671 Ok(unwrap_angle(a, cut))
1672 };
1673 let below = angle_at(t0)? < cut;
1674 let Bracket { lo, hi, .. } = bisect(t0, t1, 100, true, |mid| -> GeopResult<_> {
1675 Ok(Some((angle_at(mid)? < cut) == below))
1676 })?;
1677 let t = 0.5 * (lo + hi);
1678 let v = revolved
1679 .chart(super::geometry::point_at(&self.edges[u.0].curve, t)?)
1680 .1;
1681 crossings.push((u.0, t, c, v));
1682 }
1683 }
1684 let mut by_edge: HashMap<usize, Vec<(f64, usize, f64)>> = HashMap::new();
1686 for &(e, t, c, v) in &crossings {
1687 by_edge.entry(e).or_default().push((t, c, v));
1688 }
1689 let mut edges: Vec<usize> = by_edge.keys().copied().collect();
1690 edges.sort();
1691 let mut on_cut: Vec<Vec<(f64, usize)>> = vec![Vec::new(); cuts.len()]; for e in edges {
1693 let mut list = by_edge[&e].clone();
1694 list.sort_by(|a, b| a.0.total_cmp(&b.0));
1695 let ts: Vec<Scalar> = list.iter().map(|x| Scalar::from_f64(x.0)).collect();
1696 let (d0, d1) = self.edges[e].curve.domain();
1697 if ts
1698 .iter()
1699 .any(|t| !(t.definitely_greater(d0) && t.definitely_less(d1)))
1700 {
1701 return Err(GeopError::new(format!(
1702 "a cut crosses its loop at a vertex, at the end of the edge {}",
1703 self.edges[e].label()
1704 )));
1705 }
1706 let created = self.split_edge(e, &ts)?;
1707 for ((_, c, v), vertex) in list.into_iter().zip(created) {
1708 on_cut[c].push((v, vertex));
1709 }
1710 }
1711 let mut partner: HashMap<usize, (usize, usize)> = HashMap::new(); let mut count = 0;
1715 for (c, list) in on_cut.iter_mut().enumerate() {
1716 list.sort_by(|a, b| a.0.total_cmp(&b.0));
1717 if list.len() % 2 != 0 {
1718 return Err(GeopError::new(format!(
1719 "the cut at the angle {} crosses its loop an odd number of times, {}",
1720 cuts[c],
1721 list.len()
1722 )));
1723 }
1724 for pair in list.chunks(2) {
1725 let ((vb, b), (vt, t)) = (pair[0], pair[1]);
1726 let pb = self.vertices[b].point.to_f64_array();
1727 let pt = self.vertices[t].point.to_f64_array();
1728 let curve = self.meridian(revolved, cuts[c], (pb, vb, false), (pt, vt, false))?;
1729 let mut name = self.faces[f].name.clone();
1730 name.push(format!("m{count}"));
1731 count += 1;
1732 let m = self.edges.len();
1733 self.edges.push(Edge {
1734 curve,
1735 start: b,
1736 end: t,
1737 name,
1738 alive: true,
1739 id: None,
1740 shape: Some(revolved.iso()),
1741 });
1742 partner.insert(b, (t, m));
1743 partner.insert(t, (b, m));
1744 }
1745 }
1746
1747 let lp = self.faces[f].loops[0].clone();
1749 let starting: HashMap<usize, usize> = lp
1750 .iter()
1751 .enumerate()
1752 .map(|(i, &u)| (self.ends(u).0, i))
1753 .collect();
1754 let mut used = vec![false; lp.len()];
1755 let mut pieces: Vec<Vec<Use>> = Vec::new();
1756 for first in 0..lp.len() {
1757 if used[first] {
1758 continue;
1759 }
1760 let around = walk(first, |&i| {
1764 let end = self.ends(lp[i]).1;
1765 Ok::<_, GeopError>(Some(match partner.get(&end) {
1766 Some(&(other, _)) => starting[&other],
1767 None => (i + 1) % lp.len(),
1768 }))
1769 })?;
1770 if around.end != Walked::Closed {
1771 return Err(GeopError::new(
1772 "tracing a piece along its loop and the meridians did not close",
1773 ));
1774 }
1775 let mut piece = Vec::new();
1776 for &i in &around.nodes {
1777 used[i] = true;
1778 piece.push(lp[i]);
1779 let end = self.ends(lp[i]).1;
1780 if let Some(&(_, m)) = partner.get(&end) {
1781 piece.push((m, self.edges[m].start == end));
1782 }
1783 }
1784 pieces.push(piece);
1785 }
1786 let face = &self.faces[f];
1787 let (surface, same_sense, shell, base_name, id) = (
1788 face.surface.clone(),
1789 face.same_sense,
1790 face.shell,
1791 face.name.clone(),
1792 face.id,
1793 );
1794 for (q, piece) in pieces.into_iter().enumerate() {
1795 let mut name = base_name.clone();
1796 name.push(format!("q{q}"));
1797 self.faces.push(Face {
1798 surface: surface.clone(),
1799 same_sense,
1800 loops: vec![piece],
1801 outer: Some(0),
1802 patch: None,
1803 shell,
1804 name,
1805 id,
1806 });
1807 }
1808 self.faces.remove(f);
1809 Ok(())
1810 }
1811
1812 fn split_rings(
1816 &mut self,
1817 f: usize,
1818 rings: &[usize],
1819 cuts: &[f64],
1820 angle_of: &dyn Fn(P3) -> Option<f64>,
1821 ) -> GeopResult<HashMap<usize, Vec<usize>>> {
1822 let mut ring_vertices: HashMap<usize, Vec<usize>> = HashMap::new();
1823 let mut splits: HashMap<usize, Vec<(f64, usize, usize)>> = HashMap::new(); for &k in rings {
1825 let lp = self.faces[f].loops[k].clone();
1826 for (c, &cut) in cuts.iter().enumerate() {
1827 let (e, t) = self.crossing(&lp, cut, angle_of)?;
1828 splits.entry(e).or_default().push((t, k, c));
1829 }
1830 }
1831 let mut edges: Vec<usize> = splits.keys().copied().collect();
1832 edges.sort();
1833 for e in edges {
1834 let mut list = splits[&e].clone();
1835 list.sort_by(|a, b| a.0.total_cmp(&b.0));
1836 let ts: Vec<Scalar> = list.iter().map(|(t, _, _)| Scalar::from_f64(*t)).collect();
1837 let (lo, hi) = self.edges[e].curve.domain();
1838 if ts
1839 .iter()
1840 .any(|t| !(t.definitely_greater(lo) && t.definitely_less(hi)))
1841 {
1842 return Err(GeopError::new(format!(
1843 "a cut crosses a loop round it at a vertex: the edge {} at its parameters {ts:?}, its domain [{lo:?}, {hi:?}]",
1844 self.edges[e].label()
1845 )));
1846 }
1847 let created = self.split_edge(e, &ts)?;
1848 for ((_, ring, cut), v) in list.into_iter().zip(created) {
1849 let slots = ring_vertices
1850 .entry(ring)
1851 .or_insert_with(|| vec![usize::MAX; cuts.len()]);
1852 slots[cut] = v;
1853 }
1854 }
1855 Ok(ring_vertices)
1856 }
1857
1858 fn run_between(&self, lp: &[Use], a: usize, b: usize, along: bool) -> GeopResult<Vec<Use>> {
1861 let seq: Vec<Use> = if along {
1862 lp.to_vec()
1863 } else {
1864 lp.iter().rev().map(|&(e, fw)| (e, !fw)).collect()
1865 };
1866 let start = seq
1867 .iter()
1868 .position(|&u| self.ends(u).0 == a)
1869 .ok_or_else(|| GeopError::new("a cut vertex is not on its ring"))?;
1870 let mut run = Vec::new();
1871 for j in 0..seq.len() {
1872 let u = seq[(start + j) % seq.len()];
1873 run.push(u);
1874 if self.ends(u).1 == b {
1875 return Ok(run);
1876 }
1877 }
1878 Err(GeopError::new("a ring does not reach the next cut"))
1879 }
1880
1881 fn crossing(
1886 &self,
1887 lp: &[Use],
1888 cut: f64,
1889 angle_of: &dyn Fn(P3) -> Option<f64>,
1890 ) -> GeopResult<(usize, f64)> {
1891 use std::f64::consts::TAU;
1892 let mut prev: Option<f64> = None;
1893 let mut covered = (f64::INFINITY, f64::NEG_INFINITY);
1894 for &u in lp {
1895 let samples = self.samples(u, LOOP_SAMPLES)?;
1896 let mut last: Option<(f64, f64)> = None; for &(t, p) in &samples {
1898 let Some(a) = angle_of(p) else {
1899 continue;
1900 };
1901 let a = match prev {
1902 Some(r) => unwrap_angle(a, r),
1903 None => a,
1904 };
1905 if let Some((t_prev, a_prev)) = last {
1906 let (lo, hi) = (a_prev.min(a), a_prev.max(a));
1907 let x = cut + TAU * ((lo - cut) / TAU).ceil();
1908 if x <= hi && lo < hi {
1909 let theta = |t: f64| -> GeopResult<f64> {
1910 let p = super::geometry::point_at(&self.edges[u.0].curve, t)?;
1911 let angle = angle_of(p).ok_or_else(|| {
1912 GeopError::new("a loop round its axis runs through it")
1913 })?;
1914 Ok(unwrap_angle(angle, x) - x)
1915 };
1916 let below = theta(t_prev)? <= 0.0;
1917 let Bracket { lo, hi, .. } =
1918 bisect(t_prev, t, 100, true, |mid| -> GeopResult<_> {
1919 Ok(Some((theta(mid)? <= 0.0) == below))
1920 })?;
1921 return Ok((u.0, 0.5 * (lo + hi)));
1922 }
1923 }
1924 last = Some((t, a));
1925 prev = Some(a);
1926 covered = (covered.0.min(a), covered.1.max(a));
1927 }
1928 }
1929 Err(GeopError::new(format!(
1930 "a loop round its axis never reaches the angle {cut}: it covers the angles {covered:?}"
1931 )))
1932 }
1933
1934 fn meridian(
1937 &self,
1938 revolved: &Revolved,
1939 angle: f64,
1940 (pb, vb, _): (P3, f64, bool),
1941 (pt, vt, _): (P3, f64, bool),
1942 ) -> GeopResult<NurbCurve3D> {
1943 let turn = revolved.turn(angle)?;
1944 let profile = match &revolved.profile {
1945 Profile::Curve(curve) => {
1946 let curve = curve.to_nurbs()?;
1947 let back = turn.inverse();
1948 let at = |p: P3| -> GeopResult<Scalar> {
1949 let p = back.apply(&s3(p)).to_f64_array();
1950 super::geometry::parameter_of(&curve, p)
1951 };
1952 let (tb, tt) = (at(pb)?, at(pt)?);
1953 let (lo, hi) = curve.domain();
1954 let (a, b, reversed) = if tb.definitely_less(tt) {
1955 (tb, tt, false)
1956 } else {
1957 (tt, tb, true)
1958 };
1959 let piece = curve.sub_curve(
1960 if a.could_be_equal(lo) { lo } else { a },
1961 if b.could_be_equal(hi) { hi } else { b },
1962 )?;
1963 if reversed { piece.reverse() } else { piece }
1964 }
1965 _ => revolved.profile_curve(vb, vt)?,
1966 };
1967 Ok(profile.transform(&turn.motion()))
1968 }
1969
1970 fn spec(self, solid: bool, label: String) -> GeopResult<ImportedBody> {
1972 let Builder {
1973 vertices,
1974 edges,
1975 faces,
1976 shells,
1977 scope,
1978 ..
1979 } = self;
1980 let mut vertices = vertices;
1981 let mut edges = edges;
1982 let mut edge_map = vec![usize::MAX; edges.len()];
1984 let mut used_vertex = vec![false; vertices.len()];
1985 for face in &faces {
1986 for &(e, _) in face.loops.iter().flatten() {
1987 edge_map[e] = 0;
1988 used_vertex[edges[e].start] = true;
1989 used_vertex[edges[e].end] = true;
1990 }
1991 }
1992 let mut vertex_map = vec![usize::MAX; vertices.len()];
1993 let mut next = 0;
1994 for (v, used) in used_vertex.iter().enumerate() {
1995 if *used {
1996 vertex_map[v] = next;
1997 next += 1;
1998 }
1999 }
2000
2001 let mut face_specs = Vec::with_capacity(faces.len());
2003 let mut extents = Vec::with_capacity(faces.len());
2005 let mut face_names = Vec::with_capacity(faces.len());
2006 let mut face_shells = vec![Vec::new(); shells];
2007 let face_ctx = |face: &Face| {
2008 let what = format!("building the face #{} ({})", face.id, face.name.join(","));
2009 move |e: GeopError| e.with_context(what.clone())
2010 };
2011 let mut built = Vec::with_capacity(faces.len());
2014 for face in &faces {
2015 let ctx = face_ctx(face);
2016 let patch = patch_of(face, &edges, &scope).map_err(&ctx)?;
2017 let surface = if face.outward_is_natural() {
2018 patch.surface.clone()
2019 } else {
2020 patch.surface.reverse_u()
2021 };
2022 let grid = Grid::new(&surface);
2023 built.push((patch, surface, grid));
2024 }
2025 let (rebuild, healed) = heal(&faces, &built, &mut edges, &mut vertices, &scope)?;
2026 for (f, face) in faces.iter().enumerate() {
2027 if rebuild[f] {
2028 let surface = rebuilt_surface(face, &edges).map_err(|e| {
2029 e.with_context(format!(
2030 "rebuilding the face #{} ({}) from its edges, where its surface contradicts its neighbours'",
2031 face.id,
2032 face.name.join(",")
2033 ))
2034 })?;
2035 let grid = Grid::new(&surface);
2036 let patch = Patch {
2037 surface: surface.clone(),
2038 extent: None,
2039 kind: PatchKind::Other,
2040 };
2041 built[f] = (patch, surface, grid);
2042 }
2043 }
2044 for (face, (patch, surface, grid)) in faces.iter().zip(built) {
2045 let ctx = face_ctx(face);
2046 let natural = face.outward_is_natural();
2047 let mut loops = Vec::new();
2048 for lp in &face.loops {
2049 loops.push(
2050 fit_loop(lp, &surface, &grid, &patch, &edges, &mut vertices, &scope)
2051 .map_err(&ctx)?,
2052 );
2053 }
2054 let pieces = match face.outer {
2059 Some(k) => {
2060 let outer = loops.remove(k);
2061 vec![(outer, loops)]
2062 }
2063 None => split_by_outer_loops(&surface, loops).map_err(|e| {
2064 ctx(e.with_context(format!(
2065 "its outward normal the surface's natural one: {natural}; built over the angles and profile parameters {:?}",
2066 patch.extent
2067 )))
2068 })?,
2069 };
2070 let several = pieces.len() > 1;
2071 for (k, (outer_loop, holes)) in pieces.into_iter().enumerate() {
2072 face_shells[face.shell].push(face_specs.len());
2073 face_specs.push((surface.clone(), outer_loop, holes));
2074 extents.push(patch.extent);
2075 let mut name = face.name.clone();
2076 if several {
2077 name.push(format!("o{k}"));
2078 }
2079 face_names.push(name);
2080 }
2081 }
2082
2083 let read: Vec<NurbCurve3D> = edges.iter().map(|e| e.curve.clone()).collect();
2089 let mut total = vec![0.0f64; edges.len()];
2091 let mut history: Vec<Vec<f64>> = vec![Vec::new(); edges.len()];
2092 let mut measure = vec![true; edges.len()];
2095 refine(0, WIDENINGS, |round| -> GeopResult<_> {
2097 let mut gaps = vec![[0.0f64; 3]; edges.len()];
2098 for (((surface, outer, holes), face_name), extent) in
2099 face_specs.iter().zip(&face_names).zip(&extents)
2100 {
2101 for (on, pcurve) in outer.iter().chain(holes.iter().flatten()) {
2102 if let CoedgeOnLocal::Edge(e, _) = *on
2103 && measure[e]
2104 {
2105 let (gap, worst) =
2106 edge_gap(&edges[e].curve, surface, pcurve).map_err(|err| {
2107 err.with_context(format!(
2108 "measuring how far the edge {} lies from where its pcurve puts it",
2109 edges[e].label()
2110 ))
2111 })?;
2112 let most = total[e] + gap.iter().copied().fold(0.0, f64::max);
2115 if 2.0 * most > ACCURACY {
2116 let off = off_surface(&read[e], surface, pcurve).map_or_else(
2119 |err| format!("not measured: {err}"),
2120 |d| format!("{d:e} mm"),
2121 );
2122 let furthest = worst.map_or_else(String::new, |w| {
2123 format!(
2124 ", furthest {:e} mm at {} of the pcurve, which puts it at {:?}, the edge's nearest point {:?}",
2125 w.apart, w.at, w.point, w.near
2126 )
2127 });
2128 return Err(GeopError::new(format!(
2129 "the edge {} lies up to {most:e} mm from where its pcurve puts it on its face {}, its own points up to {off} from the surface — the curve widened by that either way would be wider than the {ACCURACY:e} mm the kernel can carry as one curve (measured {gap:?} per coordinate after widening it by {:?} in {round} rounds{furthest}; the edge from {:?} to {:?}, the pcurve from {:?} to {:?} on the patch over the angles and profile parameters {extent:?})",
2130 edges[e].label(),
2131 face_name.join(","),
2132 history[e],
2133 read[e].evaluate(read[e].domain().0)?.to_f64_array(),
2134 read[e].evaluate(read[e].domain().1)?.to_f64_array(),
2135 pcurve.evaluate(pcurve.domain().0)?,
2136 pcurve.evaluate(pcurve.domain().1)?,
2137 )));
2138 }
2139 for c in 0..3 {
2140 gaps[e][c] = gaps[e][c].max(gap[c]);
2141 }
2142 }
2143 }
2144 }
2145 if gaps.iter().all(|g| *g == [0.0; 3]) {
2146 return Ok(Refine::Done(()));
2147 }
2148 for ((edge, gap), total) in edges.iter_mut().zip(&gaps).zip(&mut total) {
2149 let gap = if round == 0 {
2159 *gap
2160 } else {
2161 gap.map(|g| 2.0 * g)
2162 };
2163 edge.curve = widened(&edge.curve, gap)?;
2164 *total += gap.iter().copied().fold(0.0, f64::max);
2165 }
2166 for (h, gap) in history.iter_mut().zip(&gaps) {
2167 h.push(gap.iter().copied().fold(0.0, f64::max));
2168 }
2169 for (m, gap) in measure.iter_mut().zip(&gaps) {
2170 *m = *gap != [0.0; 3];
2171 }
2172 Ok(Refine::Finer(round + 1))
2173 })?;
2174
2175 let mut edge_specs = Vec::new();
2176 let mut edge_names = Vec::new();
2177 for (e, edge) in edges.iter().enumerate() {
2178 if edge_map[e] == usize::MAX {
2179 continue;
2180 }
2181 edge_map[e] = edge_specs.len();
2182 edge_specs.push(EdgeSpec {
2183 curve: edge.curve.clone(),
2184 start: vertex_map[edge.start],
2185 end: vertex_map[edge.end],
2186 });
2187 edge_names.push(edge.name.clone());
2188 }
2189 let mut vertex_points = Vec::new();
2190 let mut vertex_names = Vec::new();
2191 for (v, vertex) in vertices.into_iter().enumerate() {
2192 if used_vertex[v] {
2193 vertex_points.push(vertex.point);
2194 vertex_names.push(vertex.name);
2195 }
2196 }
2197 let remap = |lp: Vec<(CoedgeOnLocal, NurbCurve2D)>| -> Vec<CoedgeSpec> {
2198 lp.into_iter()
2199 .map(|(on, pcurve)| CoedgeSpec {
2200 on: match on {
2201 CoedgeOnLocal::Edge(e, forward) => {
2202 CoedgeOn::Edge(edge_map[e], Sense::from_forward(forward))
2203 }
2204 CoedgeOnLocal::Vertex(v) => CoedgeOn::Vertex(vertex_map[v]),
2205 },
2206 pcurve,
2207 })
2208 .collect()
2209 };
2210 let faces = face_specs
2211 .into_iter()
2212 .map(|(surface, outer, holes)| FaceSpec {
2213 surface,
2214 outer: remap(outer),
2215 holes: holes.into_iter().map(remap).collect(),
2216 })
2217 .collect();
2218 let spec = BodySpec {
2219 vertices: vertex_points,
2220 edges: edge_specs,
2221 faces,
2222 shells: face_shells.into_iter().filter(|s| !s.is_empty()).collect(),
2223 solid,
2224 };
2225 Ok(ImportedBody {
2226 label,
2227 spec,
2228 vertex_names,
2229 edge_names,
2230 face_names,
2231 healed,
2232 })
2233 }
2234}
2235
2236#[derive(Clone, Copy, Debug)]
2237enum CoedgeOnLocal {
2238 Edge(usize, bool),
2239 Vertex(usize),
2240}
2241
2242struct Patch {
2244 surface: NurbSurface3D,
2245 extent: Option<[f64; 4]>,
2248 kind: PatchKind,
2249}
2250
2251#[derive(Clone, Debug)]
2254enum PatchKind {
2255 Plane,
2258 Revolved(Revolved),
2261 Other,
2262}
2263
2264#[derive(Clone, Copy, Debug)]
2267struct Pole {
2268 fixes_u: bool,
2269 at: Scalar,
2270 point: P3,
2271}
2272
2273fn poles_of(surface: &NurbSurface3D, uncertainty: f64) -> GeopResult<Vec<Pole>> {
2277 let point = |i: usize, j: usize| -> GeopResult<P3> {
2278 Ok(surface.control_points[i * surface.num_v + j]
2279 .dehomogenized()?
2280 .to_f64_array())
2281 };
2282 let (u_lo, u_hi) = surface.domain_u();
2283 let (v_lo, v_hi) = surface.domain_v();
2284 let (nu, nv) = (surface.num_u, surface.num_v);
2285 let mut poles = Vec::new();
2286 type Row = (bool, Scalar, Vec<(usize, usize)>);
2289 let rows: [Row; 4] = [
2290 (true, u_lo, (0..nv).map(|j| (0, j)).collect()),
2291 (true, u_hi, (0..nv).map(|j| (nu - 1, j)).collect()),
2292 (false, v_lo, (0..nu).map(|i| (i, 0)).collect()),
2293 (false, v_hi, (0..nu).map(|i| (i, nv - 1)).collect()),
2294 ];
2295 for (fixes_u, at, row) in rows {
2296 let points: Vec<P3> = row
2297 .iter()
2298 .map(|&(i, j)| point(i, j))
2299 .collect::<GeopResult<_>>()?;
2300 if points
2301 .iter()
2302 .all(|&p| distance(p, points[0]) <= uncertainty)
2303 {
2304 poles.push(Pole {
2305 fixes_u,
2306 at,
2307 point: points[0],
2308 });
2309 }
2310 }
2311 Ok(poles)
2312}
2313
2314fn unwrap(revolved: &Revolved, points: &[P3], closing: bool, ccw: bool) -> Vec<f64> {
2327 use std::f64::consts::TAU;
2328 let start = points
2331 .iter()
2332 .position(|&p| revolved.chart(p).0.is_some())
2333 .unwrap_or(0);
2334 let n = points.len();
2335 let mean_v = points.iter().map(|&p| revolved.chart(p).1).sum::<f64>() / n.max(1) as f64;
2336 let mut out: Vec<f64> = Vec::with_capacity(n + 1);
2337 let mut pole: Option<f64> = None;
2338 for k in 0..n + usize::from(closing) {
2339 let p = points[(start + k) % n];
2340 let (angle, v) = revolved.chart(p);
2341 let Some(a) = angle else {
2342 pole = Some(v);
2343 continue;
2344 };
2345 let a = match (out.last(), pole.take()) {
2346 (None, _) => a,
2347 (Some(&prev), None) => unwrap_angle(a, prev),
2348 (Some(&prev), Some(v)) => {
2349 let turn = (a - prev).rem_euclid(TAU);
2350 let forwards = ccw != (v > mean_v);
2351 prev + if forwards || turn == 0.0 {
2352 turn
2353 } else {
2354 turn - TAU
2355 }
2356 }
2357 };
2358 out.push(a);
2359 }
2360 out
2361}
2362
2363fn unwrap_tube(revolved: &Revolved, points: &[P3], closing: bool) -> Vec<f64> {
2367 let n = points.len();
2368 if n == 0 {
2369 return Vec::new();
2370 }
2371 let mut out: Vec<f64> = Vec::with_capacity(n + 1);
2372 for k in 0..n + usize::from(closing) {
2373 let a = revolved.chart(points[k % n]).1;
2374 out.push(match out.last() {
2375 None => a,
2376 Some(&prev) => unwrap_angle(a, prev),
2377 });
2378 }
2379 out
2380}
2381
2382fn winding(angles: &[f64]) -> i64 {
2384 match (angles.first(), angles.last()) {
2385 (Some(a), Some(b)) => ((b - a) / std::f64::consts::TAU).round() as i64,
2386 _ => 0,
2387 }
2388}
2389
2390fn choose_cuts(blocked: &[(f64, f64)]) -> Vec<f64> {
2395 use std::f64::consts::TAU;
2396 let clearance = |a: f64| -> f64 {
2397 blocked
2398 .iter()
2399 .map(|&(lo, hi)| {
2400 if hi - lo >= TAU {
2401 return 0.0;
2402 }
2403 let rel = (a - lo).rem_euclid(TAU);
2405 if rel <= hi - lo {
2406 0.0
2407 } else {
2408 (rel - (hi - lo)).min(TAU - rel)
2409 }
2410 })
2411 .fold(f64::INFINITY, f64::min)
2412 };
2413 let mut best: Option<(f64, Vec<f64>)> = None;
2414 for n in 2..=8usize {
2415 let step = TAU / n as f64;
2416 for i in 0..720 {
2417 let start = step * i as f64 / 720.0;
2418 let cuts: Vec<f64> = (0..n).map(|k| start + step * k as f64).collect();
2419 let c = cuts
2420 .iter()
2421 .map(|&a| clearance(a))
2422 .fold(f64::INFINITY, f64::min);
2423 if best.as_ref().is_none_or(|(b, _)| c > 2.0 * b) {
2425 best = Some((c, cuts));
2426 }
2427 }
2428 }
2429 best.expect("at least one choice").1
2430}
2431
2432fn widen_v(revolved: &Revolved, v0: f64, v1: f64, pole0: bool, pole1: bool) -> (f64, f64) {
2435 if matches!(revolved.profile, Profile::Curve(_)) {
2436 return (v0, v1);
2437 }
2438 let span = (v1 - v0).abs();
2439 let mut margin = 0.02 * span + 1e-3 * span.max(1e-9);
2440 if revolved.v_is_angle() {
2441 margin = margin.min((std::f64::consts::TAU - span) / 2.0 * 0.5);
2442 }
2443 let poles = revolved.poles();
2444 let lowest = poles
2445 .iter()
2446 .copied()
2447 .filter(|&p| p <= v0)
2448 .fold(f64::NEG_INFINITY, f64::max);
2449 let highest = poles
2450 .iter()
2451 .copied()
2452 .filter(|&p| p >= v1)
2453 .fold(f64::INFINITY, f64::min);
2454 let a = if pole0 { v0 } else { (v0 - margin).max(lowest) };
2455 let b = if pole1 {
2456 v1
2457 } else {
2458 (v1 + margin).min(highest)
2459 };
2460 (a, b)
2461}
2462
2463fn patch_of(face: &Face, edges: &[Edge], scope: &Scope) -> GeopResult<Patch> {
2465 let mut points = Vec::new();
2466 let mut loop_points = Vec::new();
2467 for lp in &face.loops {
2468 let mut this = Vec::new();
2469 for &(e, forward) in lp {
2470 let curve = &edges[e].curve;
2471 let (lo, hi) = curve.domain();
2472 let (lo, hi) = (lo.to_f64(), hi.to_f64());
2473 for i in 0..LOOP_SAMPLES {
2474 let f = i as f64 / LOOP_SAMPLES as f64;
2475 let f = if forward { f } else { 1.0 - f };
2476 this.push(super::geometry::point_at(curve, lo + (hi - lo) * f)?);
2477 }
2478 }
2479 points.extend(this.iter().copied());
2480 loop_points.push(this);
2481 }
2482 match &face.surface.kind {
2483 SurfaceKind::Plane(frame) => {
2484 let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
2485 for &p in &points {
2486 let l = frame.local(p);
2487 for c in 0..2 {
2488 lo[c] = lo[c].min(l[c]);
2489 hi[c] = hi[c].max(l[c]);
2490 }
2491 }
2492 let margin = 0.05 * ((hi[0] - lo[0]).max(hi[1] - lo[1])).max(scope.uncertainty);
2493 let corner = |a: f64, b: f64| s3(frame.point([a, b, 0.0]));
2494 let (a0, a1, b0, b1) = (
2495 lo[0] - margin,
2496 hi[0] + margin,
2497 lo[1] - margin,
2498 hi[1] + margin,
2499 );
2500 let surface = NurbSurface3D::bilinear(
2501 corner(a0, b0),
2502 corner(a1, b0),
2503 corner(a1, b1),
2504 corner(a0, b1),
2505 )?;
2506 Ok(Patch {
2507 surface,
2508 extent: None,
2509 kind: PatchKind::Plane,
2510 })
2511 }
2512 SurfaceKind::Nurbs(nurbs) => Ok(Patch {
2513 surface: nurbs.to_nurbs()?,
2514 extent: None,
2515 kind: PatchKind::Other,
2516 }),
2517 SurfaceKind::Extrusion { curve, vector } => {
2518 let length2 = dot(*vector, *vector);
2519 let dir =
2520 normalize(*vector).ok_or_else(|| GeopError::new("an extrusion along nothing"))?;
2521 let along = |p: P3| dot(p, dir);
2522 let (p_lo, p_hi) = points
2523 .iter()
2524 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &p| {
2525 (a.min(along(p)), b.max(along(p)))
2526 });
2527 let (c_lo, c_hi) = curve
2528 .points
2529 .iter()
2530 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &p| {
2531 (a.min(along(p)), b.max(along(p)))
2532 });
2533 let length = length2.sqrt();
2534 let (v0, v1) = ((p_lo - c_hi) / length, (p_hi - c_lo) / length);
2535 let margin = 0.05 * (v1 - v0).abs().max(scope.uncertainty / length);
2536 let (v0, v1) = (v0 - margin, v1 + margin);
2537 let curve = curve.to_nurbs()?;
2538 let start = curve.translate(s3(scale(*vector, v0)));
2539 Ok(Patch {
2540 surface: start.sweep(s3(scale(*vector, v1 - v0))),
2541 extent: None,
2542 kind: PatchKind::Other,
2543 })
2544 }
2545 SurfaceKind::Revolved(revolved) => {
2546 let [from, to, v0, v1] = match face.patch {
2547 Some(patch) => patch,
2548 None => revolved_extent(revolved, face, &loop_points, scope)?,
2549 };
2550 Ok(Patch {
2551 surface: revolved.patch(from, to, v0, v1)?,
2552 extent: Some([from, to, v0, v1]),
2553 kind: PatchKind::Revolved(revolved.clone()),
2554 })
2555 }
2556 }
2557}
2558
2559fn revolved_extent(
2563 revolved: &Revolved,
2564 face: &Face,
2565 loops: &[Vec<P3>],
2566 scope: &Scope,
2567) -> GeopResult<[f64; 4]> {
2568 let mut best: Option<(f64, f64, f64)> = None;
2570 for points in loops {
2571 let a = unwrap(revolved, points, false, face.outward_is_natural());
2572 if a.is_empty() {
2573 continue;
2574 }
2575 let lo = a.iter().copied().fold(f64::INFINITY, f64::min);
2576 let hi = a.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2577 if best.is_none_or(|(b_lo, b_hi, _)| hi - lo > b_hi - b_lo) {
2578 best = Some((lo, hi, 0.0));
2579 }
2580 }
2581 let (lo, hi, _) =
2582 best.ok_or_else(|| GeopError::new("a face whose every point lies on its axis"))?;
2583 let span = hi - lo;
2584 if span >= std::f64::consts::TAU {
2585 return Err(GeopError::new(format!(
2586 "#{}: a face spanning a full turn or more around its axis without a loop going round it — as a thread's flank does — is not supported",
2587 face.id
2588 )));
2589 }
2590 let margin = (0.02 * span + 1e-3).min((std::f64::consts::TAU - span) / 4.0);
2591 let mut v_lo = f64::INFINITY;
2593 let mut v_hi = f64::NEG_INFINITY;
2594 let mut torus_angles = Vec::new();
2595 for points in loops {
2596 for &p in points {
2597 let v = revolved.chart(p).1;
2598 torus_angles.push(v);
2599 v_lo = v_lo.min(v);
2600 v_hi = v_hi.max(v);
2601 }
2602 }
2603 if revolved.v_is_angle() {
2604 torus_angles.sort_by(f64::total_cmp);
2607 let n = torus_angles.len();
2608 let mut gap = (
2609 torus_angles[0] + std::f64::consts::TAU - torus_angles[n - 1],
2610 0usize,
2611 );
2612 for i in 1..n {
2613 let g = torus_angles[i] - torus_angles[i - 1];
2614 if g > gap.0 {
2615 gap = (g, i);
2616 }
2617 }
2618 v_lo = torus_angles[gap.1];
2619 v_hi = if gap.1 == 0 {
2620 torus_angles[n - 1]
2621 } else {
2622 torus_angles[gap.1 - 1] + std::f64::consts::TAU
2623 };
2624 }
2625 let near_pole = |v: f64| {
2626 revolved.poles().into_iter().find(|&pole| {
2627 distance(revolved.profile_point(pole), revolved.profile_point(v))
2628 <= scope.uncertainty.max(1e-9)
2629 })
2630 };
2631 let (pole0, pole1) = match &revolved.profile {
2632 Profile::Curve(_) => (None, None),
2633 _ => (near_pole(v_lo), near_pole(v_hi)),
2634 };
2635 let (v0, v1) = widen_v(
2636 revolved,
2637 pole0.unwrap_or(v_lo),
2638 pole1.unwrap_or(v_hi),
2639 pole0.is_some(),
2640 pole1.is_some(),
2641 );
2642 Ok([lo - margin, hi + margin, v0, v1])
2643}
2644
2645struct Grid {
2648 surface: SurfaceMidpoints,
2649 points: Vec<(f64, f64, P3)>,
2650}
2651
2652const GRID: usize = 12;
2653
2654const SEEDS: usize = 4;
2656
2657impl Grid {
2658 fn new(surface: &NurbSurface3D) -> Self {
2659 let surface = SurfaceMidpoints::of(surface);
2660 let (u0, u1) = surface.domain_u();
2661 let (v0, v1) = surface.domain_v();
2662 let mut points = Vec::with_capacity((GRID + 1) * (GRID + 1));
2663 for i in 0..=GRID {
2664 for j in 0..=GRID {
2665 let u = u0 + (u1 - u0) * i as f64 / GRID as f64;
2666 let v = v0 + (v1 - v0) * j as f64 / GRID as f64;
2667 points.push((u, v, surface.point(u, v)));
2668 }
2669 }
2670 Self { surface, points }
2671 }
2672
2673 fn foot(&self, p: P3, seed: Option<(f64, f64)>) -> (f64, f64) {
2680 let mut near: Vec<&(f64, f64, P3)> = self.points.iter().collect();
2681 let nearest = SEEDS.min(near.len());
2682 near.select_nth_unstable_by(nearest - 1, |a, b| {
2683 distance(a.2, p).total_cmp(&distance(b.2, p))
2684 });
2685 let seeds = near[..nearest].iter().map(|&&(u, v, _)| (u, v));
2686 let mut best = (f64::INFINITY, (near[0].0, near[0].1));
2687 for (u, v) in seed.into_iter().chain(seeds) {
2688 let (u, v) = self.surface.project(p, u, v);
2689 let d = distance(self.surface.point(u, v), p);
2690 if d < best.0 {
2691 best = (d, (u, v));
2692 }
2693 }
2694 best.1
2695 }
2696
2697 fn project(&self, surface: &NurbSurface3D, p: &Vector3) -> GeopResult<(Scalar, Scalar)> {
2700 let (u, v) = self.foot(p.to_f64_array(), None);
2701 surface.project(*p, Scalar::from_f64(u), Scalar::from_f64(v), 30)
2702 }
2703}
2704
2705fn fit_loop(
2710 lp: &[Use],
2711 surface: &NurbSurface3D,
2712 grid: &Grid,
2713 patch: &Patch,
2714 edges: &[Edge],
2715 vertices: &mut [Vertex],
2716 scope: &Scope,
2717) -> GeopResult<Vec<(CoedgeOnLocal, NurbCurve2D)>> {
2718 let n = lp.len();
2719 let ends = |(e, forward): Use| edges[e].ends(forward);
2720 let poles = poles_of(surface, scope.uncertainty)?;
2721 let pole_of = |v: usize, vertices: &[Vertex]| -> Option<Pole> {
2723 let p = vertices[v].origin.to_f64_array();
2724 poles
2725 .iter()
2726 .find(|pole| distance(pole.point, p) <= scope.uncertainty)
2727 .copied()
2728 };
2729 let mut corners: Vec<Option<Vector2>> = Vec::with_capacity(n);
2732 for &u in lp {
2733 let (_, end) = ends(u);
2734 corners.push(match pole_of(end, vertices) {
2735 Some(_) => None,
2736 None => {
2737 let (pu, pv) = grid.project(surface, &vertices[end].origin)?;
2738 Some(Vector2::from_array([pu, pv]))
2739 }
2740 });
2741 }
2742 let mut out = Vec::new();
2743 let mut pins = Vec::with_capacity(n);
2744 for k in 0..n {
2745 let u = lp[k];
2746 let (start, end) = ends(u);
2747 let edge = &edges[u.0];
2748 let curve = if u.1 {
2749 edge.curve.clone()
2750 } else {
2751 edge.curve.reverse()
2752 };
2753 let pin = |corner: Option<Vector2>, vertex: usize, at_start: bool| -> GeopResult<Vector2> {
2754 match corner {
2755 Some(uv) => Ok(uv),
2756 None => {
2757 let (lo, hi) = curve.domain();
2761 let f = Scalar::from_ratio(1, 64)?;
2762 let t = if at_start {
2763 lo.add(hi.sub(lo).mul(f))
2764 } else {
2765 hi.sub(hi.sub(lo).mul(f))
2766 };
2767 let near = curve.evaluate(t.sharpen())?;
2768 let (pu, pv) = grid.project(surface, &near)?;
2769 let pole = pole_of(vertex, vertices)
2770 .expect("a corner without a foot point is at a pole");
2771 Ok(if pole.fixes_u {
2772 Vector2::from_array([pole.at, pv])
2773 } else {
2774 Vector2::from_array([pu, pole.at])
2775 })
2776 }
2777 }
2778 };
2779 let pin_start = pin(corners[(k + n - 1) % n], start, true)?;
2780 let pin_end = pin(corners[k], end, false)?;
2781 let ctx = |e: GeopError| {
2782 e.with_context(format!("fitting the pcurve of the edge {}", edge.label()))
2783 };
2784 let at_pole = corners[(k + n - 1) % n].is_none() || corners[k].is_none();
2787 let straight = match (&patch.kind, &edge.shape) {
2788 (PatchKind::Revolved(revolved), Some(shape)) => {
2789 !at_pole && revolved.iso_line(shape, scope.uncertainty)
2790 }
2791 _ => false,
2792 };
2793 let pcurve = if let PatchKind::Plane = patch.kind {
2794 plane_pcurve(surface, &curve, pin_start, pin_end).with_context(&ctx)?
2795 } else if straight {
2796 NurbCurve2D::line(pin_start, pin_end).with_context(&ctx)?
2799 } else {
2800 surface
2801 .fit_pcurve(
2802 &curve,
2803 Some(pin_start),
2804 Some(pin_end),
2805 MAX_NODES,
2806 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
2807 )
2808 .with_context(&ctx)?
2809 };
2810 out.push((CoedgeOnLocal::Edge(u.0, u.1), pcurve));
2811 pins.push((pin_start, pin_end));
2812 }
2813 for k in 0..n {
2815 let u = lp[k];
2816 let (start, end) = ends(u);
2817 let edge = &edges[u.0];
2818 let (lo, hi) = edge.curve.domain();
2819 let (curve_start, curve_end) = if u.1 {
2820 (edge.curve.evaluate(lo)?, edge.curve.evaluate(hi)?)
2821 } else {
2822 (edge.curve.evaluate(hi)?, edge.curve.evaluate(lo)?)
2823 };
2824 let (a, b) = pins[k];
2827 let on_start = surface.evaluate(a[0], a[1])?;
2828 let on_end = surface.evaluate(b[0], b[1])?;
2829 for (v, extra) in [(start, [curve_start, on_start]), (end, [curve_end, on_end])] {
2830 for (p, source) in extra.into_iter().zip([
2831 "the end of the edge",
2832 "its foot point on this face, at the end of the edge",
2833 ]) {
2834 let off = distance(p.to_f64_array(), vertices[v].origin.to_f64_array());
2835 if off > ACCURACY {
2836 return Err(GeopError::new(format!(
2837 "the vertex {} is at {:?}, but {source} {} is {off:e} mm from it — more than the {ACCURACY:e} mm the kernel can carry as one point (the face built over the angles and profile parameters {:?})",
2838 vertices[v].label(),
2839 vertices[v].origin.to_f64_array(),
2840 edge.label(),
2841 patch.extent,
2842 )));
2843 }
2844 vertices[v].point = vertices[v].point.union(&p);
2845 }
2846 }
2847 }
2848 let mut joined = Vec::with_capacity(out.len());
2851 for k in 0..n {
2852 joined.push(out[k].clone());
2853 let next = (k + 1) % n;
2854 let (_, end) = ends(lp[k]);
2855 if corners[k].is_none() {
2856 let (_, t1) = out[k].1.domain();
2857 let (t0, _) = out[next].1.domain();
2858 let from = out[k].1.evaluate(t1)?;
2859 let to = out[next].1.evaluate(t0)?;
2860 if !from.could_be_equal(&to) {
2861 let pcurve = NurbCurve2D::line(from, to)?;
2862 joined.push((CoedgeOnLocal::Vertex(end), pcurve));
2863 }
2864 }
2865 }
2866 Ok(joined)
2867}
2868
2869fn foot(surface: &NurbSurface3D, grid: &Grid, p: P3) -> GeopResult<(P3, Option<P3>)> {
2874 let (u, v) = grid.foot(p, None);
2875 let (at, su, sv) = grid.surface.partials(u, v);
2876 let normal = match normalize(cross(su, sv)) {
2877 Some(n) => Some(n),
2878 None => surface
2879 .normal(Scalar::from_f64(u), Scalar::from_f64(v))
2880 .ok()
2881 .map(|n| n.to_f64_array()),
2882 };
2883 Ok((at, normal))
2884}
2885
2886fn off(surface: &NurbSurface3D, grid: &Grid, p: P3) -> GeopResult<f64> {
2888 Ok(distance(foot(surface, grid, p)?.0, p))
2889}
2890
2891const STRAY_SAMPLES: usize = 16;
2893
2894fn strays(curve: &CurveMidpoints, grid: &Grid) -> f64 {
2898 let (lo, hi) = curve.domain();
2899 let mut seed: Option<(f64, f64)> = None;
2900 let mut most = 0.0f64;
2901 for k in 0..=STRAY_SAMPLES {
2902 let p = curve.point(lo + (hi - lo) * k as f64 / STRAY_SAMPLES as f64);
2903 let (u, v) = grid.foot(p, seed);
2904 seed = Some((u, v));
2905 most = most.max(distance(grid.surface.point(u, v), p));
2906 }
2907 most
2908}
2909
2910const MEET_ITERATIONS: usize = 40;
2912
2913const MEET_DAMPING: f64 = 1e-6;
2920
2921fn meet(start: P3, surfaces: &[(&NurbSurface3D, &Grid)]) -> GeopResult<P3> {
2927 Ok(iterate(
2929 start,
2930 start,
2931 MEET_ITERATIONS,
2932 Repeat::FixedPoint,
2933 |&next| (next, next),
2934 |&x| -> GeopResult<Step<P3>> {
2935 let mut a: [[f64; 3]; 3] = std::array::from_fn(|i| {
2937 std::array::from_fn(|j| if i == j { MEET_DAMPING } else { 0.0 })
2938 });
2939 let mut b = [0.0f64; 3];
2940 for (surface, grid) in surfaces {
2941 let (f, normal) = foot(surface, grid, x)?;
2942 let Some(n) = normal.or_else(|| normalize(sub(x, f))) else {
2945 continue;
2946 };
2947 let r = dot(n, sub(x, f));
2948 for i in 0..3 {
2949 for j in 0..3 {
2950 a[i][j] += n[i] * n[j];
2951 }
2952 b[i] -= n[i] * r;
2953 }
2954 }
2955 let det = |m: [[f64; 3]; 3]| dot(m[0], cross(m[1], m[2]));
2956 let d = det(a);
2957 let dx: P3 = std::array::from_fn(|c| {
2959 let mut m = a;
2960 for (row, value) in m.iter_mut().zip(b) {
2961 row[c] = value;
2962 }
2963 det(m) / d
2964 });
2965 Ok(Step::Next(add(x, dx)))
2966 },
2967 )?
2968 .seed)
2969}
2970
2971const REBUILT_SAMPLES: usize = 16;
2974const MOST_REBUILT_SAMPLES: usize = 256;
2975
2976fn heal(
2998 faces: &[Face],
2999 built: &[(Patch, NurbSurface3D, Grid)],
3000 edges: &mut [Edge],
3001 vertices: &mut [Vertex],
3002 scope: &Scope,
3003) -> GeopResult<(Vec<bool>, Healing)> {
3004 let mut faces_of_edge: Vec<Vec<usize>> = vec![Vec::new(); edges.len()];
3005 let mut faces_of_vertex: Vec<Vec<usize>> = vec![Vec::new(); vertices.len()];
3006 let mut edges_of_vertex: Vec<Vec<usize>> = vec![Vec::new(); vertices.len()];
3007 for (f, face) in faces.iter().enumerate() {
3008 for &(e, _) in face.loops.iter().flatten() {
3009 let add = |list: &mut Vec<usize>, item: usize| {
3010 if !list.contains(&item) {
3011 list.push(item);
3012 }
3013 };
3014 add(&mut faces_of_edge[e], f);
3015 for v in [edges[e].start, edges[e].end] {
3016 add(&mut faces_of_vertex[v], f);
3017 add(&mut edges_of_vertex[v], e);
3018 }
3019 }
3020 }
3021 let surfaces = |fs: &[usize]| -> Vec<(&NurbSurface3D, &Grid)> {
3022 fs.iter().map(|&f| (&built[f].1, &built[f].2)).collect()
3023 };
3024 let names = |fs: &[usize]| -> String {
3025 fs.iter()
3026 .map(|&f| format!("{} (#{})", faces[f].name.join(","), faces[f].id))
3027 .collect::<Vec<_>>()
3028 .join(", ")
3029 };
3030 let furthest = |p: P3, fs: &[usize]| -> GeopResult<f64> {
3032 let mut most = 0.0f64;
3033 for (surface, grid) in surfaces(fs) {
3034 most = most.max(off(surface, grid, p)?);
3035 }
3036 Ok(most)
3037 };
3038 let chord = |e: &Edge, vertices: &[Vertex]| {
3039 distance(
3040 vertices[e.start].origin.to_f64_array(),
3041 vertices[e.end].origin.to_f64_array(),
3042 )
3043 };
3044 let mut size = Vec::with_capacity(faces.len());
3048 for face in faces {
3049 let mut area = [0.0f64; 3];
3050 for lp in &face.loops {
3051 let points = loop_points(edges, lp);
3052 for (k, &p) in points.iter().enumerate() {
3053 area = add(area, scale(cross(p, points[(k + 1) % points.len()]), 0.5));
3054 }
3055 }
3056 size.push(norm(area));
3057 }
3058
3059 let kept = |fs: &[usize], rebuild: &[bool]| -> Vec<usize> {
3062 let mut kept: Vec<usize> = fs.iter().copied().filter(|&f| !rebuild[f]).collect();
3063 kept.sort_by(|&a, &b| size[a].total_cmp(&size[b]));
3064 kept
3065 };
3066 let settle = |p: P3,
3072 fs: &[usize],
3073 reach: f64,
3074 rebuild: &mut [bool]|
3075 -> GeopResult<Option<P3>> {
3076 let kept = kept(fs, rebuild);
3077 let free: Vec<usize> = kept
3078 .iter()
3079 .copied()
3080 .filter(|&f| rebuildable(&faces[f]))
3081 .take(MOST_REBUILT_AT_ONCE)
3082 .collect();
3083 let mut choices: Vec<(f64, Vec<usize>)> = (0..1usize << free.len())
3084 .map(|mask| {
3085 let out: Vec<usize> = (0..free.len())
3086 .filter(|k| mask & (1 << k) != 0)
3087 .map(|k| free[k])
3088 .collect();
3089 (out.iter().map(|&f| size[f]).sum(), out)
3090 })
3091 .collect();
3092 choices.sort_by(|a, b| a.0.total_cmp(&b.0));
3093 let mut nearest = None;
3094 for (_, out) in choices {
3095 let rest: Vec<usize> = kept.iter().copied().filter(|f| !out.contains(f)).collect();
3096 let healed = if rest.is_empty() {
3097 None
3098 } else {
3099 let healed = meet(p, &surfaces(&rest))?;
3100 let left = furthest(healed, &rest)?;
3101 nearest.get_or_insert((left, healed));
3102 if left > scope.uncertainty || distance(healed, p) > reach {
3103 continue;
3104 }
3105 Some(healed)
3106 };
3107 for f in out {
3108 rebuild[f] = true;
3109 }
3110 return Ok(healed);
3111 }
3112 let (left, healed) = nearest.unwrap_or((0.0, p));
3113 Err(GeopError::new(format!(
3114 "the surfaces of its faces {} {} — and no choice of the faces of one loop of four edges or more among them, rebuilt from their edges instead, leaves the rest meeting there",
3115 names(&kept),
3116 if left > scope.uncertainty {
3117 format!(
3118 "do not meet near {p:?} to within the file's uncertainty of {:e} mm: they come no nearer each other than {left:e} mm, at {healed:?}",
3119 scope.uncertainty
3120 )
3121 } else {
3122 format!(
3123 "meet only {:e} mm from {p:?}, at {healed:?}, more than the {reach:e} mm it may move: half the shortest edge there",
3124 distance(healed, p)
3125 )
3126 },
3127 )))
3128 };
3129
3130 let mut rebuild = vec![false; faces.len()];
3131 let mut moved = vec![false; vertices.len()];
3132 let mut moved_vertices: Vec<(String, f64)> = Vec::new();
3134 let mut moved_edges: Vec<(String, f64)> = Vec::new();
3135 let mut vertex_apart: Vec<Option<(Vec<usize>, f64)>> = vec![None; vertices.len()];
3139 let mut edge_apart: Vec<Option<(Vec<usize>, f64)>> = vec![None; edges.len()];
3140 for apply in [false, true] {
3145 moved.fill(false);
3146 for v in 0..vertices.len() {
3147 if faces_of_vertex[v].is_empty() {
3148 continue;
3149 }
3150 let at = vertices[v].origin.to_f64_array();
3151 let what = format!("healing the vertex {} at {at:?}", vertices[v].label());
3152 let vctx = |e: GeopError| e.with_context(what.clone());
3153 let fs = kept(&faces_of_vertex[v], &rebuild);
3154 let apart = match &vertex_apart[v] {
3155 Some((measured, apart)) if *measured == fs => *apart,
3156 _ => furthest(at, &fs).map_err(vctx)?,
3157 };
3158 vertex_apart[v] = Some((fs.clone(), apart));
3159 if 2.0 * apart <= ACCURACY {
3160 continue;
3161 }
3162 let shortest = edges_of_vertex[v]
3163 .iter()
3164 .map(|&e| chord(&edges[e], vertices))
3165 .fold(f64::INFINITY, f64::min);
3166 let Some(healed) = settle(at, &fs, shortest / 2.0, &mut rebuild).map_err(vctx)? else {
3167 continue;
3169 };
3170 moved[v] = true;
3171 if !apply {
3172 continue;
3173 }
3174 vertices[v].origin = s3(healed);
3175 vertices[v].point = s3(healed);
3176 moved_vertices.push((vertices[v].label(), distance(healed, at)));
3177 }
3178
3179 for e in 0..edges.len() {
3180 if faces_of_edge[e].is_empty() || !edges[e].alive {
3181 continue;
3182 }
3183 let what = format!("healing the edge {}", edges[e].label());
3184 let ectx = |e: GeopError| e.with_context(what.clone());
3185 let old = edges[e].curve.clone();
3186 let (lo, hi) = old.domain();
3187 let (lo, hi) = (lo.to_f64(), hi.to_f64());
3188 let at = |f: f64| super::geometry::point_at(&old, lo + (hi - lo) * f);
3189 let (start, end) = (edges[e].start, edges[e].end);
3190 let fs = kept(&faces_of_edge[e], &rebuild);
3191 let apart = match &edge_apart[e] {
3192 Some((measured, apart)) if *measured == fs => *apart,
3193 _ => {
3194 let midpoints = CurveMidpoints::of(&old);
3195 let mut apart = 0.0f64;
3196 for (_, grid) in surfaces(&fs) {
3197 apart = apart.max(strays(&midpoints, grid));
3198 }
3199 apart
3200 }
3201 };
3202 edge_apart[e] = Some((fs.clone(), apart));
3203 if !moved[start] && !moved[end] && 2.0 * apart <= ACCURACY {
3204 continue;
3205 }
3206 let length = chord(&edges[e], vertices);
3209 let middle =
3210 settle(at(0.5).map_err(ectx)?, &fs, length / 2.0, &mut rebuild).map_err(ectx)?;
3211 if !apply {
3212 continue;
3213 }
3214 let fs = kept(&fs, &rebuild);
3215 let ends = [at(0.0).map_err(ectx)?, at(1.0).map_err(ectx)?];
3217 let shifts = [
3218 sub(vertices[start].origin.to_f64_array(), ends[0]),
3219 sub(vertices[end].origin.to_f64_array(), ends[1]),
3220 ];
3221 let furthest_shift = std::cell::Cell::new(0.0f64);
3225 let onto = |p: P3, f: f64| -> GeopResult<P3> {
3226 if middle.is_none() {
3227 let moved = add(p, add(scale(shifts[0], 1.0 - f), scale(shifts[1], f)));
3228 furthest_shift.set(furthest_shift.get().max(distance(moved, p)));
3229 return Ok(moved);
3230 }
3231 let healed = meet(p, &surfaces(&fs))?;
3232 let left = furthest(healed, &fs)?;
3233 let shift = distance(healed, p);
3234 furthest_shift.set(furthest_shift.get().max(shift));
3235 if left > scope.uncertainty || 2.0 * shift > length {
3236 return Err(GeopError::new(format!(
3237 "the edge {} lies {apart:e} mm off the surfaces of its faces {}{}, and they {} — so it cannot be rebuilt where they meet",
3238 edges[e].label(),
3239 names(&fs),
3240 if moved[start] || moved[end] {
3241 ", and a vertex of it was moved to where they meet"
3242 } else {
3243 ""
3244 },
3245 if left > scope.uncertainty {
3246 format!(
3247 "do not meet along it to within the file's uncertainty of {:e} mm: near its point {p:?} they come no nearer each other than {left:e} mm",
3248 scope.uncertainty
3249 )
3250 } else {
3251 format!(
3252 "meet only {shift:e} mm from its point {p:?}, more than half its length of {length:e} mm"
3253 )
3254 },
3255 )));
3256 }
3257 Ok(healed)
3258 };
3259 let rebuilt = refine(
3265 REBUILT_SAMPLES,
3266 usize::BITS as usize,
3267 |samples| -> GeopResult<_> {
3268 let mut points = vec![vertices[start].origin];
3269 for k in 1..samples {
3270 let f = k as f64 / samples as f64;
3271 points.push(s3(onto(at(f).map_err(ectx)?, f).map_err(ectx)?));
3272 }
3273 points.push(vertices[end].origin);
3274 let rebuilt = NurbCurve3D::interpolate(&points, 3).map_err(|err| {
3275 err.with_context(format!(
3276 "rebuilding the edge {} where its faces {} meet",
3277 edges[e].label(),
3278 names(&fs)
3279 ))
3280 })?;
3281 if samples >= MOST_REBUILT_SAMPLES || middle.is_none() {
3282 return Ok(Refine::Done(rebuilt));
3283 }
3284 let (r0, r1) = rebuilt.domain();
3285 let (r0, r1) = (r0.to_f64(), r1.to_f64());
3286 let mut strays = 0.0f64;
3287 for k in 0..samples {
3288 let t = r0 + (r1 - r0) * (k as f64 + 0.5) / samples as f64;
3289 let p = super::geometry::point_at(&rebuilt, t).map_err(ectx)?;
3290 strays = strays.max(furthest(p, &fs).map_err(ectx)?);
3291 }
3292 if strays <= scope.uncertainty {
3293 return Ok(Refine::Done(rebuilt));
3294 }
3295 Ok(Refine::Finer(samples * 2))
3296 },
3297 )?;
3298 let Refined::Done(rebuilt) = rebuilt else {
3299 return Err(GeopError::new(format!(
3300 "rebuilding the edge {}: its samples doubled past every cap",
3301 edges[e].label()
3302 )));
3303 };
3304 edges[e].curve = rebuilt;
3305 edges[e].shape = None;
3306 moved_edges.push((edges[e].label(), furthest_shift.get()));
3307 }
3308 }
3309 let healing = Healing {
3310 vertices: moved_vertices,
3311 edges: moved_edges,
3312 faces: (0..faces.len())
3313 .filter(|&f| rebuild[f])
3314 .map(|f| names(&[f]))
3315 .collect(),
3316 uncertainty: scope.uncertainty,
3317 };
3318 Ok((rebuild, healing))
3319}
3320
3321type Loop<T> = Vec<(T, NurbCurve2D)>;
3323
3324type FaceLoops<T> = (Loop<T>, Vec<Loop<T>>);
3326
3327const SEED: u64 = 0x57E9_0000_0000_0001;
3330
3331fn split_by_outer_loops<T: Clone>(
3337 surface: &NurbSurface3D,
3338 loops: Vec<Loop<T>>,
3339) -> GeopResult<Vec<FaceLoops<T>>> {
3340 let areas: Vec<Scalar> = (loops.iter())
3341 .map(|lp| loop_signed_area(lp.iter().map(|(_, pcurve)| pcurve)))
3342 .collect::<GeopResult<_>>()?;
3343 if let Some(k) = areas
3344 .iter()
3345 .position(|a| !a.definitely_greater(Scalar::ZERO) && !a.definitely_less(Scalar::ZERO))
3346 {
3347 return Err(GeopError::new(format!(
3348 "its loop {k} runs neither way round about its normal (signed areas {areas:?})"
3349 )));
3350 }
3351 let mut outers = Vec::new();
3352 let mut holes = Vec::new();
3353 for (lp, area) in loops.into_iter().zip(&areas) {
3354 if area.definitely_greater(Scalar::ZERO) {
3355 outers.push((lp, Vec::new(), *area));
3356 } else {
3357 holes.push(lp);
3358 }
3359 }
3360 if outers.is_empty() {
3361 return Err(GeopError::new(format!(
3362 "none of its loops runs counter-clockwise about its normal: it has no outer loop (signed areas {areas:?})"
3363 )));
3364 }
3365 for hole in holes {
3366 let first = &hole[0].1;
3367 let point = first.evaluate(first.mid_parameter()?)?;
3368 let mut owner: Option<usize> = None;
3369 for (i, (outer, _, area)) in outers.iter().enumerate() {
3370 let pcurves: Vec<&NurbCurve2D> = outer.iter().map(|(_, pcurve)| pcurve).collect();
3371 let inside = pcurves_contain(
3372 surface,
3373 &pcurves,
3374 point,
3375 MAX_NODES,
3376 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
3377 SEED,
3378 )? == PointClassification::Inside;
3379 if inside && owner.is_none_or(|o| area.definitely_less(outers[o].2)) {
3380 owner = Some(i);
3381 }
3382 }
3383 let owner = owner.ok_or_else(|| {
3384 GeopError::new(format!(
3385 "a hole of it lies in none of its outer loops (signed areas {areas:?})"
3386 ))
3387 })?;
3388 outers[owner].1.push(hole);
3389 }
3390 Ok(outers
3391 .into_iter()
3392 .map(|(outer, holes, _)| (outer, holes))
3393 .collect())
3394}
3395
3396const MOST_REBUILT_AT_ONCE: usize = 6;
3399
3400fn rebuildable(face: &Face) -> bool {
3402 matches!(face.loops.as_slice(), [lp] if lp.len() >= 4)
3403}
3404
3405fn rebuilt_surface(face: &Face, edges: &[Edge]) -> GeopResult<NurbSurface3D> {
3414 let [lp] = face.loops.as_slice() else {
3415 return Err(GeopError::new(format!(
3416 "it has {} loops: only a face of one loop is rebuilt from its edges",
3417 face.loops.len()
3418 )));
3419 };
3420 let n = lp.len();
3421 if n < 4 {
3422 return Err(GeopError::new(format!(
3423 "its loop has {n} edges: only one of four or more is rebuilt from its edges, cut into four sides"
3424 )));
3425 }
3426 let curves: Vec<NurbCurve3D> = lp
3427 .iter()
3428 .map(|&(e, forward)| {
3429 if forward {
3430 edges[e].curve.clone()
3431 } else {
3432 edges[e].curve.reverse()
3433 }
3434 })
3435 .collect();
3436 let direction = |c: &NurbCurve3D, at_end: bool| -> GeopResult<P3> {
3437 let (lo, hi) = c.domain();
3438 let t = if at_end { hi } else { lo };
3439 normalize(c.tangent(t)?.to_f64_array())
3440 .ok_or_else(|| GeopError::new("an edge of it has no direction at an end"))
3441 };
3442 let mut turns = Vec::with_capacity(n);
3444 for k in 0..n {
3445 let arriving = direction(&curves[k], true)?;
3446 let leaving = direction(&curves[(k + 1) % n], false)?;
3447 turns.push((1.0 - dot(arriving, leaving), k));
3448 }
3449 turns.sort_by(|a, b| b.0.total_cmp(&a.0));
3450 let mut corners: Vec<usize> = turns[..4].iter().map(|&(_, k)| k).collect();
3451 corners.sort();
3452 let mut sides = Vec::with_capacity(4);
3453 for j in 0..4 {
3454 let (from, to) = (corners[j] + 1, corners[(j + 1) % 4]);
3455 let count = (to + n - from) % n + 1;
3456 let chain: Vec<NurbCurve3D> = (0..count).map(|i| curves[(from + i) % n].clone()).collect();
3457 sides.push(NurbCurve3D::join(&chain)?);
3458 }
3459 NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]])
3460}
3461
3462fn plane_pcurve(
3470 surface: &NurbSurface3D,
3471 curve: &NurbCurve3D,
3472 start: Vector2,
3473 end: Vector2,
3474) -> GeopResult<NurbCurve2D> {
3475 let corner = |i: usize, j: usize| {
3476 let cp = surface.control_points[i * surface.num_v + j];
3477 Vector3::from_array([cp[0], cp[1], cp[2]])
3478 };
3479 let origin = corner(0, 0);
3480 let (e1, e2) = (corner(1, 0).sub(&origin), corner(0, 1).sub(&origin));
3481 let gram = (e1.prod_dot(&e1), e1.prod_dot(&e2), e2.prod_dot(&e2));
3482 let n = curve.control_points.len();
3483 let mut control_points = Vec::with_capacity(n);
3484 for (k, cp) in curve.control_points.iter().enumerate() {
3485 let w = cp[3];
3486 let pin = match k {
3487 0 => Some(start),
3488 _ if k + 1 == n => Some(end),
3489 _ => None,
3490 };
3491 let (u, v) = match pin {
3492 Some(p) => (p[0].mul(w), p[1].mul(w)),
3493 None => {
3494 let d = Vector3::from_array([cp[0], cp[1], cp[2]]).sub(&origin.prod_scalar(w));
3495 solve_symmetric2(gram, (d.prod_dot(&e1), d.prod_dot(&e2)))?
3496 }
3497 };
3498 control_points.push(Vector3::from_array([u, v, w]));
3499 }
3500 NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
3501}
3502
3503const ACCURACY: f64 = 1e-4;
3507
3508const WIDENINGS: usize = 4;
3511
3512const GAP_SAMPLES: usize = 64;
3515
3516const NEAREST_ITERATIONS: usize = 80;
3519
3520const NEAREST_ON_ENCLOSURE: usize = 12;
3523
3524#[derive(Clone, Copy, Debug)]
3529struct GoldenSection {
3530 a: f64,
3531 b: f64,
3532 x: f64,
3533 y: f64,
3534}
3535
3536impl GoldenSection {
3537 fn ratio() -> f64 {
3539 (5f64.sqrt() - 1.0) / 2.0
3540 }
3541
3542 fn new(a: f64, b: f64) -> Self {
3543 let ratio = Self::ratio();
3544 GoldenSection {
3545 a,
3546 b,
3547 x: b - ratio * (b - a),
3548 y: a + ratio * (b - a),
3549 }
3550 }
3551
3552 fn narrow(
3555 &mut self,
3556 steps: usize,
3557 mut merit: impl FnMut(f64) -> GeopResult<f64>,
3558 ) -> GeopResult<()> {
3559 if steps == 0 {
3560 return Ok(());
3561 }
3562 let ratio = Self::ratio();
3563 let GoldenSection {
3564 mut a,
3565 mut b,
3566 mut x,
3567 mut y,
3568 } = *self;
3569 let (mut fx, mut fy) = (merit(x)?, merit(y)?);
3570 for _ in 0..steps {
3571 if fx <= fy {
3572 b = y;
3573 (y, fy) = (x, fx);
3574 x = b - ratio * (b - a);
3575 fx = merit(x)?;
3576 } else {
3577 a = x;
3578 (x, fx) = (y, fy);
3579 y = a + ratio * (b - a);
3580 fy = merit(y)?;
3581 }
3582 }
3583 *self = GoldenSection { a, b, x, y };
3584 Ok(())
3585 }
3586}
3587
3588fn edge_gap(
3593 curve: &NurbCurve3D,
3594 surface: &NurbSurface3D,
3595 pcurve: &NurbCurve2D,
3596) -> GeopResult<([f64; 3], Option<Worst>)> {
3597 let (t0, t1) = pcurve.domain();
3598 let (c0, c1) = curve.domain();
3599 let mut gap = [0.0f64; 3];
3600 let mut worst: Option<Worst> = None;
3601 let (c0f, c1f) = (c0.to_f64(), c1.to_f64());
3604 let midpoints = CurveMidpoints::of(curve);
3605 let rows = 8 * curve.control_points.len() + 32;
3606 let table: Vec<(f64, P3)> = (0..=rows)
3607 .map(|k| {
3608 let t = c0f + (c1f - c0f) * k as f64 / rows as f64;
3609 (t, midpoints.point(t))
3610 })
3611 .collect();
3612 let step = (c1f - c0f) / rows as f64;
3613 for i in 0..=GAP_SAMPLES {
3614 let f = Scalar::from_ratio(i as i64, GAP_SAMPLES as i64)?;
3615 let t = match i {
3616 0 => t0,
3617 GAP_SAMPLES => t1,
3618 _ => Scalar::fraction(t0, t1, f),
3619 };
3620 let uv = pcurve.evaluate(t)?;
3621 let Ok(point) = surface.evaluate(uv[0], uv[1]) else {
3624 continue;
3625 };
3626 let target = point.to_f64_array();
3627 let &(seed, _) = table
3628 .iter()
3629 .min_by(|a, b| distance(a.1, target).total_cmp(&distance(b.1, target)))
3630 .expect("a table of points");
3631 let mut search = GoldenSection::new((seed - step).max(c0f), (seed + step).min(c1f));
3642 search.narrow(NEAREST_ITERATIONS - NEAREST_ON_ENCLOSURE, |t| {
3643 Ok(distance(midpoints.point(t), target))
3644 })?;
3645 search.narrow(NEAREST_ON_ENCLOSURE, |t| {
3647 Ok(distance(super::geometry::point_at(curve, t)?, target))
3648 })?;
3649 let GoldenSection { a, b, .. } = search;
3650 let candidates = [
3658 (c0f, c0),
3659 (c1f, c1),
3660 ((a + b) / 2.0, Scalar::from_f64((a + b) / 2.0)),
3661 ];
3662 let exact_gap_at = |t: f64| -> GeopResult<f64> {
3663 Ok(distance(super::geometry::point_at(curve, t)?, target))
3664 };
3665 let pinned = i == 0 || i == GAP_SAMPLES;
3666 let choices = if pinned {
3667 &candidates[..2]
3668 } else {
3669 &candidates[..]
3670 };
3671 let mut nearest: Option<(Scalar, f64)> = None;
3673 for &(t, exact) in choices {
3674 let d = exact_gap_at(t)?;
3675 if nearest.is_none_or(|(_, best)| d < best) {
3676 nearest = Some((exact, d));
3677 }
3678 }
3679 let (s, _) = nearest.expect("candidates");
3680 let near = curve.evaluate(s)?;
3681 for c in 0..3 {
3684 let mid = point[c].midpoint();
3688 let apart = (mid.sub(near[c].upper()).upper().to_f64())
3689 .max(near[c].lower().sub(mid).upper().to_f64())
3690 .max(0.0);
3691 gap[c] = gap[c].max(apart);
3692 if worst.is_none_or(|w: Worst| apart > w.apart) {
3693 worst = Some(Worst {
3694 apart,
3695 at: i as f64 / GAP_SAMPLES as f64,
3696 point: point.to_f64_array(),
3697 near: near.to_f64_array(),
3698 });
3699 }
3700 }
3701 }
3702 Ok((gap, worst))
3703}
3704
3705#[derive(Clone, Copy, Debug)]
3709struct Worst {
3710 apart: f64,
3711 at: f64,
3712 point: P3,
3713 near: P3,
3714}
3715
3716fn off_surface(
3720 curve: &NurbCurve3D,
3721 surface: &NurbSurface3D,
3722 pcurve: &NurbCurve2D,
3723) -> GeopResult<f64> {
3724 let (c0, c1) = curve.domain();
3725 let (t0, t1) = pcurve.domain();
3726 let mut most = 0.0f64;
3727 for i in 0..=GAP_SAMPLES {
3728 let f = i as f64 / GAP_SAMPLES as f64;
3729 let p = curve.evaluate(Scalar::from_f64(
3730 c0.to_f64() + (c1.to_f64() - c0.to_f64()) * f,
3731 ))?;
3732 let seed = pcurve.evaluate(Scalar::from_f64(
3733 t0.to_f64() + (t1.to_f64() - t0.to_f64()) * f,
3734 ))?;
3735 let (u, v) = surface.project(p, seed[0].sharpen(), seed[1].sharpen(), 30)?;
3736 let foot = surface.evaluate(u, v)?;
3737 most = most.max(distance(foot.to_f64_array(), p.to_f64_array()));
3738 }
3739 Ok(most)
3740}
3741
3742fn widened(curve: &NurbCurve3D, gap: [f64; 3]) -> GeopResult<NurbCurve3D> {
3745 if gap == [0.0; 3] {
3746 return Ok(curve.clone());
3747 }
3748 let control_points = curve
3749 .control_points
3750 .iter()
3751 .map(|cp| {
3752 let w = cp[3];
3753 let mut out = *cp;
3754 for c in 0..3 {
3755 let spread = Scalar::from_f64(-gap[c]).union(Scalar::from_f64(gap[c]));
3756 out[c] = cp[c].add(w.mul(spread));
3757 }
3758 out
3759 })
3760 .collect();
3761 NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
3762}