1use crate::mass_properties::{GAUSS_W, GAUSS_X};
23use crate::{interpolate_curve, interpolate_curve_closed, NurbsCurve, Vec3};
24use crate::{Approximation, KernelRefusal, KernelStage, OrRefuse};
25
26pub const SWEEP_DEFAULT_TOLERANCE: f64 = 1e-4;
37
38pub const SWEEP_MAX_STATIONS: usize = 1024;
41
42pub const SWEEP_REFINEMENT_ROUNDS: usize = 6;
47
48#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct StationParam {
53 pub segment: usize,
54 pub t: f64,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct StationBudget {
60 pub initial: usize,
62 pub stations: usize,
64 pub rounds: usize,
66 pub at_cap: bool,
68 pub rounds_exhausted: bool,
71}
72
73#[derive(Clone, Debug, PartialEq)]
75pub struct SweepStationReport {
76 pub budget: StationBudget,
77 pub tolerance: f64,
79 pub spine_deviation: f64,
84 pub frame_deviation: f64,
93 pub wall_deviation: f64,
96 pub reach: f64,
98 pub built_volume: f64,
100 pub ideal_volume: f64,
104 pub volume_residual: f64,
106 pub crude_bound: bool,
109}
110
111impl SweepStationReport {
112 pub fn approximation(&self, body: impl Into<String>) -> Approximation {
115 let cap = if self.budget.at_cap {
116 format!(
117 "; refinement stopped at the {SWEEP_MAX_STATIONS}-station cap, so the run does \
118 not meet the requested tolerance"
119 )
120 } else if self.budget.rounds_exhausted {
121 format!(
122 "; refinement stopped after its {SWEEP_REFINEMENT_ROUNDS} rounds still over the \
123 requested tolerance"
124 )
125 } else {
126 String::new()
127 };
128 let volume = if self.crude_bound {
129 format!(
130 "the volume is within {:.3e} mm³ of the kinematic sweep's (lateral area × wall \
131 deviation)",
132 self.volume_residual
133 )
134 } else {
135 format!(
136 "the volume by quadrature is {:.9} against the kinematic sweep's {:.9} \
137 (residual {:.3e} mm³)",
138 self.built_volume, self.ideal_volume, self.volume_residual
139 )
140 };
141 Approximation {
142 code: "sweep.stations".to_string(),
143 body: body.into(),
144 measured: self.spine_deviation,
145 bar: self.tolerance,
146 volume_bound: Some(self.volume_residual),
147 edges: Vec::new(),
148 budget: None,
149 message: format!(
150 "the swept wall is lofted through {} stations ({} from the floors, {} refinement \
151 round(s)); the loft's centre track stands within {:.3e} mm of the path against \
152 the requested {:.1e}{cap}; its frame stands within {:.3e} per unit reach of the \
153 transported frame (a reach of {:.3}), so the wall is within {:.3e} mm of the \
154 kinematic sweep's; {volume}",
155 self.budget.stations,
156 self.budget.initial,
157 self.budget.rounds,
158 self.spine_deviation,
159 self.tolerance,
160 self.frame_deviation,
161 self.reach,
162 self.wall_deviation,
163 ),
164 }
165 }
166}
167
168thread_local! {
169 static REFINEMENT_STUBBED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
173}
174
175pub(super) fn refinement_enabled() -> bool {
179 !REFINEMENT_STUBBED.with(|stub| stub.get())
180 && std::env::var("BREP_SWEEP_STATION_REFINEMENT").as_deref() != Ok("0")
181}
182
183
184pub(super) fn chord_parameters(points: &[Vec3], closed: bool) -> Vec<f64> {
187 let count = points.len();
188 let mut parameters = Vec::with_capacity(count + usize::from(closed));
189 let mut total = 0.0;
190 parameters.push(0.0);
191 for index in 1..count {
192 total += points[index].sub(points[index - 1]).length();
193 parameters.push(total);
194 }
195 if closed {
196 total += points[0].sub(points[count - 1]).length();
197 parameters.push(total);
198 }
199 if total > 0.0 {
200 for parameter in parameters.iter_mut() {
201 *parameter /= total;
202 }
203 }
204 parameters
205}
206
207pub(super) fn loft_parameters(sections: &[Vec<NurbsCurve>], closed: bool) -> Result<Vec<f64>, KernelRefusal> {
212 let count = sections.len();
213 let spans = count - 1 + usize::from(closed);
214 let mut accumulated = vec![0.0; spans + 1];
215 let mut columns = 0usize;
216 for curve_index in 0..sections[0].len() {
217 for control_index in 0..sections[0][curve_index].control_points.len() {
218 let mut chords = vec![0.0; spans + 1];
219 let mut total = 0.0;
220 for station in 1..=spans {
221 let previous = sections[station - 1][curve_index].control_points[control_index]
222 .point()
223 .or_refuse(KernelStage::Refine, "control_point")?;
224 let current = sections[station % count][curve_index].control_points[control_index]
225 .point()
226 .or_refuse(KernelStage::Refine, "control_point")?;
227 total += current.sub(previous).length();
228 chords[station] = total;
229 }
230 if total <= 1e-6 {
231 continue;
232 }
233 for station in 0..=spans {
234 accumulated[station] += chords[station] / total;
235 }
236 columns += 1;
237 }
238 }
239 if columns == 0 {
240 return Err(KernelRefusal::input(
241 KernelStage::Classify,
242 "coincident_sections",
243 "sweepSolid: the placed sections coincide",
244 ));
245 }
246 let mut parameters: Vec<f64> = accumulated.iter().map(|sum| sum / columns as f64).collect();
247 parameters[0] = 0.0;
248 parameters[spans] = 1.0;
249 Ok(parameters)
250}
251
252pub(super) fn interpolant(points: &[Vec3], parameters: &[f64], closed: bool) -> Result<NurbsCurve, String> {
256 if closed {
257 interpolate_curve_closed(points, parameters)
258 } else {
259 interpolate_curve(points, 3usize.min(points.len() - 1), parameters)
260 }
261}
262
263fn nearest_parameter(curve: &NurbsCurve, point: Vec3, guess: f64) -> Result<(f64, f64), String> {
268 let [lo, hi] = curve.domain()?;
269 let mut u = guess.clamp(lo, hi);
270 let mut distance = curve.evaluate(u)?.sub(point).length();
271 for _ in 0..6 {
272 let derivatives = curve.derivatives_small(u, 2)?;
273 let offset = derivatives[0].sub(point);
274 let numerator = offset.dot(derivatives[1]);
275 let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
276 if denominator.abs() <= 1e-300 {
277 break;
278 }
279 let next = (u - numerator / denominator).clamp(lo, hi);
280 let next_distance = curve.evaluate(next)?.sub(point).length();
281 if next_distance >= distance || (next - u).abs() <= 1e-15 * hi.abs().max(1.0) {
282 break;
283 }
284 u = next;
285 distance = next_distance;
286 }
287 Ok((u, distance))
288}
289
290fn span_piece(chain: &[NurbsCurve], params: &[StationParam], k: usize, closed: bool) -> Result<(usize, f64, f64), String> {
295 let count = params.len();
296 let from = params[k];
297 let next = (k + 1) % count;
298 let end = if !closed && k + 1 == count {
299 return Err("no span past the last station of an open run".into());
300 } else if next != 0 && params[next].segment == from.segment {
301 params[next].t
302 } else {
303 chain[from.segment].domain()?[1]
304 };
305 Ok((from.segment, from.t, end))
306}
307
308const SPAN_FRACTIONS: [f64; 3] = [0.25, 0.5, 0.75];
310
311pub(super) fn spine_deviations(
316 chain: &[NurbsCurve],
317 params: &[StationParam],
318 points: &[Vec3],
319 closed: bool,
320) -> Result<Vec<f64>, String> {
321 let parameters = chord_parameters(points, closed);
322 let spine = interpolant(points, ¶meters, closed)?;
323 let spans = points.len() - 1 + usize::from(closed);
324 let mut deviations = Vec::with_capacity(spans);
325 for k in 0..spans {
326 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
327 let (u_a, u_b) = (parameters[k], parameters[k + 1]);
328 let mut worst: f64 = 0.0;
329 for fraction in SPAN_FRACTIONS {
330 let truth = chain[segment].evaluate(t_a + (t_b - t_a) * fraction)?;
331 let (_, distance) = nearest_parameter(&spine, truth, u_a + (u_b - u_a) * fraction)?;
332 worst = worst.max(distance);
333 }
334 deviations.push(worst);
335 }
336 Ok(deviations)
337}
338
339pub(super) fn refine_parameters(
348 chain: &[NurbsCurve],
349 per_segment: &mut [Vec<f64>],
350 params: &[StationParam],
351 deviations: &[f64],
352 tolerance: f64,
353 closed: bool,
354 emitted: usize,
355) -> Result<(usize, bool), String> {
356 let count = params.len();
357 let mut wanted: Vec<(usize, usize)> = Vec::new();
358 for (k, &deviation) in deviations.iter().enumerate() {
359 if deviation > tolerance {
360 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
361 let [t0, t1] = chain[segment].domain()?;
362 let next = (k + 1) % count;
363 let at_joint = (t_a == t0 && (k > 0 || closed))
364 || t_b == t1 && (next != 0 || closed) && (next == 0 || params[next].segment != segment);
365 let exponent = if at_joint { 0.5 } else { 0.25 };
366 let pieces = ((deviation / tolerance).powf(exponent).ceil() as usize).clamp(2, 8);
367 wanted.push((k, pieces));
368 }
369 }
370 if wanted.is_empty() {
371 return Ok((0, false));
372 }
373 let asked: usize = wanted.iter().map(|(_, pieces)| pieces - 1).sum();
374 let room = SWEEP_MAX_STATIONS.saturating_sub(emitted);
375 let at_cap = asked > room;
376 if at_cap {
377 wanted.sort_by(|a, b| deviations[b.0].total_cmp(&deviations[a.0]));
380 }
381 let mut inserted = 0usize;
382 for (k, pieces) in wanted {
383 let mut pieces = pieces;
384 while pieces > 1 && inserted + pieces - 1 > room {
385 pieces -= 1;
386 }
387 if pieces < 2 {
388 continue;
389 }
390 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
391 for step in 1..pieces {
392 per_segment[segment].push(t_a + (t_b - t_a) * step as f64 / pieces as f64);
393 }
394 inserted += pieces - 1;
395 }
396 for list in per_segment.iter_mut() {
397 list.sort_by(|a, b| a.total_cmp(b));
398 list.dedup();
399 }
400 Ok((inserted, at_cap))
401}
402
403pub(super) fn section_moments(profile: &[NurbsCurve], origin: Vec3, pu: Vec3, pv: Vec3) -> Result<(f64, f64, f64), String> {
410 let (mut area, mut moment_a, mut moment_b) = (0.0, 0.0, 0.0);
411 for curve in profile {
412 for pair in curve.knots.windows(2) {
413 if pair[1] <= pair[0] {
414 continue;
415 }
416 let half = 0.5 * (pair[1] - pair[0]);
417 let middle = 0.5 * (pair[1] + pair[0]);
418 for index in 0..GAUSS_X.len() {
419 let derivatives = curve.derivatives_small(middle + half * GAUSS_X[index], 1)?;
420 let local = derivatives[0].sub(origin);
421 let (a, b) = (local.dot(pu), local.dot(pv));
422 let (da, db) = (derivatives[1].dot(pu), derivatives[1].dot(pv));
423 let weight = GAUSS_W[index] * half;
424 area += weight * 0.5 * (a * db - b * da);
425 moment_a += weight * 0.5 * a * a * db;
426 moment_b -= weight * 0.5 * b * b * da;
427 }
428 }
429 }
430 Ok((area, moment_a, moment_b))
431}
432
433pub(super) fn section_reach(profile: &[NurbsCurve], origin: Vec3) -> f64 {
436 profile
437 .iter()
438 .flat_map(|curve| curve.control_points.iter())
439 .filter_map(|point| point.point().ok())
440 .map(|point| point.sub(origin).length())
441 .fold(0.0, f64::max)
442}
443
444fn rotated(x: Vec3, axis: Vec3, angle: f64) -> Vec3 {
446 let (sin, cos) = angle.sin_cos();
447 x.scale(cos).add(axis.cross(x).scale(sin)).add(axis.scale(axis.dot(x) * (1.0 - cos)))
448}
449
450pub(super) struct PlacedRun<'a> {
452 pub chain: &'a [NurbsCurve],
453 pub params: &'a [StationParam],
454 pub points: &'a [Vec3],
455 pub tangents: &'a [Vec3],
456 pub axes: &'a [(Vec3, Vec3)],
458 pub centres: &'a [Vec3],
460 pub e1: &'a [Vec3],
462 pub e2: &'a [Vec3],
463 pub pu_in_frame: [f64; 3],
466 pub pv_in_frame: [f64; 3],
467 pub closed: bool,
468 pub sections: &'a [Vec<NurbsCurve>],
469 pub moments: (f64, f64, f64),
471 pub reach: f64,
472 pub shift: usize,
474 pub budget: StationBudget,
475 pub tolerance: f64,
476}
477
478fn rmf_step(r: Vec3, t: Vec3, t_next: Vec3, step: Vec3) -> Option<Vec3> {
481 let c1 = step.dot(step);
482 let candidate = if c1 <= 1e-18 {
483 r
484 } else {
485 let reflected_r = r.sub(step.scale(2.0 / c1 * step.dot(r)));
486 let reflected_t = t.sub(step.scale(2.0 / c1 * step.dot(t)));
487 let v2 = t_next.sub(reflected_t);
488 let c2 = v2.dot(v2);
489 if c2 <= 1e-18 {
490 reflected_r
491 } else {
492 reflected_r.sub(v2.scale(2.0 / c2 * v2.dot(reflected_r)))
493 }
494 };
495 candidate.sub(t_next.scale(candidate.dot(t_next))).normalized().ok()
496}
497
498const SUB_FRACTIONS: [f64; 4] = [0.25, 0.5, 0.75, 1.0];
501
502pub(super) fn station_report(run: &PlacedRun<'_>) -> Result<SweepStationReport, KernelRefusal> {
504 let refuse = |what: &str, error: String| {
505 KernelRefusal::internal(KernelStage::Refine, "station_report", format!("sweepSolid: station report ({what}): {error}"))
506 };
507 let count = run.points.len();
508 let closed = run.closed;
509 let spans = count - 1 + usize::from(closed);
510 let (area, moment_a, moment_b) = run.moments;
511 let t0 = run.tangents[0];
516 let (r0, s0) = run.axes[0];
517 let drawn = run.centres[0].sub(run.points[0]);
518 let offset = [drawn.dot(t0), drawn.dot(r0), drawn.dot(s0)];
519 let carried = offset.iter().any(|component| component.abs() > 1e-12 * run.reach.max(1e-9));
520 let centred = moment_a.abs().max(moment_b.abs()) <= 1e-9 * area.abs() * run.reach.max(1e-9);
521 let corrections = !centred || carried;
522
523 let parameters = loft_parameters(run.sections, closed)?;
526 let centre = interpolant(run.centres, ¶meters, closed).map_err(|e| refuse("centre", e))?;
527 let axis_1 = interpolant(run.e1, ¶meters, closed).map_err(|e| refuse("axis 1", e))?;
528 let axis_2 = interpolant(run.e2, ¶meters, closed).map_err(|e| refuse("axis 2", e))?;
529
530 let mut built = 0.0;
534 let mut lateral_length = 0.0;
535 for pair in centre.knots.windows(2) {
536 if pair[1] <= pair[0] {
537 continue;
538 }
539 let half = 0.5 * (pair[1] - pair[0]);
540 let middle = 0.5 * (pair[1] + pair[0]);
541 for index in 0..GAUSS_X.len() {
542 let v = middle + half * GAUSS_X[index];
543 let c = centre.derivatives_small(v, 1).map_err(|e| refuse("centre", e))?;
544 let e1 = axis_1.derivatives_small(v, 1).map_err(|e| refuse("axis 1", e))?;
545 let e2 = axis_2.derivatives_small(v, 1).map_err(|e| refuse("axis 2", e))?;
546 let normal = e1[0].cross(e2[0]);
547 let weight = GAUSS_W[index] * half;
548 built += weight * (area * c[1].dot(normal) + moment_a * e1[1].dot(normal) + moment_b * e2[1].dot(normal));
549 lateral_length += weight * c[1].length();
550 }
551 }
552
553 let [pu_t, pu_r, pu_s] = run.pu_in_frame;
557 let [pv_t, pv_r, pv_s] = run.pv_in_frame;
558 let axes_of = |t: Vec3, r: Vec3, s: Vec3| -> (Vec3, Vec3) {
559 (
560 t.scale(pu_t).add(r.scale(pu_r)).add(s.scale(pu_s)),
561 t.scale(pv_t).add(r.scale(pv_r)).add(s.scale(pv_s)),
562 )
563 };
564 let mut spine_deviation: f64 = 0.0;
565 let mut frame_deviation: f64 = 0.0;
566 let mut length = 0.0;
567 let mut moment_integral = 0.0;
568 for k in 0..spans {
569 let (segment, t_a, t_b) = span_piece(run.chain, run.params, k, closed).map_err(|e| refuse("span", e))?;
570 let curve = &run.chain[segment];
571 let next = (k + 1) % count;
572 let (u_a, u_b) = (parameters[k], parameters[k + 1]);
573 let (half, middle) = (0.5 * (t_b - t_a), 0.5 * (t_b + t_a));
575 for index in 0..GAUSS_X.len() {
576 let derivatives = curve.derivatives_small(middle + half * GAUSS_X[index], 1).map_err(|e| refuse("path", e))?;
577 length += GAUSS_W[index] * half * derivatives[1].length();
578 }
579 let (r_k, s_k) = run.axes[k];
583 let mut r = r_k;
584 let mut t_prev = run.tangents[k];
585 let mut p_prev = run.points[k];
586 let mut transported: Vec<(Vec3, Vec3, Vec3, Vec3)> = Vec::with_capacity(SUB_FRACTIONS.len());
587 let [d0, d1] = curve.domain().map_err(|e| refuse("domain", e))?;
588 for fraction in SUB_FRACTIONS {
589 let t = t_a + (t_b - t_a) * fraction;
590 let derivatives = curve.derivatives_small(t, 2).map_err(|e| refuse("path", e))?;
591 let point = derivatives[0];
592 let tangent = curve.unit_tangent(t, d0, d1).map_err(|e| refuse("tangent", e))?;
595 let kappa_n = super::sweep::curvature_vector(derivatives[1], derivatives[2]);
596 r = rmf_step(r, t_prev, tangent, point.sub(p_prev)).ok_or_else(|| refuse("transport", "the frame degenerated".into()))?;
597 transported.push((point, tangent, kappa_n, r));
598 t_prev = tangent;
599 p_prev = point;
600 }
601 let (r_end, _) = run.axes[next];
602 let t_end = run.tangents[next];
603 let r_arrived = transported[SUB_FRACTIONS.len() - 1].3;
604 let roll = r_arrived.cross(r_end).dot(t_end).atan2(r_arrived.dot(r_end));
605 let chord: f64 = {
606 let mut sum = 0.0;
607 let mut prev = run.points[k];
608 for entry in &transported {
609 sum += entry.0.sub(prev).length();
610 prev = entry.0;
611 }
612 sum
613 };
614 let _ = s_k;
615 let mut integrand: Vec<(f64, f64)> = Vec::with_capacity(5);
618 let mut chord_so_far = 0.0;
619 let mut prev = run.points[k];
620 for (which, fraction) in std::iter::once(0.0).chain(SUB_FRACTIONS).enumerate() {
621 let (point, tangent, kappa_n, r_raw) = if which == 0 {
622 let derivatives = curve.derivatives_small(t_a, 2).map_err(|e| refuse("path", e))?;
623 (run.points[k], run.tangents[k], super::sweep::curvature_vector(derivatives[1], derivatives[2]), r_k)
624 } else {
625 transported[which - 1]
626 };
627 chord_so_far += point.sub(prev).length();
628 prev = point;
629 let phi = if chord > 0.0 { roll * chord_so_far / chord } else { 0.0 };
630 let r_true = if which == 0 { r_k } else { rotated(r_raw, tangent, phi) };
631 let s_true = tangent.cross(r_true);
632 let (e1_true, e2_true) = axes_of(tangent, r_true, s_true);
633 let centre_true = point
634 .add(tangent.scale(offset[0]))
635 .add(r_true.scale(offset[1]))
636 .add(s_true.scale(offset[2]));
637 let speed = curve.derivatives_small(t_a + (t_b - t_a) * fraction, 1).map_err(|e| refuse("path", e))?[1].length();
638 if corrections {
639 let omega = if chord > 0.0 { roll / chord } else { 0.0 };
643 let t_dot = kappa_n;
644 let r_dot = tangent.scale(-kappa_n.dot(r_true)).add(s_true.scale(omega));
645 let s_dot = tangent.scale(-kappa_n.dot(s_true)).sub(r_true.scale(omega));
646 let e1_dot = t_dot.scale(pu_t).add(r_dot.scale(pu_r)).add(s_dot.scale(pu_s));
647 let e2_dot = t_dot.scale(pv_t).add(r_dot.scale(pv_r)).add(s_dot.scale(pv_s));
648 let c_dot = t_dot.scale(offset[0]).add(r_dot.scale(offset[1])).add(s_dot.scale(offset[2]));
649 let normal = e1_true.cross(e2_true);
650 integrand.push((
651 fraction,
652 (area * c_dot.dot(normal) + moment_a * e1_dot.dot(normal) + moment_b * e2_dot.dot(normal)) * speed,
653 ));
654 }
655 if which == 0 || which == SUB_FRACTIONS.len() {
656 continue;
657 }
658 let guess = u_a + (u_b - u_a) * fraction;
660 let (u, distance) = nearest_parameter(¢re, centre_true, guess).map_err(|e| refuse("projection", e))?;
661 spine_deviation = spine_deviation.max(distance);
662 let e1_built = axis_1.evaluate(u).map_err(|e| refuse("axis 1", e))?;
663 let e2_built = axis_2.evaluate(u).map_err(|e| refuse("axis 2", e))?;
664 let frame = (e1_built.sub(e1_true).length_squared() + e2_built.sub(e2_true).length_squared()).sqrt();
665 frame_deviation = frame_deviation.max(frame);
666 }
667 if corrections {
668 let h = (t_b - t_a) / 4.0;
670 let weights = [1.0, 4.0, 2.0, 4.0, 1.0];
671 moment_integral += integrand.iter().zip(weights).map(|((_, g), w)| g * w).sum::<f64>() * h / 3.0;
672 }
673 }
674 let (e1_0, e2_0) = (run.e1[0], run.e2[0]);
675 let advance = run.tangents[0].dot(e1_0.cross(e2_0));
676 let ideal = area * length * advance + moment_integral;
677 let wall_deviation = spine_deviation + run.reach * frame_deviation;
678 let (built_volume, ideal_volume, residual, crude) = if run.shift == 0 {
679 (built.abs(), ideal.abs(), (built.abs() - ideal.abs()).abs(), false)
680 } else {
681 let perimeter = section_perimeter(run.sections.first().map(|s| s.as_slice()).unwrap_or(&[]))?;
684 (f64::NAN, ideal.abs(), perimeter * lateral_length * wall_deviation, true)
685 };
686 Ok(SweepStationReport {
687 budget: run.budget,
688 tolerance: run.tolerance,
689 spine_deviation,
690 frame_deviation,
691 wall_deviation,
692 reach: run.reach,
693 built_volume,
694 ideal_volume,
695 volume_residual: residual,
696 crude_bound: crude,
697 })
698}
699
700fn section_perimeter(section: &[NurbsCurve]) -> Result<f64, KernelRefusal> {
703 let mut total = 0.0;
704 for curve in section {
705 let [start, end] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
706 let mut previous = curve.evaluate(start).or_refuse(KernelStage::Refine, "evaluate")?;
707 for index in 1..=64 {
708 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0).or_refuse(KernelStage::Refine, "evaluate")?;
709 total += point.sub(previous).length();
710 previous = point;
711 }
712 }
713 Ok(total)
714}