1use ogeom_algo::{
29 Built, History, attach_pcurve, attach_seam, edge_vertices, make_edge_between, make_face_on,
30 make_face_with_pcurves, make_shell, make_vertex, make_wire,
31};
32use ogeom_core::{OgeomResult, Tolerances, ogeom_bail, ogeom_err};
33use ogeom_geom::Curve2d as _;
34use ogeom_geom::Curve3d as _;
35use ogeom_geom::Surface as _;
36use ogeom_geom::Transformable as _;
37use ogeom_geom::{
38 BSpline2d, BSplineCurve, BSplineSurface, Curve, Line2d, LineCurve, PlanarCurve, PlaneSurface,
39 SurfaceGeometry,
40};
41use ogeom_math::Blend as _;
42use ogeom_math::{
43 Axis2, ControlGrid, Direction, Direction2, Frame, KnotVector, Plane, Point, Point2, Transform,
44 TransformKind, Vector, Weighted,
45};
46use ogeom_topo::{Location, Model, Orientation, Shape, ShapeType};
47
48use crate::sweep::{PipeLaw, SpineStation, law_normals, spine_curve_of, station_frame};
49
50mod guided;
51
52const KNOT_SAME: f64 = 1e-12;
55
56const MOST_SWEEP_SECTIONS: usize = 1025;
58
59pub fn make_ruled(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
75 make_loft_surface(model, &[a.clone(), b.clone()], false, &[], true, tol)
76}
77
78pub fn make_loft_surface(
139 model: &mut Model,
140 sections: &[Shape],
141 closed: bool,
142 guides: &[Shape],
143 ruled: bool,
144 tol: Tolerances,
145) -> OgeomResult<Built> {
146 if !guides.is_empty() {
147 return guided::guided_loft(model, sections, closed, guides, ruled, tol);
148 }
149 let least = if closed { 3 } else { 2 };
150 if sections.len() < least {
151 ogeom_bail!(
152 Construction,
153 "a {}loft surface needs at least {least} sections, given {}",
154 if closed { "closed " } else { "" },
155 sections.len()
156 );
157 }
158 let mut read: Vec<Section> = Vec::with_capacity(sections.len());
159 for shape in sections {
160 read.push(read_section(model, shape, "loft section", tol)?);
161 }
162 let closed_u = read[0].closed;
163 for (k, s) in read.iter().enumerate() {
164 if s.closed != closed_u {
165 ogeom_bail!(
166 Construction,
167 "loft sections are all closed or all open; section {k} is {} and section 0 \
168 is not",
169 if s.closed { "closed" } else { "open" }
170 );
171 }
172 }
173 if read.iter().any(|s| s.edges.len() != read[0].edges.len()) {
174 matched_by_length(&mut read, tol)?;
175 }
176 let count = read[0].edges.len();
177 for e in 0..count {
179 let curves: Vec<BSplineCurve> = read.iter().map(|s| s.edges[e].curve.clone()).collect();
180 let mut matched = made_compatible(&curves, tol)?;
181 if !ruled {
182 matched = matched.iter().map(smooth_joint).collect();
183 }
184 for (s, c) in read.iter_mut().zip(matched) {
185 s.edges[e].curve = c;
186 }
187 }
188 let n = read.len();
189 let mut chords = Vec::with_capacity(n);
190 for k in 0..n - 1 {
191 chords.push(section_chord(&read[k], &read[k + 1], k, tol)?);
192 }
193 if closed {
194 chords.push(section_chord(&read[n - 1], &read[0], n - 1, tol)?);
195 }
196
197 let (spans, surfaces, seam_v) = if ruled {
198 let mut spans: Vec<(usize, usize)> = (0..n - 1).map(|k| (k, k + 1)).collect();
199 if closed {
200 spans.push((n - 1, 0));
201 }
202 let mut surfaces = Vec::with_capacity(count);
203 for e in 0..count {
204 let mut per_span = Vec::with_capacity(spans.len());
205 for &(lo, hi) in &spans {
206 let rows = vec![
207 read[lo].edges[e].curve.control_points().to_vec(),
208 read[hi].edges[e].curve.control_points().to_vec(),
209 ];
210 let v_knots = KnotVector::clamped_uniform(1, 2)?;
211 per_span.push(surface_of(
212 read[lo].edges[e].curve.knots(),
213 v_knots,
214 &rows,
215 tol,
216 )?);
217 }
218 surfaces.push(per_span);
219 }
220 (spans, surfaces, false)
221 } else if closed {
222 let mut surfaces = Vec::with_capacity(count);
223 for e in 0..count {
224 let rows: Vec<Vec<Weighted<Point>>> = read
225 .iter()
226 .map(|s| s.edges[e].curve.control_points().to_vec())
227 .collect();
228 let (v_knots, net) = interpolated_closed(&rows, tol)?;
229 surfaces.push(vec![surface_of(
230 read[0].edges[e].curve.knots(),
231 v_knots,
232 &net,
233 tol,
234 )?]);
235 }
236 for edge in &mut read[0].edges {
239 edge.edge = None;
240 }
241 (vec![(0, 0)], surfaces, true)
242 } else {
243 let params = unit_params(&chords);
244 let mut surfaces = Vec::with_capacity(count);
245 for e in 0..count {
246 let rows: Vec<Vec<Weighted<Point>>> = read
247 .iter()
248 .map(|s| s.edges[e].curve.control_points().to_vec())
249 .collect();
250 let (v_knots, net) = interpolated(&rows, ¶ms, tol)?;
251 surfaces.push(vec![surface_of(
252 read[0].edges[e].curve.knots(),
253 v_knots,
254 &net,
255 tol,
256 )?]);
257 }
258 (vec![(0, n - 1)], surfaces, false)
259 };
260 let shape = sheet(model, &read, &surfaces, &spans, seam_v, ruled, tol)?;
261 let mut history = History::new();
262 for section in sections {
263 history.generate(section, shape.clone());
264 }
265 Ok(Built { shape, history })
266}
267
268pub fn make_sweep_surface(
292 model: &mut Model,
293 profile: &Shape,
294 spine: &Shape,
295 law: &PipeLaw<'_>,
296 tol: Tolerances,
297) -> OgeomResult<Built> {
298 let section = read_section(model, profile, "sweep profile", tol)?;
299 let target = tol.confusion() * 10.0;
300 let motions = |model: &Model, density: usize| -> OgeomResult<Placements> {
301 let stations = sweep_stations(model, spine, density, tol)?;
302 let breaks = stations
303 .windows(2)
304 .enumerate()
305 .filter(|(_, pair)| pair[0].edge != pair[1].edge)
306 .map(|(k, _)| k)
307 .collect();
308 if matches!(law, PipeLaw::Fixed) {
309 let start = stations[0].at;
310 return Ok(Placements {
311 motions: stations
312 .iter()
313 .map(|s| Transform::translation(s.at - start))
314 .collect(),
315 breaks,
316 });
317 }
318 let normals = law_normals(model, &stations, law, target, tol)?;
319 let start = station_frame(&stations[0], normals[0], tol)?;
320 let mut out = Vec::with_capacity(stations.len());
321 for (s, n) in stations.iter().zip(&normals) {
322 let frame = station_frame(s, *n, tol)?;
323 out.push(Transform::from_frame(&frame) * Transform::to_frame(&start));
324 }
325 Ok(Placements {
326 motions: out,
327 breaks,
328 })
329 };
330 let shape = swept_sheet(model, §ion, motions, target, tol)?;
331 let mut history = History::new();
332 history.generate(profile, shape.clone());
333 history.generate(spine, shape.clone());
334 if let PipeLaw::Auxiliary { guide } = law {
335 history.generate(guide, shape.clone());
336 }
337 Ok(Built { shape, history })
338}
339
340pub fn make_sweep_two_rails(
363 model: &mut Model,
364 profile: &Shape,
365 rail_a: &Shape,
366 rail_b: &Shape,
367 tol: Tolerances,
368) -> OgeomResult<Built> {
369 let section = read_section(model, profile, "sweep profile", tol)?;
370 if section.closed {
371 ogeom_bail!(
372 Construction,
373 "a two-rail profile runs from one rail to the other; a closed profile has one end"
374 );
375 }
376 let start = section.edges[0].curve.point_at(0.0, tol)?;
377 let end = section.edges[section.edges.len() - 1]
378 .curve
379 .point_at(1.0, tol)?;
380 let mut first = Rail::read(model, rail_a, tol)?;
381 let mut second = Rail::read(model, rail_b, tol)?;
382 let (a0, b0) = (first.at(0.0, tol)?.0, second.at(0.0, tol)?.0);
383 let near = tol.confusion() * 10.0;
384 if start.distance(b0) <= near && end.distance(a0) <= near {
385 core::mem::swap(&mut first, &mut second);
386 }
387 let (a0, b0) = (first.at(0.0, tol)?.0, second.at(0.0, tol)?.0);
388 if start.distance(a0) > near || end.distance(b0) > near {
389 ogeom_bail!(
390 Construction,
391 "the profile's ends must sit on the rails' starts; they are {:.3e} and {:.3e} away",
392 start.distance(a0),
393 end.distance(b0)
394 );
395 }
396 let frame_at = |f: f64| -> OgeomResult<(Frame, f64)> {
397 let (a, ta) = first.at(f, tol)?;
398 let (b, tb) = second.at(f, tol)?;
399 let chord = b - a;
400 let width = chord.magnitude();
401 if width <= tol.confusion() {
402 ogeom_bail!(
403 Construction,
404 "the rails meet at {a:?}; a profile between them has no width there"
405 );
406 }
407 let x = chord / width;
408 let mean = ta + tb;
409 let along = mean - x * mean.dot(x);
410 if along.magnitude() <= 1e-6 * mean.magnitude().max(tol.confusion()) {
411 ogeom_bail!(
412 Construction,
413 "the rails' mean direction runs along the chord between them at {a:?}"
414 );
415 }
416 Ok((
417 Frame::new(a, Direction::new(along, tol)?, Direction::new(x, tol)?, tol)?,
418 width,
419 ))
420 };
421 let (start_frame, start_width) = frame_at(0.0)?;
422 let target = tol.confusion() * 10.0;
423 let motions = |_: &Model, density: usize| -> OgeomResult<Placements> {
424 let count = 32 * density;
425 let mut out = Vec::with_capacity(count + 1);
426 for i in 0..=count {
427 #[allow(clippy::cast_precision_loss)]
428 let f = i as f64 / count as f64;
429 let (frame, width) = frame_at(f)?;
430 let scale = Transform::scaling(Point::ORIGIN, width / start_width, tol)?;
431 out.push(Transform::from_frame(&frame) * scale * Transform::to_frame(&start_frame));
432 }
433 Ok(Placements {
434 motions: out,
435 breaks: Vec::new(),
436 })
437 };
438 let shape = swept_sheet(model, §ion, motions, target, tol)?;
439 let mut history = History::new();
440 for input in [profile, rail_a, rail_b] {
441 history.generate(input, shape.clone());
442 }
443 Ok(Built { shape, history })
444}
445
446#[derive(Clone)]
449struct SectionEdge {
450 edge: Option<Shape>,
452 curve: BSplineCurve,
455 paced: bool,
457}
458
459#[derive(Clone)]
461struct Section {
462 edges: Vec<SectionEdge>,
463 closed: bool,
464}
465
466fn read_section(model: &Model, shape: &Shape, what: &str, tol: Tolerances) -> OgeomResult<Section> {
469 let edges = match model.kind_of(shape)? {
470 ShapeType::Edge => vec![shape.clone()],
471 ShapeType::Wire => model.ordered_children_of(shape)?,
472 other => ogeom_bail!(
473 Construction,
474 "a {what} is an edge or a wire, not a {other:?}"
475 ),
476 };
477 if edges.is_empty() {
478 ogeom_bail!(Construction, "the {what} has no edges");
479 }
480 let mut adoptable = true;
481 let mut out = Vec::with_capacity(edges.len());
482 for edge in &edges {
483 let (curve, range) = spine_curve_of(model, edge)?;
484 let placement = edge.transform(model.datums())?;
485 if placement.kind() != TransformKind::Identity {
486 adoptable = false;
487 }
488 let exact = curve.to_bspline_over(range, tol)?;
489 let exact = moved(&exact, &placement, tol)?;
490 let exact = if edge.orientation() == Orientation::Reversed {
491 let (knots, control) =
492 ogeom_math::bspline::reverse(exact.knots(), exact.control_points());
493 BSplineCurve::rational(knots, control)?
494 } else {
495 exact
496 };
497 out.push(SectionEdge {
498 edge: Some(edge.clone()),
499 curve: standard(&exact)?,
500 paced: true,
501 });
502 }
503 let head = out[0].curve.point_at(0.0, tol)?;
504 let tail = out[out.len() - 1].curve.point_at(1.0, tol)?;
505 let closed = head.distance(tail) <= tol.confusion();
506 if adoptable {
507 let mut ends = Vec::with_capacity(edges.len());
508 for edge in &edges {
509 match edge_vertices(model, edge)? {
510 Some(pair) => ends.push(pair),
511 None => adoptable = false,
512 }
513 }
514 if adoptable {
515 adoptable = ends.windows(2).all(|w| w[0].1.is_partner(&w[1].0))
516 && (!closed || ends[ends.len() - 1].1.is_partner(&ends[0].0));
517 }
518 }
519 if !adoptable {
520 for e in &mut out {
521 e.edge = None;
522 }
523 }
524 Ok(Section { edges: out, closed })
525}
526
527fn moved(curve: &BSplineCurve, motion: &Transform, tol: Tolerances) -> OgeomResult<BSplineCurve> {
530 if motion.kind() == TransformKind::Identity {
531 return Ok(curve.clone());
532 }
533 let control = curve
534 .control_points()
535 .iter()
536 .map(|w| Weighted::new(motion.apply(w.point()), w.weight, tol))
537 .collect::<OgeomResult<Vec<_>>>()?;
538 BSplineCurve::rational(curve.knots().clone(), control)
539}
540
541fn standard(curve: &BSplineCurve) -> OgeomResult<BSplineCurve> {
544 let knots = curve.knots().reparameterized(0.0, 1.0)?;
545 let first = curve.control_points()[0].weight;
546 let control = curve
547 .control_points()
548 .iter()
549 .map(|w| w.scale(1.0 / first))
550 .collect();
551 BSplineCurve::rational(knots, control)
552}
553
554fn matched_by_length(sections: &mut [Section], tol: Tolerances) -> OgeomResult<()> {
562 let mut lengths: Vec<Vec<f64>> = Vec::with_capacity(sections.len());
563 for (k, s) in sections.iter().enumerate() {
564 let each = s
565 .edges
566 .iter()
567 .map(|e| ogeom_algo::curve_length(&Curve::BSpline(e.curve.clone()), (0.0, 1.0), tol))
568 .collect::<OgeomResult<Vec<f64>>>()?;
569 if each.iter().sum::<f64>() <= tol.confusion() {
570 ogeom_bail!(
571 Construction,
572 "section {k} has no length to match the other sections' edges along"
573 );
574 }
575 lengths.push(each);
576 }
577 let longest = lengths
578 .iter()
579 .map(|l| l.iter().sum::<f64>())
580 .fold(0.0_f64, f64::max);
581 let same = tol.confusion() * 10.0 / longest;
582 let own: Vec<Vec<f64>> = lengths
584 .iter()
585 .map(|l| {
586 let total: f64 = l.iter().sum();
587 let mut run = 0.0;
588 l[..l.len() - 1]
589 .iter()
590 .map(|x| {
591 run += x;
592 run / total
593 })
594 .collect()
595 })
596 .collect();
597 let mut union: Vec<f64> = own.iter().flatten().copied().collect();
598 union.sort_by(f64::total_cmp);
599 let mut breaks: Vec<f64> = Vec::with_capacity(union.len());
600 for f in union {
601 if f <= same || f >= 1.0 - same {
602 continue;
603 }
604 if breaks.last().is_none_or(|b| f - b > same) {
605 breaks.push(f);
606 }
607 }
608 for ((section, l), mine) in sections.iter_mut().zip(&lengths).zip(&own) {
609 let total: f64 = l.iter().sum();
610 let mut pieces: Vec<SectionEdge> = Vec::with_capacity(breaks.len() + 1);
611 let mut cut = false;
612 let mut start = 0.0;
613 for (edge, length) in section.edges.iter().zip(l) {
614 let (lo, hi) = (start / total, (start + length) / total);
615 let inside: Vec<f64> = breaks
618 .iter()
619 .filter(|f| **f > lo + same && **f < hi - same)
620 .filter(|f| mine.iter().all(|m| (*m - **f).abs() > same))
621 .map(|f| f * total - start)
622 .collect();
623 start += length;
624 if inside.is_empty() {
625 pieces.push(edge.clone());
626 continue;
627 }
628 cut = true;
629 let whole = Curve::BSpline(edge.curve.clone());
630 let mut at = Vec::with_capacity(inside.len());
631 for along in inside {
632 at.push(ogeom_algo::parameter_at_length(
633 &whole,
634 (0.0, 1.0),
635 along,
636 tol,
637 )?);
638 }
639 let mut rest = (
640 edge.curve.knots().clone(),
641 edge.curve.control_points().to_vec(),
642 );
643 let piece = |(knots, control): (KnotVector, Vec<Weighted<Point>>)| {
648 Ok::<_, ogeom_core::OgeomError>(SectionEdge {
649 edge: None,
650 curve: BSplineCurve::rational(knots.reparameterized(0.0, 1.0)?, control)?,
651 paced: false,
652 })
653 };
654 for t in at {
655 let (before, after) = ogeom_math::bspline::split(&rest.0, &rest.1, t, tol)?;
656 pieces.push(piece(before)?);
657 rest = after;
658 }
659 pieces.push(piece(rest)?);
660 }
661 if cut {
662 for piece in &mut pieces {
663 piece.edge = None;
664 }
665 }
666 section.edges = pieces;
667 }
668 let count = sections[0].edges.len();
669 if let Some(k) = sections.iter().position(|s| s.edges.len() != count) {
670 ogeom_bail!(
671 Construction,
672 "the sections' breaks fall too close together to match by length: section 0 \
673 splits into {count} edges and section {k} into {}",
674 sections[k].edges.len()
675 );
676 }
677 Ok(())
678}
679
680pub(crate) fn made_compatible(
683 curves: &[BSplineCurve],
684 tol: Tolerances,
685) -> OgeomResult<Vec<BSplineCurve>> {
686 let degree = curves.iter().map(BSplineCurve::degree).max().unwrap_or(1);
687 let mut raised = Vec::with_capacity(curves.len());
688 for c in curves {
689 let mut c = c.clone();
690 while c.degree() < degree {
691 c = c.elevated(tol)?;
692 }
693 raised.push(c);
694 }
695 let interior = |c: &BSplineCurve| -> Vec<(f64, usize)> {
696 c.knots()
697 .distinct()
698 .into_iter()
699 .filter(|(v, _)| *v > KNOT_SAME && *v < 1.0 - KNOT_SAME)
700 .collect()
701 };
702 let mut union: Vec<(f64, usize)> = Vec::new();
703 for c in &raised {
704 for (value, mult) in interior(c) {
705 match union
706 .iter_mut()
707 .find(|(v, _)| (*v - value).abs() <= KNOT_SAME)
708 {
709 Some(entry) => entry.1 = entry.1.max(mult),
710 None => union.push((value, mult)),
711 }
712 }
713 }
714 for c in &mut raised {
715 for &(value, mult) in &union {
716 let have = interior(c)
717 .iter()
718 .find(|(v, _)| (*v - value).abs() <= KNOT_SAME)
719 .map_or(0, |entry| entry.1);
720 if have < mult {
721 *c = c.with_knot_inserted(value, mult - have, tol)?;
722 }
723 }
724 }
725 let knots = raised[0].knots().clone();
726 for c in &raised {
727 let same = c.knots().knots().len() == knots.knots().len()
728 && c.knots()
729 .knots()
730 .iter()
731 .zip(knots.knots())
732 .all(|(a, b)| (a - b).abs() <= KNOT_SAME * 10.0);
733 if !same {
734 ogeom_bail!(Construction, "the sections' knots could not be matched");
735 }
736 }
737 raised
738 .iter()
739 .map(|c| BSplineCurve::rational(knots.clone(), c.control_points().to_vec()))
740 .collect()
741}
742
743fn smooth_joint(curve: &BSplineCurve) -> BSplineCurve {
757 let degree = curve.degree();
758 let knots = curve.knots();
759 let interior: Vec<(f64, usize)> = knots
760 .distinct()
761 .into_iter()
762 .filter(|(v, _)| *v > KNOT_SAME && *v < 1.0 - KNOT_SAME)
763 .collect();
764 let control = curve.control_points();
765 let ([(at, multiplicity)], true) = (interior.as_slice(), control.len() == 2 * degree + 1)
766 else {
767 return curve.clone();
768 };
769 if *multiplicity != degree || degree == 0 {
770 return curve.clone();
771 }
772 let (start, end) = (knots.knots()[0], knots.knots()[knots.knots().len() - 1]);
773 let (left, right) = (at - start, end - at);
774 let joint = degree;
775 let (before, here, after) = (
776 control[joint - 1].point(),
777 control[joint].point(),
778 control[joint + 1].point(),
779 );
780 let chord = after - before;
783 let length = chord.magnitude();
784 if length <= 0.0 || left <= 0.0 || right <= 0.0 {
785 return curve.clone();
786 }
787 let s = (here - before).dot(chord) / (length * length);
788 let off = (here - before - chord * s).magnitude();
789 if !(s > 0.0 && s < 1.0) || off > 1e-9 * length {
790 return curve.clone();
791 }
792 let (lambda, mu) = (right / (left + right), left / (left + right));
795 let w = control[joint].weight;
796 let c_before = w * (1.0 - s) / lambda / control[joint - 1].weight;
797 let c_after = w * s / mu / control[joint + 1].weight;
798 let mut out = control.to_vec();
799 for i in 0..=degree {
800 let k = i32::try_from(i).unwrap_or(i32::MAX);
801 out[joint - i] = control[joint - i].scale(c_before.powi(k));
802 out[joint + i] = control[joint + i].scale(c_after.powi(k));
803 }
804 BSplineCurve::rational(knots.clone(), out).unwrap_or_else(|_| curve.clone())
805}
806
807fn section_chord(a: &Section, b: &Section, index: usize, tol: Tolerances) -> OgeomResult<f64> {
809 let mut sum = 0.0;
810 let mut count = 0.0;
811 for (ea, eb) in a.edges.iter().zip(&b.edges) {
812 for (p, q) in ea
813 .curve
814 .control_points()
815 .iter()
816 .zip(eb.curve.control_points())
817 {
818 sum += p.point().distance(q.point());
819 count += 1.0;
820 }
821 }
822 let chord = sum / count;
823 if chord <= tol.confusion() {
824 ogeom_bail!(
825 Construction,
826 "sections {index} and {} coincide; a skin between them has no extent",
827 index + 1
828 );
829 }
830 Ok(chord)
831}
832
833fn unit_params(chords: &[f64]) -> Vec<f64> {
835 let total: f64 = chords.iter().sum();
836 let mut params = Vec::with_capacity(chords.len() + 1);
837 let mut run = 0.0;
838 params.push(0.0);
839 for c in chords {
840 run += c;
841 params.push(run / total);
842 }
843 let last = params.len() - 1;
844 params[last] = 1.0;
845 params
846}
847
848fn interpolated(
852 rows: &[Vec<Weighted<Point>>],
853 params: &[f64],
854 tol: Tolerances,
855) -> OgeomResult<(KnotVector, Vec<Vec<Weighted<Point>>>)> {
856 let n = rows.len();
857 let degree = (n - 1).min(3);
858 let knots = KnotVector::averaged(degree, params)?;
859 let mut matrix = vec![vec![0.0; n]; n];
860 for (k, v) in params.iter().enumerate() {
861 let span = knots.span(*v, tol)?;
862 for (b, j) in knots.basis(span, *v).iter().zip(span - degree..=span) {
863 matrix[k][j] = *b;
864 }
865 }
866 let inverse = inverted(matrix)?;
867 Ok((knots, combined(&inverse, rows)))
868}
869
870fn interpolated_closed(
875 rows: &[Vec<Weighted<Point>>],
876 tol: Tolerances,
877) -> OgeomResult<(KnotVector, Vec<Vec<Weighted<Point>>>)> {
878 let n = rows.len();
879 let degree = (n - 1).min(3);
880 let shift = if degree.is_multiple_of(2) { 0.5 } else { 0.0 };
883 let count = n + degree + 2;
887 #[allow(clippy::cast_precision_loss)]
888 let knots = KnotVector::new((0..=count + degree).map(|i| i as f64).collect(), degree)?;
889 let ring = |j: usize| (j + n - 1) % n;
890 #[allow(clippy::cast_precision_loss)]
891 let at = |k: usize| (degree + 1 + k) as f64 + shift;
892 let mut matrix = vec![vec![0.0; n]; n];
893 for (k, row) in matrix.iter_mut().enumerate() {
894 let v = at(k);
895 let span = knots.span(v, tol)?;
896 for (b, j) in knots.basis(span, v).iter().zip(span - degree..=span) {
897 row[ring(j)] += *b;
898 }
899 }
900 let inverse = inverted(matrix)?;
901 let solved = combined(&inverse, rows);
902 let wrapped: Vec<Vec<Weighted<Point>>> = (0..count).map(|j| solved[ring(j)].clone()).collect();
903 #[allow(clippy::cast_precision_loss)]
904 let (from, to) = (at(0), at(n));
905 let width = rows[0].len();
906 let mut columns: Vec<Vec<Weighted<Point>>> = Vec::with_capacity(width);
907 let mut cut_knots: Option<KnotVector> = None;
908 for i in 0..width {
909 let column: Vec<Weighted<Point>> = wrapped.iter().map(|r| r[i]).collect();
910 let (_, (k1, c1)) = ogeom_math::bspline::split(&knots, &column, from, tol)?;
911 let ((k2, c2), _) = ogeom_math::bspline::split(&k1, &c1, to, tol)?;
912 cut_knots = Some(k2);
913 columns.push(c2);
914 }
915 let Some(cut_knots) = cut_knots else {
916 ogeom_bail!(Construction, "a section has no control points");
917 };
918 let l = columns[0].len();
919 let mut net: Vec<Vec<Weighted<Point>>> = (0..l)
920 .map(|j| columns.iter().map(|c| c[j]).collect())
921 .collect();
922 net[l - 1] = net[0].clone();
924 Ok((cut_knots.reparameterized(0.0, 1.0)?, net))
925}
926
927fn combined(inverse: &[Vec<f64>], rows: &[Vec<Weighted<Point>>]) -> Vec<Vec<Weighted<Point>>> {
929 let width = rows[0].len();
930 inverse
931 .iter()
932 .map(|line| {
933 (0..width)
934 .map(|i| {
935 line.iter()
936 .zip(rows)
937 .fold(Weighted::<Point>::zero(), |acc, (a, row)| {
938 acc.add(row[i].scale(*a))
939 })
940 })
941 .collect()
942 })
943 .collect()
944}
945
946fn inverted(mut a: Vec<Vec<f64>>) -> OgeomResult<Vec<Vec<f64>>> {
949 let n = a.len();
950 let mut inv: Vec<Vec<f64>> = (0..n)
951 .map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
952 .collect();
953 for col in 0..n {
954 let pivot = (col..n)
955 .max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs()))
956 .unwrap_or(col);
957 if a[pivot][col].abs() <= 1e-12 {
958 ogeom_bail!(Numeric, "the skin's interpolation system is singular");
959 }
960 a.swap(col, pivot);
961 inv.swap(col, pivot);
962 let d = a[col][col];
963 for j in 0..n {
964 a[col][j] /= d;
965 inv[col][j] /= d;
966 }
967 for r in 0..n {
968 if r == col {
969 continue;
970 }
971 let f = a[r][col];
972 if f == 0.0 {
973 continue;
974 }
975 for j in 0..n {
976 a[r][j] -= f * a[col][j];
977 inv[r][j] -= f * inv[col][j];
978 }
979 }
980 }
981 Ok(inv)
982}
983
984fn surface_of(
986 u_knots: &KnotVector,
987 v_knots: KnotVector,
988 rows: &[Vec<Weighted<Point>>],
989 tol: Tolerances,
990) -> OgeomResult<BSplineSurface> {
991 let (k, l) = (rows[0].len(), rows.len());
992 let mut points = Vec::with_capacity(k * l);
993 for i in 0..k {
994 for row in rows {
995 let w = row[i];
996 if !w.weight.is_finite() || w.weight <= tol.confusion() {
997 ogeom_bail!(
998 Construction,
999 "the skin's weights fall to {} between the sections; sections this unlike \
1000 cannot be skinned exactly",
1001 w.weight
1002 );
1003 }
1004 points.push(w);
1005 }
1006 }
1007 BSplineSurface::rational(u_knots.clone(), v_knots, ControlGrid::new(points, k, l)?)
1008}
1009
1010struct Side {
1014 edge: Shape,
1015 image: PlanarCurve,
1016 range: (f64, f64),
1017}
1018
1019#[allow(clippy::too_many_lines, reason = "one assembly, spelled out")]
1026fn sheet(
1027 model: &mut Model,
1028 sections: &[Section],
1029 surfaces: &[Vec<BSplineSurface>],
1030 spans: &[(usize, usize)],
1031 seam_v: bool,
1032 ruled: bool,
1033 tol: Tolerances,
1034) -> OgeomResult<Shape> {
1035 let count = sections[0].edges.len();
1036 let closed_u = sections[0].closed;
1037 let joints = if closed_u { count } else { count + 1 };
1038 let end_joint = |e: usize| if closed_u { (e + 1) % count } else { e + 1 };
1039
1040 for j in 0..joints {
1043 let (before, after) = if closed_u {
1044 ((j + count - 1) % count, j)
1045 } else if j == 0 || j == count {
1046 continue;
1047 } else {
1048 (j - 1, j)
1049 };
1050 let ratio = |s: &Section| {
1051 let end = s.edges[before].curve.control_points();
1052 let start = s.edges[after].curve.control_points();
1053 end[end.len() - 1].weight / start[0].weight
1054 };
1055 let first = ratio(§ions[0]);
1056 if sections
1057 .iter()
1058 .any(|s| (ratio(s) - first).abs() > 1e-9 * first.abs())
1059 {
1060 ogeom_bail!(
1061 Construction,
1062 "neighbouring section edges carry weights at their shared corner {j} that \
1063 change from section to section; their skins would part"
1064 );
1065 }
1066 }
1067
1068 let mut bounding: Vec<usize> = spans.iter().flat_map(|&(a, b)| [a, b]).collect();
1070 bounding.sort_unstable();
1071 bounding.dedup();
1072 let mut vertices: Vec<Option<Vec<Shape>>> = vec![None; sections.len()];
1073 let mut borders: Vec<Option<Vec<(Shape, bool)>>> = vec![None; sections.len()];
1074 for &k in &bounding {
1075 let section = §ions[k];
1076 let adopted = section.edges.iter().all(|e| e.edge.is_some());
1077 let mut joint_vertices = Vec::with_capacity(joints);
1078 let mut edges = Vec::with_capacity(count);
1079 if adopted {
1080 for e in §ion.edges {
1081 let Some(edge) = &e.edge else {
1082 ogeom_bail!(Construction, "an adopted section lost an edge");
1083 };
1084 let Some((start, _)) = edge_vertices(model, edge)? else {
1085 ogeom_bail!(Construction, "a section edge has no vertices");
1086 };
1087 joint_vertices.push(start);
1088 edges.push((edge.clone(), true));
1089 }
1090 if !closed_u {
1091 let Some(last) = §ion.edges[count - 1].edge else {
1092 ogeom_bail!(Construction, "an adopted section lost an edge");
1093 };
1094 let Some((_, end)) = edge_vertices(model, last)? else {
1095 ogeom_bail!(Construction, "a section edge has no vertices");
1096 };
1097 joint_vertices.push(end);
1098 }
1099 } else {
1100 for e in §ion.edges {
1101 let at = e.curve.point_at(0.0, tol)?;
1102 joint_vertices.push(make_vertex(model, at).shape);
1103 }
1104 if !closed_u {
1105 let at = section.edges[count - 1].curve.point_at(1.0, tol)?;
1106 joint_vertices.push(make_vertex(model, at).shape);
1107 }
1108 for (e, piece) in section.edges.iter().enumerate() {
1109 let edge = make_edge_between(
1110 model,
1111 Curve::BSpline(piece.curve.clone()),
1112 (0.0, 1.0),
1113 &joint_vertices[e],
1114 &joint_vertices[end_joint(e)],
1115 tol,
1116 )?
1117 .shape;
1118 edges.push((edge, false));
1119 }
1120 }
1121 vertices[k] = Some(joint_vertices);
1122 borders[k] = Some(edges);
1123 }
1124 let vertex = |k: usize, j: usize| -> OgeomResult<Shape> {
1125 vertices[k]
1126 .as_ref()
1127 .map(|v| v[j].clone())
1128 .ok_or_else(|| ogeom_err!(Construction, "section {k} bounds no face"))
1129 };
1130
1131 let mut rails: Vec<Vec<(Shape, bool)>> = Vec::with_capacity(spans.len());
1133 for (s, &(lo, hi)) in spans.iter().enumerate() {
1134 let mut per_joint = Vec::with_capacity(joints);
1135 for j in 0..joints {
1136 let (e, u) = if j < count {
1137 (j, 0.0)
1138 } else {
1139 (count - 1, 1.0)
1140 };
1141 let surface = &surfaces[e][s];
1142 let control = sections[lo].edges[e].curve.control_points();
1143 let index = if u == 0.0 { 0 } else { control.len() - 1 };
1144 let w_lo = control[index].weight;
1145 let w_hi = sections[hi].edges[e].curve.control_points()[index].weight;
1146 let (from, to) = (vertex(lo, j)?, vertex(hi, j)?);
1147 if ruled && (w_lo - w_hi).abs() <= 1e-12 * w_lo {
1150 let a = surface.point_at(u, 0.0, tol)?;
1151 let b = surface.point_at(u, 1.0, tol)?;
1152 let line: Curve = LineCurve::segment(a, b, tol)?.into();
1153 let range = line.domain();
1154 let edge = make_edge_between(model, line, range, &from, &to, tol)?.shape;
1155 per_joint.push((edge, true));
1156 } else {
1157 let curve = Curve::BSpline(surface.iso_u_curve(u, tol)?);
1158 let edge = make_edge_between(model, curve, (0.0, 1.0), &from, &to, tol)?.shape;
1159 per_joint.push((edge, false));
1160 }
1161 }
1162 rails.push(per_joint);
1163 }
1164
1165 let column = |u: f64, straight: bool, range: (f64, f64)| -> OgeomResult<PlanarCurve> {
1166 if straight {
1167 let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
1168 Ok(BSpline2d::new(knots, vec![Point2::new(u, 0.0), Point2::new(u, 1.0)], tol)?.into())
1169 } else {
1170 Ok(Line2d::over(Axis2::new(Point2::new(u, 0.0), Direction2::Y), -1.0, 2.0)?.into())
1171 }
1172 };
1173 let rail_range = |model: &Model, edge: &Shape| -> OgeomResult<(f64, f64)> {
1174 Ok(spine_curve_of(model, edge)?.1)
1175 };
1176
1177 let mut faces = Vec::with_capacity(count * spans.len());
1178 for e in 0..count {
1179 for (s, &(lo, hi)) in spans.iter().enumerate() {
1180 let surface = &surfaces[e][s];
1181 let geometry: SurfaceGeometry = surface.clone().into();
1182 let border = |k: usize| -> OgeomResult<(Shape, bool)> {
1183 borders[k]
1184 .as_ref()
1185 .map(|b| b[e].clone())
1186 .ok_or_else(|| ogeom_err!(Construction, "section {k} bounds no face"))
1187 };
1188 let (bottom, bottom_adopted) = border(lo)?;
1189 let (top, top_adopted) = border(hi)?;
1190 let (rail0, straight0) = rails[s][e].clone();
1191 let (rail1, straight1) = rails[s][end_joint(e)].clone();
1192
1193 if ruled
1194 && let Some(plane) =
1195 ruled_plane(§ions[lo].edges[e], §ions[hi].edges[e], tol)?
1196 {
1197 let reach = [0.0, 1.0]
1198 .iter()
1199 .flat_map(|u| [(*u, 0.0), (*u, 1.0)])
1200 .map(|(u, v)| {
1201 surface
1202 .point_at(u, v, tol)
1203 .map(|p| p.distance(plane.origin()))
1204 })
1205 .collect::<OgeomResult<Vec<f64>>>()?
1206 .into_iter()
1207 .fold(1.0_f64, f64::max)
1208 * 2.0;
1209 let flat: SurfaceGeometry =
1210 PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
1211 let face = make_face_with_pcurves(
1212 model,
1213 flat,
1214 &[vec![bottom, rail1, top.reversed(), rail0.reversed()]],
1215 tol,
1216 )?
1217 .shape;
1218 faces.push(face);
1219 continue;
1220 }
1221
1222 let row = |model: &mut Model,
1223 edge: &Shape,
1224 adopted: bool,
1225 v: f64|
1226 -> OgeomResult<Side> {
1227 let (image, range) = if adopted {
1228 let section = §ions[if v == 0.0 { lo } else { hi }].edges[e];
1229 adopted_row(
1230 model,
1231 edge,
1232 §ion.curve,
1233 section.paced,
1234 v,
1235 &geometry,
1236 tol,
1237 )?
1238 } else {
1239 (
1240 Line2d::over(Axis2::new(Point2::new(0.0, v), Direction2::X), -1.0, 2.0)?
1241 .into(),
1242 (0.0, 1.0),
1243 )
1244 };
1245 Ok(Side {
1246 edge: edge.clone(),
1247 image,
1248 range,
1249 })
1250 };
1251 let bottom_side = row(model, &bottom, bottom_adopted, 0.0)?;
1252 let top_side = row(model, &top, top_adopted, 1.0)?;
1253 let range0 = rail_range(model, &rail0)?;
1254 let range1 = rail_range(model, &rail1)?;
1255 let rail0_side = Side {
1256 image: column(0.0, straight0, range0)?,
1257 edge: rail0,
1258 range: range0,
1259 };
1260 let rail1_side = Side {
1261 image: column(1.0, straight1, range1)?,
1262 edge: rail1,
1263 range: range1,
1264 };
1265 debug_assert!(!seam_v || bottom_side.edge.is_partner(&top_side.edge));
1266 faces.push(sheet_face(
1267 model,
1268 geometry,
1269 bottom_side,
1270 rail1_side,
1271 top_side,
1272 rail0_side,
1273 tol,
1274 )?);
1275 }
1276 }
1277 if faces.len() == 1 {
1278 return Ok(faces.swap_remove(0));
1279 }
1280 Ok(make_shell(model, &faces)?.shape)
1281}
1282
1283fn ruled_plane(a: &SectionEdge, b: &SectionEdge, tol: Tolerances) -> OgeomResult<Option<Plane>> {
1287 let straight = |c: &BSplineCurve| c.degree() == 1 && c.control_points().len() == 2;
1288 if !straight(&a.curve) || !straight(&b.curve) {
1289 return Ok(None);
1290 }
1291 let (a0, a1) = (a.curve.point_at(0.0, tol)?, a.curve.point_at(1.0, tol)?);
1292 let (b0, b1) = (b.curve.point_at(0.0, tol)?, b.curve.point_at(1.0, tol)?);
1293 let normal = [
1294 (a1 - a0).cross(b0 - a0),
1295 (a1 - a0).cross(b1 - a1),
1296 (b1 - b0).cross(b0 - a0),
1297 ]
1298 .into_iter()
1299 .max_by(|x, y| x.magnitude().total_cmp(&y.magnitude()))
1300 .unwrap_or(Vector::new(0.0, 0.0, 0.0));
1301 if normal.magnitude() <= tol.confusion() * (a1 - a0).magnitude().max(1.0) {
1302 return Ok(None);
1303 }
1304 let plane = Plane::through(a0, Direction::new(normal, tol)?);
1305 Ok([a1, b0, b1]
1306 .iter()
1307 .all(|p| plane.distance_to(*p) <= tol.confusion())
1308 .then_some(plane))
1309}
1310
1311fn sheet_face(
1314 model: &mut Model,
1315 surface: SurfaceGeometry,
1316 bottom: Side,
1317 rail1: Side,
1318 top: Side,
1319 rail0: Side,
1320 tol: Tolerances,
1321) -> OgeomResult<Shape> {
1322 let id = model.geometry_mut().add_surface(surface);
1323 let here = Location::identity;
1324 if bottom.edge.is_partner(&top.edge) {
1325 attach_seam(
1326 model,
1327 &bottom.edge,
1328 bottom.image,
1329 top.image,
1330 id,
1331 here(),
1332 bottom.range,
1333 )?;
1334 } else {
1335 attach_pcurve(model, &bottom.edge, bottom.image, id, here(), bottom.range)?;
1336 attach_pcurve(model, &top.edge, top.image, id, here(), top.range)?;
1337 }
1338 if rail0.edge.is_partner(&rail1.edge) {
1339 attach_seam(
1340 model,
1341 &rail1.edge,
1342 rail1.image,
1343 rail0.image,
1344 id,
1345 here(),
1346 rail1.range,
1347 )?;
1348 } else {
1349 attach_pcurve(model, &rail1.edge, rail1.image, id, here(), rail1.range)?;
1350 attach_pcurve(model, &rail0.edge, rail0.image, id, here(), rail0.range)?;
1351 }
1352 let wire = make_wire(
1353 model,
1354 &[
1355 bottom.edge,
1356 rail1.edge,
1357 top.edge.reversed(),
1358 rail0.edge.reversed(),
1359 ],
1360 tol,
1361 )?
1362 .shape;
1363 Ok(make_face_on(model, id, &[wire], tol)?.shape)
1364}
1365
1366fn adopted_row(
1374 model: &mut Model,
1375 edge: &Shape,
1376 section: &BSplineCurve,
1377 paced: bool,
1378 v: f64,
1379 surface: &SurfaceGeometry,
1380 tol: Tolerances,
1381) -> OgeomResult<(PlanarCurve, (f64, f64))> {
1382 let (curve, range) = spine_curve_of(model, edge)?;
1383 let reversed = edge.orientation() == Orientation::Reversed;
1384 let even = paced
1385 && match &curve {
1386 Curve::Line(_) => true,
1387 Curve::BSpline(b) => b.knots().is_clamped(),
1388 _ => false,
1389 };
1390 if even {
1391 let (a, b) = if reversed { (1.0, 0.0) } else { (0.0, 1.0) };
1392 let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
1393 let image = BSpline2d::new(knots, vec![Point2::new(a, v), Point2::new(b, v)], tol)?;
1394 return Ok((image.into(), range));
1395 }
1396 let u_at = |t: f64, guess: f64| -> OgeomResult<f64> {
1400 foot_on(section, curve.point_at(t, tol)?, guess, tol)
1401 };
1402 let ends = if reversed { (1.0, 0.0) } else { (0.0, 1.0) };
1405 let mut breaks: Vec<(f64, f64)> = vec![(range.0, ends.0)];
1406 let mut inner: Vec<f64> = section
1407 .knots()
1408 .distinct()
1409 .into_iter()
1410 .map(|(k, _)| k)
1411 .filter(|k| *k > KNOT_SAME && *k < 1.0 - KNOT_SAME)
1412 .collect();
1413 if reversed {
1414 inner.reverse();
1415 }
1416 for knot in inner {
1417 let (mut lo, mut hi) = (breaks[breaks.len() - 1].0, range.1);
1418 for _ in 0..80 {
1419 let mid = f64::midpoint(lo, hi);
1420 let below = u_at(mid, knot)? < knot;
1421 if below != reversed {
1422 lo = mid;
1423 } else {
1424 hi = mid;
1425 }
1426 }
1427 breaks.push((f64::midpoint(lo, hi), knot));
1428 }
1429 breaks.push((range.1, ends.1));
1430 let reach: f64 = section
1433 .control_points()
1434 .windows(2)
1435 .map(|w| w[0].point().distance(w[1].point()))
1436 .sum();
1437 let target = tol.confusion() * 0.01 / reach.max(tol.confusion());
1438 const SAMPLES: u32 = 64;
1439 let mut joined: Option<(KnotVector, Vec<Weighted<Point2>>)> = None;
1440 for w in breaks.windows(2) {
1441 let ((ta, ua), (tb, ub)) = (w[0], w[1]);
1442 let mut params = Vec::with_capacity(SAMPLES as usize + 1);
1443 let mut image = Vec::with_capacity(SAMPLES as usize + 1);
1444 for i in 0..=SAMPLES {
1445 let f = f64::from(i) / f64::from(SAMPLES);
1446 let t = ta + (tb - ta) * f;
1447 let u = if i == 0 {
1448 ua
1449 } else if i == SAMPLES {
1450 ub
1451 } else {
1452 u_at(t, ua + (ub - ua) * f)?
1453 };
1454 params.push(t);
1455 image.push(Point2::new(u, v));
1456 }
1457 let fitted = ogeom_geom::fit::fit_points_2d_at(¶ms, &image, 3, target, tol)?;
1458 let mut control = fitted.curve.control_points().to_vec();
1459 let last = control.len() - 1;
1461 control[0] = Weighted::new(Point2::new(ua, v), 1.0, tol)?;
1462 control[last] = Weighted::new(Point2::new(ub, v), 1.0, tol)?;
1463 let piece = (fitted.curve.knots().clone(), control);
1464 joined = Some(match joined {
1465 None => piece,
1466 Some(before) => ogeom_math::bspline::join(&before, &piece)?,
1467 });
1468 }
1469 let Some((knots, control)) = joined else {
1470 ogeom_bail!(Construction, "a section edge has no extent");
1471 };
1472 let pcurve: PlanarCurve = BSpline2d::rational(knots, control)?.into();
1473 let mut worst: f64 = 0.0;
1474 for i in 0..=4096 {
1475 let t = range.0 + (range.1 - range.0) * f64::from(i) / 4096.0;
1476 let q = pcurve.point_at(t, tol)?;
1477 let on = surface.point_at(q.x, q.y, tol)?;
1478 worst = worst.max(on.distance(curve.point_at(t, tol)?));
1479 }
1480 if worst > tol.confusion() * 100.0 {
1481 ogeom_bail!(
1482 NotDone,
1483 "a section edge's image on the skin strays {worst:.3e} from the edge"
1484 );
1485 }
1486 if worst > tol.confusion() {
1487 let widened = ogeom_core::Tolerance::new(worst + tol.confusion())?;
1488 model.widen(edge, widened)?;
1489 if let Some((a, b)) = edge_vertices(model, edge)? {
1490 model.widen(&a, widened)?;
1491 model.widen(&b, widened)?;
1492 }
1493 }
1494 Ok((pcurve, range))
1495}
1496
1497fn foot_on(section: &BSplineCurve, p: Point, guess: f64, tol: Tolerances) -> OgeomResult<f64> {
1500 let mut u = guess;
1501 for _ in 0..64 {
1502 let c = section.point_at(u, tol)?;
1503 let d = section.d1_at(u, tol)?;
1504 let speed = d.dot(d);
1505 if speed <= f64::MIN_POSITIVE {
1506 break;
1507 }
1508 let next = (u + (p - c).dot(d) / speed).clamp(0.0, 1.0);
1509 let moved = (next - u).abs();
1510 u = next;
1511 if moved <= 1e-15 {
1512 break;
1513 }
1514 }
1515 let off = section.point_at(u, tol)?.distance(p);
1516 if off > tol.confusion() * 10.0 {
1517 ogeom_bail!(
1518 NotDone,
1519 "a section edge's point {p:?} was not found on its exact form ({off:.3e} away)"
1520 );
1521 }
1522 Ok(u)
1523}
1524
1525struct Placements {
1528 motions: Vec<Transform>,
1530 breaks: Vec<usize>,
1533}
1534
1535fn swept_sheet(
1543 model: &mut Model,
1544 profile: &Section,
1545 motions: impl Fn(&Model, usize) -> OgeomResult<Placements>,
1546 target: f64,
1547 tol: Tolerances,
1548) -> OgeomResult<Shape> {
1549 let count = profile.edges.len();
1550 let mut density = 1;
1551 let mut reached = (f64::INFINITY, 0);
1552 loop {
1553 let Placements {
1554 motions: placed,
1555 breaks,
1556 } = motions(model, density)?;
1557 let skin_count = placed.len().div_ceil(2);
1558 if skin_count > MOST_SWEEP_SECTIONS {
1559 ogeom_bail!(
1560 NotDone,
1561 "the sweep's skin reached {:.3e} against a target of {target:.3e} through {} \
1562 sections",
1563 reached.0,
1564 reached.1
1565 );
1566 }
1567 let mut sections: Vec<Section> = Vec::with_capacity(skin_count);
1568 for (k, motion) in placed.iter().step_by(2).enumerate() {
1569 let mut edges = Vec::with_capacity(count);
1570 for piece in &profile.edges {
1571 edges.push(SectionEdge {
1572 edge: if k == 0 { piece.edge.clone() } else { None },
1573 curve: moved(&piece.curve, motion, tol)?,
1574 paced: piece.paced,
1575 });
1576 }
1577 sections.push(Section {
1578 edges,
1579 closed: profile.closed,
1580 });
1581 }
1582 let mut bounds = vec![0];
1583 bounds.extend(
1584 breaks
1585 .iter()
1586 .filter(|b| **b % 2 == 0)
1587 .map(|b| b / 2)
1588 .filter(|k| (1..skin_count - 1).contains(k)),
1589 );
1590 bounds.push(skin_count - 1);
1591 let spans: Vec<(usize, usize)> = bounds.windows(2).map(|w| (w[0], w[1])).collect();
1592 let mut surfaces: Vec<Vec<BSplineSurface>> = vec![Vec::with_capacity(spans.len()); count];
1593 let mut span_params = Vec::with_capacity(spans.len());
1594 for &(lo, hi) in &spans {
1595 let mut chords = Vec::with_capacity(hi - lo);
1596 for k in lo..hi {
1597 chords.push(section_chord(§ions[k], §ions[k + 1], k, tol)?);
1598 }
1599 let params = unit_params(&chords);
1600 for (e, per_span) in surfaces.iter_mut().enumerate() {
1601 let rows: Vec<Vec<Weighted<Point>>> = sections[lo..=hi]
1602 .iter()
1603 .map(|s| s.edges[e].curve.control_points().to_vec())
1604 .collect();
1605 let (v_knots, net) = interpolated(&rows, ¶ms, tol)?;
1606 per_span.push(surface_of(
1607 profile.edges[e].curve.knots(),
1608 v_knots,
1609 &net,
1610 tol,
1611 )?);
1612 }
1613 span_params.push(params);
1614 }
1615 let mut worst: f64 = 0.0;
1618 for (s, &(lo, hi)) in spans.iter().enumerate() {
1619 let params = &span_params[s];
1620 for (e, piece) in profile.edges.iter().enumerate() {
1621 let geometry: SurfaceGeometry = surfaces[e][s].clone().into();
1622 for k in lo..hi {
1623 let motion = &placed[2 * k + 1];
1624 let guess_v = f64::midpoint(params[k - lo], params[k - lo + 1]);
1625 for i in 0..=16 {
1626 let u = f64::from(i) / 16.0;
1627 let p = motion.apply(piece.curve.point_at(u, tol)?);
1628 let foot =
1629 ogeom_algo::project_on_surface_from(&geometry, p, (u, guess_v), tol)?;
1630 worst = worst.max(foot.distance);
1631 }
1632 }
1633 }
1634 }
1635 reached = (worst, skin_count);
1636 if worst <= target {
1637 return sheet(model, §ions, &surfaces, &spans, false, false, tol);
1638 }
1639 density *= 2;
1640 }
1641}
1642
1643fn sweep_stations(
1647 model: &Model,
1648 spine: &Shape,
1649 density: usize,
1650 tol: Tolerances,
1651) -> OgeomResult<Vec<SpineStation>> {
1652 let edges: Vec<Shape> = match model.kind_of(spine)? {
1653 ShapeType::Edge => vec![spine.clone()],
1654 ShapeType::Wire => model.ordered_children_of(spine)?,
1655 other => ogeom_bail!(
1656 Construction,
1657 "a sweep runs along an edge or a wire, not a {other:?}"
1658 ),
1659 };
1660 if edges.is_empty() {
1661 ogeom_bail!(Construction, "the spine has no edge to run along");
1662 }
1663 let mut out: Vec<SpineStation> = Vec::new();
1664 for (ei, edge) in edges.iter().enumerate() {
1665 let (curve, range) = spine_curve_of(model, edge)?;
1666 let curve = curve.transformed(&edge.transform(model.datums())?, tol)?;
1667 let reversed = edge.orientation() == Orientation::Reversed;
1668 let mut turning = 0.0_f64;
1669 let mut last: Option<Vector> = None;
1670 for i in 0..=16 {
1671 let t = range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
1672 let d = curve.d1_at(t, tol)?;
1673 if d.magnitude() <= tol.confusion() {
1674 continue;
1675 }
1676 let unit = d / d.magnitude();
1677 if let Some(prev) = last {
1678 turning += prev.dot(unit).clamp(-1.0, 1.0).acos();
1679 }
1680 last = Some(unit);
1681 }
1682 #[allow(
1683 clippy::cast_possible_truncation,
1684 clippy::cast_sign_loss,
1685 reason = "a station count, bounded"
1686 )]
1687 let count =
1688 (turning / (core::f64::consts::TAU / 64.0)).ceil().max(8.0) as usize * 2 * density;
1689 for i in 0..=count {
1690 #[allow(clippy::cast_precision_loss)]
1691 let f = i as f64 / count as f64;
1692 let t = if reversed {
1693 range.1 - (range.1 - range.0) * f
1694 } else {
1695 range.0 + (range.1 - range.0) * f
1696 };
1697 let p = curve.point_at(t, tol)?;
1698 let d = curve.d1_at(t, tol)?;
1699 if d.magnitude() <= tol.confusion() {
1700 ogeom_bail!(Construction, "the spine stands still at {p:?}");
1701 }
1702 let tangent = (if reversed { -d } else { d }) / d.magnitude();
1703 if let Some(prev) = out.last()
1704 && prev.at.distance(p) <= tol.confusion()
1705 {
1706 if i == 0 && prev.tangent.dot(tangent) >= 1.0 - 1e-12 {
1707 continue;
1708 }
1709 ogeom_bail!(
1710 Construction,
1711 "the spine turns a sharp corner at {p:?}; a sweep surface follows a \
1712 spine whose direction is continuous"
1713 );
1714 }
1715 out.push(SpineStation {
1716 at: p,
1717 tangent,
1718 edge: ei,
1719 t,
1720 });
1721 }
1722 }
1723 if out[0].at.distance(out[out.len() - 1].at) <= tol.confusion() {
1724 ogeom_bail!(
1725 Construction,
1726 "a sweep surface along a closed spine is not built; sweep along an open one"
1727 );
1728 }
1729 Ok(out)
1730}
1731
1732struct Rail {
1734 pieces: Vec<(Curve, (f64, f64), bool, f64)>,
1737 total: f64,
1738}
1739
1740impl Rail {
1741 fn read(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
1742 let edges = match model.kind_of(shape)? {
1743 ShapeType::Edge => vec![shape.clone()],
1744 ShapeType::Wire => model.ordered_children_of(shape)?,
1745 other => ogeom_bail!(Construction, "a rail is an edge or a wire, not a {other:?}"),
1746 };
1747 let mut pieces = Vec::with_capacity(edges.len());
1748 let mut total = 0.0;
1749 for edge in &edges {
1750 let (curve, range) = spine_curve_of(model, edge)?;
1751 let curve = curve.transformed(&edge.transform(model.datums())?, tol)?;
1752 let length = ogeom_algo::curve_length(&curve, range, tol)?;
1753 total += length;
1754 pieces.push((
1755 curve,
1756 range,
1757 edge.orientation() == Orientation::Reversed,
1758 length,
1759 ));
1760 }
1761 if total <= tol.confusion() {
1762 ogeom_bail!(Construction, "a rail has no length");
1763 }
1764 let rail = Self { pieces, total };
1765 let (head, tail) = (rail.at(0.0, tol)?.0, rail.at(1.0, tol)?.0);
1766 if head.distance(tail) <= tol.confusion() {
1767 ogeom_bail!(
1768 Construction,
1769 "a two-rail sweep runs along open rails; a rail is closed"
1770 );
1771 }
1772 Ok(rail)
1773 }
1774
1775 fn at(&self, f: f64, tol: Tolerances) -> OgeomResult<(Point, Vector)> {
1777 let mut along = self.total * f.clamp(0.0, 1.0);
1778 let last = self.pieces.len() - 1;
1779 for (i, (curve, range, reversed, length)) in self.pieces.iter().enumerate() {
1780 if along > *length && i < last {
1781 along -= length;
1782 continue;
1783 }
1784 let into = along.min(*length);
1785 let t = if *reversed {
1786 ogeom_algo::parameter_at_length(curve, *range, length - into, tol)?
1787 } else {
1788 ogeom_algo::parameter_at_length(curve, *range, into, tol)?
1789 };
1790 let d = curve.d1_at(t, tol)?;
1791 if d.magnitude() <= tol.confusion() {
1792 ogeom_bail!(Construction, "a rail stands still at {t}");
1793 }
1794 let tangent = (if *reversed { -d } else { d }) / d.magnitude();
1795 return Ok((curve.point_at(t, tol)?, tangent));
1796 }
1797 Err(ogeom_err!(Construction, "a rail has no edges"))
1798 }
1799}