1use std::sync::Arc;
18
19use ogeom_algo::{Built, History, curve_length, find_plane};
20use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
21use ogeom_geom::Curve;
22use ogeom_math::{Axis, Direction, Point, Transform};
23use ogeom_topo::{Filter, Model, Shape, ShapeType, VertexData, explore};
24
25use super::corners::law_cornered;
26use super::placement::{PipeContact, profile_in_contact};
27use super::{
28 PipeCorners, PipeLaw, SpineStation, WalkCurve, closed_pipe_shell, closes_on_a_corner,
29 densified, evenly_by, has_sharp_corner, law_loft, make_loft, make_pipe_shell_with, moved_ring,
30 placed_path, profile_square_at_start, shell_stations, spine_closes, spine_curve_of,
31 straight_legs,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct PipeEvolution {
39 pub scale: f64,
42 pub twist: f64,
45}
46
47#[derive(Debug, Clone, PartialEq)]
51pub enum SpineLaw {
52 Linear(f64),
54 Through(Vec<(f64, f64)>),
61 Function(SpineFunction),
67}
68
69#[derive(Clone)]
72pub struct SpineFunction(Arc<dyn Fn(f64) -> f64 + Send + Sync>);
73
74impl SpineFunction {
75 pub fn new(f: impl Fn(f64) -> f64 + Send + Sync + 'static) -> Self {
77 Self(Arc::new(f))
78 }
79
80 pub fn at(&self, lambda: f64) -> f64 {
82 (self.0)(lambda)
83 }
84}
85
86impl core::fmt::Debug for SpineFunction {
87 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88 f.write_str("SpineFunction(..)")
89 }
90}
91
92impl PartialEq for SpineFunction {
94 fn eq(&self, other: &Self) -> bool {
95 Arc::ptr_eq(&self.0, &other.0)
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
103pub struct PipeLaws {
104 pub scale: SpineLaw,
106 pub twist: SpineLaw,
110 pub contact: PipeContact,
113}
114
115impl Default for PipeLaws {
117 fn default() -> Self {
118 Self {
119 scale: SpineLaw::Linear(1.0),
120 twist: SpineLaw::Linear(0.0),
121 contact: PipeContact::AsPlaced,
122 }
123 }
124}
125
126impl From<PipeEvolution> for PipeLaws {
127 fn from(evolution: PipeEvolution) -> Self {
128 Self {
129 scale: SpineLaw::Linear(evolution.scale),
130 twist: SpineLaw::Linear(evolution.twist),
131 contact: PipeContact::AsPlaced,
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq)]
139pub(super) struct Ramp {
140 shares: Vec<f64>,
141 values: Vec<f64>,
142 slopes: Vec<f64>,
143 linear: bool,
145 function: Option<FunctionRead>,
147}
148
149#[derive(Debug, Clone, PartialEq)]
152struct FunctionRead {
153 law: SpineFunction,
154 from: f64,
155 to: f64,
156 gain: f64,
157 offset: f64,
158 tilt: f64,
159}
160
161impl FunctionRead {
162 fn at(&self, lambda: f64) -> f64 {
163 self.gain * self.law.at(self.from + (self.to - self.from) * lambda)
164 + self.offset
165 + self.tilt * lambda
166 }
167}
168
169const FUNCTION_READS: u32 = 64;
174
175impl Ramp {
176 pub(super) fn level(value: f64) -> Self {
178 Self {
179 shares: vec![0.0, 1.0],
180 values: vec![value, value],
181 slopes: vec![0.0, 0.0],
182 linear: true,
183 function: None,
184 }
185 }
186
187 fn of_function(f: &SpineFunction, start: f64, what: &str) -> OgeomResult<Self> {
190 let (first, last) = (f.at(0.0), f.at(1.0));
191 if first != start {
192 ogeom_bail!(
193 Construction,
194 "a {what} law given as a function gives {start} at the spine's start, \
195 not {first}"
196 );
197 }
198 if !last.is_finite() {
199 ogeom_bail!(Construction, "a {what} law takes finite values");
200 }
201 Ok(Self {
202 shares: vec![0.0, 1.0],
203 values: vec![first, last],
204 slopes: vec![0.0, 0.0],
205 linear: false,
206 function: Some(FunctionRead {
207 law: f.clone(),
208 from: 0.0,
209 to: 1.0,
210 gain: 1.0,
211 offset: 0.0,
212 tilt: 0.0,
213 }),
214 })
215 }
216
217 fn of(law: &SpineLaw, start: f64, what: &str) -> OgeomResult<Self> {
220 let mut shares = vec![0.0];
221 let mut values = vec![start];
222 match law {
223 SpineLaw::Linear(end) => {
224 shares.push(1.0);
225 values.push(*end);
226 }
227 SpineLaw::Through(points) => {
228 for &(share, value) in points {
229 shares.push(share);
230 values.push(value);
231 }
232 }
233 SpineLaw::Function(f) => return Self::of_function(f, start, what),
234 }
235 if values.iter().any(|v| !v.is_finite()) {
236 ogeom_bail!(Construction, "a {what} law takes finite values");
237 }
238 if shares
239 .windows(2)
240 .any(|w| w[1].partial_cmp(&w[0]) != Some(core::cmp::Ordering::Greater))
241 || shares[shares.len() - 1] != 1.0
242 {
243 ogeom_bail!(
244 Construction,
245 "a {what} law's shares of the spine rise strictly from naught to one, \
246 the last of them one"
247 );
248 }
249 let n = shares.len();
254 let secants: Vec<f64> = (0..n - 1)
255 .map(|k| (values[k + 1] - values[k]) / (shares[k + 1] - shares[k]))
256 .collect();
257 let mut slopes = vec![0.0; n];
258 slopes[0] = secants[0];
259 slopes[n - 1] = secants[n - 2];
260 for k in 1..n - 1 {
261 if secants[k - 1] * secants[k] > 0.0 {
262 slopes[k] = f64::midpoint(secants[k - 1], secants[k]);
263 }
264 }
265 for k in 0..n - 1 {
266 if secants[k] == 0.0 {
267 slopes[k] = 0.0;
268 slopes[k + 1] = 0.0;
269 continue;
270 }
271 let (a, b) = (slopes[k] / secants[k], slopes[k + 1] / secants[k]);
272 let reach = a.hypot(b);
273 if reach > 3.0 {
274 slopes[k] = 3.0 * a / reach * secants[k];
275 slopes[k + 1] = 3.0 * b / reach * secants[k];
276 }
277 }
278 Ok(Self {
279 linear: matches!(law, SpineLaw::Linear(_)),
280 shares,
281 values,
282 slopes,
283 function: None,
284 })
285 }
286
287 fn span(&self, lambda: f64) -> (usize, f64) {
289 let k = self.shares[1..self.shares.len() - 1].partition_point(|s| *s < lambda);
290 let t = (lambda - self.shares[k]) / (self.shares[k + 1] - self.shares[k]);
291 (k, t)
292 }
293
294 pub(super) fn at(&self, lambda: f64) -> f64 {
296 let lambda = lambda.clamp(0.0, 1.0);
297 if let Some(f) = &self.function {
298 return f.at(lambda);
299 }
300 if self.linear {
301 return self.values[0] + (self.values[1] - self.values[0]) * lambda;
302 }
303 let (k, t) = self.span(lambda);
304 if self.values[k] == self.values[k + 1]
305 && self.slopes[k] == 0.0
306 && self.slopes[k + 1] == 0.0
307 {
308 return self.values[k];
309 }
310 let h = self.shares[k + 1] - self.shares[k];
311 let (t2, t3) = (t * t, t * t * t);
312 (2.0 * t3 - 3.0 * t2 + 1.0) * self.values[k]
313 + (t3 - 2.0 * t2 + t) * h * self.slopes[k]
314 + (3.0 * t2 - 2.0 * t3) * self.values[k + 1]
315 + (t3 - t2) * h * self.slopes[k + 1]
316 }
317
318 fn slope_at(&self, lambda: f64) -> f64 {
321 let lambda = lambda.clamp(0.0, 1.0);
322 if self.linear {
323 return self.values[1] - self.values[0];
324 }
325 let (k, t) = self.span(lambda);
326 let h = self.shares[k + 1] - self.shares[k];
327 let t2 = t * t;
328 ((6.0 * t2 - 6.0 * t) * (self.values[k] - self.values[k + 1])) / h
329 + (3.0 * t2 - 4.0 * t + 1.0) * self.slopes[k]
330 + (3.0 * t2 - 2.0 * t) * self.slopes[k + 1]
331 }
332
333 fn window(&self, from: f64, to: f64) -> Self {
337 let width = to - from;
338 if let Some(f) = &self.function {
339 let span = f.to - f.from;
340 let read = FunctionRead {
341 from: f.from + span * from,
342 to: f.from + span * to,
343 offset: f.offset + f.tilt * from,
344 tilt: f.tilt * width,
345 ..f.clone()
346 };
347 return Self {
348 shares: vec![0.0, 1.0],
349 values: vec![read.at(0.0), read.at(1.0)],
350 slopes: vec![0.0, 0.0],
351 linear: false,
352 function: Some(read),
353 };
354 }
355 if self.linear {
356 return Self {
357 shares: vec![0.0, 1.0],
358 values: vec![self.at(from), self.at(to)],
359 slopes: vec![0.0, 0.0],
360 linear: true,
361 function: None,
362 };
363 }
364 let mut shares = vec![0.0];
365 let mut values = vec![self.at(from)];
366 let mut slopes = vec![self.slope_at(from) * width];
367 for k in 0..self.shares.len() {
368 if self.shares[k] > from && self.shares[k] < to {
369 shares.push((self.shares[k] - from) / width);
370 values.push(self.values[k]);
371 slopes.push(self.slopes[k] * width);
372 }
373 }
374 shares.push(1.0);
375 values.push(self.at(to));
376 slopes.push(self.slope_at(to) * width);
377 Self {
378 shares,
379 values,
380 slopes,
381 linear: false,
382 function: None,
383 }
384 }
385
386 fn mapped(mut self, f: impl Fn(f64) -> f64, rate: f64) -> Self {
389 if let Some(read) = &mut self.function {
390 read.gain *= rate;
391 read.offset = f(read.offset);
392 read.tilt *= rate;
393 }
394 for v in &mut self.values {
395 *v = f(*v);
396 }
397 for m in &mut self.slopes {
398 *m *= rate;
399 }
400 self
401 }
402
403 pub(super) fn end(&self) -> f64 {
405 self.values[self.values.len() - 1]
406 }
407
408 fn still_ends(&self) -> (f64, f64) {
412 let n = self.shares.len();
413 if self.function.is_some() {
414 return (0.0, 1.0);
417 }
418 if self.linear {
419 return if self.values[0] == self.values[1] {
420 (1.0, 0.0)
421 } else {
422 (0.0, 1.0)
423 };
424 }
425 let flat = |k: usize| {
426 self.values[k] == self.values[k + 1]
427 && self.slopes[k] == 0.0
428 && self.slopes[k + 1] == 0.0
429 };
430 let mut lead = 0;
431 while lead + 1 < n && flat(lead) {
432 lead += 1;
433 }
434 let mut trail = n - 1;
435 while trail > 0 && flat(trail - 1) {
436 trail -= 1;
437 }
438 (self.shares[lead], self.shares[trail])
439 }
440
441 pub(super) fn is_level(&self, value: f64) -> bool {
443 self.function.is_none() && self.values.iter().all(|v| *v == value)
444 }
445
446 fn is_linear(&self) -> bool {
448 self.linear
449 }
450
451 fn function_reads(&self) -> Option<Vec<f64>> {
454 self.function.as_ref().map(|f| {
455 (0..=FUNCTION_READS)
456 .map(|k| f.at(f64::from(k) / f64::from(FUNCTION_READS)))
457 .collect()
458 })
459 }
460
461 fn least(&self) -> f64 {
464 let reads = self.function_reads().unwrap_or_else(|| self.values.clone());
465 reads.iter().copied().fold(f64::INFINITY, |a, b| {
466 if b.is_nan() {
467 f64::NEG_INFINITY
468 } else {
469 a.min(b)
470 }
471 })
472 }
473
474 fn variation(&self) -> f64 {
478 let reads = self.function_reads().unwrap_or_else(|| self.values.clone());
479 reads.windows(2).map(|w| (w[1] - w[0]).abs()).sum()
480 }
481
482 fn ending_at(&self, end: f64) -> OgeomResult<Self> {
484 if let Some(f) = &self.function {
485 let read = FunctionRead {
486 tilt: f.tilt + end - f.at(1.0),
487 ..f.clone()
488 };
489 return Ok(Self {
490 values: vec![read.at(0.0), read.at(1.0)],
491 function: Some(read),
492 ..self.clone()
493 });
494 }
495 let points: Vec<(f64, f64)> = self.shares[1..]
496 .iter()
497 .zip(&self.values[1..])
498 .map(|(s, v)| (*s, *v))
499 .collect();
500 let mut points = points;
501 if let Some(last) = points.last_mut() {
502 last.1 = end;
503 }
504 let law = if self.linear {
505 SpineLaw::Linear(end)
506 } else {
507 SpineLaw::Through(points)
508 };
509 Self::of(&law, self.values[0], "twist")
510 }
511}
512
513#[derive(Debug, Clone)]
515pub(super) struct Evolution {
516 scale: Ramp,
517 twist: Ramp,
518}
519
520impl Evolution {
521 pub(super) fn local(&self, lambda: f64, tol: Tolerances) -> OgeomResult<Transform> {
525 let (scale, twist) = (self.scale.at(lambda), self.twist.at(lambda));
526 if self.scale.function.is_some() && !(scale.is_finite() && scale > 0.0) {
528 ogeom_bail!(
529 Construction,
530 "a scale law given as a function stays above naught; it gives {scale} \
531 where the sweep reads it"
532 );
533 }
534 if self.twist.function.is_some() && !twist.is_finite() {
535 ogeom_bail!(
536 Construction,
537 "a twist law takes finite values; it gives {twist} where the sweep reads it"
538 );
539 }
540 let turn = Transform::rotation(
541 Axis {
542 location: Point::ORIGIN,
543 direction: Direction::Z,
544 },
545 twist,
546 );
547 Ok(turn * Transform::scaling(Point::ORIGIN, scale, tol)?)
548 }
549
550 pub(super) fn still_ends(&self) -> (f64, f64) {
553 let (scale, twist) = (self.scale.still_ends(), self.twist.still_ends());
554 (scale.0.min(twist.0), scale.1.max(twist.1))
555 }
556
557 pub(super) fn is_still(&self) -> bool {
559 self.scale.is_level(1.0) && self.twist.is_level(0.0)
560 }
561
562 pub(super) fn window(&self, from: f64, to: f64) -> Self {
569 let (s0, t0) = (self.scale.at(from), self.twist.at(from));
570 Self {
571 scale: self.scale.window(from, to).mapped(|v| v / s0, 1.0 / s0),
572 twist: self.twist.window(from, to).mapped(|v| v - t0, 1.0),
573 }
574 }
575}
576
577pub(super) struct SpineLengths {
579 before: Vec<f64>,
580 total: f64,
581}
582
583impl SpineLengths {
584 pub(super) fn of(curves: &[WalkCurve], tol: Tolerances) -> OgeomResult<Self> {
586 let mut before = Vec::with_capacity(curves.len());
587 let mut total = 0.0;
588 for (curve, range, _) in curves {
589 before.push(total);
590 total += curve_length(curve, *range, tol)?;
591 }
592 if total <= tol.confusion() {
593 ogeom_bail!(Construction, "the spine has no length");
594 }
595 Ok(Self { before, total })
596 }
597
598 pub(super) fn fraction(
600 &self,
601 curves: &[WalkCurve],
602 edge: usize,
603 t: f64,
604 tol: Tolerances,
605 ) -> OgeomResult<f64> {
606 let (curve, range, reversed) = &curves[edge];
607 let run = if *reversed {
608 curve_length(curve, (t, range.1), tol)?
609 } else {
610 curve_length(curve, (range.0, t), tol)?
611 };
612 Ok(((self.before[edge] + run) / self.total).clamp(0.0, 1.0))
613 }
614}
615
616pub fn make_pipe_shell_evolving(
657 model: &mut Model,
658 profile: &Shape,
659 spine: &Shape,
660 law: &PipeLaw<'_>,
661 evolution: PipeEvolution,
662 tolerance: f64,
663 tol: Tolerances,
664) -> OgeomResult<Built> {
665 make_pipe_shell_laws(
666 model,
667 profile,
668 spine,
669 law,
670 &evolution.into(),
671 PipeCorners::Mitre,
672 tolerance,
673 tol,
674 )
675}
676
677#[allow(clippy::too_many_arguments)]
717pub fn make_pipe_shell_laws(
718 model: &mut Model,
719 profile: &Shape,
720 spine: &Shape,
721 law: &PipeLaw<'_>,
722 laws: &PipeLaws,
723 corners: PipeCorners,
724 tolerance: f64,
725 tol: Tolerances,
726) -> OgeomResult<Built> {
727 if let SpineLaw::Linear(scale) = laws.scale
728 && (!scale.is_finite() || scale < 0.0)
729 {
730 ogeom_bail!(
731 Construction,
732 "a section scales to {scale} at the spine's end; a scale is naught or more"
733 );
734 }
735 if let SpineLaw::Linear(twist) = laws.twist
736 && !twist.is_finite()
737 {
738 ogeom_bail!(Construction, "a twist of {twist} is not an angle");
739 }
740 let evolution = Evolution {
741 scale: Ramp::of(&laws.scale, 1.0, "scale")?,
742 twist: Ramp::of(&laws.twist, 0.0, "twist")?,
743 };
744 let placed_spine = placed_path(model, spine, tol)?;
746 if placed_spine != *spine {
747 let mut built = make_pipe_shell_laws(
748 model,
749 profile,
750 &placed_spine,
751 law,
752 laws,
753 corners,
754 tolerance,
755 tol,
756 )?;
757 built.history.generate(spine, built.shape.clone());
758 return Ok(built);
759 }
760 let placed = profile_in_contact(model, profile, spine, laws.contact, tol)?;
761 if placed != *profile {
762 let mut built = make_pipe_shell_laws(
763 model,
764 &placed,
765 spine,
766 law,
767 &PipeLaws {
768 contact: PipeContact::AsPlaced,
769 ..laws.clone()
770 },
771 corners,
772 tolerance,
773 tol,
774 )?;
775 built.history.generate(profile, built.shape.clone());
776 return Ok(built);
777 }
778 if matches!(laws.scale, SpineLaw::Through(_) | SpineLaw::Function(_))
779 && evolution.scale.least() <= 0.0
780 {
781 ogeom_bail!(
782 Construction,
783 "a scale law through values or given as a function stays above naught; a \
784 section shrinks to a point only linearly"
785 );
786 }
787 if evolution.is_still() {
788 return make_pipe_shell_with(model, profile, spine, law, corners, tolerance, tol);
789 }
790 let rings: Vec<Shape> = match model.kind_of(profile)? {
791 ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
792 ShapeType::Wire => vec![profile.clone()],
793 other => ogeom_bail!(
794 Construction,
795 "a pipe shell sweeps a planar wire or face, not a {other:?}"
796 ),
797 };
798 let Some(plane) = find_plane(model, profile, tol)? else {
799 ogeom_bail!(Construction, "a pipe shell sweeps a planar profile");
800 };
801 let stations = shell_stations(model, spine, tol)?;
802 profile_square_at_start(model, &plane, spine, stations[0].at, tol)?;
803 if rings.len() > 1 && evolution.scale.end() == 0.0 {
804 ogeom_bail!(
805 Construction,
806 "a profile with holes cannot close on a point; its holes would meet"
807 );
808 }
809 if spine_closes(model, spine, tol)?
810 && !has_sharp_corner(model, spine, tol)?
811 && !closes_on_a_corner(model, spine, tol)?
812 && !matches!(law, PipeLaw::Fixed)
813 {
814 if !evolution.scale.is_level(1.0) {
815 ogeom_bail!(
816 Construction,
817 "a section scaling round a closed spine would not come home to itself; \
818 it scales along an open one"
819 );
820 }
821 let end = evolution.twist.end();
824 let turns = end / core::f64::consts::TAU;
825 let twist = if (turns - turns.round()).abs() * core::f64::consts::TAU > tol.angular() {
826 evolution.twist
827 } else {
828 evolution
829 .twist
830 .ending_at(turns.round() * core::f64::consts::TAU)?
831 };
832 return closed_law_pipe(model, profile, spine, law, &twist, tolerance, tol);
833 }
834 if !matches!(law, PipeLaw::Fixed)
835 && (has_sharp_corner(model, spine, tol)? || closes_on_a_corner(model, spine, tol)?)
836 {
837 return law_cornered(
838 model,
839 profile,
840 spine,
841 law,
842 corners,
843 Some(&evolution),
844 tolerance,
845 tol,
846 );
847 }
848 let shape = evolved_run(
849 model, profile, spine, law, &evolution, false, tolerance, tol,
850 )?;
851 let mut history = History::new();
852 history.generate(spine, shape.clone());
853 history.generate(profile, shape.clone());
854 Ok(Built::new(shape, history))
855}
856
857#[allow(clippy::too_many_arguments)]
862pub(super) fn evolved_run(
863 model: &mut Model,
864 profile: &Shape,
865 spine: &Shape,
866 law: &PipeLaw<'_>,
867 evolution: &Evolution,
868 exact_ends: bool,
869 tolerance: f64,
870 tol: Tolerances,
871) -> OgeomResult<Shape> {
872 let rings: Vec<Shape> = match model.kind_of(profile)? {
873 ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
874 _ => vec![profile.clone()],
875 };
876 let mut solids = Vec::with_capacity(rings.len());
877 for ring in &rings {
878 solids.push(evolving_ring(
879 model, ring, spine, law, evolution, exact_ends, tolerance, tol,
880 )?);
881 }
882 let mut shape = solids[0].clone();
883 for hole in &solids[1..] {
884 shape = ogeom_bool::cut(model, &shape, hole, tol)?.shape;
885 }
886 Ok(shape)
887}
888
889#[allow(clippy::too_many_arguments)]
891fn evolving_ring(
892 model: &mut Model,
893 ring: &Shape,
894 spine: &Shape,
895 law: &PipeLaw<'_>,
896 evolution: &Evolution,
897 exact_ends: bool,
898 tolerance: f64,
899 tol: Tolerances,
900) -> OgeomResult<Shape> {
901 let level = matches!(
902 law,
903 PipeLaw::RotationMinimizing | PipeLaw::Binormal(_) | PipeLaw::Fixed
904 );
905 let scale = evolution.scale.end();
906 if evolution.twist.is_level(0.0)
907 && evolution.scale.is_linear()
908 && level
909 && let Some(legs) = straight_legs(model, spine, tol)?
910 && legs.len() == 1
911 {
912 let (start, run) = legs[0];
913 let end = if scale == 0.0 {
914 model.add_vertex(VertexData::new(start + run))
915 } else {
916 let motion = Transform::translation(run) * Transform::scaling(start, scale, tol)?;
917 moved_ring(model, ring, &motion, tol)?
918 };
919 let shape = if scale == 0.0 || analytic_ring(model, ring, start, tol)? {
922 make_loft(model, ring, &end, tol)?.shape
923 } else {
924 crate::loft::make_loft_solid(model, &[ring.clone(), end], &[], true, tol)?.shape
925 };
926 return Ok(shape);
927 }
928 if matches!(law, PipeLaw::Fixed) {
929 ogeom_bail!(
930 Construction,
931 "a fixed section scales or twists only down one straight edge, unturned"
932 );
933 }
934 if scale == 0.0 {
935 ogeom_bail!(
936 Construction,
937 "a section shrinks to a point only down one straight edge, unturned"
938 );
939 }
940 let stations = shell_stations(model, spine, tol)?;
941 let ring_spine =
942 stations[0].at.distance(stations[stations.len() - 1].at) <= tol.confusion() * 10.0;
943 if ring_spine || has_sharp_corner(model, spine, tol)? {
944 ogeom_bail!(
945 Construction,
946 "a scaling or twisting section sweeps an open spine with no sharp corner"
947 );
948 }
949 let turned = densified(model, spine, stations.clone(), law, tolerance, tol)?.len();
952 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
953 let twisted = (evolution.twist.variation() / super::MOST_TURN).ceil() as usize + 1;
954 let wanted = turned.max(twisted);
955 let lengths = edge_lengths(model, spine, tol)?;
956 let total: f64 = lengths.iter().sum();
957 #[allow(
958 clippy::cast_precision_loss,
959 clippy::cast_possible_truncation,
960 clippy::cast_sign_loss
961 )]
962 let share = |e: usize| -> u32 {
963 let length = lengths.get(e).copied().unwrap_or(0.0);
964 ((wanted as f64 * length / total).ceil() as u32).max(2)
965 };
966 let stations = evenly_by(model, spine, stations, &share, false, tol)?;
967 Ok(law_loft(
968 model,
969 ring,
970 spine,
971 &stations,
972 law,
973 Some(evolution),
974 exact_ends,
975 tolerance,
976 tol,
977 )?
978 .shape)
979}
980
981pub(super) fn closed_law_pipe(
987 model: &mut Model,
988 profile: &Shape,
989 spine: &Shape,
990 law: &PipeLaw<'_>,
991 twist: &Ramp,
992 tolerance: f64,
993 tol: Tolerances,
994) -> OgeomResult<Built> {
995 let stations = shell_stations(model, spine, tol)?;
996 let turned = densified(model, spine, stations.clone(), law, tolerance, tol)?.len();
997 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
998 let twisted = (twist.variation() / super::MOST_TURN).ceil() as usize + 1;
999 let wanted = turned.max(twisted);
1000 let lengths = edge_lengths(model, spine, tol)?;
1001 let total: f64 = lengths.iter().sum();
1002 #[allow(
1003 clippy::cast_precision_loss,
1004 clippy::cast_possible_truncation,
1005 clippy::cast_sign_loss
1006 )]
1007 let share = |e: usize| -> u32 {
1008 let length = lengths.get(e).copied().unwrap_or(0.0);
1009 ((wanted as f64 * length / total).ceil() as u32).max(2)
1010 };
1011 let stations = evenly_by(model, spine, stations, &share, false, tol)?;
1012 let mut kept: Vec<SpineStation> = Vec::with_capacity(stations.len());
1015 for station in stations {
1016 if kept
1017 .last()
1018 .is_some_and(|p| p.at.distance(station.at) <= tol.confusion())
1019 {
1020 continue;
1021 }
1022 kept.push(station);
1023 }
1024 closed_pipe_shell(model, profile, spine, kept, law, twist, tolerance, tol)
1025}
1026
1027fn analytic_ring(
1030 model: &Model,
1031 ring: &Shape,
1032 axis_at: Point,
1033 tol: Tolerances,
1034) -> OgeomResult<bool> {
1035 let edges = explore(model, ring, Filter::OfType(ShapeType::Edge))?;
1036 let mut kinds = Vec::with_capacity(edges.len());
1037 for edge in &edges {
1038 kinds.push(spine_curve_of(model, edge)?.0);
1039 }
1040 Ok(kinds.iter().all(|c| matches!(c, Curve::Line(_)))
1041 || (kinds.len() == 1
1042 && matches!(&kinds[0], Curve::Circle(c)
1043 if c.circle().centre().distance(axis_at) <= tol.confusion())))
1044}
1045
1046fn edge_lengths(model: &Model, spine: &Shape, tol: Tolerances) -> OgeomResult<Vec<f64>> {
1048 let edges: Vec<Shape> = match model.kind_of(spine)? {
1049 ShapeType::Edge => vec![spine.clone()],
1050 _ => model.ordered_children_of(spine)?,
1051 };
1052 let mut out = Vec::with_capacity(edges.len());
1053 for edge in &edges {
1054 let (curve, range) = spine_curve_of(model, edge)?;
1055 out.push(curve_length(&curve, range, tol)?);
1056 }
1057 Ok(out)
1058}