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