1use brepkit_math::mat::Mat4;
9use brepkit_math::nurbs::curve::NurbsCurve;
10use brepkit_math::nurbs::surface_fitting::interpolate_surface;
11use brepkit_math::tolerance::Tolerance;
12use brepkit_math::vec::{Point3, Vec3};
13use brepkit_topology::Topology;
14use brepkit_topology::edge::{Edge, EdgeCurve};
15use brepkit_topology::face::{Face, FaceId, FaceSurface};
16use brepkit_topology::shell::Shell;
17use brepkit_topology::solid::{Solid, SolidId};
18use brepkit_topology::vertex::{Vertex, VertexId};
19use brepkit_topology::wire::{OrientedEdge, Wire};
20
21use crate::dot_normal_point;
22
23struct Frame {
25 origin: Point3,
26 tangent: Vec3,
27 up: Vec3,
28 right: Vec3,
29}
30
31fn compute_frames(
40 path: &NurbsCurve,
41 num_segments: usize,
42 initial_up: Vec3,
43 is_closed: bool,
44) -> Result<Vec<Frame>, crate::OperationsError> {
45 let frame_count = if is_closed {
46 num_segments
47 } else {
48 num_segments + 1
49 };
50 let mut frames = Vec::with_capacity(frame_count);
51
52 let (u0, u1) = path.domain();
56
57 let t0 = path.tangent(u0)?;
58 let up0 = orthogonalize(initial_up, t0);
59 let right0 = t0.cross(up0);
60 frames.push(Frame {
61 origin: path.evaluate(u0),
62 tangent: t0,
63 up: up0,
64 right: right0,
65 });
66
67 let last_k = if is_closed {
73 num_segments - 1
74 } else {
75 num_segments
76 };
77 for k in 1..=last_k {
78 #[allow(clippy::cast_precision_loss)]
79 let t_param = u0 + (u1 - u0) * (k as f64) / (num_segments as f64);
80
81 let origin = path.evaluate(t_param);
82 let tangent = path.tangent(t_param)?;
83
84 let prev = &frames[k - 1];
85
86 let v1 = origin - prev.origin;
88 let c1 = v1.dot(v1);
89 let (up_l, tangent_l) = if c1 < 1e-30 {
90 (prev.up, prev.tangent)
91 } else {
92 let up_r = prev.up - v1 * (2.0 * v1.dot(prev.up) / c1);
93 let t_r = prev.tangent - v1 * (2.0 * v1.dot(prev.tangent) / c1);
94 (up_r, t_r)
95 };
96
97 let v2 = tangent - tangent_l;
100 let c2 = v2.dot(v2);
101 let up = if c2 < 1e-30 {
102 orthogonalize(up_l, tangent)
103 } else {
104 let reflected = up_l - v2 * (2.0 * v2.dot(up_l) / c2);
105 orthogonalize(reflected, tangent)
106 };
107
108 let right = tangent.cross(up);
109 frames.push(Frame {
110 origin,
111 tangent,
112 up,
113 right,
114 });
115 }
116
117 Ok(frames)
118}
119
120fn profile_basis(input_normal: Vec3) -> (Vec3, Vec3, Vec3) {
130 let tangent = input_normal
131 .normalize()
132 .unwrap_or_else(|_| Vec3::new(0.0, 0.0, 1.0));
133 let up = orthogonalize(pick_reference_axis(tangent), tangent);
134 let right = tangent.cross(up);
135 (right, up, tangent)
136}
137
138fn orthogonalize(v: Vec3, tangent: Vec3) -> Vec3 {
142 let projected = v - tangent * tangent.dot(v);
143 projected.normalize().unwrap_or_else(|_| {
144 let candidate = if tangent.x().abs() < 0.9 {
146 Vec3::new(1.0, 0.0, 0.0)
147 } else {
148 Vec3::new(0.0, 1.0, 0.0)
149 };
150 let proj2 = candidate - tangent * tangent.dot(candidate);
151 proj2.normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0))
154 })
155}
156
157#[derive(Clone, Copy, PartialEq, Eq)]
159pub(crate) enum ProfilePlacement {
160 AsPositioned,
164 CentroidOnPath,
168}
169
170const PROFILE_PERP_MIN_COS: f64 = 0.99;
177
178pub(crate) fn resolve_placement(
181 placement: ProfilePlacement,
182 input_normal: Vec3,
183 path_tangent_0: Vec3,
184) -> ProfilePlacement {
185 match placement {
186 ProfilePlacement::AsPositioned
187 if input_normal.dot(path_tangent_0).abs() < PROFILE_PERP_MIN_COS =>
188 {
189 ProfilePlacement::CentroidOnPath
190 }
191 other => other,
192 }
193}
194
195fn transform_point(
202 point: Point3,
203 reference: Point3,
204 initial_right: Vec3,
205 initial_up: Vec3,
206 initial_tangent: Vec3,
207 frame: &Frame,
208) -> Point3 {
209 let offset = point - reference;
210 let local_r = initial_right.dot(offset);
211 let local_u = initial_up.dot(offset);
212 let local_t = initial_tangent.dot(offset);
213 frame.origin + frame.right * local_r + frame.up * local_u + frame.tangent * local_t
214}
215
216struct SweptWireData {
218 ring_verts: Vec<Vec<VertexId>>,
219 ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>>,
220 path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>>,
221 n: usize,
222}
223
224#[allow(clippy::too_many_arguments)]
230fn sweep_wire_through_frames(
231 topo: &mut Topology,
232 wire_id: brepkit_topology::wire::WireId,
233 reference: Point3,
234 initial_right: Vec3,
235 initial_up: Vec3,
236 initial_tangent: Vec3,
237 frames: &[Frame],
238 num_segments: usize,
239 is_closed: bool,
240) -> Result<SweptWireData, crate::OperationsError> {
241 let tol = Tolerance::new();
242
243 let wire = topo.wire(wire_id)?;
244 let oriented: Vec<_> = wire.edges().to_vec();
245 let n = oriented.len();
246
247 let mut verts: Vec<VertexId> = Vec::with_capacity(n);
248 for oe in &oriented {
249 let edge = topo.edge(oe.edge())?;
250 let vid = oe.oriented_start(edge);
251 verts.push(vid);
252 }
253
254 let positions: Vec<Point3> = verts
255 .iter()
256 .map(|&vid| {
257 topo.vertex(vid)
258 .map(brepkit_topology::vertex::Vertex::point)
259 })
260 .collect::<Result<_, _>>()?;
261
262 let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
263 for frame in frames {
264 let ring: Vec<VertexId> = positions
265 .iter()
266 .map(|&pos| {
267 let transformed = transform_point(
268 pos,
269 reference,
270 initial_right,
271 initial_up,
272 initial_tangent,
273 frame,
274 );
275 topo.add_vertex(Vertex::new(transformed, tol.linear))
276 })
277 .collect();
278 ring_verts.push(ring);
279 }
280
281 if is_closed {
283 ring_verts.push(ring_verts[0].clone());
284 }
285
286 let real_ring_count = if is_closed {
287 ring_verts.len() - 1
288 } else {
289 ring_verts.len()
290 };
291 let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
292 Vec::with_capacity(num_segments + 1);
293 for ring in &ring_verts[..real_ring_count] {
294 let edges: Vec<_> = (0..n)
295 .map(|i| {
296 let next = (i + 1) % n;
297 topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
298 })
299 .collect();
300 ring_edges.push(edges);
301 }
302 if is_closed {
303 ring_edges.push(ring_edges[0].clone());
304 }
305
306 let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
307 for seg in 0..num_segments {
308 let edges: Vec<_> = (0..n)
309 .map(|i| {
310 topo.add_edge(Edge::new(
311 ring_verts[seg][i],
312 ring_verts[seg + 1][i],
313 EdgeCurve::Line,
314 ))
315 })
316 .collect();
317 path_edges.push(edges);
318 }
319
320 Ok(SweptWireData {
321 ring_verts,
322 ring_edges,
323 path_edges,
324 n,
325 })
326}
327
328fn build_inner_side_faces(
330 topo: &mut Topology,
331 iwd: &SweptWireData,
332 num_segments: usize,
333) -> Result<Vec<FaceId>, crate::OperationsError> {
334 let mut faces = Vec::new();
335
336 for seg in 0..num_segments {
337 for i in 0..iwd.n {
338 let next_i = (i + 1) % iwd.n;
339
340 let p0 = topo.vertex(iwd.ring_verts[seg][i])?.point();
341 let p1 = topo.vertex(iwd.ring_verts[seg][next_i])?.point();
342 let p_next = topo.vertex(iwd.ring_verts[seg + 1][i])?.point();
343 let edge_dir = p1 - p0;
344 let path_dir = p_next - p0;
345 let side_normal = path_dir
347 .cross(edge_dir)
348 .normalize()
349 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
350 let side_d = dot_normal_point(side_normal, p0);
351
352 let side_wire = Wire::new(
354 vec![
355 OrientedEdge::new(iwd.path_edges[seg][i], true),
356 OrientedEdge::new(iwd.ring_edges[seg + 1][i], true),
357 OrientedEdge::new(iwd.path_edges[seg][next_i], false),
358 OrientedEdge::new(iwd.ring_edges[seg][i], false),
359 ],
360 true,
361 )
362 .map_err(crate::OperationsError::Topology)?;
363
364 let side_wire_id = topo.add_wire(side_wire);
365 let fid = topo.add_face(Face::new(
366 side_wire_id,
367 vec![],
368 FaceSurface::Plane {
369 normal: side_normal,
370 d: side_d,
371 },
372 ));
373 faces.push(fid);
374 }
375 }
376
377 Ok(faces)
378}
379
380fn build_inner_cap_wires(
382 topo: &mut Topology,
383 inner_data: &[SweptWireData],
384 ring_idx: usize,
385 reversed: bool,
386) -> Result<Vec<brepkit_topology::wire::WireId>, crate::OperationsError> {
387 let mut wires = Vec::new();
388 for iwd in inner_data {
389 let edges: Vec<OrientedEdge> = if reversed {
390 (0..iwd.n)
391 .rev()
392 .map(|i| OrientedEdge::new(iwd.ring_edges[ring_idx][i], false))
393 .collect()
394 } else {
395 (0..iwd.n)
396 .map(|i| OrientedEdge::new(iwd.ring_edges[ring_idx][i], true))
397 .collect()
398 };
399 let wire = Wire::new(edges, true).map_err(crate::OperationsError::Topology)?;
400 wires.push(topo.add_wire(wire));
401 }
402 Ok(wires)
403}
404
405#[must_use]
417pub fn densify_path_points(points: &[Point3]) -> Vec<Point3> {
418 const GAP_RATIO: f64 = 4.0;
419 const MAX_INSERT_PER_SEGMENT: usize = 256;
420
421 if points.len() < 3 {
422 return points.to_vec();
423 }
424
425 let mut gaps: Vec<f64> = points
426 .windows(2)
427 .map(|w| (w[1] - w[0]).length())
428 .filter(|d| *d > 1e-12)
429 .collect();
430 if gaps.is_empty() {
431 return points.to_vec();
432 }
433 gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
434 let median = gaps[gaps.len() / 2];
435 let max_gap = median * GAP_RATIO;
436 if max_gap <= 0.0 {
437 return points.to_vec();
438 }
439
440 let mut out: Vec<Point3> = Vec::with_capacity(points.len());
441 for w in points.windows(2) {
442 let (a, b) = (w[0], w[1]);
443 out.push(a);
444 let seg = (b - a).length();
445 if seg > max_gap {
446 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
447 let steps = ((seg / max_gap).ceil() as usize).min(MAX_INSERT_PER_SEGMENT);
448 for s in 1..steps {
449 #[allow(clippy::cast_precision_loss)]
450 let f = (s as f64) / (steps as f64);
451 out.push(a + (b - a) * f);
452 }
453 }
454 }
455 if let Some(&last) = points.last() {
456 out.push(last);
457 }
458 out
459}
460
461const STRAIGHT_SAMPLES: usize = 8;
463
464fn profile_outer_centroid(
471 topo: &Topology,
472 profile: FaceId,
473) -> Result<Point3, crate::OperationsError> {
474 let face = topo.face(profile)?;
475 let wire = topo.wire(face.outer_wire())?;
476 let (mut sx, mut sy, mut sz) = (0.0_f64, 0.0_f64, 0.0_f64);
477 let mut count = 0_usize;
478 for oe in wire.edges() {
479 let edge = topo.edge(oe.edge())?;
480 let start = topo.vertex(edge.start())?.point();
481 let end = topo.vertex(edge.end())?.point();
482 let (t0, t1) = edge.curve().domain_with_endpoints(start, end);
483 for k in 0..4 {
485 let t = t0 + (t1 - t0) * (f64::from(k) / 4.0);
486 let p = edge.curve().evaluate_with_endpoints(t, start, end);
487 sx += p.x();
488 sy += p.y();
489 sz += p.z();
490 count += 1;
491 }
492 }
493 if count == 0 {
494 return Err(crate::OperationsError::InvalidInput {
495 reason: "sweep profile has no outer-wire edges".into(),
496 });
497 }
498 #[allow(clippy::cast_precision_loss)]
499 let n = count as f64;
500 Ok(Point3::new(sx / n, sy / n, sz / n))
501}
502
503fn try_straight_extrude(
513 topo: &mut Topology,
514 profile: FaceId,
515 path: &NurbsCurve,
516 placement: ProfilePlacement,
517) -> Result<Option<SolidId>, crate::OperationsError> {
518 let tol = Tolerance::new();
519
520 let normal = match topo.face(profile)?.surface() {
521 FaceSurface::Plane { normal, .. } => *normal,
522 _ => return Ok(None),
523 };
524
525 let start = path.evaluate(0.0);
526 let end = path.evaluate(1.0);
527 let chord = end - start;
528 let length = chord.length();
529 if length < tol.linear {
530 return Ok(None); }
532 let Ok(dir) = chord.normalize() else {
533 return Ok(None);
534 };
535
536 for k in 1..STRAIGHT_SAMPLES {
539 #[allow(clippy::cast_precision_loss)]
540 let t = k as f64 / STRAIGHT_SAMPLES as f64;
541 let v = path.evaluate(t) - start;
542 let along = v.dot(dir);
543 let perp = (v - dir * along).length();
544 if perp > tol.linear * 100.0 || along < -tol.linear || along > length + tol.linear {
545 return Ok(None);
546 }
547 }
548
549 if normal.dot(dir).abs() < 1.0 - 1e-6 {
552 return Ok(None);
553 }
554
555 let moved = crate::copy::copy_face(topo, profile)?;
559 if placement == ProfilePlacement::CentroidOnPath {
560 let centroid = profile_outer_centroid(topo, profile)?;
561 let shift = start - centroid;
562 crate::transform::transform_face(
563 topo,
564 moved,
565 &Mat4::translation(shift.x(), shift.y(), shift.z()),
566 )?;
567 }
568
569 Ok(Some(crate::extrude::extrude(topo, moved, dir, length)?))
570}
571
572pub fn sweep(
590 topo: &mut Topology,
591 profile: FaceId,
592 path: &NurbsCurve,
593) -> Result<SolidId, crate::OperationsError> {
594 sweep_placed(topo, profile, path, ProfilePlacement::AsPositioned)
595}
596
597#[allow(clippy::too_many_lines)]
598pub(crate) fn sweep_placed(
599 topo: &mut Topology,
600 profile: FaceId,
601 path: &NurbsCurve,
602 placement: ProfilePlacement,
603) -> Result<SolidId, crate::OperationsError> {
604 let tol = Tolerance::new();
605
606 if path.control_points().len() < 2 {
607 return Err(crate::OperationsError::InvalidInput {
608 reason: "sweep path must have at least 2 control points".into(),
609 });
610 }
611
612 if let Some(solid) = try_straight_extrude(topo, profile, path, placement)? {
614 return Ok(solid);
615 }
616
617 let face_data = topo.face(profile)?;
618 let input_wire_id = face_data.outer_wire();
619 let inner_wire_ids: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
620
621 let start_end_coincide = tol.approx_eq(
622 (path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
623 0.0,
624 );
625 let is_closed = if start_end_coincide {
626 let mid = path.evaluate(0.5);
629 let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
630 if tol.approx_eq(mid_dist_sq, 0.0) {
631 return Err(crate::OperationsError::InvalidInput {
632 reason: "sweep path has zero length (start and end coincide)".into(),
633 });
634 }
635 true
636 } else {
637 false
638 };
639
640 let input_wire = topo.wire(input_wire_id)?;
641 let original_oriented: Vec<_> = input_wire.edges().to_vec();
642
643 if original_oriented.is_empty() {
644 return Err(crate::OperationsError::InvalidInput {
645 reason: "sweep profile has no edges".into(),
646 });
647 }
648
649 let input_oriented = crate::extrude::maybe_split_closed_wire(
652 topo,
653 &original_oriented,
654 tol.linear,
655 crate::extrude::DEFAULT_DEFLECTION,
656 )?;
657 let n = input_oriented.len();
658
659 let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
660 for oe in &input_oriented {
661 let edge = topo.edge(oe.edge())?;
662 let vid = oe.oriented_start(edge);
663 input_verts.push(vid);
664 }
665
666 let mut input_positions: Vec<Point3> = input_verts
667 .iter()
668 .map(|&vid| {
669 topo.vertex(vid)
670 .map(brepkit_topology::vertex::Vertex::point)
671 })
672 .collect::<Result<_, _>>()?;
673
674 let mut input_normal = crate::winding::newell_normal(&input_positions)
678 .normalize()
679 .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
680
681 let path_tangent_0 = path.tangent(0.0)?;
685 if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
686 input_normal = -input_normal;
688 }
689
690 let num_segments = (path.control_points().len() * 2).max(4);
691
692 let up_hint = orthogonalize(input_normal, path_tangent_0);
695
696 let frames = compute_frames(path, num_segments, up_hint, is_closed)?;
697
698 let (reference, initial_right, initial_up, initial_tangent) =
703 match resolve_placement(placement, input_normal, path_tangent_0) {
704 ProfilePlacement::AsPositioned => (
705 frames[0].origin,
706 frames[0].right,
707 frames[0].up,
708 frames[0].tangent,
709 ),
710 ProfilePlacement::CentroidOnPath => {
711 let (r, u, t) = profile_basis(input_normal);
712 (crate::winding::polygon_centroid(&input_positions), r, u, t)
713 }
714 };
715
716 let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
720
721 for frame in &frames {
722 let ring: Vec<VertexId> = input_positions
723 .iter()
724 .map(|&pos| {
725 let transformed = transform_point(
726 pos,
727 reference,
728 initial_right,
729 initial_up,
730 initial_tangent,
731 frame,
732 );
733 topo.add_vertex(Vertex::new(transformed, tol.linear))
734 })
735 .collect();
736 ring_verts.push(ring);
737 }
738
739 if is_closed {
742 ring_verts.push(ring_verts[0].clone());
743 }
744
745 let real_ring_count = if is_closed {
747 num_segments
748 } else {
749 num_segments + 1
750 };
751 let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
752 Vec::with_capacity(num_segments + 1);
753 for ring in &ring_verts[..real_ring_count] {
754 let edges: Vec<_> = (0..n)
755 .map(|i| {
756 let next = (i + 1) % n;
757 topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
758 })
759 .collect();
760 ring_edges.push(edges);
761 }
762 if is_closed {
764 ring_edges.push(ring_edges[0].clone());
765 }
766
767 let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
769 for seg in 0..num_segments {
770 let edges: Vec<_> = (0..n)
771 .map(|i| {
772 topo.add_edge(Edge::new(
773 ring_verts[seg][i],
774 ring_verts[seg + 1][i],
775 EdgeCurve::Line,
776 ))
777 })
778 .collect();
779 path_edges.push(edges);
780 }
781
782 let mut inner_swept: Vec<SweptWireData> = Vec::new();
783 for &iw_id in &inner_wire_ids {
784 inner_swept.push(sweep_wire_through_frames(
785 topo,
786 iw_id,
787 reference,
788 initial_right,
789 initial_up,
790 initial_tangent,
791 &frames,
792 num_segments,
793 is_closed,
794 )?);
795 }
796
797 let mut all_faces = Vec::with_capacity(num_segments * n + if is_closed { 0 } else { 2 });
798
799 if !is_closed {
801 let start_inner_wires = build_inner_cap_wires(topo, &inner_swept, 0, true)?;
802 let start_verts = crate::cap::ring_point_positions(topo, &ring_verts[0])?;
803 let outward = crate::cap::outward_normal(&start_verts, -frames[0].tangent)?;
804 all_faces.push(crate::cap::build_cap_face(
805 topo,
806 &ring_edges[0],
807 start_inner_wires,
808 &start_verts,
809 outward,
810 true,
811 )?);
812 }
813
814 for seg in 0..num_segments {
818 for i in 0..n {
819 let next_i = (i + 1) % n;
820
821 let p0 = topo.vertex(ring_verts[seg][i])?.point();
822 let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
823 let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
824 let edge_dir = p1 - p0;
825 let path_dir = p_next - p0;
826 let side_normal = edge_dir
827 .cross(path_dir)
828 .normalize()
829 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
830 let side_d = dot_normal_point(side_normal, p0);
831
832 let side_wire = Wire::new(
833 vec![
834 OrientedEdge::new(ring_edges[seg][i], true),
835 OrientedEdge::new(path_edges[seg][next_i], true),
836 OrientedEdge::new(ring_edges[seg + 1][i], false),
837 OrientedEdge::new(path_edges[seg][i], false),
838 ],
839 true,
840 )
841 .map_err(crate::OperationsError::Topology)?;
842
843 let side_wire_id = topo.add_wire(side_wire);
844 let side_face = topo.add_face(Face::new(
845 side_wire_id,
846 vec![],
847 FaceSurface::Plane {
848 normal: side_normal,
849 d: side_d,
850 },
851 ));
852 all_faces.push(side_face);
853 }
854 }
855
856 for iwd in &inner_swept {
857 let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
858 all_faces.extend(inner_faces);
859 }
860
861 if !is_closed {
863 let end_inner_wires = build_inner_cap_wires(topo, &inner_swept, num_segments, false)?;
864 let end_verts = crate::cap::ring_point_positions(topo, &ring_verts[num_segments])?;
865 let outward = crate::cap::outward_normal(&end_verts, frames[num_segments].tangent)?;
866 all_faces.push(crate::cap::build_cap_face(
867 topo,
868 &ring_edges[num_segments],
869 end_inner_wires,
870 &end_verts,
871 outward,
872 false,
873 )?);
874 }
875
876 let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
877 let shell_id = topo.add_shell(shell);
878 let solid = topo.add_solid(Solid::new(shell_id, vec![]));
879
880 Ok(solid)
881}
882
883#[allow(clippy::too_many_lines)]
904pub fn sweep_smooth(
905 topo: &mut Topology,
906 profile: FaceId,
907 path: &NurbsCurve,
908) -> Result<SolidId, crate::OperationsError> {
909 let tol = Tolerance::new();
910
911 if path.control_points().len() < 2 {
912 return Err(crate::OperationsError::InvalidInput {
913 reason: "sweep path must have at least 2 control points".into(),
914 });
915 }
916
917 let face_data = topo.face(profile)?;
918 let input_wire_id = face_data.outer_wire();
919 let inner_wire_ids_smooth: Vec<brepkit_topology::wire::WireId> =
920 face_data.inner_wires().to_vec();
921
922 let start_end_coincide_smooth = tol.approx_eq(
924 (path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
925 0.0,
926 );
927 let is_closed = if start_end_coincide_smooth {
928 let mid = path.evaluate(0.5);
929 let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
930 if tol.approx_eq(mid_dist_sq, 0.0) {
931 return Err(crate::OperationsError::InvalidInput {
932 reason: "sweep path has zero length".into(),
933 });
934 }
935 true
936 } else {
937 false
938 };
939
940 let input_wire = topo.wire(input_wire_id)?;
941 let original_oriented: Vec<_> = input_wire.edges().to_vec();
942
943 if original_oriented.is_empty() {
944 return Err(crate::OperationsError::InvalidInput {
945 reason: "sweep profile has no edges".into(),
946 });
947 }
948
949 let input_oriented = crate::extrude::maybe_split_closed_wire(
950 topo,
951 &original_oriented,
952 tol.linear,
953 crate::extrude::DEFAULT_DEFLECTION,
954 )?;
955 let n = input_oriented.len();
956
957 let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
958 for oe in &input_oriented {
959 let edge = topo.edge(oe.edge())?;
960 let vid = oe.oriented_start(edge);
961 input_verts.push(vid);
962 }
963
964 let mut input_positions: Vec<Point3> = input_verts
965 .iter()
966 .map(|&vid| {
967 topo.vertex(vid)
968 .map(brepkit_topology::vertex::Vertex::point)
969 })
970 .collect::<Result<_, _>>()?;
971
972 let mut input_normal = crate::winding::newell_normal(&input_positions)
975 .normalize()
976 .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
977
978 let path_tangent_0 = path.tangent(0.0)?;
980 if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
981 input_normal = -input_normal;
982 }
983
984 let num_segments = (path.control_points().len() * 2).max(4);
985 let up_hint = orthogonalize(input_normal, path_tangent_0);
986 let frames = compute_frames(path, num_segments, up_hint, is_closed)?;
987
988 let (reference, initial_right, initial_up, initial_tangent) =
992 match resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0) {
993 ProfilePlacement::AsPositioned => (
994 frames[0].origin,
995 frames[0].right,
996 frames[0].up,
997 frames[0].tangent,
998 ),
999 ProfilePlacement::CentroidOnPath => {
1000 let (r, u, t) = profile_basis(input_normal);
1001 (crate::winding::polygon_centroid(&input_positions), r, u, t)
1002 }
1003 };
1004
1005 let num_rings = frames.len();
1007 let ring_positions: Vec<Vec<Point3>> = frames
1008 .iter()
1009 .map(|frame| {
1010 input_positions
1011 .iter()
1012 .map(|&pos| {
1013 transform_point(
1014 pos,
1015 reference,
1016 initial_right,
1017 initial_up,
1018 initial_tangent,
1019 frame,
1020 )
1021 })
1022 .collect()
1023 })
1024 .collect();
1025
1026 if is_closed {
1030 return sweep(topo, profile, path);
1031 }
1032
1033 let first_ring: Vec<VertexId> = ring_positions[0]
1035 .iter()
1036 .map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
1037 .collect();
1038 let last_ring: Vec<VertexId> = ring_positions[num_rings - 1]
1039 .iter()
1040 .map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
1041 .collect();
1042
1043 let first_ring_edges: Vec<_> = (0..n)
1044 .map(|i| {
1045 let next = (i + 1) % n;
1046 topo.add_edge(Edge::new(first_ring[i], first_ring[next], EdgeCurve::Line))
1047 })
1048 .collect();
1049 let last_ring_edges: Vec<_> = (0..n)
1050 .map(|i| {
1051 let next = (i + 1) % n;
1052 topo.add_edge(Edge::new(last_ring[i], last_ring[next], EdgeCurve::Line))
1053 })
1054 .collect();
1055
1056 let mut inner_swept_smooth: Vec<SweptWireData> = Vec::new();
1057 for &iw_id in &inner_wire_ids_smooth {
1058 inner_swept_smooth.push(sweep_wire_through_frames(
1059 topo,
1060 iw_id,
1061 reference,
1062 initial_right,
1063 initial_up,
1064 initial_tangent,
1065 &frames,
1066 num_segments,
1067 is_closed,
1068 )?);
1069 }
1070
1071 let mut all_faces = Vec::with_capacity(n + 2);
1072
1073 let start_inner_wires_smooth = build_inner_cap_wires(topo, &inner_swept_smooth, 0, true)?;
1074 let start_outward = crate::cap::outward_normal(&ring_positions[0], -frames[0].tangent)?;
1075 all_faces.push(crate::cap::build_cap_face(
1076 topo,
1077 &first_ring_edges,
1078 start_inner_wires_smooth,
1079 &ring_positions[0],
1080 start_outward,
1081 true,
1082 )?);
1083
1084 let degree_u = (num_rings - 1).min(3);
1086 let degree_v = 1;
1087
1088 let rail_edges: Vec<_> = (0..n)
1092 .map(|i| topo.add_edge(Edge::new(first_ring[i], last_ring[i], EdgeCurve::Line)))
1093 .collect();
1094
1095 for i in 0..n {
1096 let next_i = (i + 1) % n;
1097
1098 let grid: Vec<Vec<Point3>> = (0..num_rings)
1100 .map(|k| vec![ring_positions[k][i], ring_positions[k][next_i]])
1101 .collect();
1102
1103 let surface =
1104 interpolate_surface(&grid, degree_u, degree_v).map_err(crate::OperationsError::Math)?;
1105
1106 let mid0 = ring_positions[0][i] + (ring_positions[0][next_i] - ring_positions[0][i]) * 0.5;
1114 let mid1 = ring_positions[1][i] + (ring_positions[1][next_i] - ring_positions[1][i]) * 0.5;
1115 let edge_dir = ring_positions[0][next_i] - ring_positions[0][i];
1116 let mut outward = edge_dir.cross(mid1 - mid0);
1117 if outward.length() < tol.linear {
1118 outward = mid0 - crate::winding::polygon_centroid(&ring_positions[0]);
1119 }
1120 let reversed = surface
1121 .normal(0.0, 0.5)
1122 .map(|nrm| nrm.dot(outward) < 0.0)
1123 .unwrap_or(false);
1124
1125 let side_wire = if reversed {
1129 Wire::new(
1130 vec![
1131 OrientedEdge::new(rail_edges[i], true),
1132 OrientedEdge::new(last_ring_edges[i], true),
1133 OrientedEdge::new(rail_edges[next_i], false),
1134 OrientedEdge::new(first_ring_edges[i], false),
1135 ],
1136 true,
1137 )
1138 } else {
1139 Wire::new(
1140 vec![
1141 OrientedEdge::new(first_ring_edges[i], true),
1142 OrientedEdge::new(rail_edges[next_i], true),
1143 OrientedEdge::new(last_ring_edges[i], false),
1144 OrientedEdge::new(rail_edges[i], false),
1145 ],
1146 true,
1147 )
1148 }
1149 .map_err(crate::OperationsError::Topology)?;
1150
1151 let side_wire_id = topo.add_wire(side_wire);
1152 let mut face = Face::new(side_wire_id, vec![], FaceSurface::Nurbs(surface));
1153 if reversed {
1154 face.set_reversed(true);
1155 }
1156 all_faces.push(topo.add_face(face));
1157 }
1158
1159 for iwd in &inner_swept_smooth {
1160 let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
1161 all_faces.extend(inner_faces);
1162 }
1163
1164 let end_inner_wires_smooth =
1165 build_inner_cap_wires(topo, &inner_swept_smooth, num_segments, false)?;
1166 let end_outward =
1167 crate::cap::outward_normal(&ring_positions[num_rings - 1], frames[num_segments].tangent)?;
1168 all_faces.push(crate::cap::build_cap_face(
1169 topo,
1170 &last_ring_edges,
1171 end_inner_wires_smooth,
1172 &ring_positions[num_rings - 1],
1173 end_outward,
1174 false,
1175 )?);
1176
1177 let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
1178 let shell_id = topo.add_shell(shell);
1179 Ok(topo.add_solid(Solid::new(shell_id, vec![])))
1180}
1181
1182#[derive(Debug, Clone, Copy, Default)]
1186pub enum SweepContactMode {
1187 #[default]
1189 RotationMinimizing,
1190 Fixed,
1192 ConstantNormal(Vec3),
1194}
1195
1196#[derive(Debug, Clone, Copy, Default)]
1201pub enum SweepCornerMode {
1202 #[default]
1205 Smooth,
1206 Miter,
1211 Round,
1214}
1215
1216#[derive(Default)]
1218pub struct SweepOptions {
1219 pub contact_mode: SweepContactMode,
1221 pub corner_mode: SweepCornerMode,
1223 pub scale_law: Option<Box<dyn Fn(f64) -> f64 + Send + Sync>>,
1226 pub segments: usize,
1228 pub aux_spine: Option<NurbsCurve>,
1233}
1234
1235impl std::fmt::Debug for SweepOptions {
1236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1237 f.debug_struct("SweepOptions")
1238 .field("contact_mode", &self.contact_mode)
1239 .field("corner_mode", &self.corner_mode)
1240 .field(
1241 "scale_law",
1242 &self.scale_law.as_ref().map(|_| "fn(f64)->f64"),
1243 )
1244 .field("segments", &self.segments)
1245 .field("aux_spine", &self.aux_spine.as_ref().map(|_| "NurbsCurve"))
1246 .finish()
1247 }
1248}
1249
1250#[allow(clippy::too_many_lines)]
1258pub fn sweep_with_options(
1259 topo: &mut Topology,
1260 profile: FaceId,
1261 path: &NurbsCurve,
1262 options: &SweepOptions,
1263) -> Result<SolidId, crate::OperationsError> {
1264 let tol = Tolerance::new();
1265
1266 if path.control_points().len() < 2 {
1267 return Err(crate::OperationsError::InvalidInput {
1268 reason: "sweep path must have at least 2 control points".into(),
1269 });
1270 }
1271
1272 if matches!(options.corner_mode, SweepCornerMode::Miter) && !detect_kinks(path).is_empty() {
1279 return sweep_miter(topo, profile, path, options);
1280 }
1281
1282 let face_data = topo.face(profile)?;
1283 let input_wire_id = face_data.outer_wire();
1284 let inner_wire_ids_opts: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
1285
1286 let start_end_coincide_opts = tol.approx_eq(
1289 (path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
1290 0.0,
1291 );
1292 if start_end_coincide_opts {
1293 let mid = path.evaluate(0.5);
1294 let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
1295 if tol.approx_eq(mid_dist_sq, 0.0) {
1296 return Err(crate::OperationsError::InvalidInput {
1297 reason: "sweep path has zero length (start and end coincide)".into(),
1298 });
1299 }
1300 return sweep(topo, profile, path);
1301 }
1302
1303 if options.scale_law.is_none()
1308 && options.aux_spine.is_none()
1309 && let Some(solid) =
1310 try_straight_extrude(topo, profile, path, ProfilePlacement::AsPositioned)?
1311 {
1312 return Ok(solid);
1313 }
1314
1315 let input_wire = topo.wire(input_wire_id)?;
1316 let original_oriented: Vec<_> = input_wire.edges().to_vec();
1317
1318 if original_oriented.is_empty() {
1319 return Err(crate::OperationsError::InvalidInput {
1320 reason: "sweep profile has no edges".into(),
1321 });
1322 }
1323
1324 let input_oriented = crate::extrude::maybe_split_closed_wire(
1325 topo,
1326 &original_oriented,
1327 tol.linear,
1328 crate::extrude::DEFAULT_DEFLECTION,
1329 )?;
1330 let n = input_oriented.len();
1331
1332 let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
1333 for oe in &input_oriented {
1334 let edge = topo.edge(oe.edge())?;
1335 let vid = oe.oriented_start(edge);
1336 input_verts.push(vid);
1337 }
1338
1339 let mut input_positions: Vec<Point3> = input_verts
1340 .iter()
1341 .map(|&vid| {
1342 topo.vertex(vid)
1343 .map(brepkit_topology::vertex::Vertex::point)
1344 })
1345 .collect::<Result<_, _>>()?;
1346
1347 let mut input_normal = crate::winding::newell_normal(&input_positions)
1350 .normalize()
1351 .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
1352
1353 let path_tangent_0 = path.tangent(0.0)?;
1355 if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
1356 input_normal = -input_normal;
1357 }
1358
1359 let num_segments = if options.segments > 0 {
1360 options.segments
1361 } else {
1362 (path.control_points().len() * 2).max(4)
1363 };
1364
1365 let frames: Vec<Frame> = if let Some(aux) = options.aux_spine.as_ref() {
1367 let mut out = Vec::with_capacity(num_segments + 1);
1371 let mut prev_up: Option<Vec3> = None;
1372 for k in 0..=num_segments {
1373 #[allow(clippy::cast_precision_loss)]
1374 let t = k as f64 / num_segments as f64;
1375 let origin = path.evaluate(t);
1376 let tangent = path.tangent(t).unwrap_or(path_tangent_0);
1377 let guide_dir = aux.evaluate(t) - origin;
1378 let up = if guide_dir.length() < 1e-9 {
1382 let seed = prev_up.unwrap_or_else(|| pick_reference_axis(tangent));
1383 orthogonalize(seed, tangent)
1384 } else {
1385 orthogonalize(guide_dir, tangent)
1386 };
1387 prev_up = Some(up);
1388 out.push(Frame {
1389 origin,
1390 tangent,
1391 up,
1392 right: tangent.cross(up),
1393 });
1394 }
1395 out
1396 } else {
1397 match options.contact_mode {
1398 SweepContactMode::RotationMinimizing => {
1399 let up_hint = orthogonalize(input_normal, path_tangent_0);
1400 compute_frames(path, num_segments, up_hint, false)?
1401 }
1402 SweepContactMode::Fixed => {
1403 let tangent0 = path_tangent_0;
1405 let up = orthogonalize(input_normal, tangent0);
1406 let right = tangent0.cross(up);
1407
1408 (0..=num_segments)
1409 .map(|k| {
1410 #[allow(clippy::cast_precision_loss)]
1411 let t = k as f64 / num_segments as f64;
1412 Frame {
1413 origin: path.evaluate(t),
1414 tangent: path.tangent(t).unwrap_or(tangent0),
1415 up,
1416 right,
1417 }
1418 })
1419 .collect()
1420 }
1421 SweepContactMode::ConstantNormal(normal_dir) => {
1422 (0..=num_segments)
1424 .map(|k| {
1425 #[allow(clippy::cast_precision_loss)]
1426 let t = k as f64 / num_segments as f64;
1427 let tangent = path.tangent(t).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
1428 let up = orthogonalize(normal_dir, tangent);
1429 let right = tangent.cross(up);
1430 Frame {
1431 origin: path.evaluate(t),
1432 tangent,
1433 up,
1434 right,
1435 }
1436 })
1437 .collect()
1438 }
1439 }
1440 };
1441
1442 let (reference, initial_right, initial_up, initial_tangent) =
1446 match resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0) {
1447 ProfilePlacement::AsPositioned => (
1448 frames[0].origin,
1449 frames[0].right,
1450 frames[0].up,
1451 frames[0].tangent,
1452 ),
1453 ProfilePlacement::CentroidOnPath => {
1454 let (r, u, t) = profile_basis(input_normal);
1455 (crate::winding::polygon_centroid(&input_positions), r, u, t)
1456 }
1457 };
1458
1459 let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
1460
1461 for (k, frame) in frames.iter().enumerate() {
1462 #[allow(clippy::cast_precision_loss)]
1463 let t = k as f64 / num_segments as f64;
1464 let scale = options.scale_law.as_ref().map_or(1.0, |law| law(t));
1465
1466 let ring: Vec<VertexId> = input_positions
1467 .iter()
1468 .map(|&pos| {
1469 let mut transformed = transform_point(
1470 pos,
1471 reference,
1472 initial_right,
1473 initial_up,
1474 initial_tangent,
1475 frame,
1476 );
1477 if (scale - 1.0).abs() > tol.linear {
1479 let offset = transformed - frame.origin;
1480 transformed = frame.origin
1481 + Vec3::new(offset.x() * scale, offset.y() * scale, offset.z() * scale);
1482 }
1483 topo.add_vertex(Vertex::new(transformed, tol.linear))
1484 })
1485 .collect();
1486 ring_verts.push(ring);
1487 }
1488
1489 let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
1491 Vec::with_capacity(num_segments + 1);
1492 for ring in &ring_verts {
1493 let edges: Vec<_> = (0..n)
1494 .map(|i| {
1495 let next = (i + 1) % n;
1496 topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
1497 })
1498 .collect();
1499 ring_edges.push(edges);
1500 }
1501
1502 let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
1503 for seg in 0..num_segments {
1504 let edges: Vec<_> = (0..n)
1505 .map(|i| {
1506 topo.add_edge(Edge::new(
1507 ring_verts[seg][i],
1508 ring_verts[seg + 1][i],
1509 EdgeCurve::Line,
1510 ))
1511 })
1512 .collect();
1513 path_edges.push(edges);
1514 }
1515
1516 let mut inner_swept_opts: Vec<SweptWireData> = Vec::new();
1518 for &iw_id in &inner_wire_ids_opts {
1519 inner_swept_opts.push(sweep_wire_through_frames(
1520 topo,
1521 iw_id,
1522 reference,
1523 initial_right,
1524 initial_up,
1525 initial_tangent,
1526 &frames,
1527 num_segments,
1528 false,
1529 )?);
1530 }
1531
1532 let mut all_faces = Vec::with_capacity(num_segments * n + 2);
1533
1534 let start_inner_wires_opts = build_inner_cap_wires(topo, &inner_swept_opts, 0, true)?;
1535 let start_verts = crate::cap::ring_point_positions(topo, &ring_verts[0])?;
1536 let start_outward = crate::cap::outward_normal(&start_verts, -frames[0].tangent)?;
1537 all_faces.push(crate::cap::build_cap_face(
1538 topo,
1539 &ring_edges[0],
1540 start_inner_wires_opts,
1541 &start_verts,
1542 start_outward,
1543 true,
1544 )?);
1545
1546 for seg in 0..num_segments {
1547 for i in 0..n {
1548 let next_i = (i + 1) % n;
1549 let p0 = topo.vertex(ring_verts[seg][i])?.point();
1550 let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
1551 let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
1552 let edge_dir = p1 - p0;
1553 let path_dir = p_next - p0;
1554 let side_normal = edge_dir
1555 .cross(path_dir)
1556 .normalize()
1557 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
1558 let side_d = dot_normal_point(side_normal, p0);
1559
1560 let side_wire = Wire::new(
1561 vec![
1562 OrientedEdge::new(ring_edges[seg][i], true),
1563 OrientedEdge::new(path_edges[seg][next_i], true),
1564 OrientedEdge::new(ring_edges[seg + 1][i], false),
1565 OrientedEdge::new(path_edges[seg][i], false),
1566 ],
1567 true,
1568 )
1569 .map_err(crate::OperationsError::Topology)?;
1570
1571 let side_wire_id = topo.add_wire(side_wire);
1572 let side_face = topo.add_face(Face::new(
1573 side_wire_id,
1574 vec![],
1575 FaceSurface::Plane {
1576 normal: side_normal,
1577 d: side_d,
1578 },
1579 ));
1580 all_faces.push(side_face);
1581 }
1582 }
1583
1584 for iwd in &inner_swept_opts {
1585 let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
1586 all_faces.extend(inner_faces);
1587 }
1588
1589 let end_inner_wires_opts = build_inner_cap_wires(topo, &inner_swept_opts, num_segments, false)?;
1590 let end_verts = crate::cap::ring_point_positions(topo, &ring_verts[num_segments])?;
1591 let end_outward = crate::cap::outward_normal(&end_verts, frames[num_segments].tangent)?;
1592 all_faces.push(crate::cap::build_cap_face(
1593 topo,
1594 &ring_edges[num_segments],
1595 end_inner_wires_opts,
1596 &end_verts,
1597 end_outward,
1598 false,
1599 )?);
1600
1601 let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
1602 let shell_id = topo.add_shell(shell);
1603 Ok(topo.add_solid(Solid::new(shell_id, vec![])))
1604}
1605
1606fn detect_kinks(path: &NurbsCurve) -> Vec<f64> {
1615 const KNOT_EPS: f64 = 1e-10;
1617 const KINK_ANGLE_RAD: f64 = 0.0175;
1619
1620 let p = path.degree();
1621 let knots = path.knots();
1622 let (u_min, u_max) = path.domain();
1623
1624 let mut kinks = Vec::new();
1625 let mut i = 0;
1626 while i < knots.len() {
1627 let u = knots[i];
1628
1629 if u <= u_min + KNOT_EPS || u >= u_max - KNOT_EPS {
1630 i += 1;
1631 continue;
1632 }
1633
1634 let mut mult = 1;
1635 while i + mult < knots.len() && (knots[i + mult] - u).abs() < KNOT_EPS {
1636 mult += 1;
1637 }
1638
1639 if mult >= p {
1644 let eps = 1e-8;
1647 if let (Ok(t_before), Ok(t_after)) = (path.tangent(u - eps), path.tangent(u + eps)) {
1648 let dot = t_before.dot(t_after).clamp(-1.0, 1.0);
1649 let angle = dot.acos();
1652 if angle > KINK_ANGLE_RAD {
1653 kinks.push(u);
1654 }
1655 }
1656 }
1657
1658 i += mult;
1659 }
1660
1661 kinks
1662}
1663
1664#[allow(clippy::too_many_lines)]
1675fn sweep_miter(
1676 topo: &mut Topology,
1677 profile: FaceId,
1678 path: &NurbsCurve,
1679 options: &SweepOptions,
1680) -> Result<SolidId, crate::OperationsError> {
1681 use brepkit_math::nurbs::knot_ops::curve_split;
1682
1683 let tol = Tolerance::new();
1684
1685 let kinks = detect_kinks(path);
1688 debug_assert!(
1689 !kinks.is_empty(),
1690 "sweep_miter should only be called when the path has kinks"
1691 );
1692
1693 let face_data = topo.face(profile)?;
1694 let mut input_normal = match face_data.surface() {
1695 FaceSurface::Plane { normal, .. } => *normal,
1696 _ => {
1697 return Err(crate::OperationsError::InvalidInput {
1698 reason: "sweep of non-planar faces is not supported".into(),
1699 });
1700 }
1701 };
1702 let input_wire_id = face_data.outer_wire();
1703 let inner_wire_ids: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
1704
1705 let input_wire = topo.wire(input_wire_id)?;
1706 let original_oriented: Vec<_> = input_wire.edges().to_vec();
1707 if original_oriented.is_empty() {
1708 return Err(crate::OperationsError::InvalidInput {
1709 reason: "sweep profile has no edges".into(),
1710 });
1711 }
1712 let input_oriented = crate::extrude::maybe_split_closed_wire(
1713 topo,
1714 &original_oriented,
1715 tol.linear,
1716 crate::extrude::DEFAULT_DEFLECTION,
1717 )?;
1718 let n = input_oriented.len();
1719
1720 let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
1721 for oe in &input_oriented {
1722 let edge = topo.edge(oe.edge())?;
1723 let vid = oe.oriented_start(edge);
1724 input_verts.push(vid);
1725 }
1726 let mut input_positions: Vec<Point3> = input_verts
1727 .iter()
1728 .map(|&vid| {
1729 topo.vertex(vid)
1730 .map(brepkit_topology::vertex::Vertex::point)
1731 })
1732 .collect::<Result<_, _>>()?;
1733
1734 let (domain_start, _domain_end) = path.domain();
1736 let path_tangent_0 = path.tangent(domain_start)?;
1737 if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
1738 input_normal = -input_normal;
1739 }
1740
1741 let placement = resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0);
1748 let reference = match placement {
1749 ProfilePlacement::AsPositioned => path.evaluate(domain_start),
1750 ProfilePlacement::CentroidOnPath => crate::winding::polygon_centroid(&input_positions),
1751 };
1752 let mut global_basis: Option<(Vec3, Vec3, Vec3)> = None;
1753
1754 let mut sub_paths: Vec<NurbsCurve> = Vec::with_capacity(kinks.len() + 1);
1756 let mut remaining = path.clone();
1757 let mut offset = domain_start;
1758
1759 for &kink_u in &kinks {
1760 let split_u = kink_u - offset + remaining.domain().0;
1764 let (left, right) = curve_split(&remaining, split_u)?;
1765 sub_paths.push(left);
1766 offset = kink_u;
1767 remaining = right;
1768 }
1769 sub_paths.push(remaining);
1770
1771 let mut all_faces: Vec<FaceId> = Vec::new();
1772
1773 let mut prev_end_ring: Option<Vec<VertexId>> = None;
1776 let mut prev_end_ring_edges: Option<Vec<brepkit_topology::edge::EdgeId>> = None;
1777 let mut prev_end_on_bisector = false;
1778 let mut prev_up: Option<(Vec3, Vec3)> = None; for (seg_idx, sub_path) in sub_paths.iter().enumerate() {
1781 let is_first = seg_idx == 0;
1782 let is_last = seg_idx == sub_paths.len() - 1;
1783
1784 let sub_tangent_0 = sub_path.tangent(sub_path.domain().0)?;
1785 let up_hint = match prev_up {
1793 None => orthogonalize(input_normal, sub_tangent_0),
1794 Some((up_prev, t_prev)) => {
1795 let cross = t_prev.cross(sub_tangent_0);
1796 let transported = match cross.normalize() {
1797 Ok(axis) => {
1798 let angle = t_prev.dot(sub_tangent_0).clamp(-1.0, 1.0).acos();
1799 crate::revolve::rotate_vec(up_prev, axis, angle)
1800 }
1801 Err(_) => up_prev, };
1803 orthogonalize(transported, sub_tangent_0)
1804 }
1805 };
1806
1807 let num_segments = if options.segments > 0 {
1808 options.segments
1809 } else {
1810 (sub_path.control_points().len() * 2).max(4)
1811 };
1812
1813 let sub_frames = match options.contact_mode {
1814 SweepContactMode::RotationMinimizing => {
1815 compute_frames(sub_path, num_segments, up_hint, false)?
1816 }
1817 SweepContactMode::Fixed => {
1818 let up = orthogonalize(input_normal, sub_tangent_0);
1819 let right = sub_tangent_0.cross(up);
1820 (0..=num_segments)
1821 .map(|k| {
1822 let (u0, u1) = sub_path.domain();
1823 #[allow(clippy::cast_precision_loss)]
1824 let t = u0 + (u1 - u0) * (k as f64 / num_segments as f64);
1825 Frame {
1826 origin: sub_path.evaluate(t),
1827 tangent: sub_path.tangent(t).unwrap_or(sub_tangent_0),
1828 up,
1829 right,
1830 }
1831 })
1832 .collect()
1833 }
1834 SweepContactMode::ConstantNormal(normal_dir) => (0..=num_segments)
1835 .map(|k| {
1836 let (u0, u1) = sub_path.domain();
1837 #[allow(clippy::cast_precision_loss)]
1838 let t = u0 + (u1 - u0) * (k as f64 / num_segments as f64);
1839 let tangent = sub_path.tangent(t).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
1840 let up = orthogonalize(normal_dir, tangent);
1841 let right = tangent.cross(up);
1842 Frame {
1843 origin: sub_path.evaluate(t),
1844 tangent,
1845 up,
1846 right,
1847 }
1848 })
1849 .collect(),
1850 };
1851
1852 if global_basis.is_none() {
1853 global_basis = Some((sub_frames[0].right, sub_frames[0].up, sub_frames[0].tangent));
1854 }
1855 if let Some(last) = sub_frames.last() {
1856 prev_up = Some((last.up, last.tangent));
1857 }
1858 let (initial_right, initial_up, initial_tangent) = match (placement, global_basis) {
1859 (ProfilePlacement::AsPositioned, Some((r, u, t))) => (r, u, t),
1860 _ => (sub_frames[0].right, sub_frames[0].up, sub_frames[0].tangent),
1861 };
1862
1863 let mut ring_positions: Vec<Vec<Point3>> = sub_frames
1866 .iter()
1867 .map(|frame| {
1868 input_positions
1869 .iter()
1870 .map(|&pos| {
1871 transform_point(
1872 pos,
1873 reference,
1874 initial_right,
1875 initial_up,
1876 initial_tangent,
1877 frame,
1878 )
1879 })
1880 .collect()
1881 })
1882 .collect();
1883
1884 let mut exit_on_bisector = false;
1894 if !is_last && matches!(placement, ProfilePlacement::AsPositioned) {
1895 let kink_u = kinks[seg_idx];
1896 let eps = 1e-8;
1897 let t_b = path.tangent(kink_u - eps)?;
1898 let t_a = path.tangent(kink_u + eps)?;
1899 if let Ok(bisector) = (t_b + t_a).normalize() {
1900 let kink_point = path.evaluate(kink_u);
1901 let t_end = sub_frames[num_segments].tangent;
1902 let denom = bisector.dot(t_end);
1903 let spacing = (sub_frames[num_segments].origin
1904 - sub_frames[num_segments - 1].origin)
1905 .length();
1906 if denom.abs() > 0.1 {
1907 let slid: Vec<Point3> = ring_positions[num_segments]
1908 .iter()
1909 .map(|&p| p + t_end * (bisector.dot(kink_point - p) / denom))
1910 .collect();
1911 let max_slide = ring_positions[num_segments]
1912 .iter()
1913 .zip(&slid)
1914 .map(|(&p, &q)| (q - p).length())
1915 .fold(0.0_f64, f64::max);
1916 if max_slide < spacing * 0.95 {
1917 ring_positions[num_segments] = slid;
1918 exit_on_bisector = true;
1919 }
1920 }
1921 }
1922 }
1923
1924 let entry_shared = match (&prev_end_ring, prev_end_on_bisector) {
1928 (Some(prev_ring), true) => {
1929 let t_start = sub_frames[0].tangent;
1930 let mut ahead = true;
1931 for (i, &vid) in prev_ring.iter().enumerate() {
1932 let p_prev = topo.vertex(vid)?.point();
1933 if (ring_positions[1][i] - p_prev).dot(t_start) <= tol.linear {
1934 ahead = false;
1935 break;
1936 }
1937 }
1938 ahead
1939 }
1940 _ => false,
1941 };
1942
1943 let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
1944 for (ring_idx, positions) in ring_positions.iter().enumerate() {
1945 if let (0, true, Some(prev_ring)) = (ring_idx, entry_shared, prev_end_ring.as_ref()) {
1946 ring_verts.push(prev_ring.clone());
1947 continue;
1948 }
1949 let ring: Vec<VertexId> = positions
1950 .iter()
1951 .map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
1952 .collect();
1953 ring_verts.push(ring);
1954 }
1955
1956 #[allow(clippy::useless_let_if_seq)]
1960 let mut miter_ring_edges_for_reuse: Option<Vec<brepkit_topology::edge::EdgeId>> = None;
1961 if entry_shared {
1962 miter_ring_edges_for_reuse.clone_from(&prev_end_ring_edges);
1963 } else if let Some(ref prev_ring) = prev_end_ring {
1964 let kink_idx = seg_idx - 1;
1966 let kink_u = kinks[kink_idx];
1967 let eps = 1e-8;
1968
1969 let t_before = path.tangent(kink_u - eps)?;
1971 let t_after = path.tangent(kink_u + eps)?;
1972
1973 let bisector = (t_before + t_after).normalize().unwrap_or(t_before);
1975
1976 let kink_point = path.evaluate(kink_u);
1978
1979 let miter_ring: Vec<VertexId> = (0..n)
1984 .map(|i| {
1985 let prev_pos = topo
1986 .vertex(prev_ring[i])
1987 .map(brepkit_topology::vertex::Vertex::point)
1988 .unwrap_or(kink_point);
1989 let curr_pos = topo
1990 .vertex(ring_verts[0][i])
1991 .map(brepkit_topology::vertex::Vertex::point)
1992 .unwrap_or(kink_point);
1993
1994 let ray_dir = curr_pos - prev_pos;
1997 let denom = bisector.dot(ray_dir);
1998 let miter_pos = if denom.abs() > tol.linear {
1999 let d = bisector.dot(Vec3::new(
2000 kink_point.x() - prev_pos.x(),
2001 kink_point.y() - prev_pos.y(),
2002 kink_point.z() - prev_pos.z(),
2003 ));
2004 let t_intersect = d / denom;
2005 prev_pos + ray_dir * t_intersect
2006 } else {
2007 Point3::new(
2009 (prev_pos.x() + curr_pos.x()) * 0.5,
2010 (prev_pos.y() + curr_pos.y()) * 0.5,
2011 (prev_pos.z() + curr_pos.z()) * 0.5,
2012 )
2013 };
2014 topo.add_vertex(Vertex::new(miter_pos, tol.linear))
2015 })
2016 .collect();
2017
2018 let miter_ring_edges: Vec<brepkit_topology::edge::EdgeId> = (0..n)
2019 .map(|i| {
2020 let next = (i + 1) % n;
2021 topo.add_edge(Edge::new(miter_ring[i], miter_ring[next], EdgeCurve::Line))
2022 })
2023 .collect();
2024
2025 let prev_ring_edges = prev_end_ring_edges.as_ref().ok_or_else(|| {
2028 crate::OperationsError::InvalidInput {
2029 reason: "internal error: missing previous ring edges".into(),
2030 }
2031 })?;
2032
2033 let prev_to_miter_path_edges: Vec<brepkit_topology::edge::EdgeId> = (0..n)
2034 .map(|i| topo.add_edge(Edge::new(prev_ring[i], miter_ring[i], EdgeCurve::Line)))
2035 .collect();
2036
2037 for i in 0..n {
2038 let next_i = (i + 1) % n;
2039
2040 let p0 = topo.vertex(prev_ring[i])?.point();
2041 let p1 = topo.vertex(prev_ring[next_i])?.point();
2042 let p_next = topo.vertex(miter_ring[i])?.point();
2043 let edge_dir = p1 - p0;
2044 let path_dir = p_next - p0;
2045 let side_normal = edge_dir
2046 .cross(path_dir)
2047 .normalize()
2048 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
2049 let side_d = dot_normal_point(side_normal, p0);
2050
2051 let side_wire = Wire::new(
2052 vec![
2053 OrientedEdge::new(prev_ring_edges[i], true),
2054 OrientedEdge::new(prev_to_miter_path_edges[next_i], true),
2055 OrientedEdge::new(miter_ring_edges[i], false),
2056 OrientedEdge::new(prev_to_miter_path_edges[i], false),
2057 ],
2058 true,
2059 )
2060 .map_err(crate::OperationsError::Topology)?;
2061
2062 let side_wire_id = topo.add_wire(side_wire);
2063 all_faces.push(topo.add_face(Face::new(
2064 side_wire_id,
2065 vec![],
2066 FaceSurface::Plane {
2067 normal: side_normal,
2068 d: side_d,
2069 },
2070 )));
2071 }
2072
2073 ring_verts[0] = miter_ring;
2078 miter_ring_edges_for_reuse = Some(miter_ring_edges);
2079 }
2080
2081 let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
2085 Vec::with_capacity(num_segments + 1);
2086 for (ring_idx, ring) in ring_verts.iter().enumerate() {
2087 if ring_idx == 0 {
2088 if let Some(ref reused) = miter_ring_edges_for_reuse {
2089 ring_edges.push(reused.clone());
2090 } else {
2091 let edges: Vec<_> = (0..n)
2092 .map(|i| {
2093 let next = (i + 1) % n;
2094 topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
2095 })
2096 .collect();
2097 ring_edges.push(edges);
2098 }
2099 } else {
2100 let edges: Vec<_> = (0..n)
2101 .map(|i| {
2102 let next = (i + 1) % n;
2103 topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
2104 })
2105 .collect();
2106 ring_edges.push(edges);
2107 }
2108 }
2109
2110 let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
2111 Vec::with_capacity(num_segments);
2112 for seg in 0..num_segments {
2113 let edges: Vec<_> = (0..n)
2114 .map(|i| {
2115 topo.add_edge(Edge::new(
2116 ring_verts[seg][i],
2117 ring_verts[seg + 1][i],
2118 EdgeCurve::Line,
2119 ))
2120 })
2121 .collect();
2122 path_edges.push(edges);
2123 }
2124
2125 let mut inner_swept: Vec<SweptWireData> = Vec::new();
2126 for &iw_id in &inner_wire_ids {
2127 inner_swept.push(sweep_wire_through_frames(
2128 topo,
2129 iw_id,
2130 reference,
2131 initial_right,
2132 initial_up,
2133 initial_tangent,
2134 &sub_frames,
2135 num_segments,
2136 false,
2137 )?);
2138 }
2139
2140 if is_first {
2142 let start_reversed_edges: Vec<OrientedEdge> = (0..n)
2143 .rev()
2144 .map(|i| OrientedEdge::new(ring_edges[0][i], false))
2145 .collect();
2146 let start_wire =
2147 Wire::new(start_reversed_edges, true).map_err(crate::OperationsError::Topology)?;
2148 let start_wire_id = topo.add_wire(start_wire);
2149 let start_inner_wires = build_inner_cap_wires(topo, &inner_swept, 0, true)?;
2150
2151 let start_normal = -sub_frames[0].tangent;
2152 let start_d = dot_normal_point(start_normal, topo.vertex(ring_verts[0][0])?.point());
2153 all_faces.push(topo.add_face(Face::new(
2154 start_wire_id,
2155 start_inner_wires,
2156 FaceSurface::Plane {
2157 normal: start_normal,
2158 d: start_d,
2159 },
2160 )));
2161 }
2162
2163 for seg in 0..num_segments {
2164 for i in 0..n {
2165 let next_i = (i + 1) % n;
2166 let p0 = topo.vertex(ring_verts[seg][i])?.point();
2167 let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
2168 let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
2169 let edge_dir = p1 - p0;
2170 let path_dir = p_next - p0;
2171 let side_normal = edge_dir
2172 .cross(path_dir)
2173 .normalize()
2174 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
2175 let side_d = dot_normal_point(side_normal, p0);
2176
2177 let side_wire = Wire::new(
2178 vec![
2179 OrientedEdge::new(ring_edges[seg][i], true),
2180 OrientedEdge::new(path_edges[seg][next_i], true),
2181 OrientedEdge::new(ring_edges[seg + 1][i], false),
2182 OrientedEdge::new(path_edges[seg][i], false),
2183 ],
2184 true,
2185 )
2186 .map_err(crate::OperationsError::Topology)?;
2187
2188 let side_wire_id = topo.add_wire(side_wire);
2189 all_faces.push(topo.add_face(Face::new(
2190 side_wire_id,
2191 vec![],
2192 FaceSurface::Plane {
2193 normal: side_normal,
2194 d: side_d,
2195 },
2196 )));
2197 }
2198 }
2199
2200 for iwd in &inner_swept {
2201 let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
2202 all_faces.extend(inner_faces);
2203 }
2204
2205 if is_last {
2207 let end_edges: Vec<OrientedEdge> = (0..n)
2208 .map(|i| OrientedEdge::new(ring_edges[num_segments][i], true))
2209 .collect();
2210 let end_wire = Wire::new(end_edges, true).map_err(crate::OperationsError::Topology)?;
2211 let end_wire_id = topo.add_wire(end_wire);
2212 let end_inner_wires = build_inner_cap_wires(topo, &inner_swept, num_segments, false)?;
2213
2214 let end_normal = sub_frames[num_segments].tangent;
2215 let end_d = dot_normal_point(
2216 end_normal,
2217 topo.vertex(ring_verts[num_segments][0])?.point(),
2218 );
2219 all_faces.push(topo.add_face(Face::new(
2220 end_wire_id,
2221 end_inner_wires,
2222 FaceSurface::Plane {
2223 normal: end_normal,
2224 d: end_d,
2225 },
2226 )));
2227 }
2228
2229 prev_end_ring = Some(ring_verts[num_segments].clone());
2231 prev_end_ring_edges = Some(ring_edges[num_segments].clone());
2232 prev_end_on_bisector = exit_on_bisector;
2233 }
2234
2235 let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
2236 let shell_id = topo.add_shell(shell);
2237 Ok(topo.add_solid(Solid::new(shell_id, vec![])))
2238}
2239
2240pub fn multi_section_sweep(
2259 topo: &mut Topology,
2260 spine: &NurbsCurve,
2261 sections: &[(FaceId, f64)],
2262 ruled: bool,
2263) -> Result<SolidId, crate::OperationsError> {
2264 if sections.len() < 2 {
2265 return Err(crate::OperationsError::InvalidInput {
2266 reason: format!(
2267 "multi-section sweep requires at least 2 sections, got {}",
2268 sections.len()
2269 ),
2270 });
2271 }
2272 if spine.control_points().len() < 2 {
2273 return Err(crate::OperationsError::InvalidInput {
2274 reason: "multi-section sweep spine must have at least 2 control points".into(),
2275 });
2276 }
2277 for &(_, p) in sections {
2278 if !(0.0..=1.0).contains(&p) {
2279 return Err(crate::OperationsError::InvalidInput {
2280 reason: format!("section parameter {p} is outside [0, 1]"),
2281 });
2282 }
2283 }
2284
2285 let dense_segments: usize = 64;
2288 let t_start = spine.tangent(0.0)?;
2289 let initial_up = orthogonalize(pick_reference_axis(t_start), t_start);
2290 let dense = compute_frames(spine, dense_segments, initial_up, false)?;
2291
2292 let mut ordered: Vec<(FaceId, f64)> = sections.to_vec();
2295 ordered.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
2296
2297 let mut placed: Vec<FaceId> = Vec::with_capacity(ordered.len());
2298 for (face_id, p) in ordered {
2299 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2300 let idx = ((p * dense_segments as f64).round() as usize).min(dense_segments);
2301 let tangent = spine.tangent(p)?;
2302 let up = orthogonalize(dense[idx].up, tangent);
2303 let frame = Frame {
2304 origin: spine.evaluate(p),
2305 tangent,
2306 up,
2307 right: tangent.cross(up),
2308 };
2309 let mat = profile_to_frame_matrix(topo, face_id, &frame)?;
2310 let placed_face = crate::copy::copy_face(topo, face_id)?;
2311 crate::transform::transform_face(topo, placed_face, &mat)?;
2312 placed.push(placed_face);
2313 }
2314
2315 if ruled {
2316 crate::loft::loft(topo, &placed)
2317 } else {
2318 crate::loft::loft_smooth(topo, &placed)
2319 }
2320}
2321
2322fn pick_reference_axis(dir: Vec3) -> Vec3 {
2324 if dir.x().abs() < 0.9 {
2325 Vec3::new(1.0, 0.0, 0.0)
2326 } else {
2327 Vec3::new(0.0, 1.0, 0.0)
2328 }
2329}
2330
2331fn profile_to_frame_matrix(
2336 topo: &Topology,
2337 face_id: FaceId,
2338 frame: &Frame,
2339) -> Result<Mat4, crate::OperationsError> {
2340 let boundary = crate::boolean::face_polygon(topo, face_id)?;
2344 if boundary.is_empty() {
2345 return Err(crate::OperationsError::InvalidInput {
2346 reason: "multi-section sweep profile has no boundary".into(),
2347 });
2348 }
2349 let normal = match topo.face(face_id)?.surface() {
2354 FaceSurface::Plane { normal, .. } => normal.normalize().unwrap_or(*normal),
2355 _ => crate::winding::newell_normal(&boundary)
2356 .normalize()
2357 .unwrap_or(Vec3::new(0.0, 0.0, 1.0)),
2358 };
2359 let centroid = crate::winding::polygon_centroid(&boundary);
2360
2361 let p_x = orthogonalize(pick_reference_axis(normal), normal);
2367 let p_y = p_x.cross(normal);
2368
2369 let t_cols = [
2372 [frame.right.x(), frame.up.x(), frame.tangent.x()],
2373 [frame.right.y(), frame.up.y(), frame.tangent.y()],
2374 [frame.right.z(), frame.up.z(), frame.tangent.z()],
2375 ];
2376 let l_cols = [
2377 [p_x.x(), p_y.x(), normal.x()],
2378 [p_x.y(), p_y.y(), normal.y()],
2379 [p_x.z(), p_y.z(), normal.z()],
2380 ];
2381 let mut rot = [[0.0_f64; 3]; 3];
2382 for i in 0..3 {
2383 for j in 0..3 {
2384 rot[i][j] = (0..3).map(|k| t_cols[i][k] * l_cols[j][k]).sum();
2385 }
2386 }
2387
2388 let c = [centroid.x(), centroid.y(), centroid.z()];
2390 let o = [frame.origin.x(), frame.origin.y(), frame.origin.z()];
2391 let trans: [f64; 3] =
2392 std::array::from_fn(|i| o[i] - (0..3).map(|k| rot[i][k] * c[k]).sum::<f64>());
2393
2394 Ok(Mat4([
2395 [rot[0][0], rot[0][1], rot[0][2], trans[0]],
2396 [rot[1][0], rot[1][1], rot[1][2], trans[1]],
2397 [rot[2][0], rot[2][1], rot[2][2], trans[2]],
2398 [0.0, 0.0, 0.0, 1.0],
2399 ]))
2400}
2401
2402pub fn sweep_guided(
2414 topo: &mut Topology,
2415 profile: FaceId,
2416 spine: &NurbsCurve,
2417 aux: NurbsCurve,
2418) -> Result<SolidId, crate::OperationsError> {
2419 sweep_with_options(
2420 topo,
2421 profile,
2422 spine,
2423 &SweepOptions {
2424 aux_spine: Some(aux),
2425 ..Default::default()
2426 },
2427 )
2428}
2429
2430mod spine;
2431
2432pub fn sweep_along_edges(
2445 topo: &mut Topology,
2446 profile: FaceId,
2447 edges: &[brepkit_topology::edge::EdgeId],
2448) -> Result<SolidId, crate::OperationsError> {
2449 if edges.is_empty() {
2450 return Err(crate::OperationsError::InvalidInput {
2451 reason: "sweep_along_edges requires at least one edge".into(),
2452 });
2453 }
2454
2455 if let Some(solid) = spine::try_analytic_spine_sweep(topo, profile, edges)? {
2456 return Ok(solid);
2457 }
2458
2459 let tol = Tolerance::new();
2460
2461 let mut points: Vec<Point3> = Vec::new();
2463 for &eid in edges {
2464 let edge_data = topo.edge(eid)?;
2465 let start = topo.vertex(edge_data.start())?.point();
2466 if points
2467 .last()
2468 .is_none_or(|p: &Point3| (*p - start).length() > tol.linear)
2469 {
2470 points.push(start);
2471 }
2472
2473 match edge_data.curve() {
2474 EdgeCurve::NurbsCurve(curve) => {
2475 let (u0, u1) = curve.domain();
2476 let n_samples = 4;
2477 for i in 1..n_samples {
2478 #[allow(clippy::cast_precision_loss)]
2479 let frac = i as f64 / n_samples as f64;
2480 points.push(curve.evaluate(u0 + frac * (u1 - u0)));
2481 }
2482 }
2483 EdgeCurve::Circle(circle) => {
2484 let t_start = circle.project(start);
2485 let end_pt = topo.vertex(edge_data.end())?.point();
2486 let mut t_end = circle.project(end_pt);
2487 if t_end <= t_start {
2488 t_end += std::f64::consts::TAU;
2489 }
2490 let n_samples = 8;
2491 for i in 1..n_samples {
2492 #[allow(clippy::cast_precision_loss)]
2493 let t = t_start + (t_end - t_start) * (i as f64) / (n_samples as f64);
2494 points.push(circle.evaluate(t));
2495 }
2496 }
2497 EdgeCurve::Ellipse(ellipse) => {
2498 let t_start = ellipse.project(start);
2499 let end_pt = topo.vertex(edge_data.end())?.point();
2500 let mut t_end = ellipse.project(end_pt);
2501 if t_end <= t_start {
2502 t_end += std::f64::consts::TAU;
2503 }
2504 let n_samples = 8;
2505 for i in 1..n_samples {
2506 #[allow(clippy::cast_precision_loss)]
2507 let t = t_start + (t_end - t_start) * (i as f64) / (n_samples as f64);
2508 points.push(ellipse.evaluate(t));
2509 }
2510 }
2511 EdgeCurve::Line => {}
2512 }
2513
2514 let end = topo.vertex(edge_data.end())?.point();
2515 points.push(end);
2516 }
2517
2518 if points.len() < 2 {
2519 return Err(crate::OperationsError::InvalidInput {
2520 reason: "sweep_along_edges: need at least 2 distinct points".into(),
2521 });
2522 }
2523
2524 let points = densify_path_points(&points);
2527 let degree = std::cmp::min(3, points.len() - 1);
2528 let path_curve = brepkit_math::nurbs::fitting::interpolate(&points, degree)?;
2529
2530 sweep(topo, profile, &path_curve)
2531}
2532
2533#[cfg(test)]
2534mod tests;