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
168pub(super) fn refinement_enabled() -> bool {
172 switch!(live "BREP_SWEEP_STATION_REFINEMENT", true)
173}
174
175
176pub(super) fn chord_parameters(points: &[Vec3], closed: bool) -> Vec<f64> {
179 let count = points.len();
180 let mut parameters = Vec::with_capacity(count + usize::from(closed));
181 let mut total = 0.0;
182 parameters.push(0.0);
183 for index in 1..count {
184 total += points[index].sub(points[index - 1]).length();
185 parameters.push(total);
186 }
187 if closed {
188 total += points[0].sub(points[count - 1]).length();
189 parameters.push(total);
190 }
191 if total > 0.0 {
192 for parameter in parameters.iter_mut() {
193 *parameter /= total;
194 }
195 }
196 parameters
197}
198
199pub(super) fn loft_parameters(sections: &[Vec<NurbsCurve>], closed: bool) -> Result<Vec<f64>, KernelRefusal> {
204 let count = sections.len();
205 let spans = count - 1 + usize::from(closed);
206 let mut accumulated = vec![0.0; spans + 1];
207 let mut columns = 0usize;
208 for curve_index in 0..sections[0].len() {
209 for control_index in 0..sections[0][curve_index].control_points.len() {
210 let mut chords = vec![0.0; spans + 1];
211 let mut total = 0.0;
212 for station in 1..=spans {
213 let previous = sections[station - 1][curve_index].control_points[control_index]
214 .point()
215 .or_refuse(KernelStage::Refine, "control_point")?;
216 let current = sections[station % count][curve_index].control_points[control_index]
217 .point()
218 .or_refuse(KernelStage::Refine, "control_point")?;
219 total += current.sub(previous).length();
220 chords[station] = total;
221 }
222 if total <= 1e-6 {
223 continue;
224 }
225 for station in 0..=spans {
226 accumulated[station] += chords[station] / total;
227 }
228 columns += 1;
229 }
230 }
231 if columns == 0 {
232 return Err(KernelRefusal::input(
233 KernelStage::Classify,
234 "coincident_sections",
235 "sweepSolid: the placed sections coincide",
236 ));
237 }
238 let mut parameters: Vec<f64> = accumulated.iter().map(|sum| sum / columns as f64).collect();
239 parameters[0] = 0.0;
240 parameters[spans] = 1.0;
241 Ok(parameters)
242}
243
244pub(super) fn interpolant(points: &[Vec3], parameters: &[f64], closed: bool) -> Result<NurbsCurve, String> {
248 if closed {
249 interpolate_curve_closed(points, parameters)
250 } else {
251 interpolate_curve(points, 3usize.min(points.len() - 1), parameters)
252 }
253}
254
255fn nearest_parameter(curve: &NurbsCurve, point: Vec3, guess: f64) -> Result<(f64, f64), String> {
260 let [lo, hi] = curve.domain()?;
261 let (u, _) = crate::projection::curve_foot_newton(curve, point, guess, [lo, hi],
262 crate::projection::CurveFootPolicy {
263 domain: crate::projection::FootDomain::Clamp,
264 iterations: 6,
265 slope_guard: 1e-300,
266 step_limit: f64::INFINITY,
267 parameter_tolerance: 1e-15 * hi.abs().max(1.0),
268 mode: crate::projection::FootMode::Monotone,
269 })?;
270 Ok((u, curve.evaluate(u)?.sub(point).length()))
271}
272
273fn span_piece(chain: &[NurbsCurve], params: &[StationParam], k: usize, closed: bool) -> Result<(usize, f64, f64), String> {
278 let count = params.len();
279 let from = params[k];
280 let next = (k + 1) % count;
281 let end = if !closed && k + 1 == count {
282 return Err("no span past the last station of an open run".into());
283 } else if next != 0 && params[next].segment == from.segment {
284 params[next].t
285 } else {
286 chain[from.segment].domain()?[1]
287 };
288 Ok((from.segment, from.t, end))
289}
290
291const SPAN_FRACTIONS: [f64; 3] = [0.25, 0.5, 0.75];
293
294pub(super) fn spine_deviations(
299 chain: &[NurbsCurve],
300 params: &[StationParam],
301 points: &[Vec3],
302 closed: bool,
303) -> Result<Vec<f64>, String> {
304 let parameters = chord_parameters(points, closed);
305 let spine = interpolant(points, ¶meters, closed)?;
306 let spans = points.len() - 1 + usize::from(closed);
307 let mut deviations = Vec::with_capacity(spans);
308 for k in 0..spans {
309 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
310 let (u_a, u_b) = (parameters[k], parameters[k + 1]);
311 let mut worst: f64 = 0.0;
312 for fraction in SPAN_FRACTIONS {
313 let truth = chain[segment].evaluate(t_a + (t_b - t_a) * fraction)?;
314 let (_, distance) = nearest_parameter(&spine, truth, u_a + (u_b - u_a) * fraction)?;
315 worst = worst.max(distance);
316 }
317 deviations.push(worst);
318 }
319 Ok(deviations)
320}
321
322pub(super) fn refine_parameters(
331 chain: &[NurbsCurve],
332 per_segment: &mut [Vec<f64>],
333 params: &[StationParam],
334 deviations: &[f64],
335 tolerance: f64,
336 closed: bool,
337 emitted: usize,
338) -> Result<(usize, bool), String> {
339 let count = params.len();
340 let mut wanted: Vec<(usize, usize)> = Vec::new();
341 for (k, &deviation) in deviations.iter().enumerate() {
342 if deviation > tolerance {
343 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
344 let [t0, t1] = chain[segment].domain()?;
345 let next = (k + 1) % count;
346 let at_joint = (t_a == t0 && (k > 0 || closed))
347 || t_b == t1 && (next != 0 || closed) && (next == 0 || params[next].segment != segment);
348 let exponent = if at_joint { 0.5 } else { 0.25 };
349 let pieces = ((deviation / tolerance).powf(exponent).ceil() as usize).clamp(2, 8);
350 wanted.push((k, pieces));
351 }
352 }
353 if wanted.is_empty() {
354 return Ok((0, false));
355 }
356 let asked: usize = wanted.iter().map(|(_, pieces)| pieces - 1).sum();
357 let room = SWEEP_MAX_STATIONS.saturating_sub(emitted);
358 let at_cap = asked > room;
359 if at_cap {
360 wanted.sort_by(|a, b| deviations[b.0].total_cmp(&deviations[a.0]));
363 }
364 let mut inserted = 0usize;
365 for (k, pieces) in wanted {
366 let mut pieces = pieces;
367 while pieces > 1 && inserted + pieces - 1 > room {
368 pieces -= 1;
369 }
370 if pieces < 2 {
371 continue;
372 }
373 let (segment, t_a, t_b) = span_piece(chain, params, k, closed)?;
374 for step in 1..pieces {
375 per_segment[segment].push(t_a + (t_b - t_a) * step as f64 / pieces as f64);
376 }
377 inserted += pieces - 1;
378 }
379 for list in per_segment.iter_mut() {
380 list.sort_by(|a, b| a.total_cmp(b));
381 list.dedup();
382 }
383 Ok((inserted, at_cap))
384}
385
386pub(super) fn section_moments(profile: &[NurbsCurve], origin: Vec3, pu: Vec3, pv: Vec3) -> Result<(f64, f64, f64), String> {
393 let (mut area, mut moment_a, mut moment_b) = (0.0, 0.0, 0.0);
394 for curve in profile {
395 for pair in curve.knots.windows(2) {
396 if pair[1] <= pair[0] {
397 continue;
398 }
399 let half = 0.5 * (pair[1] - pair[0]);
400 let middle = 0.5 * (pair[1] + pair[0]);
401 for index in 0..GAUSS_X.len() {
402 let derivatives = curve.derivatives_small(middle + half * GAUSS_X[index], 1)?;
403 let local = derivatives[0].sub(origin);
404 let (a, b) = (local.dot(pu), local.dot(pv));
405 let (da, db) = (derivatives[1].dot(pu), derivatives[1].dot(pv));
406 let weight = GAUSS_W[index] * half;
407 area += weight * 0.5 * (a * db - b * da);
408 moment_a += weight * 0.5 * a * a * db;
409 moment_b -= weight * 0.5 * b * b * da;
410 }
411 }
412 }
413 Ok((area, moment_a, moment_b))
414}
415
416pub(super) fn section_reach(profile: &[NurbsCurve], origin: Vec3) -> f64 {
419 profile
420 .iter()
421 .flat_map(|curve| curve.control_points.iter())
422 .filter_map(|point| point.point().ok())
423 .map(|point| point.sub(origin).length())
424 .fold(0.0, f64::max)
425}
426
427fn rotated(x: Vec3, axis: Vec3, angle: f64) -> Vec3 {
429 x.rotate_about(axis, angle)
430}
431
432pub(super) struct PlacedRun<'a> {
434 pub chain: &'a [NurbsCurve],
435 pub params: &'a [StationParam],
436 pub points: &'a [Vec3],
437 pub tangents: &'a [Vec3],
438 pub axes: &'a [(Vec3, Vec3)],
440 pub centres: &'a [Vec3],
442 pub e1: &'a [Vec3],
444 pub e2: &'a [Vec3],
445 pub pu_in_frame: [f64; 3],
448 pub pv_in_frame: [f64; 3],
449 pub closed: bool,
450 pub sections: &'a [Vec<NurbsCurve>],
451 pub moments: (f64, f64, f64),
453 pub reach: f64,
454 pub shift: usize,
456 pub budget: StationBudget,
457 pub tolerance: f64,
458}
459
460fn rmf_step(r: Vec3, t: Vec3, t_next: Vec3, step: Vec3) -> Option<Vec3> {
463 let c1 = step.dot(step);
464 let candidate = if c1 <= 1e-18 {
465 r
466 } else {
467 let reflected_r = r.sub(step.scale(2.0 / c1 * step.dot(r)));
468 let reflected_t = t.sub(step.scale(2.0 / c1 * step.dot(t)));
469 let v2 = t_next.sub(reflected_t);
470 let c2 = v2.dot(v2);
471 if c2 <= 1e-18 {
472 reflected_r
473 } else {
474 reflected_r.sub(v2.scale(2.0 / c2 * v2.dot(reflected_r)))
475 }
476 };
477 candidate.sub(t_next.scale(candidate.dot(t_next))).normalized().ok()
478}
479
480const SUB_FRACTIONS: [f64; 4] = [0.25, 0.5, 0.75, 1.0];
483
484pub(super) fn station_report(run: &PlacedRun<'_>) -> Result<SweepStationReport, KernelRefusal> {
486 let refuse = |what: &str, error: String| {
487 KernelRefusal::internal(KernelStage::Refine, "station_report", format!("sweepSolid: station report ({what}): {error}"))
488 };
489 let count = run.points.len();
490 let closed = run.closed;
491 let spans = count - 1 + usize::from(closed);
492 let (area, moment_a, moment_b) = run.moments;
493 let t0 = run.tangents[0];
498 let (r0, s0) = run.axes[0];
499 let drawn = run.centres[0].sub(run.points[0]);
500 let offset = [drawn.dot(t0), drawn.dot(r0), drawn.dot(s0)];
501 let carried = offset.iter().any(|component| component.abs() > 1e-12 * run.reach.max(1e-9));
502 let centred = moment_a.abs().max(moment_b.abs()) <= 1e-9 * area.abs() * run.reach.max(1e-9);
503 let corrections = !centred || carried;
504
505 let parameters = loft_parameters(run.sections, closed)?;
508 let centre = interpolant(run.centres, ¶meters, closed).map_err(|e| refuse("centre", e))?;
509 let axis_1 = interpolant(run.e1, ¶meters, closed).map_err(|e| refuse("axis 1", e))?;
510 let axis_2 = interpolant(run.e2, ¶meters, closed).map_err(|e| refuse("axis 2", e))?;
511
512 let mut built = 0.0;
516 let mut lateral_length = 0.0;
517 for pair in centre.knots.windows(2) {
518 if pair[1] <= pair[0] {
519 continue;
520 }
521 let half = 0.5 * (pair[1] - pair[0]);
522 let middle = 0.5 * (pair[1] + pair[0]);
523 for index in 0..GAUSS_X.len() {
524 let v = middle + half * GAUSS_X[index];
525 let c = centre.derivatives_small(v, 1).map_err(|e| refuse("centre", e))?;
526 let e1 = axis_1.derivatives_small(v, 1).map_err(|e| refuse("axis 1", e))?;
527 let e2 = axis_2.derivatives_small(v, 1).map_err(|e| refuse("axis 2", e))?;
528 let normal = e1[0].cross(e2[0]);
529 let weight = GAUSS_W[index] * half;
530 built += weight * (area * c[1].dot(normal) + moment_a * e1[1].dot(normal) + moment_b * e2[1].dot(normal));
531 lateral_length += weight * c[1].length();
532 }
533 }
534
535 let [pu_t, pu_r, pu_s] = run.pu_in_frame;
539 let [pv_t, pv_r, pv_s] = run.pv_in_frame;
540 let axes_of = |t: Vec3, r: Vec3, s: Vec3| -> (Vec3, Vec3) {
541 (
542 t.scale(pu_t).add(r.scale(pu_r)).add(s.scale(pu_s)),
543 t.scale(pv_t).add(r.scale(pv_r)).add(s.scale(pv_s)),
544 )
545 };
546 let mut spine_deviation: f64 = 0.0;
547 let mut frame_deviation: f64 = 0.0;
548 let mut length = 0.0;
549 let mut moment_integral = 0.0;
550 for k in 0..spans {
551 let (segment, t_a, t_b) = span_piece(run.chain, run.params, k, closed).map_err(|e| refuse("span", e))?;
552 let curve = &run.chain[segment];
553 let next = (k + 1) % count;
554 let (u_a, u_b) = (parameters[k], parameters[k + 1]);
555 let (half, middle) = (0.5 * (t_b - t_a), 0.5 * (t_b + t_a));
557 for index in 0..GAUSS_X.len() {
558 let derivatives = curve.derivatives_small(middle + half * GAUSS_X[index], 1).map_err(|e| refuse("path", e))?;
559 length += GAUSS_W[index] * half * derivatives[1].length();
560 }
561 let (r_k, s_k) = run.axes[k];
565 let mut r = r_k;
566 let mut t_prev = run.tangents[k];
567 let mut p_prev = run.points[k];
568 let mut transported: Vec<(Vec3, Vec3, Vec3, Vec3)> = Vec::with_capacity(SUB_FRACTIONS.len());
569 let [d0, d1] = curve.domain().map_err(|e| refuse("domain", e))?;
570 for fraction in SUB_FRACTIONS {
571 let t = t_a + (t_b - t_a) * fraction;
572 let derivatives = curve.derivatives_small(t, 2).map_err(|e| refuse("path", e))?;
573 let point = derivatives[0];
574 let tangent = curve.unit_tangent(t, d0, d1).map_err(|e| refuse("tangent", e))?;
577 let kappa_n = super::sweep::curvature_vector(derivatives[1], derivatives[2]);
578 r = rmf_step(r, t_prev, tangent, point.sub(p_prev)).ok_or_else(|| refuse("transport", "the frame degenerated".into()))?;
579 transported.push((point, tangent, kappa_n, r));
580 t_prev = tangent;
581 p_prev = point;
582 }
583 let (r_end, _) = run.axes[next];
584 let t_end = run.tangents[next];
585 let r_arrived = transported[SUB_FRACTIONS.len() - 1].3;
586 let roll = r_arrived.cross(r_end).dot(t_end).atan2(r_arrived.dot(r_end));
587 let chord: f64 = {
588 let mut sum = 0.0;
589 let mut prev = run.points[k];
590 for entry in &transported {
591 sum += entry.0.sub(prev).length();
592 prev = entry.0;
593 }
594 sum
595 };
596 let _ = s_k;
597 let mut integrand: Vec<(f64, f64)> = Vec::with_capacity(5);
600 let mut chord_so_far = 0.0;
601 let mut prev = run.points[k];
602 for (which, fraction) in std::iter::once(0.0).chain(SUB_FRACTIONS).enumerate() {
603 let (point, tangent, kappa_n, r_raw) = if which == 0 {
604 let derivatives = curve.derivatives_small(t_a, 2).map_err(|e| refuse("path", e))?;
605 (run.points[k], run.tangents[k], super::sweep::curvature_vector(derivatives[1], derivatives[2]), r_k)
606 } else {
607 transported[which - 1]
608 };
609 chord_so_far += point.sub(prev).length();
610 prev = point;
611 let phi = if chord > 0.0 { roll * chord_so_far / chord } else { 0.0 };
612 let r_true = if which == 0 { r_k } else { rotated(r_raw, tangent, phi) };
613 let s_true = tangent.cross(r_true);
614 let (e1_true, e2_true) = axes_of(tangent, r_true, s_true);
615 let centre_true = point
616 .add(tangent.scale(offset[0]))
617 .add(r_true.scale(offset[1]))
618 .add(s_true.scale(offset[2]));
619 let speed = curve.derivatives_small(t_a + (t_b - t_a) * fraction, 1).map_err(|e| refuse("path", e))?[1].length();
620 if corrections {
621 let omega = if chord > 0.0 { roll / chord } else { 0.0 };
625 let t_dot = kappa_n;
626 let r_dot = tangent.scale(-kappa_n.dot(r_true)).add(s_true.scale(omega));
627 let s_dot = tangent.scale(-kappa_n.dot(s_true)).sub(r_true.scale(omega));
628 let e1_dot = t_dot.scale(pu_t).add(r_dot.scale(pu_r)).add(s_dot.scale(pu_s));
629 let e2_dot = t_dot.scale(pv_t).add(r_dot.scale(pv_r)).add(s_dot.scale(pv_s));
630 let c_dot = t_dot.scale(offset[0]).add(r_dot.scale(offset[1])).add(s_dot.scale(offset[2]));
631 let normal = e1_true.cross(e2_true);
632 integrand.push((
633 fraction,
634 (area * c_dot.dot(normal) + moment_a * e1_dot.dot(normal) + moment_b * e2_dot.dot(normal)) * speed,
635 ));
636 }
637 if which == 0 || which == SUB_FRACTIONS.len() {
638 continue;
639 }
640 let guess = u_a + (u_b - u_a) * fraction;
642 let (u, distance) = nearest_parameter(¢re, centre_true, guess).map_err(|e| refuse("projection", e))?;
643 spine_deviation = spine_deviation.max(distance);
644 let e1_built = axis_1.evaluate(u).map_err(|e| refuse("axis 1", e))?;
645 let e2_built = axis_2.evaluate(u).map_err(|e| refuse("axis 2", e))?;
646 let frame = (e1_built.sub(e1_true).length_squared() + e2_built.sub(e2_true).length_squared()).sqrt();
647 frame_deviation = frame_deviation.max(frame);
648 }
649 if corrections {
650 let h = (t_b - t_a) / 4.0;
652 let weights = [1.0, 4.0, 2.0, 4.0, 1.0];
653 moment_integral += integrand.iter().zip(weights).map(|((_, g), w)| g * w).sum::<f64>() * h / 3.0;
654 }
655 }
656 let (e1_0, e2_0) = (run.e1[0], run.e2[0]);
657 let advance = run.tangents[0].dot(e1_0.cross(e2_0));
658 let ideal = area * length * advance + moment_integral;
659 let wall_deviation = spine_deviation + run.reach * frame_deviation;
660 let (built_volume, ideal_volume, residual, crude) = if run.shift == 0 {
661 (built.abs(), ideal.abs(), (built.abs() - ideal.abs()).abs(), false)
662 } else {
663 let perimeter = section_perimeter(run.sections.first().map(|s| s.as_slice()).unwrap_or(&[]))?;
666 (f64::NAN, ideal.abs(), perimeter * lateral_length * wall_deviation, true)
667 };
668 Ok(SweepStationReport {
669 budget: run.budget,
670 tolerance: run.tolerance,
671 spine_deviation,
672 frame_deviation,
673 wall_deviation,
674 reach: run.reach,
675 built_volume,
676 ideal_volume,
677 volume_residual: residual,
678 crude_bound: crude,
679 })
680}
681
682fn section_perimeter(section: &[NurbsCurve]) -> Result<f64, KernelRefusal> {
685 let mut total = 0.0;
686 for curve in section {
687 let [start, end] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
688 let mut previous = curve.evaluate(start).or_refuse(KernelStage::Refine, "evaluate")?;
689 for index in 1..=64 {
690 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0).or_refuse(KernelStage::Refine, "evaluate")?;
691 total += point.sub(previous).length();
692 previous = point;
693 }
694 }
695 Ok(total)
696}