1use crate::{
70 extrude_profile_brep, interpolate_curve, loft_profile_brep, make_arc, make_line,
71 revolve_profile_brep, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3,
72};
73use std::f64::consts::{PI, TAU};
74
75#[derive(Clone, Copy, Debug, PartialEq)]
77pub enum OutlinePiece {
78 Line { from: [f64; 2], to: [f64; 2] },
80 Arc { centre: [f64; 2], radius: f64, start: f64, end: f64 },
83}
84
85impl OutlinePiece {
86 fn point(&self, s: f64) -> [f64; 2] {
87 match *self {
88 OutlinePiece::Line { from, to } => {
89 [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s]
90 }
91 OutlinePiece::Arc { centre, radius, start, end } => {
92 let angle = start + (end - start) * s;
93 [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
94 }
95 }
96 }
97
98 fn is_full_circle(&self) -> bool {
99 matches!(*self, OutlinePiece::Arc { start, end, .. } if ((end - start).abs() - TAU).abs() < 1e-12)
100 }
101
102 fn distance(&self, p: [f64; 2]) -> f64 {
104 match *self {
105 OutlinePiece::Line { from, to } => {
106 let d = [to[0] - from[0], to[1] - from[1]];
107 let length2 = d[0] * d[0] + d[1] * d[1];
108 let t = if length2 > 0.0 {
109 (((p[0] - from[0]) * d[0] + (p[1] - from[1]) * d[1]) / length2).clamp(0.0, 1.0)
110 } else {
111 0.0
112 };
113 let q = [from[0] + d[0] * t - p[0], from[1] + d[1] * t - p[1]];
114 q[0].hypot(q[1])
115 }
116 OutlinePiece::Arc { centre, radius, start, end } => {
117 let v = [p[0] - centre[0], p[1] - centre[1]];
118 let angle = v[1].atan2(v[0]);
119 if self.is_full_circle() || angle_within(angle, start, end) {
120 (v[0].hypot(v[1]) - radius).abs()
121 } else {
122 let a = self.point(0.0);
123 let b = self.point(1.0);
124 (a[0] - p[0]).hypot(a[1] - p[1]).min((b[0] - p[0]).hypot(b[1] - p[1]))
125 }
126 }
127 }
128 }
129
130 fn ray_crossings(&self, p: [f64; 2]) -> usize {
133 match *self {
134 OutlinePiece::Line { from, to } => {
135 let (a, b) = (from, to);
136 if (a[1] > p[1]) != (b[1] > p[1]) {
137 let x = a[0] + (p[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
138 usize::from(x > p[0])
139 } else {
140 0
141 }
142 }
143 OutlinePiece::Arc { centre, radius, start, end } => {
144 let dy = p[1] - centre[1];
145 if dy.abs() >= radius {
146 return 0;
147 }
148 let dx = (radius * radius - dy * dy).sqrt();
149 let mut count = 0;
150 for x in [centre[0] - dx, centre[0] + dx] {
151 if x <= p[0] {
152 continue;
153 }
154 let angle = dy.atan2(x - centre[0]);
155 if self.is_full_circle() || angle_within(angle, start, end) {
156 count += 1;
159 }
160 }
161 count
162 }
163 }
164 }
165}
166
167fn angle_within(angle: f64, start: f64, end: f64) -> bool {
169 let (low, high) = if end >= start { (start, end) } else { (end, start) };
170 let shifted = low + (angle - low).rem_euclid(TAU);
171 shifted <= high
172}
173
174fn scale_of(outline: &[OutlinePiece]) -> f64 {
175 outline
176 .iter()
177 .flat_map(|piece| [piece.point(0.0), piece.point(1.0)])
178 .map(|p| p[0].abs().max(p[1].abs()))
179 .fold(1.0f64, f64::max)
180}
181
182fn outline_signed_distance(outline: &[OutlinePiece], p: [f64; 2]) -> f64 {
184 let distance = outline
185 .iter()
186 .map(|piece| piece.distance(p))
187 .fold(f64::INFINITY, f64::min);
188 let crossings: usize = outline.iter().map(|piece| piece.ray_crossings(p)).sum();
189 if crossings % 2 == 1 {
190 -distance
191 } else {
192 distance
193 }
194}
195
196fn validate_outline(outline: &[OutlinePiece], what: &str) -> Result<(), KernelRefusal> {
197 if outline.is_empty() {
198 return Err(KernelRefusal::input(
199 KernelStage::Collect,
200 "outline_empty",
201 format!("sweepSolidWithin: the {what} outline is empty"),
202 ));
203 }
204 let scale = outline
205 .iter()
206 .flat_map(|piece| [piece.point(0.0), piece.point(1.0)])
207 .map(|p| p[0].abs().max(p[1].abs()))
208 .fold(1.0f64, f64::max);
209 for (index, piece) in outline.iter().enumerate() {
210 let next = &outline[(index + 1) % outline.len()];
211 let a = piece.point(1.0);
212 let b = next.point(0.0);
213 if (a[0] - b[0]).hypot(a[1] - b[1]) > 1e-9 * scale {
214 return Err(KernelRefusal::input(
215 KernelStage::Collect,
216 "outline_open",
217 format!("sweepSolidWithin: the {what} outline is not closed after piece {index}"),
218 ));
219 }
220 }
221 Ok(())
222}
223
224#[derive(Clone, Debug)]
226pub enum SweptTool {
227 Revolved {
232 axis_origin: Vec3,
233 axis_direction: Vec3,
234 radial: Vec3,
235 meridian: Vec<OutlinePiece>,
236 },
237 Prism {
241 origin: Vec3,
242 normal: Vec3,
243 x_axis: Vec3,
244 outline: Vec<OutlinePiece>,
245 from: f64,
246 to: f64,
247 },
248}
249
250#[derive(Clone, Copy, Debug)]
255pub struct ScrewStep {
256 pub axis_origin: Vec3,
257 pub axis_direction: Vec3,
258 pub turn: f64,
259 pub advance: f64,
260}
261
262#[derive(Clone, Debug)]
265pub struct SolidMotion {
266 pub steps: Vec<ScrewStep>,
267 pub from: f64,
268 pub to: f64,
269}
270
271#[derive(Clone, Copy, Debug)]
276pub struct SweepClip {
277 pub axis_origin: Vec3,
278 pub axis_direction: Vec3,
279 pub reference: Vec3,
280 pub radius: [f64; 2],
281 pub axial: [f64; 2],
282 pub angle: Option<[f64; 2]>,
283}
284
285pub const SOLID_SWEEP_REFUSAL: &str = "sweepSolidWithin";
287
288fn refuse(what: &'static str, message: String) -> KernelRefusal {
289 KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_REFUSAL}: {message}"))
290}
291
292fn rotate(x: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
293 origin.add(x.sub(origin).rotate_about(axis, angle))
294}
295
296impl ScrewStep {
297 fn apply(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
298 rotate(x, self.axis_origin, axis, self.turn * t).add(axis.scale(self.advance * t))
299 }
300 fn invert(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
301 rotate(x.sub(axis.scale(self.advance * t)), self.axis_origin, axis, -self.turn * t)
302 }
303}
304
305#[derive(Clone, Copy, Debug)]
307struct Frame {
308 origin: Vec3,
309 ex: Vec3,
310 ey: Vec3,
311 ez: Vec3,
312}
313
314impl Frame {
315 fn local(&self, x: Vec3) -> [f64; 3] {
316 let d = x.sub(self.origin);
317 [d.dot(self.ex), d.dot(self.ey), d.dot(self.ez)]
318 }
319 fn world(&self, p: [f64; 3]) -> Vec3 {
320 self.origin
321 .add(self.ex.scale(p[0]))
322 .add(self.ey.scale(p[1]))
323 .add(self.ez.scale(p[2]))
324 }
325}
326
327#[derive(Clone, Copy, Debug, PartialEq)]
328enum Class {
329 Screw(f64),
331 Turn,
332 Planar,
333}
334
335#[derive(Clone, Copy, Debug)]
338enum Source {
339 RevolvedFace { piece: usize },
341 RevolvedCorner { piece: usize },
343 PrismFace { piece: usize },
345 PrismVertex { piece: usize },
347 PrismEnd { piece: usize, end: bool },
350}
351
352struct Engine<'a> {
353 tool: &'a SweptTool,
354 motion: &'a SolidMotion,
355 axes: Vec<Vec3>,
356 frame: Frame,
357 class: Class,
358 clip: &'a SweepClip,
359 sources: Vec<Source>,
360 grids: Vec<Option<Grid>>,
362 critical: Vec<Vec<(f64, f64)>>,
364 critical_params: Vec<Vec<(f64, f64)>>,
367 angular: bool,
369 prism_height: f64,
371 scale: f64,
372 cache: std::cell::RefCell<std::collections::HashMap<u64, Vec<Interval>>>,
375 wrap: Option<Vec<usize>>,
379 counters: std::cell::Cell<(usize, usize, usize, f64, f64)>,
382}
383
384struct Grid {
385 nu: usize,
386 nv: usize,
387 u: [f64; 2],
388 v: [f64; 2],
389 v_periodic: bool,
390 values: Vec<(f64, f64)>,
391}
392
393#[derive(Clone, Copy, Debug)]
395struct Interval {
396 lo: f64,
397 hi: f64,
398 lo_source: usize,
399 hi_source: usize,
400}
401
402fn wrap_pi(angle: f64) -> f64 {
403 let wrapped = (angle + PI).rem_euclid(TAU) - PI;
404 wrapped
405}
406
407impl<'a> Engine<'a> {
408 fn motion_apply(&self, t: f64, x: Vec3) -> Vec3 {
409 self.motion
410 .steps
411 .iter()
412 .zip(&self.axes)
413 .fold(x, |x, (step, axis)| step.apply(t, x, *axis))
414 }
415
416 fn motion_invert(&self, t: f64, x: Vec3) -> Vec3 {
417 self.motion
418 .steps
419 .iter()
420 .zip(&self.axes)
421 .rev()
422 .fold(x, |x, (step, axis)| step.invert(t, x, *axis))
423 }
424
425 fn reduce(&self, p: [f64; 3]) -> (f64, f64) {
427 let level = p[0].hypot(p[1]);
428 match self.class {
429 Class::Screw(c) => (level, p[1].atan2(p[0]) - p[2] / c),
430 Class::Turn => (level, p[2]),
431 Class::Planar => (level, p[1].atan2(p[0])),
432 }
433 }
434
435 fn tool_signed_distance(&self, x: Vec3) -> f64 {
436 match self.tool {
437 SweptTool::Revolved { axis_origin, axis_direction, meridian, .. } => {
438 let axis = axis_direction.normalized_or(*axis_direction);
439 let d = x.sub(*axis_origin);
440 let e = d.dot(axis);
441 let radial = d.sub(axis.scale(e)).length();
442 outline_signed_distance(meridian, [radial, e])
443 }
444 SweptTool::Prism { origin, normal, x_axis, outline, from, to } => {
445 let n = normal.normalized_or(*normal);
446 let xa = x_axis.normalized_or(*x_axis);
447 let ya = n.cross(xa);
448 let d = x.sub(*origin);
449 let h = d.dot(n);
450 let planar = outline_signed_distance(outline, [d.dot(xa), d.dot(ya)]);
451 planar.max(from - h).max(h - to)
452 }
453 }
454 }
455
456 fn source_point(&self, source: Source, u: f64, v: f64) -> Vec3 {
459 match (self.tool, source) {
460 (SweptTool::Revolved { axis_origin, axis_direction, radial, meridian }, _) => {
461 let axis = axis_direction.normalized_or(*axis_direction);
462 let r0 = radial.sub(axis.scale(radial.dot(axis))).normalized_or(*radial);
463 let r1 = axis.cross(r0);
464 let (m, phi) = match source {
465 Source::RevolvedFace { piece } => (meridian[piece].point(u), v),
466 Source::RevolvedCorner { piece } => (meridian[piece].point(0.0), u),
467 _ => unreachable!(),
468 };
469 axis_origin
470 .add(r0.scale(m[0] * phi.cos()))
471 .add(r1.scale(m[0] * phi.sin()))
472 .add(axis.scale(m[1]))
473 }
474 (SweptTool::Prism { origin, normal, x_axis, outline, .. }, _) => {
475 let n = normal.normalized_or(*normal);
476 let xa = x_axis.normalized_or(*x_axis);
477 let ya = n.cross(xa);
478 let (q, t) = match source {
479 Source::PrismFace { piece } => (outline[piece].point(u), v),
480 Source::PrismVertex { piece } => (outline[piece].point(0.0), u),
481 Source::PrismEnd { piece, end } => (
482 outline[piece].point(u),
483 if end { self.motion.to } else { self.motion.from },
484 ),
485 _ => unreachable!(),
486 };
487 let x = origin
488 .add(xa.scale(q[0]))
489 .add(ya.scale(q[1]))
490 .add(n.scale(self.prism_height));
491 self.motion_apply(t, x)
492 }
493 }
494 }
495
496 fn source_reduced(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
497 self.reduce(self.frame.local(self.source_point(source, u, v)))
498 }
499
500 fn is_face(source: Source) -> bool {
501 matches!(source, Source::RevolvedFace { .. } | Source::PrismFace { .. })
502 }
503
504 fn domain(&self, source: Source) -> ([f64; 2], [f64; 2], bool) {
506 let time = [self.motion.from, self.motion.to];
507 match source {
508 Source::RevolvedFace { .. } => ([0.0, 1.0], [0.0, TAU], true),
509 Source::RevolvedCorner { .. } => ([0.0, TAU], [0.0, 0.0], false),
510 Source::PrismFace { .. } => ([0.0, 1.0], time, false),
511 Source::PrismVertex { .. } => (time, [0.0, 0.0], false),
512 Source::PrismEnd { .. } => ([0.0, 1.0], [0.0, 0.0], false),
513 }
514 }
515
516 fn u_periodic(&self, source: Source) -> bool {
519 match (self.tool, source) {
520 (SweptTool::Revolved { meridian, .. }, Source::RevolvedFace { piece }) => meridian[piece].is_full_circle(),
521 (SweptTool::Prism { outline, .. }, Source::PrismFace { piece }) => outline[piece].is_full_circle(),
522 _ => false,
523 }
524 }
525
526 fn o_difference(&self, a: f64, b: f64) -> f64 {
527 if self.angular {
528 wrap_pi(a - b)
529 } else {
530 a - b
531 }
532 }
533
534 fn face_extremum(&self, source: Source, level: f64, mut u: f64, mut v: f64) -> Option<(f64, f64, f64)> {
536 let (ur, vr, periodic) = self.domain(source);
537 let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
538 let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
539 let residual = |u: f64, v: f64| -> [f64; 2] {
540 let (l, o) = self.source_reduced(source, u, v);
541 let (lu1, ou1) = self.source_reduced(source, u + hu, v);
542 let (lu0, ou0) = self.source_reduced(source, u - hu, v);
543 let (lv1, ov1) = self.source_reduced(source, u, v + hv);
544 let (lv0, ov0) = self.source_reduced(source, u, v - hv);
545 let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
546 let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
547 let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
548 let norm = lu.hypot(lv).max(1e-300);
550 let _ = o;
551 [l - level, (ou * lv - ov * lu) / norm]
552 };
553 let mut f = residual(u, v);
554 for _ in 0..60 {
555 let (du_step, dv_step) = (hu * 10.0, hv * 10.0);
556 let fu1 = residual(u + du_step, v);
557 let fu0 = residual(u - du_step, v);
558 let fv1 = residual(u, v + dv_step);
559 let fv0 = residual(u, v - dv_step);
560 let j = [
561 [(fu1[0] - fu0[0]) / (2.0 * du_step), (fv1[0] - fv0[0]) / (2.0 * dv_step)],
562 [(fu1[1] - fu0[1]) / (2.0 * du_step), (fv1[1] - fv0[1]) / (2.0 * dv_step)],
563 ];
564 let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
565 if !det.is_finite() || det.abs() < 1e-300 {
566 return None;
567 }
568 let du = -(f[0] * j[1][1] - f[1] * j[0][1]) / det;
569 let dv = -(j[0][0] * f[1] - j[1][0] * f[0]) / det;
570 let norm0 = f[0].hypot(f[1]);
571 let mut step = 1.0;
572 let mut accepted = false;
573 while step > 1e-4 {
574 let (nu, nv) = (u + step * du, v + step * dv);
575 let candidate = residual(nu, nv);
576 if candidate[0].hypot(candidate[1]) < norm0 || norm0 < 1e-14 * self.scale {
577 u = nu;
578 v = nv;
579 f = candidate;
580 accepted = true;
581 break;
582 }
583 step *= 0.5;
584 }
585 if !accepted {
586 break;
587 }
588 if (du * step).abs() < 1e-15 * (ur[1] - ur[0]) && (dv * step).abs() < 1e-15 * (vr[1] - vr[0]).max(1.0) {
589 break;
590 }
591 }
592 if periodic {
593 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
594 }
595 if self.u_periodic(source) {
596 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
597 }
598 let tolerance = 1e-9;
599 if u < ur[0] - tolerance || u > ur[1] + tolerance || (!periodic && (v < vr[0] - tolerance || v > vr[1] + tolerance)) {
600 return None;
601 }
602 if f[0].abs() > 1e-10 * self.scale || f[1].abs() > 1e-6 {
603 return None;
604 }
605 let (l, o) = self.source_reduced(source, u.clamp(ur[0], ur[1]), v);
606 Some((l, o, u))
607 }
608
609 fn build_grid(&self, source: Source) -> Grid {
610 let (u, v, v_periodic) = self.domain(source);
611 let (nu, nv) = match source {
612 Source::RevolvedFace { .. } => (64, 256),
613 _ => {
614 let turn = self
615 .motion
616 .steps
617 .iter()
618 .map(|step| step.turn.abs())
619 .fold(0.0, f64::max)
620 * (self.motion.to - self.motion.from).abs();
621 (48, ((turn * 128.0) as usize).clamp(256, 4096))
622 }
623 };
624 let mut values = Vec::with_capacity(nu * nv);
625 for i in 0..nu {
626 let uu = u[0] + (u[1] - u[0]) * i as f64 / (nu - 1) as f64;
627 for j in 0..nv {
628 let vv = if v_periodic {
629 v[0] + (v[1] - v[0]) * j as f64 / nv as f64
630 } else {
631 v[0] + (v[1] - v[0]) * j as f64 / (nv - 1) as f64
632 };
633 values.push(self.source_reduced(source, uu, vv));
634 }
635 }
636 Grid { nu, nv, u, v, v_periodic, values }
637 }
638
639 fn grid_param(grid: &Grid, i: usize, j: usize) -> (f64, f64) {
640 let u = grid.u[0] + (grid.u[1] - grid.u[0]) * i as f64 / (grid.nu - 1) as f64;
641 let v = if grid.v_periodic {
642 grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / grid.nv as f64
643 } else {
644 grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / (grid.nv - 1) as f64
645 };
646 (u, v)
647 }
648
649 fn source_candidates(&self, index: usize, level: f64, out: &mut Vec<(f64, usize)>) {
651 let source = self.sources[index];
652 if Self::is_face(source) {
653 let grid = self.grids[index].as_ref().expect("face grid");
654 let at = |i: usize, j: usize| grid.values[i * grid.nv + j % grid.nv];
657 let jmax = if grid.v_periodic { grid.nv } else { grid.nv - 1 };
658 let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
659 for i in 0..grid.nu {
660 for j in 0..jmax {
661 let a = at(i, j);
662 let b = at(i, j + 1);
663 if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
664 let w = (level - a.0) / (b.0 - a.0);
665 let o = a.1 + self.o_difference(b.1, a.1) * w;
666 let (u, v0) = Self::grid_param(grid, i, j);
667 let (_, v1) = Self::grid_param(grid, i, j + 1);
668 let v1 = if grid.v_periodic && j + 1 == grid.nv { grid.v[1] } else { v1 };
669 crossings.push((i, j, o, u, v0 + (v1 - v0) * w));
670 }
671 }
672 }
673 for i in 0..grid.nu.saturating_sub(1) {
674 for j in 0..grid.nv {
675 let a = at(i, j);
676 let b = at(i + 1, j);
677 if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
678 let w = (level - a.0) / (b.0 - a.0);
679 let o = a.1 + self.o_difference(b.1, a.1) * w;
680 let (u0, v) = Self::grid_param(grid, i, j);
681 let (u1, _) = Self::grid_param(grid, i + 1, j);
682 crossings.push((i, j, o, u0 + (u1 - u0) * w, v));
683 }
684 }
685 }
686 let mut found: Vec<(f64, f64, f64)> = Vec::new();
687 let push = |found: &mut Vec<(f64, f64, f64)>, result: Option<(f64, f64, f64)>| {
688 if let Some((_, o_new, u_new)) = result {
689 if !found
690 .iter()
691 .any(|&(o_old, u_old, _)| self.o_difference(o_old, o_new).abs() < 1e-11 && (u_old - u_new).abs() < 1e-6)
692 {
693 found.push((o_new, u_new, 0.0));
694 }
695 }
696 };
697 if let Some(params) = self.critical_params.get(index) {
700 let (ur, vr, _) = self.domain(source);
701 for &(u, v) in params {
702 for (du, dv) in [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] {
703 let (su, sv) = (u + du * 1e-3 * (ur[1] - ur[0]), v + dv * 1e-3 * (vr[1] - vr[0]));
704 push(&mut found, self.face_extremum(source, level, su, sv));
705 }
706 }
707 }
708 let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
709 for (k, &(i, j, _, _, _)) in crossings.iter().enumerate() {
710 buckets.entry((i, j)).or_default().push(k);
711 }
712 for (k, &(i, j, o, u, v)) in crossings.iter().enumerate() {
713 let mut is_max = true;
714 let mut is_min = true;
715 let mut neighbours = Vec::new();
716 for di in [-1i64, 0, 1] {
717 for dj in [-1i64, 0, 1] {
718 let i2 = i as i64 + di;
719 if i2 < 0 {
720 continue;
721 }
722 let j2 = j as i64 + dj;
723 let j2 = if grid.v_periodic { j2.rem_euclid(grid.nv as i64) } else { j2 };
724 if j2 < 0 {
725 continue;
726 }
727 if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
728 neighbours.extend(list.iter().copied());
729 }
730 }
731 }
732 for m in neighbours {
733 if m == k {
734 continue;
735 }
736 let o2 = crossings[m].2;
737 {
738 let d = self.o_difference(o2, o);
739 if d > 0.0 {
740 is_max = false;
741 }
742 if d < 0.0 {
743 is_min = false;
744 }
745 }
746 }
747 if !(is_max || is_min) {
748 continue;
749 }
750 push(&mut found, self.face_extremum(source, level, u, v));
751 }
752 out.extend(found.into_iter().map(|(o, _, _)| (o, index)));
753 } else {
754 let (range, _, _) = self.domain(source);
755 let periodic = matches!(source, Source::RevolvedCorner { .. });
756 let n = match source {
757 Source::RevolvedCorner { .. } | Source::PrismEnd { .. } => 1024,
758 _ => self.grids_len_for_time(),
759 };
760 let _ = periodic;
761 let mut samples: Vec<f64> = (0..=n).map(|k| range[0] + (range[1] - range[0]) * k as f64 / n as f64).collect();
765 if let Some(extra) = self.critical_params.get(index) {
766 samples.extend(extra.iter().map(|p| p.0).filter(|t| *t > range[0] && *t < range[1]));
767 samples.sort_by(|a, b| a.total_cmp(b));
768 }
769 let mut previous = self.source_reduced(source, samples[0], 0.0).0 - level;
770 for k in 1..samples.len() {
771 let p = samples[k];
772 let value = self.source_reduced(source, p, 0.0).0 - level;
773 if previous == 0.0 || previous * value < 0.0 {
774 let (mut a, mut b) = (samples[k - 1], p);
775 let fa0 = previous;
776 for _ in 0..200 {
777 let m = 0.5 * (a + b);
778 let fm = self.source_reduced(source, m, 0.0).0 - level;
779 if (fm < 0.0) == (fa0 < 0.0) && fm != 0.0 {
780 a = m;
781 } else {
782 b = m;
783 }
784 if (b - a).abs() < 1e-15 * (range[1] - range[0]).abs().max(1.0) {
785 break;
786 }
787 }
788 let (_, o) = self.source_reduced(source, 0.5 * (a + b), 0.0);
789 out.push((o, index));
790 }
791 previous = value;
792 }
793 }
794 }
795
796 fn grids_len_for_time(&self) -> usize {
797 let turn = self
798 .motion
799 .steps
800 .iter()
801 .map(|step| step.turn.abs())
802 .fold(0.0, f64::max)
803 * (self.motion.to - self.motion.from).abs();
804 ((turn * 256.0) as usize).clamp(1024, 8192)
805 }
806
807 fn lift(&self, level: f64, o: f64) -> [f64; 3] {
809 let axial = 0.5 * (self.clip.axial[0] + self.clip.axial[1]);
810 match self.class {
811 Class::Screw(c) => {
812 let alpha = o + axial / c;
813 [level * alpha.cos(), level * alpha.sin(), axial]
814 }
815 Class::Turn => {
816 let theta = self.clip.angle.map(|a| 0.5 * (a[0] + a[1])).unwrap_or(0.0);
817 [level * theta.cos(), level * theta.sin(), o]
818 }
819 Class::Planar => [level * o.cos(), level * o.sin(), axial],
820 }
821 }
822
823 fn removed(&self, level: f64, o: f64) -> bool {
833 let x = self.frame.world(self.lift(level, o));
834 let (t0, t1) = (self.motion.from, self.motion.to);
835 let speed = self.speed_bound(x);
836 let floor = (t1 - t0) * 1e-8;
841 let cap = 200_000usize;
842 let touching = 1e-10 * self.scale;
843 let clock = web_time::Instant::now();
844 let mut t = t0;
845 let mut steps = 0usize;
846 let inside = loop {
847 steps += 1;
848 let distance = self.tool_signed_distance(self.motion_invert(t, x));
849 if distance <= touching || steps > cap {
850 break true;
851 }
852 if t >= t1 {
853 break false;
854 }
855 t = (t + (distance / speed).max(floor)).min(t1);
856 };
857 let mut c = self.counters.get();
858 c.1 += 1;
859 c.2 += steps;
860 c.4 += clock.elapsed().as_secs_f64();
861 self.counters.set(c);
862 inside
863 }
864
865 fn speed_bound(&self, x: Vec3) -> f64 {
867 let centre = self.frame.origin;
868 let span = self.motion.from.abs().max(self.motion.to.abs());
869 let mut reach = x.sub(centre).length();
870 for step in &self.motion.steps {
871 reach += 2.0 * step.axis_origin.sub(centre).length() + step.advance.abs() * span;
872 }
873 let speed: f64 = self
874 .motion
875 .steps
876 .iter()
877 .map(|step| step.turn.abs() * (reach + step.axis_origin.sub(centre).length()) + step.advance.abs())
878 .sum();
879 speed.max(1e-300)
880 }
881
882 fn intervals_at(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
885 if let Some(hit) = self.cache.borrow().get(&level.to_bits()) {
886 return Ok(hit.clone());
887 }
888 let clock = web_time::Instant::now();
889 let before = self.counters.get();
890 let intervals = self.intervals_uncached(level)?;
891 if clock.elapsed().as_secs_f64() > 0.5 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
892 let after = self.counters.get();
893 eprintln!(
894 " slow level {level:.12}: {:.2}s, candidates {:.2}s, oracle calls {} steps {} ({:.2}s), {} intervals",
895 clock.elapsed().as_secs_f64(),
896 after.3 - before.3,
897 after.1 - before.1,
898 after.2 - before.2,
899 after.4 - before.4,
900 intervals.len()
901 );
902 }
903 self.cache.borrow_mut().insert(level.to_bits(), intervals.clone());
904 Ok(intervals)
905 }
906
907 fn intervals_uncached(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
908 let clock = web_time::Instant::now();
909 let mut candidates = Vec::new();
910 for index in 0..self.sources.len() {
911 self.source_candidates(index, level, &mut candidates);
912 }
913 let mut c = self.counters.get();
914 c.0 += 1;
915 c.3 += clock.elapsed().as_secs_f64();
916 self.counters.set(c);
917 if self.angular {
918 for candidate in &mut candidates {
919 candidate.0 = candidate.0.rem_euclid(TAU);
920 }
921 }
922 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
923 candidates.dedup_by(|b, a| (b.0 - a.0).abs() < 1e-12);
924 if candidates.is_empty() {
925 let probes = 64;
926 let (base, span) = if self.angular { (0.0, TAU) } else { (-self.scale * 4.0, self.scale * 8.0) };
927 if (0..probes).all(|k| self.removed(level, base + span * (k as f64 + 0.5) / probes as f64)) {
928 return Err(refuse(
929 "whole_level",
930 format!("the sweep covers every angle at level {level}, which this construction does not build"),
931 ));
932 }
933 return Ok(Vec::new());
934 }
935 let n = candidates.len();
936 let gaps = if self.angular { n } else { n - 1 };
937 let mut removed = Vec::with_capacity(gaps);
938 for k in 0..gaps {
939 let a = candidates[k].0;
940 let b = if k + 1 < n { candidates[k + 1].0 } else { candidates[0].0 + TAU };
941 if (b - a) * level.max(1.0) < 1e-9 * self.scale {
945 removed.push(true);
946 continue;
947 }
948 removed.push(self.removed(level, 0.5 * (a + b)));
949 }
950 let mut intervals = Vec::new();
951 if self.angular && removed.iter().all(|r| *r) {
952 return Err(refuse(
953 "whole_level",
954 format!("the sweep covers every angle at level {level}, which this construction does not build"),
955 ));
956 }
957 let start = if self.angular {
959 removed.iter().position(|r| !r).map(|k| (k + 1) % n).unwrap_or(0)
960 } else {
961 0
962 };
963 let mut k = 0;
964 while k < gaps {
965 let g = (start + k) % n.max(1);
966 if !removed[g] {
967 k += 1;
968 continue;
969 }
970 let first = g;
971 let mut last = g;
972 while k + 1 < gaps && removed[(start + k + 1) % n] {
973 k += 1;
974 last = (start + k) % n;
975 }
976 let lo = candidates[first];
977 let hi_index = (last + 1) % n;
978 let hi = candidates[hi_index];
979 let mut hi_value = hi.0;
980 if self.angular && hi_value <= lo.0 {
981 hi_value += TAU;
982 }
983 intervals.push(Interval {
984 lo: lo.0,
985 hi: hi_value,
986 lo_source: lo.1,
987 hi_source: hi.1,
988 });
989 k += 1;
990 }
991 intervals.sort_by(|a, b| a.lo.total_cmp(&b.lo));
992 Ok(intervals)
993 }
994
995 fn characteristic(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
999 let (ur, vr, _) = self.domain(source);
1000 let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
1001 let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
1002 let (l, _) = self.source_reduced(source, u, v);
1003 let (lu1, ou1) = self.source_reduced(source, u + hu, v);
1004 let (lu0, ou0) = self.source_reduced(source, u - hu, v);
1005 let (lv1, ov1) = self.source_reduced(source, u, v + hv);
1006 let (lv0, ov0) = self.source_reduced(source, u, v - hv);
1007 let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
1008 let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
1009 let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
1010 ((ou * lv - ov * lu) / lu.hypot(lv).max(1e-300), l)
1011 }
1012
1013 fn folds_and_kinks(&self, source: Source, grid: &Grid, out: &mut Vec<(f64, f64)>, params: &mut Vec<(f64, f64)>) {
1023 let (ur, vr, v_periodic) = self.domain(source);
1024 let u_periodic = self.u_periodic(source);
1025 let (nu, nv) = (grid.nu, grid.nv);
1026 let mut d = vec![0.0; nu * nv];
1027 for i in 0..nu {
1028 for j in 0..nv {
1029 let (u, v) = Self::grid_param(grid, i, j);
1030 d[i * nv + j] = self.characteristic(source, u, v).0;
1031 }
1032 }
1033 let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
1035 let jmax = if v_periodic { nv } else { nv - 1 };
1036 for i in 0..nu {
1037 for j in 0..jmax {
1038 let (a, b) = (d[i * nv + j], d[i * nv + (j + 1) % nv]);
1039 if a * b < 0.0 {
1040 let w = a / (a - b);
1041 let (u, v0) = Self::grid_param(grid, i, j);
1042 let (_, v1) = Self::grid_param(grid, i, j + 1);
1043 let v1 = if v_periodic && j + 1 == nv { vr[1] } else { v1 };
1044 let v = v0 + (v1 - v0) * w;
1045 crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1046 }
1047 }
1048 }
1049 for i in 0..nu - 1 {
1050 for j in 0..nv {
1051 let (a, b) = (d[i * nv + j], d[(i + 1) * nv + j]);
1052 if a * b < 0.0 {
1053 let w = a / (a - b);
1054 let (u0, v) = Self::grid_param(grid, i, j);
1055 let (u1, _) = Self::grid_param(grid, i + 1, j);
1056 let u = u0 + (u1 - u0) * w;
1057 crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1058 }
1059 }
1060 }
1061 let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
1062 for (k, c) in crossings.iter().enumerate() {
1063 buckets.entry((c.0, c.1)).or_default().push(k);
1064 }
1065 let ku = 1e-4 * (ur[1] - ur[0]);
1066 let kv = 1e-4 * (vr[1] - vr[0]);
1067 let fold = |u: f64, v: f64| -> [f64; 2] {
1068 let (dd, _) = self.characteristic(source, u, v);
1069 let du = (self.characteristic(source, u + ku, v).0 - self.characteristic(source, u - ku, v).0) / (2.0 * ku);
1070 let dv = (self.characteristic(source, u, v + kv).0 - self.characteristic(source, u, v - kv).0) / (2.0 * kv);
1071 let lu = (self.source_reduced(source, u + ku, v).0 - self.source_reduced(source, u - ku, v).0) / (2.0 * ku);
1072 let lv = (self.source_reduced(source, u, v + kv).0 - self.source_reduced(source, u, v - kv).0) / (2.0 * kv);
1073 let norm = (lu.hypot(lv) * du.hypot(dv)).max(1e-300);
1074 [dd, (lu * dv - lv * du) / norm]
1075 };
1076 for (k, &(i, j, u0, v0, level)) in crossings.iter().enumerate() {
1077 let mut is_min = true;
1078 let mut is_max = true;
1079 for di in [-1i64, 0, 1] {
1080 for dj in [-1i64, 0, 1] {
1081 let i2 = i as i64 + di;
1082 let j2 = if v_periodic { (j as i64 + dj).rem_euclid(nv as i64) } else { j as i64 + dj };
1083 if i2 < 0 || j2 < 0 {
1084 continue;
1085 }
1086 if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
1087 for &m in list {
1088 if m == k {
1089 continue;
1090 }
1091 if crossings[m].4 < level {
1092 is_min = false;
1093 }
1094 if crossings[m].4 > level {
1095 is_max = false;
1096 }
1097 }
1098 }
1099 }
1100 }
1101 if !(is_min || is_max) {
1102 continue;
1103 }
1104 let (mut u, mut v) = (u0, v0);
1105 let mut converged = false;
1106 for _ in 0..40 {
1107 let f = fold(u, v);
1108 let (su, sv) = (ku * 10.0, kv * 10.0);
1109 let fu1 = fold(u + su, v);
1110 let fu0 = fold(u - su, v);
1111 let fv1 = fold(u, v + sv);
1112 let fv0 = fold(u, v - sv);
1113 let j00 = (fu1[0] - fu0[0]) / (2.0 * su);
1114 let j01 = (fv1[0] - fv0[0]) / (2.0 * sv);
1115 let j10 = (fu1[1] - fu0[1]) / (2.0 * su);
1116 let j11 = (fv1[1] - fv0[1]) / (2.0 * sv);
1117 let det = j00 * j11 - j01 * j10;
1118 if !det.is_finite() || det.abs() < 1e-300 {
1119 break;
1120 }
1121 let du = -(f[0] * j11 - f[1] * j01) / det;
1122 let dv = -(j00 * f[1] - j10 * f[0]) / det;
1123 let cap = 4.0 * ((ur[1] - ur[0]) / (nu as f64)).max((vr[1] - vr[0]) / (nv as f64));
1125 let scale = (du.hypot(dv) / cap).max(1.0);
1126 u += du / scale;
1127 v += dv / scale;
1128 if du.hypot(dv) < 1e-12 * (ur[1] - ur[0]).max(vr[1] - vr[0]) {
1129 converged = true;
1130 break;
1131 }
1132 }
1133 let f = fold(u, v);
1134 if !(converged || (f[0].abs() < 1e-9 && f[1].abs() < 1e-5)) {
1135 continue;
1136 }
1137 if u_periodic {
1138 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1139 }
1140 if v_periodic {
1141 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1142 }
1143 if u > ur[0] && u < ur[1] && (v_periodic || (v > vr[0] && v < vr[1])) {
1144 out.push(self.source_reduced(source, u, v));
1145 params.push((u, v));
1146 }
1147 }
1148 let mut sides: Vec<(bool, f64)> = Vec::new(); if !u_periodic {
1151 sides.push((true, ur[0]));
1152 sides.push((true, ur[1]));
1153 }
1154 if !v_periodic {
1155 sides.push((false, vr[0]));
1156 sides.push((false, vr[1]));
1157 }
1158 for (along_v, fixed) in sides {
1159 let (range, n) = if along_v { (vr, nv * 4) } else { (ur, nu * 4) };
1160 let at = |t: f64| if along_v { (fixed, t) } else { (t, fixed) };
1161 let value = |t: f64| {
1162 let (u, v) = at(t);
1163 self.characteristic(source, u, v).0
1164 };
1165 let mut previous = value(range[0]);
1166 for k in 1..=n {
1167 let t = range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1168 let current = value(t);
1169 if previous * current < 0.0 {
1170 let (mut a, mut b) = (t - (range[1] - range[0]) / n as f64, t);
1171 let fa = previous;
1172 for _ in 0..100 {
1173 let m = 0.5 * (a + b);
1174 if (value(m) < 0.0) == (fa < 0.0) {
1175 a = m;
1176 } else {
1177 b = m;
1178 }
1179 }
1180 let (u, v) = at(0.5 * (a + b));
1181 out.push(self.source_reduced(source, u, v));
1182 params.push((u, v));
1183 }
1184 previous = current;
1185 }
1186 }
1187 }
1188
1189 fn source_critical(&self, index: usize) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
1190 let source = self.sources[index];
1191 let mut out = Vec::new();
1192 let mut params = Vec::new();
1193 if Self::is_face(source) {
1194 let grid = self.grids[index].as_ref().expect("face grid");
1195 let (ur, vr, periodic) = self.domain(source);
1196 let at = |i: usize, j: usize| grid.values[i * grid.nv + j].0;
1197 let u_periodic = self.u_periodic(source);
1198 let rows = if u_periodic { grid.nu - 1 } else { grid.nu };
1200 for i in 0..rows {
1201 if !u_periodic && (i == 0 || i + 1 == grid.nu) {
1202 continue;
1203 }
1204 for j in 0..grid.nv {
1205 if !periodic && (j == 0 || j + 1 == grid.nv) {
1206 continue;
1207 }
1208 let here = at(i, j);
1209 let mut is_min = true;
1210 let mut is_max = true;
1211 for di in [-1i64, 0, 1] {
1212 for dj in [-1i64, 0, 1] {
1213 if di == 0 && dj == 0 {
1214 continue;
1215 }
1216 let i2 = ((i as i64 + di).rem_euclid(rows as i64)) as usize;
1217 let j2 = ((j as i64 + dj).rem_euclid(grid.nv as i64)) as usize;
1218 let there = at(i2, j2);
1219 if there < here {
1220 is_min = false;
1221 }
1222 if there > here {
1223 is_max = false;
1224 }
1225 }
1226 }
1227 if !(is_min || is_max) {
1228 continue;
1229 }
1230 let (mut u, mut v) = Self::grid_param(grid, i, j);
1232 let hu = 1e-5 * (ur[1] - ur[0]);
1233 let hv = 1e-5 * (vr[1] - vr[0]);
1234 let level = |u: f64, v: f64| self.source_reduced(source, u, v).0;
1235 for _ in 0..50 {
1236 let f = level(u, v);
1237 let fu = (level(u + hu, v) - level(u - hu, v)) / (2.0 * hu);
1238 let fv = (level(u, v + hv) - level(u, v - hv)) / (2.0 * hv);
1239 let fuu = (level(u + hu, v) - 2.0 * f + level(u - hu, v)) / (hu * hu);
1240 let fvv = (level(u, v + hv) - 2.0 * f + level(u, v - hv)) / (hv * hv);
1241 let fuv = (level(u + hu, v + hv) - level(u + hu, v - hv) - level(u - hu, v + hv)
1242 + level(u - hu, v - hv))
1243 / (4.0 * hu * hv);
1244 let det = fuu * fvv - fuv * fuv;
1245 if det.abs() < 1e-300 {
1246 break;
1247 }
1248 let du = -(fu * fvv - fv * fuv) / det;
1249 let dv = -(fuu * fv - fuv * fu) / det;
1250 u += du;
1251 v += dv;
1252 if du.abs() < 1e-14 && dv.abs() < 1e-14 {
1253 break;
1254 }
1255 }
1256 if u_periodic {
1257 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1258 }
1259 if periodic {
1260 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1261 }
1262 if u >= ur[0] && u <= ur[1] && (periodic || (v >= vr[0] && v <= vr[1])) {
1263 out.push(self.source_reduced(source, u, v));
1264 params.push((u, v));
1265 }
1266 }
1267 }
1268 self.folds_and_kinks(source, grid, &mut out, &mut params);
1269 } else {
1270 let (range, _, _) = self.domain(source);
1271 let n = 2048;
1272 let param = |k: usize| range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1273 let values: Vec<f64> = (0..=n).map(|k| self.source_reduced(source, param(k), 0.0).0).collect();
1274 for k in 1..n {
1275 let (a, b, c) = (values[k - 1], values[k], values[k + 1]);
1276 if (b <= a && b <= c) || (b >= a && b >= c) {
1277 let sign = if b <= a && b <= c { 1.0 } else { -1.0 };
1278 let (mut lo, mut hi) = (param(k - 1), param(k + 1));
1279 let ratio = 0.618_033_988_749_894_9;
1280 for _ in 0..100 {
1281 let c1 = hi - ratio * (hi - lo);
1282 let d1 = lo + ratio * (hi - lo);
1283 if sign * self.source_reduced(source, c1, 0.0).0 < sign * self.source_reduced(source, d1, 0.0).0 {
1284 hi = d1;
1285 } else {
1286 lo = c1;
1287 }
1288 }
1289 out.push(self.source_reduced(source, 0.5 * (lo + hi), 0.0));
1290 params.push((0.5 * (lo + hi), 0.0));
1291 }
1292 }
1293 }
1294 (out, params)
1295 }
1296}
1297
1298fn same_or_bounding(engine: &Engine, a: usize, b: usize) -> bool {
1304 if a == b {
1305 return true;
1306 }
1307 let pieces = match engine.tool {
1308 SweptTool::Revolved { meridian, .. } => meridian.len(),
1309 SweptTool::Prism { outline, .. } => outline.len(),
1310 };
1311 let face = |s: Source| match s {
1313 Source::RevolvedFace { piece } | Source::PrismFace { piece } => Some(piece),
1314 _ => None,
1315 };
1316 let edge = |s: Source| match s {
1317 Source::RevolvedCorner { piece } | Source::PrismVertex { piece } => Some(piece),
1318 _ => None,
1319 };
1320 let (sa, sb) = (engine.sources[a], engine.sources[b]);
1321 let same_piece = |x: usize, y: usize| {
1323 x == y || engine.wrap.as_ref().is_some_and(|map| map[x] == y || map[y] == x)
1324 };
1325 let bounds = |f: usize, e: usize| {
1326 same_piece(e, f) || same_piece(e, (f + 1) % pieces)
1327 };
1328 let _ = bounds;
1329 match (face(sa), edge(sa), face(sb), edge(sb)) {
1330 (Some(f), _, Some(g), _) => same_piece(f, g),
1331 (_, Some(e), _, Some(g)) => same_piece(e, g),
1332 _ => false,
1333 }
1334}
1335
1336fn compatible(engine: &Engine, a: &[Interval], b: &[Interval]) -> bool {
1339 let angular = engine.angular;
1340 if a.len() != b.len() {
1341 return false;
1342 }
1343 let mut used = vec![false; b.len()];
1345 for x in a {
1346 let mid = 0.5 * (x.lo + x.hi);
1347 let mut best = None;
1348 let mut best_distance = f64::INFINITY;
1349 for (k, y) in b.iter().enumerate() {
1350 let other = 0.5 * (y.lo + y.hi);
1351 let d = if angular { wrap_pi(mid - other).abs() } else { (mid - other).abs() };
1352 if d < best_distance {
1353 best_distance = d;
1354 best = Some(k);
1355 }
1356 }
1357 let Some(k) = best else { return false };
1358 let y = &b[k];
1359 let overlap = if angular {
1360 let shift = (wrap_pi(y.lo - x.lo)) - (y.lo - x.lo);
1361 let (ylo, yhi) = (y.lo + shift, y.hi + shift);
1362 x.hi.min(yhi) - x.lo.max(ylo)
1363 } else {
1364 x.hi.min(y.hi) - x.lo.max(y.lo)
1365 };
1366 if used[k]
1367 || overlap < 0.0
1368 || !same_or_bounding(engine, x.lo_source, y.lo_source)
1369 || !same_or_bounding(engine, x.hi_source, y.hi_source)
1370 {
1371 return false;
1372 }
1373 used[k] = true;
1374 }
1375 true
1376}
1377
1378struct Piece {
1381 points: Vec<(f64, f64)>,
1382 sigma: Vec<f64>,
1383 level_segment: bool,
1385}
1386
1387pub fn sweep_solid_within(
1390 tool: &SweptTool,
1391 motion: &SolidMotion,
1392 clip: &SweepClip,
1393) -> Result<Vec<BrepSolid>, KernelRefusal> {
1394 let ez = clip
1396 .axis_direction
1397 .normalized()
1398 .or_refuse(KernelStage::Collect, "clip_axis")?;
1399 let ex = clip
1400 .reference
1401 .sub(ez.scale(clip.reference.dot(ez)))
1402 .normalized()
1403 .or_refuse(KernelStage::Collect, "clip_reference")?;
1404 let frame = Frame { origin: clip.axis_origin, ex, ey: ez.cross(ex), ez };
1405 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
1406 return Err(KernelRefusal::input(
1407 KernelStage::Collect,
1408 "motion_range",
1409 format!("{SOLID_SWEEP_REFUSAL}: the motion's range must run forwards"),
1410 ));
1411 }
1412 if motion.steps.is_empty() {
1413 return Err(KernelRefusal::input(KernelStage::Collect, "motion_empty", format!("{SOLID_SWEEP_REFUSAL}: no motion")));
1414 }
1415 let mut axes = Vec::with_capacity(motion.steps.len());
1416 for step in &motion.steps {
1417 axes.push(step.axis_direction.normalized().or_refuse(KernelStage::Collect, "step_axis")?);
1418 }
1419 if !(clip.radius[0] >= 0.0 && clip.radius[1] > clip.radius[0] && clip.axial[1] > clip.axial[0]) {
1420 return Err(KernelRefusal::input(
1421 KernelStage::Collect,
1422 "clip_range",
1423 format!("{SOLID_SWEEP_REFUSAL}: the clip's ranges must be increasing"),
1424 ));
1425 }
1426 let on_axis = |origin: Vec3, axis: Vec3| -> bool {
1427 let parallel = axis.cross(ez).length() < 1e-12;
1428 let offset = origin.sub(clip.axis_origin);
1429 parallel && offset.sub(ez.scale(offset.dot(ez))).length() < 1e-9 * (1.0 + clip.radius[1])
1430 };
1431 let class = match tool {
1432 SweptTool::Revolved { meridian, .. } => {
1433 validate_outline(meridian, "meridian")?;
1434 if motion.steps.len() != 1 || !on_axis(motion.steps[0].axis_origin, axes[0]) {
1435 return Err(refuse(
1436 "motion_class",
1437 "a solid of revolution is swept here only by one screw or turn about the clip's axis".into(),
1438 ));
1439 }
1440 let step = motion.steps[0];
1441 let sense = axes[0].dot(ez).signum();
1442 if step.turn == 0.0 {
1443 return Err(refuse("motion_class", "a slide of a solid of revolution is not built here".into()));
1444 }
1445 if step.advance == 0.0 {
1446 Class::Turn
1447 } else {
1448 Class::Screw((step.advance * sense) / (step.turn * sense))
1450 }
1451 }
1452 SweptTool::Prism { normal, outline, from, to, origin, .. } => {
1453 validate_outline(outline, "prism")?;
1454 let n = normal.normalized().or_refuse(KernelStage::Collect, "prism_normal")?;
1455 if n.cross(ez).length() > 1e-12 {
1456 return Err(refuse("motion_class", "a prism is swept here only along its own direction's clip axis".into()));
1457 }
1458 for (step, axis) in motion.steps.iter().zip(&axes) {
1459 let turn_ok = step.turn == 0.0 || axis.cross(n).length() < 1e-12;
1460 let advance_ok = step.advance == 0.0 || (step.turn == 0.0 && axis.dot(n).abs() < 1e-12);
1461 if !(turn_ok && advance_ok) {
1462 return Err(refuse(
1463 "motion_class",
1464 "a prism is swept here only by turns about axes along it and slides across it".into(),
1465 ));
1466 }
1467 }
1468 let base = clip.axis_origin.sub(*origin).dot(n);
1470 let sense = ez.dot(n);
1471 let h0 = base + sense * clip.axial[0];
1472 let h1 = base + sense * clip.axial[1];
1473 if h0.min(h1) < *from || h0.max(h1) > *to {
1474 return Err(refuse(
1475 "clip_past_prism",
1476 "the clip reaches past the prism's ends, where the sweep is not a prism".into(),
1477 ));
1478 }
1479 Class::Planar
1480 }
1481 };
1482 let prism_height = match tool {
1483 SweptTool::Prism { from, to, .. } => 0.5 * (from + to),
1484 _ => 0.0,
1485 };
1486
1487 let wrap: Option<Vec<usize>> = match tool {
1492 SweptTool::Prism { origin, normal, x_axis, outline, .. } => {
1493 let probe = Engine {
1494 tool,
1495 motion,
1496 axes: axes.clone(),
1497 frame,
1498 class,
1499 clip,
1500 sources: Vec::new(),
1501 grids: Vec::new(),
1502 critical: Vec::new(),
1503 critical_params: Vec::new(),
1504 angular: true,
1505 prism_height,
1506 scale: 1.0,
1507 cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1508 wrap: None,
1509 counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1510 };
1511 let n = normal.normalized_or(*normal);
1512 let xa = x_axis.normalized_or(*x_axis);
1513 let ya = n.cross(xa);
1514 let vertex = |k: usize, t: f64| {
1515 let q = outline[k].point(0.0);
1516 probe.motion_apply(t, origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(prism_height)))
1517 };
1518 let size = scale_of(outline);
1519 let mut map = Vec::with_capacity(outline.len());
1520 for k in 0..outline.len() {
1521 let end = vertex(k, motion.to);
1522 match (0..outline.len()).find(|&j| vertex(j, motion.from).sub(end).length() < 1e-9 * size) {
1523 Some(j) => map.push(j),
1524 None => break,
1525 }
1526 }
1527 let consistent = map.len() == outline.len()
1528 && (0..outline.len()).all(|k| map[(k + 1) % outline.len()] == (map[k] + 1) % outline.len());
1529 consistent.then_some(map)
1530 }
1531 SweptTool::Revolved { .. } => None,
1532 };
1533 let periodic_motion = wrap.is_some();
1534 let mut sources = Vec::new();
1535 match tool {
1536 SweptTool::Revolved { meridian, .. } => {
1537 let on_axis = |p: [f64; 2]| p[0].abs() <= 1e-12 * scale_of(meridian);
1540 for piece in 0..meridian.len() {
1541 let lies_on_axis = matches!(meridian[piece], OutlinePiece::Line { from, to } if on_axis(from) && on_axis(to));
1542 if !lies_on_axis {
1543 sources.push(Source::RevolvedFace { piece });
1544 }
1545 if !meridian[piece].is_full_circle() && !on_axis(meridian[piece].point(0.0)) {
1546 sources.push(Source::RevolvedCorner { piece });
1547 }
1548 }
1549 }
1550 SweptTool::Prism { outline, .. } => {
1551 for piece in 0..outline.len() {
1552 sources.push(Source::PrismFace { piece });
1553 if !outline[piece].is_full_circle() {
1554 sources.push(Source::PrismVertex { piece });
1555 }
1556 if !periodic_motion {
1557 sources.push(Source::PrismEnd { piece, end: false });
1558 sources.push(Source::PrismEnd { piece, end: true });
1559 }
1560 }
1561 }
1562 }
1563 let scale = clip.radius[1].max(1.0);
1564 let mut engine = Engine {
1565 tool,
1566 motion,
1567 axes,
1568 frame,
1569 class,
1570 clip,
1571 sources,
1572 grids: Vec::new(),
1573 critical: Vec::new(),
1574 critical_params: Vec::new(),
1575 angular: class != Class::Turn,
1576 prism_height,
1577 scale,
1578 cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1579 wrap,
1580 counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1581 };
1582 engine.grids = engine
1583 .sources
1584 .iter()
1585 .map(|source| if Engine::is_face(*source) { Some(engine.build_grid(*source)) } else { None })
1586 .collect();
1587 let critical: Vec<_> = (0..engine.sources.len()).map(|k| engine.source_critical(k)).collect();
1588 engine.critical_params = critical.iter().map(|c| c.1.clone()).collect();
1589 engine.critical = critical.into_iter().map(|c| c.0).collect();
1590
1591 if matches!(class, Class::Screw(_) | Class::Turn) {
1593 check_ends_clear(&engine)?;
1594 }
1595
1596 let (l0, l1) = (clip.radius[0], clip.radius[1]);
1598 let scan = 192;
1599 let mut levels: Vec<f64> = (0..=scan).map(|k| l0 + (l1 - l0) * k as f64 / scan as f64).collect();
1600 for level in &mut levels {
1602 for critical in engine.critical.iter().flatten() {
1603 if (critical.0 - *level).abs() < 1e-9 * scale {
1604 *level += 1e-7 * (l1 - l0);
1605 }
1606 }
1607 }
1608 let mut structure = Vec::with_capacity(levels.len());
1609 for &level in &levels {
1610 structure.push(engine.intervals_at(level)?);
1611 }
1612 let debug = std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok();
1613 let mut breaks = vec![l0];
1614 for k in 0..scan {
1615 find_events(&engine, levels[k], &structure[k], levels[k + 1], &structure[k + 1], &mut breaks, 0)?;
1616 }
1617 breaks.push(l1);
1618 let last = breaks.len() - 1;
1623 for level in breaks[1..last].iter_mut() {
1624 let mut nearest = *level;
1625 let mut distance = 1e-5 * scale;
1626 for critical in engine.critical.iter().flatten() {
1627 if (critical.0 - *level).abs() < distance {
1628 distance = (critical.0 - *level).abs();
1629 nearest = critical.0;
1630 }
1631 }
1632 *level = nearest;
1633 }
1634 breaks.sort_by(|a, b| a.total_cmp(b));
1635 let mut kept: Vec<f64> = Vec::with_capacity(breaks.len());
1640 for (index, &level) in breaks.iter().enumerate() {
1641 let is_end = index == 0 || index + 1 == breaks.len();
1642 match kept.last() {
1643 Some(&previous) if level - previous < 1e-5 * scale => {
1644 if is_end {
1645 kept.pop();
1646 kept.push(level);
1647 }
1648 }
1649 _ => kept.push(level),
1650 }
1651 }
1652 let mut breaks = kept;
1653
1654 let mut fold_ends: Vec<f64> = Vec::new();
1664 for index in 1..breaks.len().saturating_sub(1) {
1665 let level = breaks[index];
1666 let snapped = engine.critical.iter().flatten().any(|c| (c.0 - level).abs() < 1e-9 * scale);
1667 if snapped {
1668 continue;
1669 }
1670 let room = (level - breaks[index - 1]).min(breaks[index + 1] - level);
1671 let delta = (5e-6 * scale).min(room / 8.0);
1674 let below = engine.intervals_at(level - delta)?;
1675 let above = engine.intervals_at(level + delta)?;
1676 if below.len() <= above.len() {
1677 continue;
1678 }
1679 let mut refined: Vec<(f64, f64, usize, usize)> = Vec::new();
1681 for k in 0..below.len() {
1682 let (x, y) = (below[k], below[(k + 1) % below.len()]);
1683 if !engine.angular && k + 1 == below.len() {
1684 break;
1685 }
1686 let gap_at = |intervals: &[Interval]| -> Option<(f64, f64)> {
1687 let mut best: Option<(f64, f64, f64)> = None;
1689 for i in 0..intervals.len() {
1690 let (p, q) = (intervals[i], intervals[(i + 1) % intervals.len()]);
1691 let d = engine.o_difference(p.hi, x.hi).abs() + engine.o_difference(q.lo, y.lo).abs();
1692 if best.map_or(true, |b| d < b.2) {
1693 let gap = engine.o_difference(q.lo, p.hi);
1694 best = Some((gap, p.hi + 0.5 * gap, d));
1695 }
1696 }
1697 best.map(|b| (b.0, b.1))
1698 };
1699 let mut samples = Vec::new();
1700 for factor in [1.0, 2.0, 4.0] {
1701 let l = level - delta * factor;
1702 let intervals = engine.intervals_at(l)?;
1703 if intervals.len() != below.len() {
1704 break;
1705 }
1706 if let Some((gap, middle)) = gap_at(&intervals) {
1707 samples.push((l, gap, middle));
1708 }
1709 }
1710 if samples.len() < 3 || samples.iter().any(|s| s.1 <= 0.0 || s.1 > 0.5) {
1711 continue;
1712 }
1713 let quad = |values: [f64; 3]| -> (f64, f64, f64) {
1715 let (x0, x1, x2) = (samples[0].0, samples[1].0, samples[2].0);
1716 let (y0, y1, y2) = (values[0], values[1], values[2]);
1717 let d01 = (y0 - y1) / (x0 - x1);
1718 let d12 = (y1 - y2) / (x1 - x2);
1719 let a2 = (d01 - d12) / (x0 - x2);
1720 let a1 = d01 - a2 * (x0 + x1);
1721 let a0 = y0 - a1 * x0 - a2 * x0 * x0;
1722 (a0, a1, a2)
1723 };
1724 let (c0, c1, c2) = quad([samples[0].1.powi(2), samples[1].1.powi(2), samples[2].1.powi(2)]);
1725 let root = if c2.abs() < 1e-300 {
1726 -c0 / c1
1727 } else {
1728 let disc = c1 * c1 - 4.0 * c2 * c0;
1729 if disc < 0.0 {
1730 continue;
1731 }
1732 let r1 = (-c1 + disc.sqrt()) / (2.0 * c2);
1733 let r2 = (-c1 - disc.sqrt()) / (2.0 * c2);
1734 if (r1 - level).abs() < (r2 - level).abs() { r1 } else { r2 }
1735 };
1736 if !(root > samples[0].0 && (root - level).abs() < delta) {
1737 continue;
1738 }
1739 let (m0, m1, m2) = quad([samples[0].2, samples[1].2, samples[2].2]);
1740 let middle = m0 + m1 * root + m2 * root * root;
1741 refined.push((root, middle, x.hi_source, y.lo_source));
1742 }
1743 if !refined.is_empty() {
1744 let root = refined.iter().map(|r| r.0).sum::<f64>() / refined.len() as f64;
1747 for &(own, middle, a_source, b_source) in &refined {
1748 if debug {
1749 eprintln!(" fold: break {level:.12} -> {own:.12} at o {middle:.12}");
1750 }
1751 engine.critical[a_source].push((root, middle));
1752 engine.critical[b_source].push((root, middle));
1753 }
1754 breaks[index] = root;
1755 fold_ends.push(root);
1756 }
1757 }
1758
1759 if debug {
1760 eprintln!("solid sweep: after events {:?} breaks {}", engine.counters.get(), breaks.len());
1761 }
1762 let mut pieces: Vec<Piece> = Vec::new();
1764 let mut band_ends: Vec<(f64, f64, Vec<(usize, usize)>)> = Vec::new();
1766 for window in breaks.windows(2) {
1767 let (a, b) = (window[0], window[1]);
1768 if b - a < 1e-12 * scale {
1769 continue;
1770 }
1771 let mid = engine.intervals_at(0.5 * (a + b))?;
1772 if mid.is_empty() {
1773 continue;
1774 }
1775 let (band_pieces, _, _) = sample_band(&engine, a, b, &mid, &fold_ends)?;
1776 let first = pieces.len();
1777 let pairs = (0..band_pieces.len() / 2).map(|i| (first + 2 * i, first + 2 * i + 1)).collect();
1778 pieces.extend(band_pieces);
1779 band_ends.push((a, b, pairs));
1780 }
1781 if pieces.is_empty() {
1782 return Ok(Vec::new());
1783 }
1784 if debug {
1785 eprintln!("solid sweep: after bands {:?}", engine.counters.get());
1786 for (k, critical) in engine.critical.iter().enumerate() {
1787 eprintln!(" source {k} {:?}: critical {:?}", engine.sources[k], critical);
1788 }
1789 }
1790
1791 let snap_length = 1e-5 * scale;
1799 let mut break_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1800 break_levels.sort_by(|a, b| a.total_cmp(b));
1801 break_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1802 for &level in &break_levels {
1803 let tolerance = if engine.angular { snap_length / level.max(1e-12) } else { snap_length };
1804 let mut ends: Vec<(usize, bool)> = Vec::new();
1805 for (index, piece) in pieces.iter().enumerate() {
1806 if (piece.points[0].0 - level).abs() < 1e-12 * scale {
1807 ends.push((index, false));
1808 }
1809 if (piece.points.last().expect("nonempty").0 - level).abs() < 1e-12 * scale {
1810 ends.push((index, true));
1811 }
1812 }
1813 let value = |pieces: &Vec<Piece>, (index, last): (usize, bool)| {
1814 if last { pieces[index].points.last().expect("nonempty").1 } else { pieces[index].points[0].1 }
1815 };
1816 for i in 0..ends.len() {
1817 let anchor = value(&pieces, ends[i]);
1818 for j in i + 1..ends.len() {
1819 let other = value(&pieces, ends[j]);
1820 let d = engine.o_difference(other, anchor);
1821 if d.abs() < tolerance && d != 0.0 {
1822 let (index, last) = ends[j];
1823 let target = other - d;
1824 let points = &mut pieces[index].points;
1825 if last {
1826 points.last_mut().expect("nonempty").1 = target;
1827 } else {
1828 points[0].1 = target;
1829 }
1830 }
1831 }
1832 }
1833 }
1834
1835 let mut all_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1837 all_levels.sort_by(|a, b| a.total_cmp(b));
1838 all_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1839 for &level in &all_levels {
1840 let mut below: Vec<(f64, f64)> = Vec::new();
1841 let mut above: Vec<(f64, f64)> = Vec::new();
1842 for (a, b, pairs) in &band_ends {
1843 for &(lo, hi) in pairs {
1844 let (lo, hi) = (&pieces[lo].points, &pieces[hi].points);
1845 if (b - level).abs() < 1e-12 * scale {
1846 below.push((lo.last().expect("nonempty").1, hi.last().expect("nonempty").1));
1847 }
1848 if (a - level).abs() < 1e-12 * scale {
1849 above.push((lo[0].1, hi[0].1));
1850 }
1851 }
1852 }
1853 let tolerance = if engine.angular { 1e-9 * scale / level.max(1e-12) } else { 1e-9 * scale };
1854 for (lo, hi) in symmetric_difference(&below, &above, engine.angular, tolerance) {
1855 let steps = 2;
1856 pieces.push(Piece {
1857 points: (0..=steps)
1858 .map(|k| (level, lo + (hi - lo) * k as f64 / steps as f64))
1859 .collect(),
1860 sigma: (0..=steps).map(|k| k as f64 / steps as f64).collect(),
1861 level_segment: true,
1862 });
1863 }
1864 }
1865
1866 if std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1867 eprintln!("solid sweep: breaks {breaks:?}");
1868 for piece in &pieces {
1869 let a = piece.points[0];
1870 let b = *piece.points.last().expect("nonempty");
1871 eprintln!(
1872 " piece {} ({:.9}, {:.9}) -> ({:.9}, {:.9}) n={}",
1873 if piece.level_segment { "LEVEL" } else { "edge " },
1874 a.0, a.1, b.0, b.1, piece.points.len()
1875 );
1876 if std::env::var("BREP_DEBUG_SOLID_SWEEP").as_deref() == Ok("2") {
1877 for p in piece.points.iter().take(6) {
1878 eprintln!(" {:.12} {:.12}", p.0, p.1);
1879 }
1880 }
1881 }
1882 }
1883 let loops = assemble_loops(&engine, pieces)?;
1885 let mut bodies = Vec::new();
1886 for pieces in loops {
1887 bodies.push(build_body(&engine, &pieces)?);
1888 }
1889 Ok(bodies)
1890}
1891
1892fn find_events(
1893 engine: &Engine,
1894 a: f64,
1895 sa: &[Interval],
1896 b: f64,
1897 sb: &[Interval],
1898 breaks: &mut Vec<f64>,
1899 depth: usize,
1900) -> Result<(), KernelRefusal> {
1901 if compatible(engine, sa, sb) {
1902 return Ok(());
1903 }
1904 if depth <= 2 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1905 let summary = |s: &[Interval]| -> Vec<(usize, usize, f64, f64)> {
1906 s.iter().map(|x| (x.lo_source, x.hi_source, (x.lo * 1e4).round() / 1e4, (x.hi * 1e4).round() / 1e4)).collect()
1907 };
1908 eprintln!(" find_events depth {depth} [{a:.9}, {b:.9}] {:?}\n -> {:?} counters {:?}", summary(sa), summary(sb), engine.counters.get());
1909 }
1910 if b - a < 1e-13 * engine.scale || depth > 60 {
1911 breaks.push(0.5 * (a + b));
1912 return Ok(());
1913 }
1914 let m = 0.5 * (a + b);
1915 let sm = engine.intervals_at(m)?;
1916 if !compatible(engine, sa, &sm) && !compatible(engine, &sm, sb) && depth > 8 {
1919 breaks.push(m);
1920 return Ok(());
1921 }
1922 find_events(engine, a, sa, m, &sm, breaks, depth + 1)?;
1923 find_events(engine, m, &sm, b, sb, breaks, depth + 1)
1924}
1925
1926fn end_value(engine: &Engine, source: usize, level: f64, guess: f64) -> f64 {
1929 let mut candidates = Vec::new();
1930 engine.source_candidates(source, level, &mut candidates);
1931 let mut options: Vec<f64> = candidates.iter().map(|c| c.0).collect();
1932 for critical in &engine.critical[source] {
1933 if (critical.0 - level).abs() < 1e-7 * engine.scale {
1934 options.push(critical.1);
1935 }
1936 }
1937 let mut best = guess;
1938 let mut distance = f64::INFINITY;
1939 for option in options {
1940 let d = engine.o_difference(option, guess).abs();
1941 if d < distance {
1942 distance = d;
1943 best = guess + engine.o_difference(option, guess);
1944 }
1945 }
1946 best
1947}
1948
1949#[allow(clippy::type_complexity)]
1952fn sample_band(
1953 engine: &Engine,
1954 a: f64,
1955 b: f64,
1956 mid: &[Interval],
1957 fold_ends: &[f64],
1958) -> Result<(Vec<Piece>, Vec<(f64, f64)>, Vec<(f64, f64)>), KernelRefusal> {
1959 let count = mid.len();
1960 let n = 24usize;
1961 let near_fold = fold_ends.iter().any(|f| (f - a).abs() < 1e-12 * engine.scale || (f - b).abs() < 1e-12 * engine.scale);
1964 let n_cap = if near_fold {
1965 let keep = (1e-4 * engine.scale).min((b - a) / 50.0);
1966 let ratio = (1.0 - 2.0 * keep / (b - a)).clamp(-1.0, 1.0);
1968 ((PI / ratio.acos()).floor() as usize).max(8)
1969 } else {
1970 192
1971 };
1972 let mut n = n.min(n_cap);
1973 loop {
1974 let sigma: Vec<f64> = (0..=n).map(|k| k as f64 / n as f64).collect();
1975 let level_at = |s: f64| a + (b - a) * (1.0 - (PI * s).cos()) / 2.0;
1976 let mut lo = vec![vec![f64::NAN; n + 1]; count];
1978 let mut hi = vec![vec![f64::NAN; n + 1]; count];
1979 let mut lo_src = vec![vec![0usize; n + 1]; count];
1980 let mut hi_src = vec![vec![0usize; n + 1]; count];
1981 let mid_index = n / 2;
1982 let mid_intervals = engine.intervals_at(level_at(sigma[mid_index]))?;
1983 if mid_intervals.len() != count {
1984 return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
1985 }
1986 for (i, interval) in mid_intervals.iter().enumerate() {
1987 lo[i][mid_index] = interval.lo;
1988 hi[i][mid_index] = interval.hi;
1989 lo_src[i][mid_index] = interval.lo_source;
1990 hi_src[i][mid_index] = interval.hi_source;
1991 }
1992 for direction in [-1i64, 1] {
1993 let mut k = mid_index as i64 + direction;
1994 while k > 0 && (k as usize) < n {
1995 let ku = k as usize;
1996 let previous = (ku as i64 - direction) as usize;
1997 let intervals = engine.intervals_at(level_at(sigma[ku]))?;
1998 if intervals.len() != count {
1999 return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2000 }
2001 for i in 0..count {
2002 let centre = 0.5 * (lo[i][previous] + hi[i][previous]);
2003 let nearest = intervals
2004 .iter()
2005 .min_by(|x, y| {
2006 let dx = engine.o_difference(0.5 * (x.lo + x.hi), centre).abs();
2007 let dy = engine.o_difference(0.5 * (y.lo + y.hi), centre).abs();
2008 dx.total_cmp(&dy)
2009 })
2010 .expect("nonempty");
2011 let shift = engine.o_difference(nearest.lo, lo[i][previous]) - (nearest.lo - lo[i][previous]);
2012 lo[i][ku] = nearest.lo + shift;
2013 hi[i][ku] = nearest.hi + shift;
2014 lo_src[i][ku] = nearest.lo_source;
2015 hi_src[i][ku] = nearest.hi_source;
2016 }
2017 k += direction;
2018 }
2019 }
2020 for i in 0..count {
2023 for (end, inner, inner2, level) in [(0usize, 1usize, 2usize, a), (n, n - 1, n - 2, b)] {
2024 let extrapolate = |values: &Vec<f64>| 2.0 * values[inner] - values[inner2];
2025 lo[i][end] = end_value(engine, lo_src[i][inner], level, extrapolate(&lo[i]));
2026 hi[i][end] = end_value(engine, hi_src[i][inner], level, extrapolate(&hi[i]));
2027 }
2028 }
2029 let mut worst = 0.0f64;
2031 let mut worst_at = (0usize, 0usize, 0usize, 0.0f64, 0.0f64);
2032 for i in 0..count {
2033 for (side, values) in [&lo[i], &hi[i]].into_iter().enumerate() {
2034 let points: Vec<Vec3> = (0..=n)
2035 .map(|k| reduced_to_plane(engine, level_at(sigma[k]), values[k]))
2036 .collect();
2037 let curve = fit_piece(&points)?;
2038 let parameters = chord_parameters(&points);
2039 for k in (1..n - 1).step_by((n / 8).max(1)) {
2040 let s = 0.5 * (sigma[k] + sigma[k + 1]);
2041 let level = level_at(s);
2042 let intervals = engine.intervals_at(level)?;
2043 let guess = 0.5 * (values[k] + values[k + 1]);
2044 let exact = intervals
2045 .iter()
2046 .flat_map(|x| [x.lo, x.hi])
2047 .map(|o| guess + engine.o_difference(o, guess))
2048 .min_by(|x, y| (x - guess).abs().total_cmp(&(y - guess).abs()))
2049 .unwrap_or(guess);
2050 let truth = reduced_to_plane(engine, level, exact);
2051 let bracket = (parameters[k.saturating_sub(1)], parameters[(k + 2).min(n)]);
2052 let off = distance_in_bracket(&curve, truth, bracket.0, bracket.1)?;
2053 if off > worst {
2054 worst = off;
2055 worst_at = (i, side, k, level, exact);
2056 }
2057 }
2058 }
2059 }
2060 if n >= n_cap && worst > 1e-6 * engine.scale {
2061 return Err(refuse(
2062 "fit_tolerance",
2063 format!(
2064 "a boundary piece of the band {a}..{b} stays {worst:.3e} off its envelope at {n} samples \
2065 (interval {}, {}, sample {}, level {}, o {})",
2066 worst_at.0,
2067 if worst_at.1 == 0 { "lo" } else { "hi" },
2068 worst_at.2,
2069 worst_at.3,
2070 worst_at.4
2071 ),
2072 ));
2073 }
2074 if worst <= 1e-8 * engine.scale || n >= n_cap {
2075 let mut pieces = Vec::new();
2076 for i in 0..count {
2077 for values in [&lo[i], &hi[i]] {
2078 pieces.push(Piece {
2079 points: (0..=n).map(|k| (level_at(sigma[k]), values[k])).collect(),
2080 sigma: sigma.clone(),
2081 level_segment: false,
2082 });
2083 }
2084 }
2085 let bottoms = (0..count).map(|i| (lo[i][0], hi[i][0])).collect();
2086 let tops = (0..count).map(|i| (lo[i][n], hi[i][n])).collect();
2087 return Ok((pieces, bottoms, tops));
2088 }
2089 n = (n * 2).min(n_cap);
2090 }
2091}
2092
2093fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
2099 let mut parameters = vec![0.0; points.len()];
2100 for k in 1..points.len() {
2101 parameters[k] = parameters[k - 1] + points[k].sub(points[k - 1]).length();
2102 }
2103 let total = parameters[points.len() - 1].max(1e-300);
2104 for parameter in &mut parameters {
2105 *parameter /= total;
2106 }
2107 let last = points.len() - 1;
2108 parameters[last] = 1.0;
2109 parameters
2110}
2111
2112fn fit_piece(points: &[Vec3]) -> Result<NurbsCurve, KernelRefusal> {
2113 interpolate_curve(points, 3, &chord_parameters(points)).or_refuse(KernelStage::Fragment, "piece_fit")
2114}
2115
2116fn distance_in_bracket(curve: &NurbsCurve, target: Vec3, a: f64, b: f64) -> Result<f64, KernelRefusal> {
2118 let distance = |t: f64| -> Result<f64, KernelRefusal> {
2119 Ok(curve.evaluate(t).or_refuse(KernelStage::Fragment, "piece_eval")?.sub(target).length())
2120 };
2121 let ratio = 0.618_033_988_749_894_9;
2122 let (mut lo, mut hi) = (a, b);
2123 for _ in 0..80 {
2124 let c = hi - ratio * (hi - lo);
2125 let d = lo + ratio * (hi - lo);
2126 if distance(c)? < distance(d)? {
2127 hi = d;
2128 } else {
2129 lo = c;
2130 }
2131 }
2132 distance(0.5 * (lo + hi))
2133}
2134
2135fn symmetric_difference(below: &[(f64, f64)], above: &[(f64, f64)], angular: bool, tolerance: f64) -> Vec<(f64, f64)> {
2137 let normalise = |list: &[(f64, f64)]| -> Vec<(f64, f64)> {
2139 let mut out = Vec::new();
2140 for &(lo, hi) in list {
2141 if angular {
2142 let shift = lo.rem_euclid(TAU) - lo;
2143 let (lo, hi) = (lo + shift, hi + shift);
2144 if hi > TAU {
2145 out.push((lo, TAU));
2146 out.push((0.0, hi - TAU));
2147 } else {
2148 out.push((lo, hi));
2149 }
2150 } else {
2151 out.push((lo, hi));
2152 }
2153 }
2154 out
2155 };
2156 let a = normalise(below);
2157 let b = normalise(above);
2158 let mut cuts: Vec<f64> = a.iter().chain(&b).flat_map(|&(lo, hi)| [lo, hi]).collect();
2159 cuts.sort_by(|x, y| x.total_cmp(y));
2160 cuts.dedup_by(|y, x| (*y - *x).abs() < tolerance);
2161 let inside = |list: &Vec<(f64, f64)>, x: f64| list.iter().any(|&(lo, hi)| x > lo && x < hi);
2162 let mut out: Vec<(f64, f64)> = Vec::new();
2163 for w in cuts.windows(2) {
2164 let m = 0.5 * (w[0] + w[1]);
2165 if inside(&a, m) != inside(&b, m) && w[1] - w[0] > tolerance {
2166 if let Some(last) = out.last_mut() {
2167 if (last.1 - w[0]).abs() < tolerance {
2168 last.1 = w[1];
2169 continue;
2170 }
2171 }
2172 out.push((w[0], w[1]));
2173 }
2174 }
2175 if angular && out.len() > 1 {
2176 let first = out[0];
2177 let last = *out.last().expect("nonempty");
2178 if first.0.abs() < tolerance && (last.1 - TAU).abs() < tolerance {
2179 out.pop();
2180 out[0] = (last.0 - TAU, first.1);
2181 }
2182 }
2183 out
2184}
2185
2186fn reduced_to_plane(engine: &Engine, level: f64, o: f64) -> Vec3 {
2188 let clip = engine.clip;
2189 match engine.class {
2190 Class::Screw(c) => {
2191 let z = clip.axial[0];
2192 let alpha = o + z / c;
2193 engine.frame.world([level * alpha.cos(), level * alpha.sin(), z])
2194 }
2195 Class::Turn => {
2196 let theta = clip.angle.map(|a| a[0]).unwrap_or(0.0);
2197 engine.frame.world([level * theta.cos(), level * theta.sin(), o])
2198 }
2199 Class::Planar => engine.frame.world([level * o.cos(), level * o.sin(), clip.axial[0]]),
2200 }
2201}
2202
2203fn assemble_loops(engine: &Engine, pieces: Vec<Piece>) -> Result<Vec<Vec<Piece>>, KernelRefusal> {
2205 let tolerance = 1e-6 * engine.scale;
2206 let ends: Vec<(Vec3, Vec3)> = pieces
2207 .iter()
2208 .map(|piece| {
2209 let first = piece.points[0];
2210 let last = *piece.points.last().expect("nonempty");
2211 (reduced_to_plane(engine, first.0, first.1), reduced_to_plane(engine, last.0, last.1))
2212 })
2213 .collect();
2214 let mut used = vec![false; pieces.len()];
2215 let mut orders: Vec<Vec<(usize, bool)>> = Vec::new();
2216 for start in 0..pieces.len() {
2217 if used[start] {
2218 continue;
2219 }
2220 used[start] = true;
2221 let mut order = vec![(start, false)];
2222 let origin = ends[start].0;
2223 let mut cursor = ends[start].1;
2224 let mut guard = 0;
2225 while cursor.sub(origin).length() > tolerance {
2226 guard += 1;
2227 if guard > pieces.len() {
2228 return Err(refuse("loop_open", "the section's boundary does not close".into()));
2229 }
2230 let mut next = None;
2231 let mut best = tolerance;
2232 for k in 0..pieces.len() {
2233 if used[k] {
2234 continue;
2235 }
2236 for (reversed, end) in [(false, ends[k].0), (true, ends[k].1)] {
2237 let d = end.sub(cursor).length();
2238 if d < best {
2239 best = d;
2240 next = Some((k, reversed));
2241 }
2242 }
2243 }
2244 let Some((k, reversed)) = next else {
2245 return Err(refuse(
2246 "loop_open",
2247 format!("the section's boundary does not close (a gap at {:?})", cursor),
2248 ));
2249 };
2250 used[k] = true;
2251 order.push((k, reversed));
2252 cursor = if reversed { ends[k].0 } else { ends[k].1 };
2253 }
2254 orders.push(order);
2255 }
2256 if orders.len() > 1 {
2257 let polygon = |order: &Vec<(usize, bool)>| -> Vec<Vec3> {
2259 order
2260 .iter()
2261 .flat_map(|&(k, _)| pieces[k].points.iter().map(|p| reduced_to_plane(engine, p.0, p.1)))
2262 .collect()
2263 };
2264 let polygons: Vec<Vec<Vec3>> = orders.iter().map(polygon).collect();
2265 for (i, a) in polygons.iter().enumerate() {
2266 for (j, b) in polygons.iter().enumerate() {
2267 if i != j && point_in_polygon(engine, a[0], b) {
2268 return Err(refuse("section_hole", "the section has a hole, which is not built here".into()));
2269 }
2270 }
2271 }
2272 }
2273 let mut slots: Vec<Option<Piece>> = pieces.into_iter().map(Some).collect();
2274 Ok(orders
2275 .into_iter()
2276 .map(|order| {
2277 order
2278 .into_iter()
2279 .map(|(k, reversed)| {
2280 let mut piece = slots[k].take().expect("each piece once");
2281 if reversed {
2282 piece.points.reverse();
2283 piece.sigma = piece.sigma.iter().rev().map(|s| 1.0 - s).collect();
2284 }
2285 piece
2286 })
2287 .collect()
2288 })
2289 .collect())
2290}
2291
2292fn point_in_polygon(engine: &Engine, p: Vec3, polygon: &[Vec3]) -> bool {
2293 let local = |x: Vec3| -> [f64; 2] {
2294 let l = engine.frame.local(x);
2295 match engine.class {
2296 Class::Turn => [l[0].hypot(l[1]), l[2]],
2297 _ => [l[0], l[1]],
2298 }
2299 };
2300 let q = local(p);
2301 let mut inside = false;
2302 for k in 0..polygon.len() {
2303 let a = local(polygon[k]);
2304 let b = local(polygon[(k + 1) % polygon.len()]);
2305 if (a[1] > q[1]) != (b[1] > q[1]) {
2306 let x = a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
2307 if x > q[0] {
2308 inside = !inside;
2309 }
2310 }
2311 }
2312 inside
2313}
2314
2315fn build_body(engine: &Engine, pieces: &[Piece]) -> Result<BrepSolid, KernelRefusal> {
2317 let clip = engine.clip;
2318 let mut profile: Vec<NurbsCurve> = Vec::new();
2319 let count = pieces.len();
2321 let mut start_points: Vec<Vec3> = pieces
2322 .iter()
2323 .map(|p| reduced_to_plane(engine, p.points[0].0, p.points[0].1))
2324 .collect();
2325 for (index, piece) in pieces.iter().enumerate() {
2328 if piece.level_segment && engine.class != Class::Turn {
2329 let (level, o1) = *piece.points.last().expect("nonempty");
2330 start_points[(index + 1) % count] = reduced_to_plane(engine, level, o1);
2331 }
2332 }
2333 for (index, piece) in pieces.iter().enumerate() {
2334 let start = start_points[index];
2335 let end = start_points[(index + 1) % count];
2336 if piece.level_segment {
2337 let (level, o0) = piece.points[0];
2338 let o1 = piece.points.last().expect("nonempty").1;
2339 let curve = match engine.class {
2340 Class::Turn => make_line(start, end),
2341 Class::Screw(c) => {
2342 let shift = clip.axial[0] / c;
2343 level_arc(engine, level, o0 + shift, o1 + shift)
2344 }
2345 Class::Planar => level_arc(engine, level, o0, o1),
2346 }
2347 .or_refuse(KernelStage::Fragment, "level_segment")?;
2348 profile.push(curve);
2349 continue;
2350 }
2351 let mut points: Vec<Vec3> = piece
2352 .points
2353 .iter()
2354 .map(|p| reduced_to_plane(engine, p.0, p.1))
2355 .collect();
2356 points[0] = start;
2357 let last = points.len() - 1;
2358 points[last] = end;
2359 profile.push(fit_piece(&points)?);
2360 }
2361 match engine.class {
2362 Class::Screw(c) => {
2363 let span = (clip.axial[1] - clip.axial[0]) / c; let sections = ((span.abs() / TAU * 64.0).ceil() as usize).max(8) + 1;
2365 let mut lofted = Vec::with_capacity(sections);
2366 for k in 0..sections {
2367 let angle = span * k as f64 / (sections - 1) as f64;
2368 let lift = engine.frame.ez.scale(c * angle);
2369 let moved: Vec<NurbsCurve> = profile
2370 .iter()
2371 .map(|curve| transform_curve(curve, |x| rotate(x, clip.axis_origin, engine.frame.ez, angle).add(lift)))
2372 .collect::<Result<_, _>>()?;
2373 lofted.push(moved);
2374 }
2375 loft_profile_brep(&lofted)
2376 }
2377 Class::Turn => {
2378 let angle = clip.angle.map(|a| a[1] - a[0]).unwrap_or(TAU);
2379 revolve_profile_brep(&profile, clip.axis_origin, engine.frame.ez, angle)
2380 .or_refuse(KernelStage::Fragment, "revolve")
2381 }
2382 Class::Planar => extrude_profile_brep(&profile, engine.frame.ez, clip.axial[1] - clip.axial[0]),
2383 }
2384}
2385
2386fn level_arc(engine: &Engine, level: f64, a0: f64, a1: f64) -> Result<NurbsCurve, String> {
2387 let centre = engine.frame.world([0.0, 0.0, match engine.class {
2388 Class::Screw(_) | Class::Planar => engine.clip.axial[0],
2389 Class::Turn => 0.0,
2390 }]);
2391 let (start, end) = if a1 >= a0 { (a0, a1) } else { (a1, a0) };
2392 let arc = make_arc(centre, engine.frame.ex, engine.frame.ey, level, start, end)?;
2393 if a1 >= a0 {
2394 Ok(arc)
2395 } else {
2396 arc.reversed()
2397 }
2398}
2399
2400fn transform_curve(curve: &NurbsCurve, map: impl Fn(Vec3) -> Vec3) -> Result<NurbsCurve, KernelRefusal> {
2401 let control_points = curve
2402 .control_points
2403 .iter()
2404 .map(|p| {
2405 let w = p.w;
2406 let x = map(Vec3::new(p.x / w, p.y / w, p.z / w));
2407 crate::Vec4 { x: x.x * w, y: x.y * w, z: x.z * w, w }
2408 })
2409 .collect();
2410 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points).or_refuse(KernelStage::Fragment, "curve_new")
2411}
2412
2413fn check_ends_clear(engine: &Engine) -> Result<(), KernelRefusal> {
2415 let clip = engine.clip;
2416 let mut samples: Vec<[f64; 3]> = Vec::new();
2418 for (index, source) in engine.sources.iter().enumerate() {
2419 if let Some(grid) = &engine.grids[index] {
2420 for i in 0..grid.nu {
2421 for j in 0..grid.nv {
2422 let (u, v) = Engine::grid_param(grid, i, j);
2423 samples.push(engine.frame.local(engine.source_point(*source, u, v)));
2424 }
2425 }
2426 }
2427 }
2428 let margin = 1e-6 * engine.scale;
2429 for t in [engine.motion.from, engine.motion.to] {
2430 for p in &samples {
2431 let x = engine.frame.local(engine.motion_apply(t, engine.frame.world(*p)));
2432 let level = x[0].hypot(x[1]);
2433 if level > clip.radius[1] + margin || level < clip.radius[0] - margin {
2434 continue;
2435 }
2436 if x[2] < clip.axial[0] - margin || x[2] > clip.axial[1] + margin {
2437 continue;
2438 }
2439 if let Some([a0, a1]) = clip.angle {
2440 let angle = x[1].atan2(x[0]);
2441 let mid = 0.5 * (a0 + a1);
2442 let rel = wrap_pi(angle - mid);
2443 if rel.abs() > 0.5 * (a1 - a0) + 1e-9 {
2444 continue;
2445 }
2446 }
2447 return Err(refuse(
2448 "ends_reach_clip",
2449 format!(
2450 "the tool reaches the clip region at the motion's {} (t = {t}); the sweep is \
2451 not invariant there, so the region would be wrong",
2452 if t == engine.motion.from { "start" } else { "end" }
2453 ),
2454 ));
2455 }
2456 }
2457 Ok(())
2458}
2459