1use super::*;
2
3pub fn offset_sphere_face(
78 solid: &BrepSolid,
79 face_id: u64,
80 distance: f64,
81) -> Result<BrepSolid, String> {
82 if !distance.is_finite() {
83 return Err("offset_sphere_face: distance must be finite".into());
84 }
85 let scale = solid_model_scale(solid);
86 let tolerance = (scale * 1e-7).max(1e-9);
87 let plane_tolerance = (scale * 1e-6).max(1e-7);
88
89 let (pshell, pface) = find_face(solid, face_id)
90 .ok_or_else(|| format!("offset_sphere_face: no face {face_id}"))?;
91 let pushed = &solid.shells[pshell].faces[pface];
92 let Some(AnalyticSurface::Sphere { frame, radius }) = pushed.surface.analytic() else {
93 return Err("offset_sphere_face: the pushed face is not a sphere".into());
94 };
95 let center = frame.origin;
96 let axis = frame.axis;
97 let seam_x = frame.x_axis;
98 let seam_y = frame.y_axis;
99 let radius = *radius;
100 if radius <= tolerance {
101 return Err("offset_sphere_face: degenerate sphere radius".into());
102 }
103
104 let [u0, u1] = pushed.surface.domain_u()?;
109 let [v0, v1] = pushed.surface.domain_v()?;
110 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
111 let point = pushed.surface.evaluate(um, vm)?;
112 let mut normal = pushed.surface.normal(um, vm)?;
113 if !pushed.same_sense {
114 normal = normal.scale(-1.0);
115 }
116 let radial = point.sub(center);
117 if radial.length() <= tolerance {
118 return Err("offset_sphere_face: degenerate radial direction".into());
119 }
120 let outward_sign = if normal.dot(radial) >= 0.0 { 1.0 } else { -1.0 };
121 let radius_new = radius + distance * outward_sign;
122 if radius_new <= tolerance {
123 return Err(
124 "offset_sphere_face: the push collapses the sphere to or past its centre — refusing"
125 .into(),
126 );
127 }
128
129 let k = radius_new / radius;
132 let scale_about_center = AffineTransform::new([
133 k, 0.0, 0.0, (1.0 - k) * center.x,
134 0.0, k, 0.0, (1.0 - k) * center.y,
135 0.0, 0.0, k, (1.0 - k) * center.z,
136 0.0, 0.0, 0.0, 1.0,
137 ])?;
138 let s_prime = transform_surface(&pushed.surface, scale_about_center)?;
139 match s_prime.analytic() {
141 Some(AnalyticSurface::Sphere { frame, radius }) => {
142 if frame.origin.sub(center).length() > plane_tolerance
143 || (radius - radius_new).abs() > plane_tolerance.max(radius_new * 1e-9)
144 {
145 return Err("offset_sphere_face: offset carrier is not the expected \
146 concentric sphere — refusing"
147 .into());
148 }
149 }
150 _ => {
151 return Err(
152 "offset_sphere_face: offset carrier did not re-recognise as a sphere — refusing"
153 .into(),
154 )
155 }
156 }
157
158 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
162 for shell in &solid.shells {
163 for face in &shell.faces {
164 for loop_record in &face.loops {
165 for coedge in &loop_record.coedges {
166 faces_of_edge
167 .entry(coedge.edge_id)
168 .or_default()
169 .push(face.id);
170 }
171 }
172 }
173 }
174 let edge_by_id: HashMap<u64, &EdgeRecord> =
175 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
176
177 let [dv0, dv1] = s_prime.domain_v()?;
180
181 enum RimKind {
188 OffCentrePlane(Plane),
189 NeighbourSphere,
190 CoaxialRuled,
191 }
192 let mut edges_to_scale: HashSet<u64> = HashSet::default();
193 let mut through_centre_caps: HashSet<u64> = HashSet::default();
194 let mut own_only_edges: Vec<u64> = Vec::new();
195 let mut all_boundary_vertices: HashSet<u64> = HashSet::default();
196 let mut raw_rims: Vec<(u64, u64, RimKind)> = Vec::new();
197
198 for loop_record in &pushed.loops {
199 for coedge in &loop_record.coedges {
200 let edge = *edge_by_id
201 .get(&coedge.edge_id)
202 .ok_or_else(|| format!("offset_sphere_face: missing edge {}", coedge.edge_id))?;
203 all_boundary_vertices.insert(edge.start_vertex_id);
204 all_boundary_vertices.insert(edge.end_vertex_id);
205
206 let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
207 if incident.iter().all(|f| *f == face_id) {
208 own_only_edges.push(edge.id); continue;
210 }
211 let neighbour = *incident
212 .iter()
213 .find(|f| **f != face_id)
214 .ok_or_else(|| format!("offset_sphere_face: edge {} has no neighbour", edge.id))?;
215 let (nshell, nface) = find_face(solid, neighbour)
216 .ok_or_else(|| format!("offset_sphere_face: missing neighbour {neighbour}"))?;
217 let neighbour_surface = &solid.shells[nshell].faces[nface].surface;
218 match neighbour_surface.analytic() {
219 Some(AnalyticSurface::Plane { .. }) => {
220 let plane =
221 plane_of_surface(neighbour_surface, plane_tolerance, "offset_sphere_face")?;
222 if center.sub(plane.origin).dot(plane.normal).abs() <= plane_tolerance {
223 through_centre_caps.insert(neighbour);
225 edges_to_scale.insert(edge.id);
226 } else {
227 raw_rims.push((edge.id, neighbour, RimKind::OffCentrePlane(plane)));
228 }
229 }
230 Some(AnalyticSurface::Sphere { .. }) => {
231 raw_rims.push((edge.id, neighbour, RimKind::NeighbourSphere));
232 }
233 Some(AnalyticSurface::RuledRevolution { .. }) => {
234 if !super::face_offset::ruled_neighbour_is_coaxial(
235 neighbour_surface,
236 center,
237 axis,
238 scale,
239 ) {
240 return Err("offset_sphere_face: a cylinder/cone rim neighbour is \
241 NON-coaxial (only coaxial sphere × ruled intersections are \
242 supported) — refusing"
243 .into());
244 }
245 raw_rims.push((edge.id, neighbour, RimKind::CoaxialRuled));
246 }
247 Some(AnalyticSurface::Torus { .. }) => {
248 return Err("offset_sphere_face: a toroidal rim neighbour is deferred \
249 (torus is untouched in this slice)"
250 .into());
251 }
252 _ => {
253 return Err("offset_sphere_face: a rim neighbour is not a supported plane, \
254 sphere, or coaxial cylinder/cone (general revolution and \
255 free-form neighbours are deferred in this slice)"
256 .into());
257 }
258 }
259 }
260 }
261
262 let mut closure_vertices: HashSet<u64> = HashSet::default();
265 for (edge_id, _, _) in &raw_rims {
266 let edge = *edge_by_id.get(edge_id).unwrap();
267 if edge.start_vertex_id != edge.end_vertex_id {
268 return Err(
269 "offset_sphere_face: a recomputed rim is not a single closed edge \
270 (multi-edge / open rims are deferred in this slice)"
271 .into(),
272 );
273 }
274 closure_vertices.insert(edge.start_vertex_id);
275 }
276
277 let mut coupled_meridians: HashSet<u64> = HashSet::default();
281 for own in &own_only_edges {
282 let e = *edge_by_id.get(own).unwrap();
283 if e.degenerate {
284 edges_to_scale.insert(*own);
285 continue;
286 }
287 if closure_vertices.contains(&e.start_vertex_id)
288 || closure_vertices.contains(&e.end_vertex_id)
289 {
290 coupled_meridians.insert(*own);
291 } else {
292 edges_to_scale.insert(*own);
293 }
294 }
295 let vertices_to_scale: HashSet<u64> = all_boundary_vertices
297 .iter()
298 .copied()
299 .filter(|v| !closure_vertices.contains(v))
300 .collect();
301
302 struct RimBuild {
304 edge_id: u64,
305 neighbour_id: u64,
306 neighbour_is_sphere: bool,
307 closure_vertex: u64,
308 circle: NurbsCurve,
309 closure_point: Vec3,
310 v_rim: f64,
311 pushed_pcurve: NurbsCurve,
312 }
313 let mut rim_builds: Vec<RimBuild> = Vec::new();
314 for (edge_id, neighbour, kind) in &raw_rims {
315 let edge = *edge_by_id.get(edge_id).unwrap();
316 let closure_vertex = edge.start_vertex_id;
317 let seam_coupled = coupled_meridians.iter().any(|m| {
318 let e = *edge_by_id.get(m).unwrap();
319 e.start_vertex_id == closure_vertex || e.end_vertex_id == closure_vertex
320 });
321
322 let mut circle = if seam_coupled {
323 match kind {
329 RimKind::OffCentrePlane(plane) => {
330 if plane.normal.dot(axis).abs() < 1.0 - 1e-6 {
331 return Err(
332 "offset_sphere_face: a seam-crossing rim on an OBLIQUE plane is \
333 deferred in this slice (only axis-perpendicular caps)"
334 .into(),
335 );
336 }
337 let a = plane.origin.sub(center).dot(axis);
338 let rr2 = radius_new * radius_new - a * a;
339 if rr2 <= tolerance * tolerance {
340 return Err(
341 "offset_sphere_face: the push pulls the grown sphere off its cap \
342 plane (the cap vanishes) — refusing"
343 .into(),
344 );
345 }
346 crate::make_arc(
347 center.add(axis.scale(a)),
348 seam_x,
349 seam_y,
350 rr2.sqrt(),
351 0.0,
352 std::f64::consts::TAU,
353 )?
354 }
355 RimKind::NeighbourSphere => {
356 return Err(
357 "offset_sphere_face: a sphere neighbour whose rim crosses the \
358 pushed sphere's seam is deferred in this slice"
359 .into(),
360 )
361 }
362 RimKind::CoaxialRuled => {
363 let (ns, nf) = find_face(solid, *neighbour).unwrap();
369 let neighbour_surface = &solid.shells[ns].faces[nf].surface;
370 let curves = intersect_analytic_pair(&s_prime, neighbour_surface, tolerance)
371 .filter(|curves| !curves.is_empty())
372 .ok_or_else(|| {
373 "offset_sphere_face: the grown sphere no longer meets its coaxial \
374 cylinder/cone neighbour — refusing"
375 .to_string()
376 })?;
377 let old_mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1))?;
378 nearest_circle(&curves, old_mid)?
379 }
380 }
381 } else {
382 let (ns, nf) = find_face(solid, *neighbour).unwrap();
383 let neighbour_surface = &solid.shells[ns].faces[nf].surface;
384 let curves = intersect_analytic_pair(&s_prime, neighbour_surface, tolerance)
385 .filter(|curves| !curves.is_empty())
386 .ok_or_else(|| {
387 "offset_sphere_face: the grown sphere no longer meets a neighbour (the cap \
388 vanishes / tangent / separated) — refusing"
389 .to_string()
390 })?;
391 let old_mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1))?;
392 nearest_circle(&curves, old_mid)?
393 };
394
395 let closure_old = edge.curve.evaluate(edge.t0)?;
398 let old_tan = edge
399 .curve
400 .evaluate(edge.t0 + 0.01 * (edge.t1 - edge.t0))?
401 .sub(closure_old);
402 let [c0, c1] = circle.domain()?;
403 let new_start = circle.evaluate(c0)?;
404 let new_tan = circle.evaluate(c0 + 0.01 * (c1 - c0))?.sub(new_start);
405 if old_tan.dot(new_tan) < 0.0 {
406 circle = circle.reversed()?;
407 }
408 let closure_point = circle.evaluate(circle.domain()?[0])?;
409 let pushed_pcurve = build_pcurve_on_surface(&s_prime, &circle)?;
410 let [q0, q1] = pushed_pcurve.domain()?;
413 let v_rim = pushed_pcurve.evaluate(0.5 * (q0 + q1))?.y;
414
415 rim_builds.push(RimBuild {
416 edge_id: *edge_id,
417 neighbour_id: *neighbour,
418 neighbour_is_sphere: matches!(kind, RimKind::NeighbourSphere),
419 closure_vertex,
420 circle,
421 closure_point,
422 v_rim,
423 pushed_pcurve,
424 });
425 }
426
427 let iso_meridian = s_prime.iso_curve_u(0.0)?;
431
432 let mut result = solid.clone();
434
435 for edge in &mut result.edges {
437 if edges_to_scale.contains(&edge.id) {
438 edge.curve = transform_curve(&edge.curve, scale_about_center)?;
439 }
440 }
441 for rb in &rim_builds {
443 if let Some(edge) = result.edges.iter_mut().find(|e| e.id == rb.edge_id) {
444 let [d0, d1] = rb.circle.domain()?;
445 edge.curve = rb.circle.clone();
446 edge.t0 = d0;
447 edge.t1 = d1;
448 }
449 }
450 for mer in &coupled_meridians {
453 let e = *edge_by_id.get(mer).unwrap();
454 let cv = if closure_vertices.contains(&e.start_vertex_id) {
455 e.start_vertex_id
456 } else {
457 e.end_vertex_id
458 };
459 let v_rim = rim_builds
460 .iter()
461 .find(|rb| rb.closure_vertex == cv)
462 .map(|rb| rb.v_rim)
463 .ok_or_else(|| {
464 "offset_sphere_face: a seam meridian is not paired with a rim — refusing"
465 .to_string()
466 })?;
467 let closure_at_start = e.start_vertex_id == cv;
468 let fixed_vertex = if closure_at_start {
469 e.end_vertex_id
470 } else {
471 e.start_vertex_id
472 };
473 let fixed_old = solid
474 .vertices
475 .iter()
476 .find(|v| v.id == fixed_vertex)
477 .map(|v| v.point)
478 .ok_or_else(|| format!("offset_sphere_face: missing seam vertex {fixed_vertex}"))?;
479 let fixed_new = scale_about_center.point(fixed_old);
480 let fixed_projection = project_point_to_curve(&iso_meridian, fixed_new)?;
481 if fixed_projection.distance > plane_tolerance {
482 return Err(format!(
483 "offset_sphere_face: a coupled own-edge is not on the sphere seam \
484 (off by {:.3e}) — refusing",
485 fixed_projection.distance
486 ));
487 }
488 let v_start = if closure_at_start {
489 v_rim
490 } else {
491 fixed_projection.u
492 };
493 let v_end = if closure_at_start {
494 fixed_projection.u
495 } else {
496 v_rim
497 };
498 let (curve, t0, t1) = if v_start <= v_end {
499 (iso_meridian.clone(), v_start, v_end)
500 } else {
501 (
502 iso_meridian.reversed()?,
503 dv0 + dv1 - v_start,
504 dv0 + dv1 - v_end,
505 )
506 };
507 if let Some(edge) = result.edges.iter_mut().find(|x| x.id == *mer) {
508 edge.curve = curve;
509 edge.t0 = t0;
510 edge.t1 = t1;
511 }
512 }
513
514 for vertex in &mut result.vertices {
516 if vertices_to_scale.contains(&vertex.id) {
517 vertex.point = scale_about_center.point(vertex.point);
518 }
519 }
520 for rb in &rim_builds {
521 if let Some(v) = result.vertices.iter_mut().find(|v| v.id == rb.closure_vertex) {
522 v.point = rb.closure_point;
523 }
524 }
525
526 {
530 let pface_rec = &mut result.shells[pshell].faces[pface];
531 for loop_record in &mut pface_rec.loops {
532 for coedge in &mut loop_record.coedges {
533 if let Some(rb) = rim_builds.iter().find(|rb| rb.edge_id == coedge.edge_id) {
534 let mut pcurve = rb.pushed_pcurve.clone();
535 if !coedge.forward {
536 pcurve = pcurve.reversed()?;
537 }
538 coedge.pcurve = pcurve;
539 } else if coupled_meridians.contains(&coedge.edge_id) {
540 let e = *edge_by_id.get(&coedge.edge_id).unwrap();
541 let cv = if closure_vertices.contains(&e.start_vertex_id) {
542 e.start_vertex_id
543 } else {
544 e.end_vertex_id
545 };
546 let v_rim = rim_builds
547 .iter()
548 .find(|rb| rb.closure_vertex == cv)
549 .map(|rb| rb.v_rim)
550 .unwrap();
551 let closure_at_pcurve_start = (e.start_vertex_id == cv) == coedge.forward;
552 coedge.pcurve =
553 patch_meridian_pcurve(&coedge.pcurve, v_rim, closure_at_pcurve_start)?;
554 }
555 }
556 }
557 }
558 result.shells[pshell].faces[pface].surface = s_prime;
559
560 let result_vertex: HashMap<u64, Vec3> =
564 result.vertices.iter().map(|v| (v.id, v.point)).collect();
565 let mut ruled_seams: HashSet<u64> = HashSet::default();
566 for rb in &rim_builds {
567 let (ns, nf) = find_face(&result, rb.neighbour_id)
568 .ok_or_else(|| format!("offset_sphere_face: missing neighbour {}", rb.neighbour_id))?;
569 if !matches!(
570 result.shells[ns].faces[nf].surface.analytic(),
571 Some(AnalyticSurface::RuledRevolution { .. })
572 ) {
573 continue;
574 }
575 for loop_record in &result.shells[ns].faces[nf].loops {
576 for coedge in &loop_record.coedges {
577 let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
578 format!("offset_sphere_face: missing edge {}", coedge.edge_id)
579 })?;
580 let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
581 let is_seam = incident.iter().all(|f| *f == rb.neighbour_id);
582 let touches_rim = closure_vertices.contains(&edge.start_vertex_id)
583 || closure_vertices.contains(&edge.end_vertex_id);
584 if is_seam && touches_rim {
585 ruled_seams.insert(edge.id);
586 }
587 }
588 }
589 }
590 let rebuilt_ruled_seams: HashSet<u64> = ruled_seams.iter().copied().collect();
591 for seam_id in ruled_seams {
592 let edge = *edge_by_id
593 .get(&seam_id)
594 .ok_or_else(|| format!("offset_sphere_face: missing ruled seam {seam_id}"))?;
595 let curve = make_line(
596 result_vertex[&edge.start_vertex_id],
597 result_vertex[&edge.end_vertex_id],
598 )?;
599 if let Some(out) = result
600 .edges
601 .iter_mut()
602 .find(|candidate| candidate.id == seam_id)
603 {
604 let [t0, t1] = curve.domain()?;
605 out.curve = curve;
606 out.t0 = t0;
607 out.t1 = t1;
608 }
609 }
610
611 let mut relocated: HashMap<u64, Vec3> = HashMap::default();
637 for vertex in &result.vertices {
638 let Some(old) = solid.vertices.iter().find(|o| o.id == vertex.id) else {
639 continue;
640 };
641 if vertex.point.sub(old.point).length() > tolerance {
642 relocated.insert(vertex.id, vertex.point);
643 }
644 }
645 let already_rebuilt: HashSet<u64> = edges_to_scale
646 .iter()
647 .copied()
648 .chain(rim_builds.iter().map(|rb| rb.edge_id))
649 .chain(coupled_meridians.iter().copied())
650 .chain(rebuilt_ruled_seams.iter().copied())
651 .collect();
652 let mut resized_edges: HashSet<u64> = HashSet::default();
653 for edge in &mut result.edges {
654 if edge.degenerate || already_rebuilt.contains(&edge.id) {
655 continue;
656 }
657 for at_start in [true, false] {
658 let vertex_id = if at_start {
659 edge.start_vertex_id
660 } else {
661 edge.end_vertex_id
662 };
663 let Some(&target) = relocated.get(&vertex_id) else {
664 continue;
665 };
666 let projection = project_point_to_curve(&edge.curve, target)?;
667 if projection.distance > 10.0 * tolerance {
668 return Err(format!(
669 "offset_sphere_face: the push moves rim corner (vertex {vertex_id}) OFF \
670 fixed edge {} by {:.3e} — a rim corner that leaves its own side edge \
671 needs the corner re-solved against the neighbour carriers, which is \
672 deferred — refusing",
673 edge.id, projection.distance
674 ));
675 }
676 let [d0, d1] = edge.curve.domain()?;
677 let span = (d1 - d0).max(1e-12);
678 let fixed_t = if at_start { edge.t1 } else { edge.t0 };
679 let old_t = if at_start { edge.t0 } else { edge.t1 };
680 let new_t = projection.u;
681 if (new_t - fixed_t) * (old_t - fixed_t) <= 0.0
682 || (new_t - fixed_t).abs() <= 1e-7 * span
683 {
684 return Err(format!(
685 "offset_sphere_face: the push collapses or inverts the trim of fixed edge \
686 {} at rim corner (vertex {vertex_id}) — refusing",
687 edge.id
688 ));
689 }
690 if at_start {
691 edge.t0 = new_t;
692 } else {
693 edge.t1 = new_t;
694 }
695 resized_edges.insert(edge.id);
696 }
697 }
698
699 let final_edges: HashMap<u64, EdgeRecord> =
701 result.edges.iter().map(|e| (e.id, e.clone())).collect();
702 for rb in &rim_builds {
703 let (ns, nf) = find_face(&result, rb.neighbour_id)
704 .ok_or_else(|| format!("offset_sphere_face: missing neighbour {}", rb.neighbour_id))?;
705 if rb.neighbour_is_sphere {
706 let nsurf = result.shells[ns].faces[nf].surface.clone();
708 for loop_record in &mut result.shells[ns].faces[nf].loops {
709 for coedge in &mut loop_record.coedges {
710 if coedge.edge_id == rb.edge_id {
711 let mut pcurve = build_pcurve_on_surface(&nsurf, &rb.circle)?;
712 if !coedge.forward {
713 pcurve = pcurve.reversed()?;
714 }
715 coedge.pcurve = pcurve;
716 }
717 }
718 }
719 } else if matches!(
720 result.shells[ns].faces[nf].surface.analytic(),
721 Some(AnalyticSurface::RuledRevolution { .. })
722 ) {
723 super::face_offset::retrim_offset_ruled_face(
724 &mut result,
725 rb.neighbour_id,
726 &final_edges,
727 tolerance,
728 )?;
729 } else {
730 let plane = plane_of_surface(
731 &result.shells[ns].faces[nf].surface,
732 plane_tolerance,
733 "offset_sphere_face",
734 )?;
735 retrim_planar_face(
736 &mut result.shells[ns].faces[nf],
737 &plane,
738 &final_edges,
739 scale,
740 "offset_sphere_face",
741 )?;
742 refit_subrange_pcurves(
743 &mut result.shells[ns].faces[nf],
744 &final_edges,
745 tolerance,
746 )?;
747 }
748 }
749 for cap in &through_centre_caps {
751 let (cshell, cface) = find_face(&result, *cap)
752 .ok_or_else(|| format!("offset_sphere_face: missing cap {cap}"))?;
753 let plane = plane_of_surface(
754 &result.shells[cshell].faces[cface].surface,
755 plane_tolerance,
756 "offset_sphere_face",
757 )?;
758 retrim_planar_face(
759 &mut result.shells[cshell].faces[cface],
760 &plane,
761 &final_edges,
762 scale,
763 "offset_sphere_face",
764 )?;
765 refit_subrange_pcurves(
766 &mut result.shells[cshell].faces[cface],
767 &final_edges,
768 tolerance,
769 )?;
770 }
771 for shell in &mut result.shells {
775 for face in &mut shell.faces {
776 if face
777 .loops
778 .iter()
779 .flat_map(|l| &l.coedges)
780 .any(|c| resized_edges.contains(&c.edge_id))
781 {
782 refit_touched_pcurves(
783 face,
784 &final_edges,
785 &resized_edges,
786 true,
787 tolerance,
788 "offset_sphere_face",
789 )?;
790 }
791 }
792 }
793
794 let issues = result.validate();
795 if !issues.is_empty() {
796 return Err(format!(
797 "offset_sphere_face: pushed solid failed validation: {issues:?}"
798 ));
799 }
800 if let (Ok(before), Ok(after)) = (solid_signed_volume(solid), solid_signed_volume(&result)) {
801 if before * after <= 0.0 {
802 return Err("offset_sphere_face: the push inverts the solid — refusing".into());
803 }
804 }
805 Ok(result)
806}
807
808fn refit_subrange_pcurves(
823 face: &mut FaceRecord,
824 edges: &HashMap<u64, EdgeRecord>,
825 tolerance: f64,
826) -> Result<(), String> {
827 let face_edges: HashSet<u64> = face
828 .loops
829 .iter()
830 .flat_map(|loop_record| loop_record.coedges.iter().map(|c| c.edge_id))
831 .collect();
832 refit_touched_pcurves(
833 face,
834 edges,
835 &face_edges,
836 true,
837 tolerance,
838 "offset_sphere_face",
839 )
840}
841
842fn nearest_circle(curves: &[NurbsCurve], reference: Vec3) -> Result<NurbsCurve, String> {
845 let mut best: Option<(f64, &NurbsCurve)> = None;
846 for curve in curves {
847 let [d0, d1] = curve.domain()?;
848 let mid = curve.evaluate(0.5 * (d0 + d1))?;
849 let distance = mid.sub(reference).length();
850 if best.map(|(known, _)| distance < known).unwrap_or(true) {
851 best = Some((distance, curve));
852 }
853 }
854 best.map(|(_, curve)| curve.clone())
855 .ok_or_else(|| "offset_sphere_face: the sphere ∩ neighbour intersection is empty".into())
856}
857
858fn patch_meridian_pcurve(
863 pcurve: &NurbsCurve,
864 v_new: f64,
865 closure_at_start: bool,
866) -> Result<NurbsCurve, String> {
867 let [q0, q1] = pcurve.domain()?;
868 let start = pcurve.evaluate(q0)?;
869 let end = pcurve.evaluate(q1)?;
870 let (v_start, v_end) = if closure_at_start {
871 (v_new, end.y)
872 } else {
873 (start.y, v_new)
874 };
875 make_line(
876 Vec3::new(start.x, v_start, 0.0),
877 Vec3::new(end.x, v_end, 0.0),
878 )
879}
880
881