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
288const WHOLE_LEVEL: &str = "the sweep covers every angle at level";
290
291const TURNS_PER_ROW: f64 = 2.0;
294
295fn thread_rows_on() -> bool {
299 crate::switch!("BREP_SWEEP_THREAD_ROWS", true)
300}
301
302fn whole_level_on() -> bool {
305 crate::switch!("BREP_SWEEP_WHOLE_LEVEL", true)
306}
307
308fn refuse(what: &'static str, message: String) -> KernelRefusal {
309 KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_REFUSAL}: {message}"))
310}
311
312fn rotate(x: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
313 origin.add(x.sub(origin).rotate_about(axis, angle))
314}
315
316impl ScrewStep {
317 fn apply(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
318 rotate(x, self.axis_origin, axis, self.turn * t).add(axis.scale(self.advance * t))
319 }
320 fn invert(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
321 rotate(x.sub(axis.scale(self.advance * t)), self.axis_origin, axis, -self.turn * t)
322 }
323}
324
325#[derive(Clone, Copy, Debug)]
327struct Frame {
328 origin: Vec3,
329 ex: Vec3,
330 ey: Vec3,
331 ez: Vec3,
332}
333
334impl Frame {
335 fn local(&self, x: Vec3) -> [f64; 3] {
336 let d = x.sub(self.origin);
337 [d.dot(self.ex), d.dot(self.ey), d.dot(self.ez)]
338 }
339 fn world(&self, p: [f64; 3]) -> Vec3 {
340 self.origin
341 .add(self.ex.scale(p[0]))
342 .add(self.ey.scale(p[1]))
343 .add(self.ez.scale(p[2]))
344 }
345}
346
347#[derive(Clone, Copy, Debug, PartialEq)]
348enum Class {
349 Screw(f64),
351 Turn,
352 Planar,
353}
354
355#[derive(Clone, Copy, Debug)]
358enum Source {
359 RevolvedFace { piece: usize },
361 RevolvedCorner { piece: usize },
363 PrismFace { piece: usize },
365 PrismVertex { piece: usize },
367 PrismEnd { piece: usize, end: bool },
370 ScrewPrismSide { piece: usize },
373 ScrewPrismEdge { piece: usize },
375 ScrewPrismRim { piece: usize, end: bool },
379}
380
381struct Engine<'a> {
382 tool: &'a SweptTool,
383 motion: &'a SolidMotion,
384 axes: Vec<Vec3>,
385 frame: Frame,
386 class: Class,
387 clip: &'a SweepClip,
388 crest_gap: bool,
390 sources: Vec<Source>,
391 grids: Vec<Option<Grid>>,
393 critical: Vec<Vec<(f64, f64)>>,
395 critical_params: Vec<Vec<(f64, f64)>>,
398 angular: bool,
400 prism_height: f64,
402 scale: f64,
403 cache: std::cell::RefCell<std::collections::HashMap<u64, Vec<Interval>>>,
406 wrap: Option<Vec<usize>>,
410 counters: std::cell::Cell<(usize, usize, usize, f64, f64)>,
413}
414
415struct Grid {
416 nu: usize,
417 nv: usize,
418 u: [f64; 2],
419 v: [f64; 2],
420 v_periodic: bool,
421 values: Vec<(f64, f64)>,
422}
423
424#[derive(Clone, Copy, Debug)]
426struct Interval {
427 lo: f64,
428 hi: f64,
429 lo_source: usize,
430 hi_source: usize,
431}
432
433fn wrap_pi(angle: f64) -> f64 {
434 let wrapped = (angle + PI).rem_euclid(TAU) - PI;
435 wrapped
436}
437
438impl<'a> Engine<'a> {
439 fn motion_apply(&self, t: f64, x: Vec3) -> Vec3 {
440 self.motion
441 .steps
442 .iter()
443 .zip(&self.axes)
444 .fold(x, |x, (step, axis)| step.apply(t, x, *axis))
445 }
446
447 fn motion_invert(&self, t: f64, x: Vec3) -> Vec3 {
448 self.motion
449 .steps
450 .iter()
451 .zip(&self.axes)
452 .rev()
453 .fold(x, |x, (step, axis)| step.invert(t, x, *axis))
454 }
455
456 fn reduce(&self, p: [f64; 3]) -> (f64, f64) {
458 let level = p[0].hypot(p[1]);
459 match self.class {
460 Class::Screw(c) => (level, p[1].atan2(p[0]) - p[2] / c),
461 Class::Turn => (level, p[2]),
462 Class::Planar => (level, p[1].atan2(p[0])),
463 }
464 }
465
466 fn tool_signed_distance(&self, x: Vec3) -> f64 {
467 match self.tool {
468 SweptTool::Revolved { axis_origin, axis_direction, meridian, .. } => {
469 let axis = axis_direction.normalized_or(*axis_direction);
470 let d = x.sub(*axis_origin);
471 let e = d.dot(axis);
472 let radial = d.sub(axis.scale(e)).length();
473 outline_signed_distance(meridian, [radial, e])
474 }
475 SweptTool::Prism { origin, normal, x_axis, outline, from, to } => {
476 let n = normal.normalized_or(*normal);
477 let xa = x_axis.normalized_or(*x_axis);
478 let ya = n.cross(xa);
479 let d = x.sub(*origin);
480 let h = d.dot(n);
481 let planar = outline_signed_distance(outline, [d.dot(xa), d.dot(ya)]);
482 planar.max(from - h).max(h - to)
483 }
484 }
485 }
486
487 fn source_point(&self, source: Source, u: f64, v: f64) -> Vec3 {
490 match (self.tool, source) {
491 (SweptTool::Revolved { axis_origin, axis_direction, radial, meridian }, _) => {
492 let axis = axis_direction.normalized_or(*axis_direction);
493 let r0 = radial.sub(axis.scale(radial.dot(axis))).normalized_or(*radial);
494 let r1 = axis.cross(r0);
495 let (m, phi) = match source {
496 Source::RevolvedFace { piece } => (meridian[piece].point(u), v),
497 Source::RevolvedCorner { piece } => (meridian[piece].point(0.0), u),
498 _ => unreachable!(),
499 };
500 axis_origin
501 .add(r0.scale(m[0] * phi.cos()))
502 .add(r1.scale(m[0] * phi.sin()))
503 .add(axis.scale(m[1]))
504 }
505 (SweptTool::Prism { origin, normal, x_axis, outline, from, to }, _) => {
506 let n = normal.normalized_or(*normal);
507 let xa = x_axis.normalized_or(*x_axis);
508 let ya = n.cross(xa);
509 let at_own_place = match source {
510 Source::ScrewPrismSide { piece } => Some((outline[piece].point(u), v)),
511 Source::ScrewPrismEdge { piece } => Some((outline[piece].point(0.0), u)),
512 Source::ScrewPrismRim { piece, end } => Some((outline[piece].point(u), if end { *to } else { *from })),
513 _ => None,
514 };
515 if let Some((q, h)) = at_own_place {
516 return origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(h));
517 }
518 let (q, t) = match source {
519 Source::PrismFace { piece } => (outline[piece].point(u), v),
520 Source::PrismVertex { piece } => (outline[piece].point(0.0), u),
521 Source::PrismEnd { piece, end } => (
522 outline[piece].point(u),
523 if end { self.motion.to } else { self.motion.from },
524 ),
525 _ => unreachable!(),
526 };
527 let x = origin
528 .add(xa.scale(q[0]))
529 .add(ya.scale(q[1]))
530 .add(n.scale(self.prism_height));
531 self.motion_apply(t, x)
532 }
533 }
534 }
535
536 fn source_reduced(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
537 self.reduce(self.frame.local(self.source_point(source, u, v)))
538 }
539
540 fn is_face(source: Source) -> bool {
541 matches!(source, Source::RevolvedFace { .. } | Source::PrismFace { .. } | Source::ScrewPrismSide { .. })
542 }
543
544 fn domain(&self, source: Source) -> ([f64; 2], [f64; 2], bool) {
546 let time = [self.motion.from, self.motion.to];
547 match source {
548 Source::RevolvedFace { .. } => ([0.0, 1.0], [0.0, TAU], true),
549 Source::RevolvedCorner { .. } => ([0.0, TAU], [0.0, 0.0], false),
550 Source::PrismFace { .. } => ([0.0, 1.0], time, false),
551 Source::PrismVertex { .. } => (time, [0.0, 0.0], false),
552 Source::PrismEnd { .. } => ([0.0, 1.0], [0.0, 0.0], false),
553 Source::ScrewPrismSide { .. } => ([0.0, 1.0], self.prism_extent(), false),
554 Source::ScrewPrismEdge { .. } => (self.prism_extent(), [0.0, 0.0], false),
555 Source::ScrewPrismRim { .. } => ([0.0, 1.0], [0.0, 0.0], false),
556 }
557 }
558
559 fn prism_extent(&self) -> [f64; 2] {
560 match self.tool {
561 SweptTool::Prism { from, to, .. } => [*from, *to],
562 SweptTool::Revolved { .. } => [0.0, 0.0],
563 }
564 }
565
566 fn u_periodic(&self, source: Source) -> bool {
569 match (self.tool, source) {
570 (SweptTool::Revolved { meridian, .. }, Source::RevolvedFace { piece }) => meridian[piece].is_full_circle(),
571 (SweptTool::Prism { outline, .. }, Source::PrismFace { piece } | Source::ScrewPrismSide { piece }) => outline[piece].is_full_circle(),
572 _ => false,
573 }
574 }
575
576 fn o_difference(&self, a: f64, b: f64) -> f64 {
577 if self.angular {
578 wrap_pi(a - b)
579 } else {
580 a - b
581 }
582 }
583
584 fn face_extremum(&self, source: Source, level: f64, mut u: f64, mut v: f64) -> Option<(f64, f64, f64)> {
586 let (ur, vr, periodic) = self.domain(source);
587 let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
588 let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
589 let residual = |u: f64, v: f64| -> [f64; 2] {
590 let (l, o) = self.source_reduced(source, u, v);
591 let (lu1, ou1) = self.source_reduced(source, u + hu, v);
592 let (lu0, ou0) = self.source_reduced(source, u - hu, v);
593 let (lv1, ov1) = self.source_reduced(source, u, v + hv);
594 let (lv0, ov0) = self.source_reduced(source, u, v - hv);
595 let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
596 let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
597 let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
598 let norm = lu.hypot(lv).max(1e-300);
600 let _ = o;
601 [l - level, (ou * lv - ov * lu) / norm]
602 };
603 let mut f = residual(u, v);
604 for _ in 0..60 {
605 let (du_step, dv_step) = (hu * 10.0, hv * 10.0);
606 let fu1 = residual(u + du_step, v);
607 let fu0 = residual(u - du_step, v);
608 let fv1 = residual(u, v + dv_step);
609 let fv0 = residual(u, v - dv_step);
610 let j = [
611 [(fu1[0] - fu0[0]) / (2.0 * du_step), (fv1[0] - fv0[0]) / (2.0 * dv_step)],
612 [(fu1[1] - fu0[1]) / (2.0 * du_step), (fv1[1] - fv0[1]) / (2.0 * dv_step)],
613 ];
614 let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
615 if !det.is_finite() || det.abs() < 1e-300 {
616 return None;
617 }
618 let du = -(f[0] * j[1][1] - f[1] * j[0][1]) / det;
619 let dv = -(j[0][0] * f[1] - j[1][0] * f[0]) / det;
620 let norm0 = f[0].hypot(f[1]);
621 let mut step = 1.0;
622 let mut accepted = false;
623 while step > 1e-4 {
624 let (nu, nv) = (u + step * du, v + step * dv);
625 let candidate = residual(nu, nv);
626 if candidate[0].hypot(candidate[1]) < norm0 || norm0 < 1e-14 * self.scale {
627 u = nu;
628 v = nv;
629 f = candidate;
630 accepted = true;
631 break;
632 }
633 step *= 0.5;
634 }
635 if !accepted {
636 break;
637 }
638 if (du * step).abs() < 1e-15 * (ur[1] - ur[0]) && (dv * step).abs() < 1e-15 * (vr[1] - vr[0]).max(1.0) {
639 break;
640 }
641 }
642 if periodic {
643 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
644 }
645 if self.u_periodic(source) {
646 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
647 }
648 let tolerance = 1e-9;
649 if u < ur[0] - tolerance || u > ur[1] + tolerance || (!periodic && (v < vr[0] - tolerance || v > vr[1] + tolerance)) {
650 return None;
651 }
652 if f[0].abs() > 1e-10 * self.scale || f[1].abs() > 1e-6 {
653 return None;
654 }
655 let (l, o) = self.source_reduced(source, u.clamp(ur[0], ur[1]), v);
656 Some((l, o, u))
657 }
658
659 fn build_grid(&self, source: Source) -> Grid {
660 let (u, v, v_periodic) = self.domain(source);
661 let (nu, nv) = match source {
662 Source::RevolvedFace { .. } => (64, 256),
663 Source::ScrewPrismSide { .. } => (64, 64),
664 _ => {
665 let turn = self
666 .motion
667 .steps
668 .iter()
669 .map(|step| step.turn.abs())
670 .fold(0.0, f64::max)
671 * (self.motion.to - self.motion.from).abs();
672 (48, ((turn * 128.0) as usize).clamp(256, 4096))
673 }
674 };
675 let mut values = Vec::with_capacity(nu * nv);
676 for i in 0..nu {
677 let uu = u[0] + (u[1] - u[0]) * i as f64 / (nu - 1) as f64;
678 for j in 0..nv {
679 let vv = if v_periodic {
680 v[0] + (v[1] - v[0]) * j as f64 / nv as f64
681 } else {
682 v[0] + (v[1] - v[0]) * j as f64 / (nv - 1) as f64
683 };
684 values.push(self.source_reduced(source, uu, vv));
685 }
686 }
687 Grid { nu, nv, u, v, v_periodic, values }
688 }
689
690 fn grid_param(grid: &Grid, i: usize, j: usize) -> (f64, f64) {
691 let u = grid.u[0] + (grid.u[1] - grid.u[0]) * i as f64 / (grid.nu - 1) as f64;
692 let v = if grid.v_periodic {
693 grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / grid.nv as f64
694 } else {
695 grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / (grid.nv - 1) as f64
696 };
697 (u, v)
698 }
699
700 fn source_candidates(&self, index: usize, level: f64, out: &mut Vec<(f64, usize)>) {
702 let source = self.sources[index];
703 if Self::is_face(source) {
704 let grid = self.grids[index].as_ref().expect("face grid");
705 let at = |i: usize, j: usize| grid.values[i * grid.nv + j % grid.nv];
708 let jmax = if grid.v_periodic { grid.nv } else { grid.nv - 1 };
709 let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
710 for i in 0..grid.nu {
711 for j in 0..jmax {
712 let a = at(i, j);
713 let b = at(i, j + 1);
714 if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
715 let w = (level - a.0) / (b.0 - a.0);
716 let o = a.1 + self.o_difference(b.1, a.1) * w;
717 let (u, v0) = Self::grid_param(grid, i, j);
718 let (_, v1) = Self::grid_param(grid, i, j + 1);
719 let v1 = if grid.v_periodic && j + 1 == grid.nv { grid.v[1] } else { v1 };
720 crossings.push((i, j, o, u, v0 + (v1 - v0) * w));
721 }
722 }
723 }
724 for i in 0..grid.nu.saturating_sub(1) {
725 for j in 0..grid.nv {
726 let a = at(i, j);
727 let b = at(i + 1, j);
728 if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
729 let w = (level - a.0) / (b.0 - a.0);
730 let o = a.1 + self.o_difference(b.1, a.1) * w;
731 let (u0, v) = Self::grid_param(grid, i, j);
732 let (u1, _) = Self::grid_param(grid, i + 1, j);
733 crossings.push((i, j, o, u0 + (u1 - u0) * w, v));
734 }
735 }
736 }
737 let mut found: Vec<(f64, f64, f64)> = Vec::new();
738 let push = |found: &mut Vec<(f64, f64, f64)>, result: Option<(f64, f64, f64)>| {
739 if let Some((_, o_new, u_new)) = result {
740 if !found
741 .iter()
742 .any(|&(o_old, u_old, _)| self.o_difference(o_old, o_new).abs() < 1e-11 && (u_old - u_new).abs() < 1e-6)
743 {
744 found.push((o_new, u_new, 0.0));
745 }
746 }
747 };
748 if let Some(params) = self.critical_params.get(index) {
751 let (ur, vr, _) = self.domain(source);
752 for &(u, v) in params {
753 for (du, dv) in [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] {
754 let (su, sv) = (u + du * 1e-3 * (ur[1] - ur[0]), v + dv * 1e-3 * (vr[1] - vr[0]));
755 push(&mut found, self.face_extremum(source, level, su, sv));
756 }
757 }
758 }
759 let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
760 for (k, &(i, j, _, _, _)) in crossings.iter().enumerate() {
761 buckets.entry((i, j)).or_default().push(k);
762 }
763 for (k, &(i, j, o, u, v)) in crossings.iter().enumerate() {
764 let mut is_max = true;
765 let mut is_min = true;
766 let mut neighbours = Vec::new();
767 for di in [-1i64, 0, 1] {
768 for dj in [-1i64, 0, 1] {
769 let i2 = i as i64 + di;
770 if i2 < 0 {
771 continue;
772 }
773 let j2 = j as i64 + dj;
774 let j2 = if grid.v_periodic { j2.rem_euclid(grid.nv as i64) } else { j2 };
775 if j2 < 0 {
776 continue;
777 }
778 if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
779 neighbours.extend(list.iter().copied());
780 }
781 }
782 }
783 for m in neighbours {
784 if m == k {
785 continue;
786 }
787 let o2 = crossings[m].2;
788 {
789 let d = self.o_difference(o2, o);
790 if d > 0.0 {
791 is_max = false;
792 }
793 if d < 0.0 {
794 is_min = false;
795 }
796 }
797 }
798 if !(is_max || is_min) {
799 continue;
800 }
801 push(&mut found, self.face_extremum(source, level, u, v));
802 }
803 out.extend(found.into_iter().map(|(o, _, _)| (o, index)));
804 } else {
805 let (range, _, _) = self.domain(source);
806 let periodic = matches!(source, Source::RevolvedCorner { .. });
807 let n = match source {
808 Source::RevolvedCorner { .. } | Source::PrismEnd { .. } | Source::ScrewPrismEdge { .. } | Source::ScrewPrismRim { .. } => 1024,
809 _ => self.grids_len_for_time(),
810 };
811 let _ = periodic;
812 let mut samples: Vec<f64> = (0..=n).map(|k| range[0] + (range[1] - range[0]) * k as f64 / n as f64).collect();
816 if let Some(extra) = self.critical_params.get(index) {
817 samples.extend(extra.iter().map(|p| p.0).filter(|t| *t > range[0] && *t < range[1]));
818 samples.sort_by(|a, b| a.total_cmp(b));
819 }
820 let mut previous = self.source_reduced(source, samples[0], 0.0).0 - level;
821 for k in 1..samples.len() {
822 let p = samples[k];
823 let value = self.source_reduced(source, p, 0.0).0 - level;
824 if previous == 0.0 {
825 out.push((self.source_reduced(source, samples[k - 1], 0.0).1, index));
829 } else if previous * value < 0.0 {
830 let (mut a, mut b) = (samples[k - 1], p);
831 let fa0 = previous;
832 for _ in 0..200 {
833 let m = 0.5 * (a + b);
834 let fm = self.source_reduced(source, m, 0.0).0 - level;
835 if (fm < 0.0) == (fa0 < 0.0) && fm != 0.0 {
836 a = m;
837 } else {
838 b = m;
839 }
840 if (b - a).abs() < 1e-15 * (range[1] - range[0]).abs().max(1.0) {
841 break;
842 }
843 }
844 let (_, o) = self.source_reduced(source, 0.5 * (a + b), 0.0);
845 out.push((o, index));
846 }
847 previous = value;
848 }
849 if previous == 0.0 {
850 let last = *samples.last().expect("nonempty");
851 out.push((self.source_reduced(source, last, 0.0).1, index));
852 }
853 }
854 }
855
856 fn grids_len_for_time(&self) -> usize {
857 let turn = self
858 .motion
859 .steps
860 .iter()
861 .map(|step| step.turn.abs())
862 .fold(0.0, f64::max)
863 * (self.motion.to - self.motion.from).abs();
864 ((turn * 256.0) as usize).clamp(1024, 8192)
865 }
866
867 fn lift(&self, level: f64, o: f64) -> [f64; 3] {
869 let axial = 0.5 * (self.clip.axial[0] + self.clip.axial[1]);
870 match self.class {
871 Class::Screw(c) => {
872 let alpha = o + axial / c;
873 [level * alpha.cos(), level * alpha.sin(), axial]
874 }
875 Class::Turn => {
876 let theta = self.clip.angle.map(|a| 0.5 * (a[0] + a[1])).unwrap_or(0.0);
877 [level * theta.cos(), level * theta.sin(), o]
878 }
879 Class::Planar => [level * o.cos(), level * o.sin(), axial],
880 }
881 }
882
883 fn removed(&self, level: f64, o: f64) -> bool {
893 let x = self.frame.world(self.lift(level, o));
894 let (t0, t1) = (self.motion.from, self.motion.to);
895 let speed = self.speed_bound(x);
896 let floor = (t1 - t0) * 1e-8;
901 let cap = 200_000usize;
902 let touching = 1e-10 * self.scale;
903 let clock = web_time::Instant::now();
904 let mut t = t0;
905 let mut steps = 0usize;
906 let inside = loop {
907 steps += 1;
908 let distance = self.tool_signed_distance(self.motion_invert(t, x));
909 if distance <= touching || steps > cap {
910 break true;
911 }
912 if t >= t1 {
913 break false;
914 }
915 t = (t + (distance / speed).max(floor)).min(t1);
916 };
917 let mut c = self.counters.get();
918 c.1 += 1;
919 c.2 += steps;
920 c.4 += clock.elapsed().as_secs_f64();
921 self.counters.set(c);
922 inside
923 }
924
925 fn speed_bound(&self, x: Vec3) -> f64 {
927 let centre = self.frame.origin;
928 let span = self.motion.from.abs().max(self.motion.to.abs());
929 let mut reach = x.sub(centre).length();
930 for step in &self.motion.steps {
931 reach += 2.0 * step.axis_origin.sub(centre).length() + step.advance.abs() * span;
932 }
933 let speed: f64 = self
934 .motion
935 .steps
936 .iter()
937 .map(|step| step.turn.abs() * (reach + step.axis_origin.sub(centre).length()) + step.advance.abs())
938 .sum();
939 speed.max(1e-300)
940 }
941
942 fn intervals_at(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
945 if let Some(hit) = self.cache.borrow().get(&level.to_bits()) {
946 return Ok(hit.clone());
947 }
948 let clock = web_time::Instant::now();
949 let before = self.counters.get();
950 let intervals = self.intervals_uncached(level)?;
951 if clock.elapsed().as_secs_f64() > 0.5 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
952 let after = self.counters.get();
953 eprintln!(
954 " slow level {level:.12}: {:.2}s, candidates {:.2}s, oracle calls {} steps {} ({:.2}s), {} intervals",
955 clock.elapsed().as_secs_f64(),
956 after.3 - before.3,
957 after.1 - before.1,
958 after.2 - before.2,
959 after.4 - before.4,
960 intervals.len()
961 );
962 }
963 self.cache.borrow_mut().insert(level.to_bits(), intervals.clone());
964 Ok(intervals)
965 }
966
967 fn intervals_uncached(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
968 let clock = web_time::Instant::now();
969 let mut candidates = Vec::new();
970 for index in 0..self.sources.len() {
971 self.source_candidates(index, level, &mut candidates);
972 }
973 let mut c = self.counters.get();
974 c.0 += 1;
975 c.3 += clock.elapsed().as_secs_f64();
976 self.counters.set(c);
977 if self.angular {
978 for candidate in &mut candidates {
979 candidate.0 = candidate.0.rem_euclid(TAU);
980 }
981 }
982 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
983 candidates.dedup_by(|b, a| (b.0 - a.0).abs() < 1e-12);
984 if candidates.is_empty() {
985 let probes = 64;
986 let (base, span) = if self.angular { (0.0, TAU) } else { (-self.scale * 4.0, self.scale * 8.0) };
987 if (0..probes).all(|k| self.removed(level, base + span * (k as f64 + 0.5) / probes as f64)) {
988 return Err(refuse(
989 "whole_level",
990 format!("{WHOLE_LEVEL} {level}, which this construction does not build"),
991 ));
992 }
993 return Ok(Vec::new());
994 }
995 let n = candidates.len();
996 let gaps = if self.angular { n } else { n - 1 };
997 let mut removed = Vec::with_capacity(gaps);
998 for k in 0..gaps {
999 let a = candidates[k].0;
1000 let b = if k + 1 < n { candidates[k + 1].0 } else { candidates[0].0 + TAU };
1001 if (b - a) * level.max(1.0) < 1e-9 * self.scale {
1005 removed.push(true);
1006 continue;
1007 }
1008 removed.push(self.removed(level, 0.5 * (a + b)));
1009 }
1010 let mut intervals = Vec::new();
1011 if self.crest_gap && self.angular && removed.iter().all(|r| *r) {
1014 let width = |k: usize| if k + 1 < n { candidates[k + 1].0 - candidates[k].0 } else { candidates[0].0 + TAU - candidates[k].0 };
1015 if let Some(widest) = (0..gaps).max_by(|&a, &b| width(a).total_cmp(&width(b))) {
1016 removed[widest] = false;
1017 }
1018 }
1019 if self.angular && removed.iter().all(|r| *r) {
1020 return Err(refuse(
1021 "whole_level",
1022 format!("{WHOLE_LEVEL} {level}, which this construction does not build"),
1023 ));
1024 }
1025 let start = if self.angular {
1027 removed.iter().position(|r| !r).map(|k| (k + 1) % n).unwrap_or(0)
1028 } else {
1029 0
1030 };
1031 let mut k = 0;
1032 while k < gaps {
1033 let g = (start + k) % n.max(1);
1034 if !removed[g] {
1035 k += 1;
1036 continue;
1037 }
1038 let first = g;
1039 let mut last = g;
1040 while k + 1 < gaps && removed[(start + k + 1) % n] {
1041 k += 1;
1042 last = (start + k) % n;
1043 }
1044 let lo = candidates[first];
1045 let hi_index = (last + 1) % n;
1046 let hi = candidates[hi_index];
1047 let mut hi_value = hi.0;
1048 if self.angular && hi_value <= lo.0 {
1049 hi_value += TAU;
1050 }
1051 intervals.push(Interval {
1052 lo: lo.0,
1053 hi: hi_value,
1054 lo_source: lo.1,
1055 hi_source: hi.1,
1056 });
1057 k += 1;
1058 }
1059 intervals.sort_by(|a, b| a.lo.total_cmp(&b.lo));
1060 Ok(intervals)
1061 }
1062
1063 fn characteristic(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
1067 let (ur, vr, _) = self.domain(source);
1068 let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
1069 let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
1070 let (l, _) = self.source_reduced(source, u, v);
1071 let (lu1, ou1) = self.source_reduced(source, u + hu, v);
1072 let (lu0, ou0) = self.source_reduced(source, u - hu, v);
1073 let (lv1, ov1) = self.source_reduced(source, u, v + hv);
1074 let (lv0, ov0) = self.source_reduced(source, u, v - hv);
1075 let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
1076 let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
1077 let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
1078 ((ou * lv - ov * lu) / lu.hypot(lv).max(1e-300), l)
1079 }
1080
1081 fn folds_and_kinks(&self, source: Source, grid: &Grid, out: &mut Vec<(f64, f64)>, params: &mut Vec<(f64, f64)>) {
1091 let (ur, vr, v_periodic) = self.domain(source);
1092 let u_periodic = self.u_periodic(source);
1093 let (nu, nv) = (grid.nu, grid.nv);
1094 let mut d = vec![0.0; nu * nv];
1095 for i in 0..nu {
1096 for j in 0..nv {
1097 let (u, v) = Self::grid_param(grid, i, j);
1098 d[i * nv + j] = self.characteristic(source, u, v).0;
1099 }
1100 }
1101 let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
1103 let jmax = if v_periodic { nv } else { nv - 1 };
1104 for i in 0..nu {
1105 for j in 0..jmax {
1106 let (a, b) = (d[i * nv + j], d[i * nv + (j + 1) % nv]);
1107 if a * b < 0.0 {
1108 let w = a / (a - b);
1109 let (u, v0) = Self::grid_param(grid, i, j);
1110 let (_, v1) = Self::grid_param(grid, i, j + 1);
1111 let v1 = if v_periodic && j + 1 == nv { vr[1] } else { v1 };
1112 let v = v0 + (v1 - v0) * w;
1113 crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1114 }
1115 }
1116 }
1117 for i in 0..nu - 1 {
1118 for j in 0..nv {
1119 let (a, b) = (d[i * nv + j], d[(i + 1) * nv + j]);
1120 if a * b < 0.0 {
1121 let w = a / (a - b);
1122 let (u0, v) = Self::grid_param(grid, i, j);
1123 let (u1, _) = Self::grid_param(grid, i + 1, j);
1124 let u = u0 + (u1 - u0) * w;
1125 crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1126 }
1127 }
1128 }
1129 let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
1130 for (k, c) in crossings.iter().enumerate() {
1131 buckets.entry((c.0, c.1)).or_default().push(k);
1132 }
1133 let ku = 1e-4 * (ur[1] - ur[0]);
1134 let kv = 1e-4 * (vr[1] - vr[0]);
1135 let fold = |u: f64, v: f64| -> [f64; 2] {
1136 let (dd, _) = self.characteristic(source, u, v);
1137 let du = (self.characteristic(source, u + ku, v).0 - self.characteristic(source, u - ku, v).0) / (2.0 * ku);
1138 let dv = (self.characteristic(source, u, v + kv).0 - self.characteristic(source, u, v - kv).0) / (2.0 * kv);
1139 let lu = (self.source_reduced(source, u + ku, v).0 - self.source_reduced(source, u - ku, v).0) / (2.0 * ku);
1140 let lv = (self.source_reduced(source, u, v + kv).0 - self.source_reduced(source, u, v - kv).0) / (2.0 * kv);
1141 let norm = (lu.hypot(lv) * du.hypot(dv)).max(1e-300);
1142 [dd, (lu * dv - lv * du) / norm]
1143 };
1144 for (k, &(i, j, u0, v0, level)) in crossings.iter().enumerate() {
1145 let mut is_min = true;
1146 let mut is_max = true;
1147 for di in [-1i64, 0, 1] {
1148 for dj in [-1i64, 0, 1] {
1149 let i2 = i as i64 + di;
1150 let j2 = if v_periodic { (j as i64 + dj).rem_euclid(nv as i64) } else { j as i64 + dj };
1151 if i2 < 0 || j2 < 0 {
1152 continue;
1153 }
1154 if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
1155 for &m in list {
1156 if m == k {
1157 continue;
1158 }
1159 if crossings[m].4 < level {
1160 is_min = false;
1161 }
1162 if crossings[m].4 > level {
1163 is_max = false;
1164 }
1165 }
1166 }
1167 }
1168 }
1169 if !(is_min || is_max) {
1170 continue;
1171 }
1172 let (mut u, mut v) = (u0, v0);
1173 let mut converged = false;
1174 for _ in 0..40 {
1175 let f = fold(u, v);
1176 let (su, sv) = (ku * 10.0, kv * 10.0);
1177 let fu1 = fold(u + su, v);
1178 let fu0 = fold(u - su, v);
1179 let fv1 = fold(u, v + sv);
1180 let fv0 = fold(u, v - sv);
1181 let j00 = (fu1[0] - fu0[0]) / (2.0 * su);
1182 let j01 = (fv1[0] - fv0[0]) / (2.0 * sv);
1183 let j10 = (fu1[1] - fu0[1]) / (2.0 * su);
1184 let j11 = (fv1[1] - fv0[1]) / (2.0 * sv);
1185 let det = j00 * j11 - j01 * j10;
1186 if !det.is_finite() || det.abs() < 1e-300 {
1187 break;
1188 }
1189 let du = -(f[0] * j11 - f[1] * j01) / det;
1190 let dv = -(j00 * f[1] - j10 * f[0]) / det;
1191 let cap = 4.0 * ((ur[1] - ur[0]) / (nu as f64)).max((vr[1] - vr[0]) / (nv as f64));
1193 let scale = (du.hypot(dv) / cap).max(1.0);
1194 u += du / scale;
1195 v += dv / scale;
1196 if du.hypot(dv) < 1e-12 * (ur[1] - ur[0]).max(vr[1] - vr[0]) {
1197 converged = true;
1198 break;
1199 }
1200 }
1201 let f = fold(u, v);
1202 if !(converged || (f[0].abs() < 1e-9 && f[1].abs() < 1e-5)) {
1203 continue;
1204 }
1205 if u_periodic {
1206 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1207 }
1208 if v_periodic {
1209 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1210 }
1211 if u > ur[0] && u < ur[1] && (v_periodic || (v > vr[0] && v < vr[1])) {
1212 out.push(self.source_reduced(source, u, v));
1213 params.push((u, v));
1214 }
1215 }
1216 let mut sides: Vec<(bool, f64)> = Vec::new(); if !u_periodic {
1219 sides.push((true, ur[0]));
1220 sides.push((true, ur[1]));
1221 }
1222 if !v_periodic {
1223 sides.push((false, vr[0]));
1224 sides.push((false, vr[1]));
1225 }
1226 for (along_v, fixed) in sides {
1227 let (range, n) = if along_v { (vr, nv * 4) } else { (ur, nu * 4) };
1228 let at = |t: f64| if along_v { (fixed, t) } else { (t, fixed) };
1229 let value = |t: f64| {
1230 let (u, v) = at(t);
1231 self.characteristic(source, u, v).0
1232 };
1233 let mut previous = value(range[0]);
1234 for k in 1..=n {
1235 let t = range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1236 let current = value(t);
1237 if previous * current < 0.0 {
1238 let (mut a, mut b) = (t - (range[1] - range[0]) / n as f64, t);
1239 let fa = previous;
1240 for _ in 0..100 {
1241 let m = 0.5 * (a + b);
1242 if (value(m) < 0.0) == (fa < 0.0) {
1243 a = m;
1244 } else {
1245 b = m;
1246 }
1247 }
1248 let (u, v) = at(0.5 * (a + b));
1249 out.push(self.source_reduced(source, u, v));
1250 params.push((u, v));
1251 }
1252 previous = current;
1253 }
1254 }
1255 }
1256
1257 fn source_critical(&self, index: usize) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
1258 let source = self.sources[index];
1259 let mut out = Vec::new();
1260 let mut params = Vec::new();
1261 if Self::is_face(source) {
1262 let grid = self.grids[index].as_ref().expect("face grid");
1263 let (ur, vr, periodic) = self.domain(source);
1264 let at = |i: usize, j: usize| grid.values[i * grid.nv + j].0;
1265 let u_periodic = self.u_periodic(source);
1266 let rows = if u_periodic { grid.nu - 1 } else { grid.nu };
1268 for i in 0..rows {
1269 if !u_periodic && (i == 0 || i + 1 == grid.nu) {
1270 continue;
1271 }
1272 for j in 0..grid.nv {
1273 if !periodic && (j == 0 || j + 1 == grid.nv) {
1274 continue;
1275 }
1276 let here = at(i, j);
1277 let mut is_min = true;
1278 let mut is_max = true;
1279 for di in [-1i64, 0, 1] {
1280 for dj in [-1i64, 0, 1] {
1281 if di == 0 && dj == 0 {
1282 continue;
1283 }
1284 let i2 = ((i as i64 + di).rem_euclid(rows as i64)) as usize;
1285 let j2 = ((j as i64 + dj).rem_euclid(grid.nv as i64)) as usize;
1286 let there = at(i2, j2);
1287 if there < here {
1288 is_min = false;
1289 }
1290 if there > here {
1291 is_max = false;
1292 }
1293 }
1294 }
1295 if !(is_min || is_max) {
1296 continue;
1297 }
1298 let (mut u, mut v) = Self::grid_param(grid, i, j);
1300 let hu = 1e-5 * (ur[1] - ur[0]);
1301 let hv = 1e-5 * (vr[1] - vr[0]);
1302 let level = |u: f64, v: f64| self.source_reduced(source, u, v).0;
1303 for _ in 0..50 {
1304 let f = level(u, v);
1305 let fu = (level(u + hu, v) - level(u - hu, v)) / (2.0 * hu);
1306 let fv = (level(u, v + hv) - level(u, v - hv)) / (2.0 * hv);
1307 let fuu = (level(u + hu, v) - 2.0 * f + level(u - hu, v)) / (hu * hu);
1308 let fvv = (level(u, v + hv) - 2.0 * f + level(u, v - hv)) / (hv * hv);
1309 let fuv = (level(u + hu, v + hv) - level(u + hu, v - hv) - level(u - hu, v + hv)
1310 + level(u - hu, v - hv))
1311 / (4.0 * hu * hv);
1312 let det = fuu * fvv - fuv * fuv;
1313 if det.abs() < 1e-300 {
1314 break;
1315 }
1316 let du = -(fu * fvv - fv * fuv) / det;
1317 let dv = -(fuu * fv - fuv * fu) / det;
1318 u += du;
1319 v += dv;
1320 if du.abs() < 1e-14 && dv.abs() < 1e-14 {
1321 break;
1322 }
1323 }
1324 if u_periodic {
1325 u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1326 }
1327 if periodic {
1328 v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1329 }
1330 if u >= ur[0] && u <= ur[1] && (periodic || (v >= vr[0] && v <= vr[1])) {
1331 out.push(self.source_reduced(source, u, v));
1332 params.push((u, v));
1333 }
1334 }
1335 }
1336 self.folds_and_kinks(source, grid, &mut out, &mut params);
1337 } else {
1338 let (range, _, _) = self.domain(source);
1339 let n = 2048;
1340 let param = |k: usize| range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1341 let values: Vec<f64> = (0..=n).map(|k| self.source_reduced(source, param(k), 0.0).0).collect();
1342 for k in 1..n {
1343 let (a, b, c) = (values[k - 1], values[k], values[k + 1]);
1344 if (b <= a && b <= c) || (b >= a && b >= c) {
1345 let sign = if b <= a && b <= c { 1.0 } else { -1.0 };
1346 let (mut lo, mut hi) = (param(k - 1), param(k + 1));
1347 let ratio = 0.618_033_988_749_894_9;
1348 for _ in 0..100 {
1349 let c1 = hi - ratio * (hi - lo);
1350 let d1 = lo + ratio * (hi - lo);
1351 if sign * self.source_reduced(source, c1, 0.0).0 < sign * self.source_reduced(source, d1, 0.0).0 {
1352 hi = d1;
1353 } else {
1354 lo = c1;
1355 }
1356 }
1357 out.push(self.source_reduced(source, 0.5 * (lo + hi), 0.0));
1358 params.push((0.5 * (lo + hi), 0.0));
1359 }
1360 }
1361 }
1362 (out, params)
1363 }
1364}
1365
1366fn same_or_bounding(engine: &Engine, a: usize, b: usize) -> bool {
1372 if a == b {
1373 return true;
1374 }
1375 let pieces = match engine.tool {
1376 SweptTool::Revolved { meridian, .. } => meridian.len(),
1377 SweptTool::Prism { outline, .. } => outline.len(),
1378 };
1379 let face = |s: Source| match s {
1381 Source::RevolvedFace { piece } | Source::PrismFace { piece } | Source::ScrewPrismSide { piece } => Some(piece),
1382 _ => None,
1383 };
1384 let edge = |s: Source| match s {
1385 Source::RevolvedCorner { piece } | Source::PrismVertex { piece } | Source::ScrewPrismEdge { piece } => Some(piece),
1386 _ => None,
1387 };
1388 let (sa, sb) = (engine.sources[a], engine.sources[b]);
1389 let same_piece = |x: usize, y: usize| {
1391 x == y || engine.wrap.as_ref().is_some_and(|map| map[x] == y || map[y] == x)
1392 };
1393 let bounds = |f: usize, e: usize| {
1394 same_piece(e, f) || same_piece(e, (f + 1) % pieces)
1395 };
1396 let _ = bounds;
1397 match (face(sa), edge(sa), face(sb), edge(sb)) {
1398 (Some(f), _, Some(g), _) => same_piece(f, g),
1399 (_, Some(e), _, Some(g)) => same_piece(e, g),
1400 _ => false,
1401 }
1402}
1403
1404fn compatible(engine: &Engine, a: &[Interval], b: &[Interval]) -> bool {
1407 let angular = engine.angular;
1408 if a.len() != b.len() {
1409 return false;
1410 }
1411 let mut used = vec![false; b.len()];
1413 for x in a {
1414 let mid = 0.5 * (x.lo + x.hi);
1415 let mut best = None;
1416 let mut best_distance = f64::INFINITY;
1417 for (k, y) in b.iter().enumerate() {
1418 let other = 0.5 * (y.lo + y.hi);
1419 let d = if angular { wrap_pi(mid - other).abs() } else { (mid - other).abs() };
1420 if d < best_distance {
1421 best_distance = d;
1422 best = Some(k);
1423 }
1424 }
1425 let Some(k) = best else { return false };
1426 let y = &b[k];
1427 let overlap = if angular {
1428 let shift = (wrap_pi(y.lo - x.lo)) - (y.lo - x.lo);
1429 let (ylo, yhi) = (y.lo + shift, y.hi + shift);
1430 x.hi.min(yhi) - x.lo.max(ylo)
1431 } else {
1432 x.hi.min(y.hi) - x.lo.max(y.lo)
1433 };
1434 if used[k]
1435 || overlap < 0.0
1436 || !same_or_bounding(engine, x.lo_source, y.lo_source)
1437 || !same_or_bounding(engine, x.hi_source, y.hi_source)
1438 {
1439 return false;
1440 }
1441 used[k] = true;
1442 }
1443 true
1444}
1445
1446struct Piece {
1449 points: Vec<(f64, f64)>,
1450 sigma: Vec<f64>,
1451 level_segment: bool,
1453}
1454
1455pub fn sweep_solid_within(
1458 tool: &SweptTool,
1459 motion: &SolidMotion,
1460 clip: &SweepClip,
1461) -> Result<Vec<BrepSolid>, KernelRefusal> {
1462 let narrowed: SweepClip;
1463 let ez = clip
1465 .axis_direction
1466 .normalized()
1467 .or_refuse(KernelStage::Collect, "clip_axis")?;
1468 let ex = clip
1469 .reference
1470 .sub(ez.scale(clip.reference.dot(ez)))
1471 .normalized()
1472 .or_refuse(KernelStage::Collect, "clip_reference")?;
1473 let frame = Frame { origin: clip.axis_origin, ex, ey: ez.cross(ex), ez };
1474 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
1475 return Err(KernelRefusal::input(
1476 KernelStage::Collect,
1477 "motion_range",
1478 format!("{SOLID_SWEEP_REFUSAL}: the motion's range must run forwards"),
1479 ));
1480 }
1481 if motion.steps.is_empty() {
1482 return Err(KernelRefusal::input(KernelStage::Collect, "motion_empty", format!("{SOLID_SWEEP_REFUSAL}: no motion")));
1483 }
1484 let mut axes = Vec::with_capacity(motion.steps.len());
1485 for step in &motion.steps {
1486 axes.push(step.axis_direction.normalized().or_refuse(KernelStage::Collect, "step_axis")?);
1487 }
1488 if !(clip.radius[0] >= 0.0 && clip.radius[1] > clip.radius[0] && clip.axial[1] > clip.axial[0]) {
1489 return Err(KernelRefusal::input(
1490 KernelStage::Collect,
1491 "clip_range",
1492 format!("{SOLID_SWEEP_REFUSAL}: the clip's ranges must be increasing"),
1493 ));
1494 }
1495 let on_axis = |origin: Vec3, axis: Vec3| -> bool {
1496 let parallel = axis.cross(ez).length() < 1e-12;
1497 let offset = origin.sub(clip.axis_origin);
1498 parallel && offset.sub(ez.scale(offset.dot(ez))).length() < 1e-9 * (1.0 + clip.radius[1])
1499 };
1500 let class = 'class: { match tool {
1501 SweptTool::Revolved { meridian, .. } => {
1502 validate_outline(meridian, "meridian")?;
1503 if motion.steps.len() != 1 || !on_axis(motion.steps[0].axis_origin, axes[0]) {
1504 return Err(refuse(
1505 "motion_class",
1506 "a solid of revolution is swept here only by one screw or turn about the clip's axis".into(),
1507 ));
1508 }
1509 let step = motion.steps[0];
1510 let sense = axes[0].dot(ez).signum();
1511 if step.turn == 0.0 {
1512 return Err(refuse("motion_class", "a slide of a solid of revolution is not built here".into()));
1513 }
1514 if step.advance == 0.0 {
1515 Class::Turn
1516 } else {
1517 Class::Screw((step.advance * sense) / (step.turn * sense))
1519 }
1520 }
1521 SweptTool::Prism { normal, outline, from, to, origin, .. } => {
1522 validate_outline(outline, "prism")?;
1523 let n = normal.normalized().or_refuse(KernelStage::Collect, "prism_normal")?;
1524 let screw = motion.steps.len() == 1
1525 && motion.steps[0].turn != 0.0
1526 && motion.steps[0].advance != 0.0
1527 && on_axis(motion.steps[0].axis_origin, axes[0])
1528 && n.cross(ez).length() <= 1e-12;
1529 if screw {
1530 let step = motion.steps[0];
1532 let sense = axes[0].dot(ez).signum();
1533 break 'class Class::Screw((step.advance * sense) / (step.turn * sense));
1534 }
1535 if n.cross(ez).length() > 1e-12 {
1536 return Err(refuse("motion_class", "a prism is swept here only along its own direction's clip axis".into()));
1537 }
1538 for (step, axis) in motion.steps.iter().zip(&axes) {
1539 let turn_ok = step.turn == 0.0 || axis.cross(n).length() < 1e-12;
1540 let advance_ok = step.advance == 0.0 || (step.turn == 0.0 && axis.dot(n).abs() < 1e-12);
1541 if !(turn_ok && advance_ok) {
1542 return Err(refuse(
1543 "motion_class",
1544 "a prism is swept here only by turns about axes along it and slides across it".into(),
1545 ));
1546 }
1547 }
1548 let base = clip.axis_origin.sub(*origin).dot(n);
1550 let sense = ez.dot(n);
1551 let h0 = base + sense * clip.axial[0];
1552 let h1 = base + sense * clip.axial[1];
1553 if h0.min(h1) < *from || h0.max(h1) > *to {
1554 return Err(refuse(
1555 "clip_past_prism",
1556 "the clip reaches past the prism's ends, where the sweep is not a prism".into(),
1557 ));
1558 }
1559 Class::Planar
1560 }
1561 } };
1562 let prism_height = match tool {
1563 SweptTool::Prism { from, to, .. } => 0.5 * (from + to),
1564 _ => 0.0,
1565 };
1566
1567 let wrap: Option<Vec<usize>> = match tool {
1572 SweptTool::Prism { .. } if matches!(class, Class::Screw(_)) => None,
1573 SweptTool::Prism { origin, normal, x_axis, outline, .. } => {
1574 let probe = Engine {
1575 tool,
1576 motion,
1577 axes: axes.clone(),
1578 frame,
1579 class,
1580 clip,
1581 crest_gap: false,
1582 sources: Vec::new(),
1583 grids: Vec::new(),
1584 critical: Vec::new(),
1585 critical_params: Vec::new(),
1586 angular: true,
1587 prism_height,
1588 scale: 1.0,
1589 cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1590 wrap: None,
1591 counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1592 };
1593 let n = normal.normalized_or(*normal);
1594 let xa = x_axis.normalized_or(*x_axis);
1595 let ya = n.cross(xa);
1596 let vertex = |k: usize, t: f64| {
1597 let q = outline[k].point(0.0);
1598 probe.motion_apply(t, origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(prism_height)))
1599 };
1600 let size = scale_of(outline);
1601 let mut map = Vec::with_capacity(outline.len());
1602 for k in 0..outline.len() {
1603 let end = vertex(k, motion.to);
1604 match (0..outline.len()).find(|&j| vertex(j, motion.from).sub(end).length() < 1e-9 * size) {
1605 Some(j) => map.push(j),
1606 None => break,
1607 }
1608 }
1609 let consistent = map.len() == outline.len()
1610 && (0..outline.len()).all(|k| map[(k + 1) % outline.len()] == (map[k] + 1) % outline.len());
1611 consistent.then_some(map)
1612 }
1613 SweptTool::Revolved { .. } => None,
1614 };
1615 let periodic_motion = wrap.is_some();
1616 let mut sources = Vec::new();
1617 match tool {
1618 SweptTool::Revolved { meridian, .. } => {
1619 let on_axis = |p: [f64; 2]| p[0].abs() <= 1e-12 * scale_of(meridian);
1622 for piece in 0..meridian.len() {
1623 let lies_on_axis = matches!(meridian[piece], OutlinePiece::Line { from, to } if on_axis(from) && on_axis(to));
1624 if !lies_on_axis {
1625 sources.push(Source::RevolvedFace { piece });
1626 }
1627 if !meridian[piece].is_full_circle() && !on_axis(meridian[piece].point(0.0)) {
1628 sources.push(Source::RevolvedCorner { piece });
1629 }
1630 }
1631 }
1632 SweptTool::Prism { outline, .. } if matches!(class, Class::Screw(_)) => {
1633 for piece in 0..outline.len() {
1634 sources.push(Source::ScrewPrismSide { piece });
1635 if !outline[piece].is_full_circle() {
1636 sources.push(Source::ScrewPrismEdge { piece });
1637 }
1638 sources.push(Source::ScrewPrismRim { piece, end: false });
1639 sources.push(Source::ScrewPrismRim { piece, end: true });
1640 }
1641 }
1642 SweptTool::Prism { outline, .. } => {
1643 for piece in 0..outline.len() {
1644 sources.push(Source::PrismFace { piece });
1645 if !outline[piece].is_full_circle() {
1646 sources.push(Source::PrismVertex { piece });
1647 }
1648 if !periodic_motion {
1649 sources.push(Source::PrismEnd { piece, end: false });
1650 sources.push(Source::PrismEnd { piece, end: true });
1651 }
1652 }
1653 }
1654 }
1655 let scale = clip.radius[1].max(1.0);
1656 let mut engine = Engine {
1657 tool,
1658 motion,
1659 axes,
1660 frame,
1661 class,
1662 clip,
1663 crest_gap: false,
1664 sources,
1665 grids: Vec::new(),
1666 critical: Vec::new(),
1667 critical_params: Vec::new(),
1668 angular: class != Class::Turn,
1669 prism_height,
1670 scale,
1671 cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1672 wrap,
1673 counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1674 };
1675 engine.grids = engine
1676 .sources
1677 .iter()
1678 .map(|source| if Engine::is_face(*source) { Some(engine.build_grid(*source)) } else { None })
1679 .collect();
1680 let critical: Vec<_> = (0..engine.sources.len()).map(|k| engine.source_critical(k)).collect();
1681 engine.critical_params = critical.iter().map(|c| c.1.clone()).collect();
1682 engine.critical = critical.into_iter().map(|c| c.0).collect();
1683
1684 if matches!(class, Class::Screw(_) | Class::Turn) {
1686 check_ends_clear(&engine)?;
1687 }
1688
1689 let mut crest = None;
1692 if matches!(class, Class::Screw(_)) && clip.radius[0] == 0.0 && clip.angle.is_none() && whole_level_on() {
1693 crest = crest_level(&engine)?;
1694 if let Some(level) = crest {
1695 narrowed = SweepClip { radius: [0.0, level], ..clip.clone() };
1696 engine.clip = &narrowed;
1697 engine.crest_gap = true;
1698 }
1699 }
1700
1701 let (l0, l1) = (engine.clip.radius[0], engine.clip.radius[1]);
1703 let scan = 192;
1704 let mut levels: Vec<f64> = (0..=scan).map(|k| l0 + (l1 - l0) * k as f64 / scan as f64).collect();
1705 for level in &mut levels {
1707 for critical in engine.critical.iter().flatten() {
1708 if (critical.0 - *level).abs() < 1e-9 * scale {
1709 *level += 1e-7 * (l1 - l0);
1710 }
1711 }
1712 }
1713 let mut structure = Vec::with_capacity(levels.len());
1714 for &level in &levels {
1715 structure.push(engine.intervals_at(level)?);
1716 }
1717 let debug = std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok();
1718 let mut breaks = vec![l0];
1719 for k in 0..scan {
1720 find_events(&engine, levels[k], &structure[k], levels[k + 1], &structure[k + 1], &mut breaks, 0)?;
1721 }
1722 breaks.push(l1);
1723 let last = breaks.len() - 1;
1728 for level in breaks[1..last].iter_mut() {
1729 let mut nearest = *level;
1730 let mut distance = 1e-5 * scale;
1731 for critical in engine.critical.iter().flatten() {
1732 if (critical.0 - *level).abs() < distance {
1733 distance = (critical.0 - *level).abs();
1734 nearest = critical.0;
1735 }
1736 }
1737 *level = nearest;
1738 }
1739 breaks.sort_by(|a, b| a.total_cmp(b));
1740 let mut kept: Vec<f64> = Vec::with_capacity(breaks.len());
1745 for (index, &level) in breaks.iter().enumerate() {
1746 let is_end = index == 0 || index + 1 == breaks.len();
1747 match kept.last() {
1748 Some(&previous) if level - previous < 1e-5 * scale => {
1749 if is_end {
1750 kept.pop();
1751 kept.push(level);
1752 }
1753 }
1754 _ => kept.push(level),
1755 }
1756 }
1757 let mut breaks = kept;
1758
1759 let mut fold_ends: Vec<f64> = Vec::new();
1769 for index in 1..breaks.len().saturating_sub(1) {
1770 let level = breaks[index];
1771 let snapped = engine.critical.iter().flatten().any(|c| (c.0 - level).abs() < 1e-9 * scale);
1772 if snapped {
1773 continue;
1774 }
1775 let room = (level - breaks[index - 1]).min(breaks[index + 1] - level);
1776 let delta = (5e-6 * scale).min(room / 8.0);
1779 let below = engine.intervals_at(level - delta)?;
1780 let above = engine.intervals_at(level + delta)?;
1781 if below.len() <= above.len() {
1782 continue;
1783 }
1784 let mut refined: Vec<(f64, f64, usize, usize)> = Vec::new();
1786 for k in 0..below.len() {
1787 let (x, y) = (below[k], below[(k + 1) % below.len()]);
1788 if !engine.angular && k + 1 == below.len() {
1789 break;
1790 }
1791 let gap_at = |intervals: &[Interval]| -> Option<(f64, f64)> {
1792 let mut best: Option<(f64, f64, f64)> = None;
1794 for i in 0..intervals.len() {
1795 let (p, q) = (intervals[i], intervals[(i + 1) % intervals.len()]);
1796 let d = engine.o_difference(p.hi, x.hi).abs() + engine.o_difference(q.lo, y.lo).abs();
1797 if best.map_or(true, |b| d < b.2) {
1798 let gap = engine.o_difference(q.lo, p.hi);
1799 best = Some((gap, p.hi + 0.5 * gap, d));
1800 }
1801 }
1802 best.map(|b| (b.0, b.1))
1803 };
1804 let mut samples = Vec::new();
1805 for factor in [1.0, 2.0, 4.0] {
1806 let l = level - delta * factor;
1807 let intervals = engine.intervals_at(l)?;
1808 if intervals.len() != below.len() {
1809 break;
1810 }
1811 if let Some((gap, middle)) = gap_at(&intervals) {
1812 samples.push((l, gap, middle));
1813 }
1814 }
1815 if samples.len() < 3 || samples.iter().any(|s| s.1 <= 0.0 || s.1 > 0.5) {
1816 continue;
1817 }
1818 let quad = |values: [f64; 3]| -> (f64, f64, f64) {
1820 let (x0, x1, x2) = (samples[0].0, samples[1].0, samples[2].0);
1821 let (y0, y1, y2) = (values[0], values[1], values[2]);
1822 let d01 = (y0 - y1) / (x0 - x1);
1823 let d12 = (y1 - y2) / (x1 - x2);
1824 let a2 = (d01 - d12) / (x0 - x2);
1825 let a1 = d01 - a2 * (x0 + x1);
1826 let a0 = y0 - a1 * x0 - a2 * x0 * x0;
1827 (a0, a1, a2)
1828 };
1829 let (c0, c1, c2) = quad([samples[0].1.powi(2), samples[1].1.powi(2), samples[2].1.powi(2)]);
1830 let root = if c2.abs() < 1e-300 {
1831 -c0 / c1
1832 } else {
1833 let disc = c1 * c1 - 4.0 * c2 * c0;
1834 if disc < 0.0 {
1835 continue;
1836 }
1837 let r1 = (-c1 + disc.sqrt()) / (2.0 * c2);
1838 let r2 = (-c1 - disc.sqrt()) / (2.0 * c2);
1839 if (r1 - level).abs() < (r2 - level).abs() { r1 } else { r2 }
1840 };
1841 if !(root > samples[0].0 && (root - level).abs() < delta) {
1842 continue;
1843 }
1844 let (m0, m1, m2) = quad([samples[0].2, samples[1].2, samples[2].2]);
1845 let middle = m0 + m1 * root + m2 * root * root;
1846 refined.push((root, middle, x.hi_source, y.lo_source));
1847 }
1848 if !refined.is_empty() {
1849 let root = refined.iter().map(|r| r.0).sum::<f64>() / refined.len() as f64;
1852 for &(own, middle, a_source, b_source) in &refined {
1853 if debug {
1854 eprintln!(" fold: break {level:.12} -> {own:.12} at o {middle:.12}");
1855 }
1856 engine.critical[a_source].push((root, middle));
1857 engine.critical[b_source].push((root, middle));
1858 }
1859 breaks[index] = root;
1860 fold_ends.push(root);
1861 }
1862 }
1863
1864 if debug {
1865 eprintln!("solid sweep: after events {:?} breaks {}", engine.counters.get(), breaks.len());
1866 }
1867 let mut pieces: Vec<Piece> = Vec::new();
1869 let mut band_ends: Vec<(f64, f64, Vec<(usize, usize)>)> = Vec::new();
1871 for window in breaks.windows(2) {
1872 let (a, b) = (window[0], window[1]);
1873 if b - a < 1e-12 * scale {
1874 continue;
1875 }
1876 let mid = engine.intervals_at(0.5 * (a + b))?;
1877 if mid.is_empty() {
1878 continue;
1879 }
1880 let (band_pieces, _, _) = sample_band(&engine, a, b, &mid, &fold_ends)?;
1881 let first = pieces.len();
1882 let pairs = (0..band_pieces.len() / 2).map(|i| (first + 2 * i, first + 2 * i + 1)).collect();
1883 pieces.extend(band_pieces);
1884 band_ends.push((a, b, pairs));
1885 }
1886 if pieces.is_empty() {
1887 return Ok(Vec::new());
1888 }
1889 if debug {
1890 eprintln!("solid sweep: after bands {:?}", engine.counters.get());
1891 for (k, critical) in engine.critical.iter().enumerate() {
1892 eprintln!(" source {k} {:?}: critical {:?}", engine.sources[k], critical);
1893 }
1894 }
1895
1896 let snap_length = 1e-5 * scale;
1904 let mut break_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1905 break_levels.sort_by(|a, b| a.total_cmp(b));
1906 break_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1907 for &level in &break_levels {
1908 let tolerance = if engine.angular { snap_length / level.max(1e-12) } else { snap_length };
1909 let mut ends: Vec<(usize, bool)> = Vec::new();
1910 for (index, piece) in pieces.iter().enumerate() {
1911 if (piece.points[0].0 - level).abs() < 1e-12 * scale {
1912 ends.push((index, false));
1913 }
1914 if (piece.points.last().expect("nonempty").0 - level).abs() < 1e-12 * scale {
1915 ends.push((index, true));
1916 }
1917 }
1918 let value = |pieces: &Vec<Piece>, (index, last): (usize, bool)| {
1919 if last { pieces[index].points.last().expect("nonempty").1 } else { pieces[index].points[0].1 }
1920 };
1921 for i in 0..ends.len() {
1922 let anchor = value(&pieces, ends[i]);
1923 for j in i + 1..ends.len() {
1924 let other = value(&pieces, ends[j]);
1925 let d = engine.o_difference(other, anchor);
1926 if d.abs() < tolerance && d != 0.0 {
1927 let (index, last) = ends[j];
1928 let target = other - d;
1929 let points = &mut pieces[index].points;
1930 if last {
1931 points.last_mut().expect("nonempty").1 = target;
1932 } else {
1933 points[0].1 = target;
1934 }
1935 }
1936 }
1937 }
1938 }
1939
1940 let mut all_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1942 all_levels.sort_by(|a, b| a.total_cmp(b));
1943 all_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1944 for &level in &all_levels {
1945 let mut below: Vec<(f64, f64)> = Vec::new();
1946 let mut above: Vec<(f64, f64)> = Vec::new();
1947 for (a, b, pairs) in &band_ends {
1948 for &(lo, hi) in pairs {
1949 let (lo, hi) = (&pieces[lo].points, &pieces[hi].points);
1950 if (b - level).abs() < 1e-12 * scale {
1951 below.push((lo.last().expect("nonempty").1, hi.last().expect("nonempty").1));
1952 }
1953 if (a - level).abs() < 1e-12 * scale {
1954 above.push((lo[0].1, hi[0].1));
1955 }
1956 }
1957 }
1958 let tolerance = if engine.angular { 1e-9 * scale / level.max(1e-12) } else { 1e-9 * scale };
1959 for (lo, hi) in symmetric_difference(&below, &above, engine.angular, tolerance) {
1960 let steps = 2;
1961 pieces.push(Piece {
1962 points: (0..=steps)
1963 .map(|k| (level, lo + (hi - lo) * k as f64 / steps as f64))
1964 .collect(),
1965 sigma: (0..=steps).map(|k| k as f64 / steps as f64).collect(),
1966 level_segment: true,
1967 });
1968 }
1969 }
1970
1971 if std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1972 eprintln!("solid sweep: breaks {breaks:?}");
1973 for piece in &pieces {
1974 let a = piece.points[0];
1975 let b = *piece.points.last().expect("nonempty");
1976 eprintln!(
1977 " piece {} ({:.9}, {:.9}) -> ({:.9}, {:.9}) n={}",
1978 if piece.level_segment { "LEVEL" } else { "edge " },
1979 a.0, a.1, b.0, b.1, piece.points.len()
1980 );
1981 if std::env::var("BREP_DEBUG_SOLID_SWEEP").as_deref() == Ok("2") {
1982 for p in piece.points.iter().take(6) {
1983 eprintln!(" {:.12} {:.12}", p.0, p.1);
1984 }
1985 }
1986 }
1987 }
1988 if let Some(level) = crest {
1990 pieces.retain(|piece| !(piece.level_segment && (piece.points[0].0 - level).abs() < 1e-9 * scale));
1991 }
1992 let loops = assemble_loops(&engine, pieces)?;
1994 let mut bodies = Vec::new();
1995 for pieces in loops {
1996 bodies.push(build_body(&engine, &pieces)?);
1997 }
1998 if crest.is_some() {
1999 let [kept] = bodies.as_slice() else {
2000 return Err(refuse("whole_level", "what stays below the crest is not one body".into()));
2001 };
2002 return clip_less(clip, frame_base(&engine), kept);
2003 }
2004 Ok(bodies)
2005}
2006
2007fn carry_along_screw(body: &mut BrepSolid, engine: &Engine, c: f64, span: f64, segments: usize) -> Result<(), KernelRefusal> {
2013 let origin = engine.clip.axis_origin;
2014 let ez = engine.frame.ez;
2015 let carry = |start: Vec3| -> Vec<Vec3> {
2017 let h = span / segments as f64;
2018 let axial = ez.scale(start.sub(origin).dot(ez));
2019 let radial = start.sub(origin).sub(axial);
2020 let at = |k: usize| {
2021 let angle = h * k as f64;
2022 let r = radial.rotate_about(ez, angle);
2023 let value = origin.add(axial).add(r).add(ez.scale(c * angle));
2024 (value, ez.cross(r).add(ez.scale(c)), r.scale(-1.0))
2025 };
2026 let piece = |k: usize| {
2027 let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
2028 [
2029 f0,
2030 f0.add(d0.scale(h / 5.0)),
2031 f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
2032 f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
2033 f1.sub(d1.scale(h / 5.0)),
2034 f1,
2035 ]
2036 };
2037 let mut points = Vec::with_capacity(3 * segments + 3);
2038 let first = piece(0);
2039 points.extend_from_slice(&first[..3]);
2040 for k in 0..segments - 1 {
2041 let (here, next) = (piece(k), piece(k + 1));
2042 points.push(here[3]);
2043 points.push(here[4].scale(2.0).sub(here[3]));
2044 points.push(next[2]);
2045 }
2046 let last = piece(segments - 1);
2047 points.extend_from_slice(&last[3..]);
2048 points
2049 };
2050 let knots = |lo: f64, hi: f64| -> Vec<f64> {
2051 let mut knots = vec![lo; 6];
2052 for k in 1..segments {
2053 knots.extend([lo + (hi - lo) * k as f64 / segments as f64; 3]);
2054 }
2055 knots.extend([hi; 6]);
2056 knots
2057 };
2058 let point = |p: &crate::Vec4| Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w);
2059 let moved = |p: &crate::Vec4, angle: f64| -> Vec3 {
2060 rotate(point(p), origin, ez, angle).add(ez.scale(c * angle))
2061 };
2062 let tolerance = 1e-7 * engine.scale;
2063 for face in body.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
2064 let rows = &face.surface.control_points;
2065 if rows[0].len() != segments + 1 {
2066 continue; }
2068 if rows.iter().any(|row| moved(&row[0], span).sub(point(&row[segments])).length() > tolerance) {
2070 return Err(refuse("screw_loft", "a lofted wall does not follow the screw".into()));
2071 }
2072 let lifted: Vec<Vec<crate::Vec4>> = rows
2073 .iter()
2074 .map(|row| {
2075 let w = row[0].w;
2076 carry(point(&row[0])).into_iter().map(|q| crate::Vec4::from_point(q, w)).collect()
2077 })
2078 .collect();
2079 let v = &face.surface.knots_v;
2080 let (lo, hi) = (v[0], *v.last().expect("knots"));
2081 face.surface = crate::NurbsSurface::new(face.surface.degree_u, 5, face.surface.knots_u.clone(), knots(lo, hi), lifted)
2082 .or_refuse(KernelStage::Fragment, "screw_surface")?;
2083 }
2084 let height = c * span;
2085 let points: rustc_hash::FxHashMap<u64, Vec3> = body.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
2086 for edge in &mut body.edges {
2087 let (from, to) = (points[&edge.start_vertex_id], points[&edge.end_vertex_id]);
2088 let rise = to.sub(from).dot(ez);
2089 if (rise.abs() - height.abs()).abs() > tolerance {
2090 continue; }
2092 let domain = edge.curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
2093 let forward = rise * height > 0.0;
2094 let mut path = carry(if forward { from } else { to });
2095 if !forward {
2096 path.reverse();
2097 }
2098 edge.curve = NurbsCurve::new(5, knots(domain[0], domain[1]), path.into_iter().map(|q| crate::Vec4::from_point(q, 1.0)).collect())
2099 .or_refuse(KernelStage::Fragment, "screw_edge")?;
2100 }
2101 Ok(())
2102}
2103
2104fn stack_screw_rows(
2108 row: &BrepSolid,
2109 engine: &Engine,
2110 c: f64,
2111 span: f64,
2112 segments: usize,
2113 rows: usize,
2114) -> Result<BrepSolid, KernelRefusal> {
2115 let origin = engine.clip.axis_origin;
2116 let ez = engine.frame.ez;
2117 let moved = |x: Vec3, copy: usize| -> Vec3 {
2118 let angle = span * copy as f64;
2119 rotate(x, origin, ez, angle).add(ez.scale(c * angle))
2120 };
2121 let tolerance = 1e-7 * engine.scale;
2122 let height = |x: Vec3| x.sub(origin).dot(ez);
2123 let points: rustc_hash::FxHashMap<u64, Vec3> = row.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
2124 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
2125 for vertex in &row.vertices {
2126 low = low.min(height(vertex.point));
2127 high = high.max(height(vertex.point));
2128 }
2129 let at = |id: u64, level: f64| (height(points[&id]) - level).abs() <= tolerance;
2130 let in_level = |edge: &crate::topology::EdgeRecord, level: f64| at(edge.start_vertex_id, level) && at(edge.end_vertex_id, level);
2131 let (bottom, top) = if c * span > 0.0 { (low, high) } else { (high, low) };
2132 let mut vertex_below: rustc_hash::FxHashMap<u64, u64> = Default::default();
2134 for vertex in row.vertices.iter().filter(|vertex| at(vertex.id, bottom)) {
2135 let up = moved(vertex.point, 1);
2136 let below = row.vertices.iter().find(|other| at(other.id, top) && other.point.sub(up).length() <= tolerance);
2137 let Some(below) = below else { return Err(refuse("screw_rows", "a row's section does not meet the next".into())) };
2138 vertex_below.insert(vertex.id, below.id);
2139 }
2140 let mut edge_below: rustc_hash::FxHashMap<u64, u64> = Default::default();
2141 for edge in row.edges.iter().filter(|edge| in_level(edge, bottom)) {
2142 let ends = (vertex_below[&edge.start_vertex_id], vertex_below[&edge.end_vertex_id]);
2143 let below = row.edges.iter().find(|other| in_level(other, top) && (other.start_vertex_id, other.end_vertex_id) == ends);
2144 let Some(below) = below else { return Err(refuse("screw_rows", "a row's section edge does not meet the next".into())) };
2145 edge_below.insert(edge.id, below.id);
2146 }
2147 let face_ids = row.shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| {
2148 std::iter::once(face.id).chain(face.loops.iter().flat_map(|l| std::iter::once(l.id).chain(l.coedges.iter().map(|c| c.id))))
2149 });
2150 let stride = row
2151 .vertices
2152 .iter()
2153 .map(|v| v.id)
2154 .chain(row.edges.iter().map(|e| e.id))
2155 .chain(face_ids)
2156 .max()
2157 .unwrap_or(0)
2158 + 1;
2159 let vertex_id = |id: u64, copy: usize| match vertex_below.get(&id) {
2160 Some(&below) if copy > 0 => below + stride * (copy as u64 - 1),
2161 _ => id + stride * copy as u64,
2162 };
2163 let edge_id = |id: u64, copy: usize| match edge_below.get(&id) {
2164 Some(&below) if copy > 0 => below + stride * (copy as u64 - 1),
2165 _ => id + stride * copy as u64,
2166 };
2167 let is_cap = |face: &crate::FaceRecord| face.surface.control_points[0].len() != 3 * segments + 3;
2168 let cap_level = |face: &crate::FaceRecord, level: f64| {
2169 face.loops.iter().flat_map(|l| &l.coedges).all(|coedge| {
2170 row.edges.iter().find(|edge| edge.id == coedge.edge_id).is_some_and(|edge| in_level(edge, level))
2171 })
2172 };
2173 let mut stacked = row.clone();
2174 stacked.mass_properties_cache = Default::default();
2175 stacked.vertices.clear();
2176 stacked.edges.clear();
2177 for shell in &mut stacked.shells {
2178 shell.faces.clear();
2179 }
2180 for copy in 0..rows {
2181 for vertex in &row.vertices {
2182 if copy == 0 || !vertex_below.contains_key(&vertex.id) {
2183 stacked.vertices.push(crate::topology::VertexRecord { id: vertex_id(vertex.id, copy), point: moved(vertex.point, copy) });
2184 }
2185 }
2186 for edge in &row.edges {
2187 if copy == 0 || !edge_below.contains_key(&edge.id) {
2188 stacked.edges.push(crate::topology::EdgeRecord {
2189 id: edge_id(edge.id, copy),
2190 curve: transform_curve(&edge.curve, |x| moved(x, copy))?,
2191 t0: edge.t0,
2192 t1: edge.t1,
2193 start_vertex_id: vertex_id(edge.start_vertex_id, copy),
2194 end_vertex_id: vertex_id(edge.end_vertex_id, copy),
2195 degenerate: edge.degenerate,
2196 name: edge.name.clone(),
2197 });
2198 }
2199 }
2200 for (target, shell) in stacked.shells.iter_mut().zip(&row.shells) {
2201 for face in &shell.faces {
2202 if is_cap(face) && !((copy == 0 && cap_level(face, bottom)) || (copy + 1 == rows && cap_level(face, top))) {
2203 continue;
2204 }
2205 let control_points = face
2206 .surface
2207 .control_points
2208 .iter()
2209 .map(|line| {
2210 line.iter()
2211 .map(|p| {
2212 let w = p.w;
2213 crate::Vec4::from_point(moved(Vec3::new(p.x / w, p.y / w, p.z / w), copy), w)
2214 })
2215 .collect()
2216 })
2217 .collect();
2218 let surface = crate::NurbsSurface::new(
2219 face.surface.degree_u,
2220 face.surface.degree_v,
2221 face.surface.knots_u.clone(),
2222 face.surface.knots_v.clone(),
2223 control_points,
2224 )
2225 .or_refuse(KernelStage::Fragment, "screw_rows")?;
2226 let mut copied = face.clone();
2227 copied.surface = surface;
2228 copied.id += stride * copy as u64;
2229 for loop_record in &mut copied.loops {
2230 loop_record.id += stride * copy as u64;
2231 for coedge in &mut loop_record.coedges {
2232 coedge.id += stride * copy as u64;
2233 coedge.edge_id = edge_id(coedge.edge_id, copy);
2234 }
2235 }
2236 target.faces.push(copied);
2237 }
2238 }
2239 }
2240 let issues = stacked.validate();
2241 if issues.iter().any(|issue| issue.severity == "error") {
2242 return Err(refuse("screw_rows", format!("the stacked thread is invalid: {issues:?}")));
2243 }
2244 Ok(stacked)
2245}
2246
2247fn frame_base(engine: &Engine) -> Vec3 {
2249 engine.frame.world([0.0, 0.0, engine.clip.axial[0]])
2250}
2251
2252fn clip_less(clip: &SweepClip, base: Vec3, kept: &BrepSolid) -> Result<Vec<BrepSolid>, KernelRefusal> {
2254 let direction = clip.axis_direction.normalized().or_refuse(KernelStage::Collect, "clip_axis")?;
2255 let cylinder = crate::make_cylinder_brep(base, direction, clip.radius[1], clip.axial[1] - clip.axial[0])
2256 .or_refuse(KernelStage::Fragment, "clip_cylinder")?;
2257 if thread_rows_on() {
2258 if let Some(direct) = clip_less_direct(&cylinder, base, direction, kept)? {
2259 return Ok(vec![direct]);
2260 }
2261 }
2262 let r = crate::boolean_operation(&cylinder, kept, crate::BooleanOperation::Subtract, &Default::default())?;
2263 Ok(vec![r])
2264}
2265
2266fn clip_less_direct(
2271 cylinder: &BrepSolid,
2272 base: Vec3,
2273 direction: Vec3,
2274 kept: &BrepSolid,
2275) -> Result<Option<BrepSolid>, KernelRefusal> {
2276 if kept.shells.len() != 1 || cylinder.shells.len() != 1 {
2277 return Ok(None);
2278 }
2279 let mut stride = 0;
2280 for shell in &cylinder.shells {
2281 for face in &shell.faces {
2282 stride = stride.max(face.id);
2283 for loop_record in &face.loops {
2284 stride = stride.max(loop_record.id);
2285 stride = loop_record.coedges.iter().fold(stride, |m, c| m.max(c.id));
2286 }
2287 }
2288 }
2289 stride = cylinder.vertices.iter().fold(stride, |m, v| m.max(v.id));
2290 stride = cylinder.edges.iter().fold(stride, |m, e| m.max(e.id)) + 1;
2291 let corners = |surface: &crate::NurbsSurface| -> Result<([f64; 4], [Vec3; 3]), String> {
2292 let (u0, u1) = (surface.knots_u[0], *surface.knots_u.last().ok_or("knots")?);
2293 let (v0, v1) = (surface.knots_v[0], *surface.knots_v.last().ok_or("knots")?);
2294 Ok(([u0, u1, v0, v1], [surface.evaluate(u0, v0)?, surface.evaluate(u1, v0)?, surface.evaluate(u0, v1)?]))
2295 };
2296 let mut result = cylinder.clone();
2297 result.mass_properties_cache = Default::default();
2298 result.genus = 1;
2299 result.vertices.extend(kept.vertices.iter().map(|v| crate::topology::VertexRecord { id: v.id + stride, point: v.point }));
2300 for edge in &kept.edges {
2301 let mut edge = edge.clone();
2302 edge.id += stride;
2303 edge.start_vertex_id += stride;
2304 edge.end_vertex_id += stride;
2305 result.edges.push(edge);
2306 }
2307 let mut holes = 0;
2308 let turned = |loop_record: &crate::topology::LoopRecord| -> Result<crate::topology::LoopRecord, KernelRefusal> {
2309 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
2310 for coedge in loop_record.coedges.iter().rev() {
2311 coedges.push(crate::topology::CoedgeRecord {
2312 id: coedge.id + stride,
2313 edge_id: coedge.edge_id + stride,
2314 forward: !coedge.forward,
2315 pcurve: coedge.pcurve.reversed().or_refuse(KernelStage::Fragment, "clip_less_pcurve")?,
2316 });
2317 }
2318 Ok(crate::topology::LoopRecord { id: loop_record.id + stride, coedges })
2319 };
2320 for face in &kept.shells[0].faces {
2321 let mut loops = Vec::with_capacity(face.loops.len());
2322 for loop_record in &face.loops {
2323 loops.push(turned(loop_record)?);
2324 }
2325 if !face.surface.is_affine().unwrap_or(false) {
2326 result.shells[0].faces.push(crate::FaceRecord {
2327 id: face.id + stride,
2328 surface: face.surface.clone(),
2329 same_sense: !face.same_sense,
2330 loops,
2331 name: face.name.clone(),
2332 });
2333 continue;
2334 }
2335 let (from_box, from) = corners(&face.surface).or_refuse(KernelStage::Fragment, "clip_less_chart")?;
2337 let height = from[0].sub(base).dot(direction);
2338 let mut caps = result.shells[0].faces.iter_mut().filter(|cap| {
2339 cap.surface.is_affine().unwrap_or(false)
2340 && cap.surface.evaluate(cap.surface.knots_u[0], cap.surface.knots_v[0]).is_ok_and(|p| (p.sub(base).dot(direction) - height).abs() < 1e-9 * (1.0 + height.abs()))
2341 });
2342 let (Some(cap), None) = (caps.next(), caps.next()) else { return Ok(None) };
2343 let (to_box, to) = corners(&cap.surface).or_refuse(KernelStage::Fragment, "clip_less_chart")?;
2344 let (eu, ev) = (to[1].sub(to[0]), to[2].sub(to[0]));
2345 if eu.dot(ev).abs() > 1e-9 * eu.length() * ev.length() {
2346 return Ok(None);
2347 }
2348 let chart = |p: Vec3| {
2349 let d = p.sub(to[0]);
2350 [
2351 to_box[0] + (to_box[1] - to_box[0]) * d.dot(eu) / eu.dot(eu),
2352 to_box[2] + (to_box[3] - to_box[2]) * d.dot(ev) / ev.dot(ev),
2353 ]
2354 };
2355 let source = |a: f64, b: f64| from[0].add(from[1].sub(from[0]).scale(a)).add(from[2].sub(from[0]).scale(b));
2356 let (origin, across, up) = (chart(source(0.0, 0.0)), chart(source(1.0, 0.0)), chart(source(0.0, 1.0)));
2357 for loop_record in &mut loops {
2358 for coedge in &mut loop_record.coedges {
2359 let curve = &coedge.pcurve;
2360 let control_points = curve
2361 .control_points
2362 .iter()
2363 .map(|p| {
2364 let a = (p.x / p.w - from_box[0]) / (from_box[1] - from_box[0]);
2365 let b = (p.y / p.w - from_box[2]) / (from_box[3] - from_box[2]);
2366 let x = origin[0] + a * (across[0] - origin[0]) + b * (up[0] - origin[0]);
2367 let y = origin[1] + a * (across[1] - origin[1]) + b * (up[1] - origin[1]);
2368 crate::Vec4 { x: x * p.w, y: y * p.w, z: p.z, w: p.w }
2369 })
2370 .collect();
2371 coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2372 .or_refuse(KernelStage::Fragment, "clip_less_pcurve")?;
2373 }
2374 }
2375 cap.loops.extend(loops);
2376 holes += 1;
2377 }
2378 if holes != 2 || result.validate().iter().any(|issue| issue.severity == "error") {
2379 return Ok(None);
2380 }
2381 Ok(Some(result))
2382}
2383
2384fn crest_level(engine: &Engine) -> Result<Option<f64>, KernelRefusal> {
2388 let whole = |level: f64| -> Result<bool, KernelRefusal> {
2389 match engine.intervals_at(level) {
2390 Ok(_) => Ok(false),
2391 Err(error) if error.message.contains(WHOLE_LEVEL) => Ok(true),
2392 Err(error) => Err(error),
2393 }
2394 };
2395 let [l0, l1] = engine.clip.radius;
2396 let scan = 64;
2397 let level_at = |k: usize| l0 + (l1 - l0) * k as f64 / scan as f64;
2398 if !whole(l1)? {
2399 return Ok(None);
2400 }
2401 let mut first = scan;
2402 while first > 0 && whole(level_at(first - 1))? {
2403 first -= 1;
2404 }
2405 if first == 0 {
2406 return Ok(None);
2407 }
2408 let (mut below, mut above) = (level_at(first - 1), level_at(first));
2409 for _ in 0..60 {
2410 let middle = 0.5 * (below + above);
2411 if whole(middle)? {
2412 above = middle;
2413 } else {
2414 below = middle;
2415 }
2416 }
2417 let gap = |level: f64| -> Result<f64, KernelRefusal> {
2422 Ok(TAU - engine.intervals_at(level)?.iter().map(|interval| interval.hi - interval.lo).sum::<f64>())
2423 };
2424 let step = 4e-5 * (l1 - l0);
2425 let (x0, x1, x2) = (below - 3.0 * step, below - 2.0 * step, below - step);
2426 let (g0, g1, g2) = (gap(x0)?, gap(x1)?, gap(x2)?);
2427 if !(g2 > 0.0 && g1 > g2 && g0 > g1) || g2 > 16.0 * (g1 - g2) {
2428 return Ok(None);
2429 }
2430 let curve = |x: f64| {
2431 g0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
2432 + g1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
2433 + g2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
2434 };
2435 let (mut lo, mut hi) = (x2, below + step);
2436 if curve(hi) > 0.0 {
2437 return Ok(None);
2438 }
2439 for _ in 0..80 {
2440 let middle = 0.5 * (lo + hi);
2441 if curve(middle) > 0.0 {
2442 lo = middle;
2443 } else {
2444 hi = middle;
2445 }
2446 }
2447 Ok(Some(0.5 * (lo + hi)))
2448}
2449
2450fn find_events(
2451 engine: &Engine,
2452 a: f64,
2453 sa: &[Interval],
2454 b: f64,
2455 sb: &[Interval],
2456 breaks: &mut Vec<f64>,
2457 depth: usize,
2458) -> Result<(), KernelRefusal> {
2459 if compatible(engine, sa, sb) {
2460 return Ok(());
2461 }
2462 if depth <= 2 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
2463 let summary = |s: &[Interval]| -> Vec<(usize, usize, f64, f64)> {
2464 s.iter().map(|x| (x.lo_source, x.hi_source, (x.lo * 1e4).round() / 1e4, (x.hi * 1e4).round() / 1e4)).collect()
2465 };
2466 eprintln!(" find_events depth {depth} [{a:.9}, {b:.9}] {:?}\n -> {:?} counters {:?}", summary(sa), summary(sb), engine.counters.get());
2467 }
2468 if b - a < 1e-13 * engine.scale || depth > 60 {
2469 breaks.push(0.5 * (a + b));
2470 return Ok(());
2471 }
2472 let m = 0.5 * (a + b);
2473 let sm = engine.intervals_at(m)?;
2474 if !compatible(engine, sa, &sm) && !compatible(engine, &sm, sb) && depth > 8 {
2477 breaks.push(m);
2478 return Ok(());
2479 }
2480 find_events(engine, a, sa, m, &sm, breaks, depth + 1)?;
2481 find_events(engine, m, &sm, b, sb, breaks, depth + 1)
2482}
2483
2484fn end_value(engine: &Engine, source: usize, level: f64, guess: f64, fallback: f64) -> f64 {
2487 let mut candidates = Vec::new();
2488 engine.source_candidates(source, level, &mut candidates);
2489 let mut options: Vec<f64> = candidates.iter().map(|c| c.0).collect();
2490 for critical in &engine.critical[source] {
2491 if (critical.0 - level).abs() < 1e-7 * engine.scale {
2492 options.push(critical.1);
2493 }
2494 }
2495 let mut best = fallback;
2496 let mut distance = f64::INFINITY;
2497 for option in options {
2498 let d = engine.o_difference(option, guess).abs();
2499 if d < distance {
2500 distance = d;
2501 best = guess + engine.o_difference(option, guess);
2502 }
2503 }
2504 best
2505}
2506
2507#[allow(clippy::type_complexity)]
2510fn sample_band(
2511 engine: &Engine,
2512 a: f64,
2513 b: f64,
2514 mid: &[Interval],
2515 fold_ends: &[f64],
2516) -> Result<(Vec<Piece>, Vec<(f64, f64)>, Vec<(f64, f64)>), KernelRefusal> {
2517 let count = mid.len();
2518 let n = 24usize;
2519 let near_fold = fold_ends.iter().any(|f| (f - a).abs() < 1e-12 * engine.scale || (f - b).abs() < 1e-12 * engine.scale);
2522 let n_cap = if near_fold {
2523 let keep = (1e-4 * engine.scale).min((b - a) / 50.0);
2524 let ratio = (1.0 - 2.0 * keep / (b - a)).clamp(-1.0, 1.0);
2526 ((PI / ratio.acos()).floor() as usize).max(8)
2527 } else {
2528 192
2529 };
2530 let mut n = n.min(n_cap);
2531 loop {
2532 let sigma: Vec<f64> = (0..=n).map(|k| k as f64 / n as f64).collect();
2533 let level_at = |s: f64| a + (b - a) * (1.0 - (PI * s).cos()) / 2.0;
2534 let mut lo = vec![vec![f64::NAN; n + 1]; count];
2536 let mut hi = vec![vec![f64::NAN; n + 1]; count];
2537 let mut lo_src = vec![vec![0usize; n + 1]; count];
2538 let mut hi_src = vec![vec![0usize; n + 1]; count];
2539 let mid_index = n / 2;
2540 let mid_intervals = engine.intervals_at(level_at(sigma[mid_index]))?;
2541 if mid_intervals.len() != count {
2542 return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2543 }
2544 for (i, interval) in mid_intervals.iter().enumerate() {
2545 lo[i][mid_index] = interval.lo;
2546 hi[i][mid_index] = interval.hi;
2547 lo_src[i][mid_index] = interval.lo_source;
2548 hi_src[i][mid_index] = interval.hi_source;
2549 }
2550 for direction in [-1i64, 1] {
2551 let mut k = mid_index as i64 + direction;
2552 while k > 0 && (k as usize) < n {
2553 let ku = k as usize;
2554 let previous = (ku as i64 - direction) as usize;
2555 let intervals = engine.intervals_at(level_at(sigma[ku]))?;
2556 if intervals.len() != count {
2557 return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2558 }
2559 for i in 0..count {
2560 let centre = 0.5 * (lo[i][previous] + hi[i][previous]);
2561 let nearest = intervals
2562 .iter()
2563 .min_by(|x, y| {
2564 let dx = engine.o_difference(0.5 * (x.lo + x.hi), centre).abs();
2565 let dy = engine.o_difference(0.5 * (y.lo + y.hi), centre).abs();
2566 dx.total_cmp(&dy)
2567 })
2568 .expect("nonempty");
2569 let shift = engine.o_difference(nearest.lo, lo[i][previous]) - (nearest.lo - lo[i][previous]);
2570 lo[i][ku] = nearest.lo + shift;
2571 hi[i][ku] = nearest.hi + shift;
2572 lo_src[i][ku] = nearest.lo_source;
2573 hi_src[i][ku] = nearest.hi_source;
2574 }
2575 k += direction;
2576 }
2577 }
2578 for i in 0..count {
2581 for (end, inner, inner2, inner3, level) in [(0usize, 1usize, 2usize, 3usize, a), (n, n - 1, n - 2, n - 3, b)] {
2582 let near = |values: &Vec<f64>| 2.0 * values[inner] - values[inner2];
2589 let quadratic = |values: &Vec<f64>| 3.0 * values[inner] - 3.0 * values[inner2] + values[inner3];
2590 lo[i][end] = end_value(engine, lo_src[i][inner], level, near(&lo[i]), quadratic(&lo[i]));
2591 hi[i][end] = end_value(engine, hi_src[i][inner], level, near(&hi[i]), quadratic(&hi[i]));
2592 }
2593 }
2594 let mut worst = 0.0f64;
2596 let mut worst_at = (0usize, 0usize, 0usize, 0.0f64, 0.0f64);
2597 for i in 0..count {
2598 for (side, values) in [&lo[i], &hi[i]].into_iter().enumerate() {
2599 let points: Vec<Vec3> = (0..=n)
2600 .map(|k| reduced_to_plane(engine, level_at(sigma[k]), values[k]))
2601 .collect();
2602 let curve = fit_piece(&points)?;
2603 let parameters = chord_parameters(&points);
2604 for k in (1..n - 1).step_by((n / 8).max(1)).chain([0, n - 1]) {
2606 let s = 0.5 * (sigma[k] + sigma[k + 1]);
2607 let level = level_at(s);
2608 let intervals = engine.intervals_at(level)?;
2609 let guess = 0.5 * (values[k] + values[k + 1]);
2610 let exact = intervals
2611 .iter()
2612 .flat_map(|x| [x.lo, x.hi])
2613 .map(|o| guess + engine.o_difference(o, guess))
2614 .min_by(|x, y| (x - guess).abs().total_cmp(&(y - guess).abs()))
2615 .unwrap_or(guess);
2616 let truth = reduced_to_plane(engine, level, exact);
2617 let bracket = (parameters[k.saturating_sub(1)], parameters[(k + 2).min(n)]);
2618 let off = distance_in_bracket(&curve, truth, bracket.0, bracket.1)?;
2619 if off > worst {
2620 worst = off;
2621 worst_at = (i, side, k, level, exact);
2622 }
2623 }
2624 }
2625 }
2626 if n >= n_cap && worst > 1e-6 * engine.scale {
2627 return Err(refuse(
2628 "fit_tolerance",
2629 format!(
2630 "a boundary piece of the band {a}..{b} stays {worst:.3e} off its envelope at {n} samples \
2631 (interval {}, {}, sample {}, level {}, o {})",
2632 worst_at.0,
2633 if worst_at.1 == 0 { "lo" } else { "hi" },
2634 worst_at.2,
2635 worst_at.3,
2636 worst_at.4
2637 ),
2638 ));
2639 }
2640 if worst <= 1e-8 * engine.scale || n >= n_cap {
2641 let mut pieces = Vec::new();
2642 for i in 0..count {
2643 for values in [&lo[i], &hi[i]] {
2644 pieces.push(Piece {
2645 points: (0..=n).map(|k| (level_at(sigma[k]), values[k])).collect(),
2646 sigma: sigma.clone(),
2647 level_segment: false,
2648 });
2649 }
2650 }
2651 let bottoms = (0..count).map(|i| (lo[i][0], hi[i][0])).collect();
2652 let tops = (0..count).map(|i| (lo[i][n], hi[i][n])).collect();
2653 return Ok((pieces, bottoms, tops));
2654 }
2655 n = (n * 2).min(n_cap);
2656 }
2657}
2658
2659fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
2665 let mut parameters = vec![0.0; points.len()];
2666 for k in 1..points.len() {
2667 parameters[k] = parameters[k - 1] + points[k].sub(points[k - 1]).length();
2668 }
2669 let total = parameters[points.len() - 1].max(1e-300);
2670 for parameter in &mut parameters {
2671 *parameter /= total;
2672 }
2673 let last = points.len() - 1;
2674 parameters[last] = 1.0;
2675 parameters
2676}
2677
2678fn fit_piece(points: &[Vec3]) -> Result<NurbsCurve, KernelRefusal> {
2679 interpolate_curve(points, 3, &chord_parameters(points)).or_refuse(KernelStage::Fragment, "piece_fit")
2680}
2681
2682fn distance_in_bracket(curve: &NurbsCurve, target: Vec3, a: f64, b: f64) -> Result<f64, KernelRefusal> {
2684 let distance = |t: f64| -> Result<f64, KernelRefusal> {
2685 Ok(curve.evaluate(t).or_refuse(KernelStage::Fragment, "piece_eval")?.sub(target).length())
2686 };
2687 let ratio = 0.618_033_988_749_894_9;
2688 let (mut lo, mut hi) = (a, b);
2689 for _ in 0..80 {
2690 let c = hi - ratio * (hi - lo);
2691 let d = lo + ratio * (hi - lo);
2692 if distance(c)? < distance(d)? {
2693 hi = d;
2694 } else {
2695 lo = c;
2696 }
2697 }
2698 distance(0.5 * (lo + hi))
2699}
2700
2701fn symmetric_difference(below: &[(f64, f64)], above: &[(f64, f64)], angular: bool, tolerance: f64) -> Vec<(f64, f64)> {
2703 let normalise = |list: &[(f64, f64)]| -> Vec<(f64, f64)> {
2705 let mut out = Vec::new();
2706 for &(lo, hi) in list {
2707 if angular {
2708 let shift = lo.rem_euclid(TAU) - lo;
2709 let (lo, hi) = (lo + shift, hi + shift);
2710 if hi > TAU {
2711 out.push((lo, TAU));
2712 out.push((0.0, hi - TAU));
2713 } else {
2714 out.push((lo, hi));
2715 }
2716 } else {
2717 out.push((lo, hi));
2718 }
2719 }
2720 out
2721 };
2722 let a = normalise(below);
2723 let b = normalise(above);
2724 let mut cuts: Vec<f64> = a.iter().chain(&b).flat_map(|&(lo, hi)| [lo, hi]).collect();
2725 cuts.sort_by(|x, y| x.total_cmp(y));
2726 cuts.dedup_by(|y, x| (*y - *x).abs() < tolerance);
2727 let inside = |list: &Vec<(f64, f64)>, x: f64| list.iter().any(|&(lo, hi)| x > lo && x < hi);
2728 let mut out: Vec<(f64, f64)> = Vec::new();
2729 for w in cuts.windows(2) {
2730 let m = 0.5 * (w[0] + w[1]);
2731 if inside(&a, m) != inside(&b, m) && w[1] - w[0] > tolerance {
2732 if let Some(last) = out.last_mut() {
2733 if (last.1 - w[0]).abs() < tolerance {
2734 last.1 = w[1];
2735 continue;
2736 }
2737 }
2738 out.push((w[0], w[1]));
2739 }
2740 }
2741 if angular && out.len() > 1 {
2742 let first = out[0];
2743 let last = *out.last().expect("nonempty");
2744 if first.0.abs() < tolerance && (last.1 - TAU).abs() < tolerance {
2745 out.pop();
2746 out[0] = (last.0 - TAU, first.1);
2747 }
2748 }
2749 out
2750}
2751
2752fn reduced_to_plane(engine: &Engine, level: f64, o: f64) -> Vec3 {
2754 let clip = engine.clip;
2755 match engine.class {
2756 Class::Screw(c) => {
2757 let z = clip.axial[0];
2758 let alpha = o + z / c;
2759 engine.frame.world([level * alpha.cos(), level * alpha.sin(), z])
2760 }
2761 Class::Turn => {
2762 let theta = clip.angle.map(|a| a[0]).unwrap_or(0.0);
2763 engine.frame.world([level * theta.cos(), level * theta.sin(), o])
2764 }
2765 Class::Planar => engine.frame.world([level * o.cos(), level * o.sin(), clip.axial[0]]),
2766 }
2767}
2768
2769fn assemble_loops(engine: &Engine, pieces: Vec<Piece>) -> Result<Vec<Vec<Piece>>, KernelRefusal> {
2771 let tolerance = 1e-6 * engine.scale;
2772 let ends: Vec<(Vec3, Vec3)> = pieces
2773 .iter()
2774 .map(|piece| {
2775 let first = piece.points[0];
2776 let last = *piece.points.last().expect("nonempty");
2777 (reduced_to_plane(engine, first.0, first.1), reduced_to_plane(engine, last.0, last.1))
2778 })
2779 .collect();
2780 let mut used = vec![false; pieces.len()];
2781 let mut orders: Vec<Vec<(usize, bool)>> = Vec::new();
2782 for start in 0..pieces.len() {
2783 if used[start] {
2784 continue;
2785 }
2786 used[start] = true;
2787 let mut order = vec![(start, false)];
2788 let origin = ends[start].0;
2789 let mut cursor = ends[start].1;
2790 let mut guard = 0;
2791 while cursor.sub(origin).length() > tolerance {
2792 guard += 1;
2793 if guard > pieces.len() {
2794 return Err(refuse("loop_open", "the section's boundary does not close".into()));
2795 }
2796 let mut next = None;
2797 let mut best = tolerance;
2798 for k in 0..pieces.len() {
2799 if used[k] {
2800 continue;
2801 }
2802 for (reversed, end) in [(false, ends[k].0), (true, ends[k].1)] {
2803 let d = end.sub(cursor).length();
2804 if d < best {
2805 best = d;
2806 next = Some((k, reversed));
2807 }
2808 }
2809 }
2810 let Some((k, reversed)) = next else {
2811 return Err(refuse(
2812 "loop_open",
2813 format!("the section's boundary does not close (a gap at {:?})", cursor),
2814 ));
2815 };
2816 used[k] = true;
2817 order.push((k, reversed));
2818 cursor = if reversed { ends[k].0 } else { ends[k].1 };
2819 }
2820 orders.push(order);
2821 }
2822 if orders.len() > 1 {
2823 let polygon = |order: &Vec<(usize, bool)>| -> Vec<Vec3> {
2825 order
2826 .iter()
2827 .flat_map(|&(k, _)| pieces[k].points.iter().map(|p| reduced_to_plane(engine, p.0, p.1)))
2828 .collect()
2829 };
2830 let polygons: Vec<Vec<Vec3>> = orders.iter().map(polygon).collect();
2831 for (i, a) in polygons.iter().enumerate() {
2832 for (j, b) in polygons.iter().enumerate() {
2833 if i != j && point_in_polygon(engine, a[0], b) {
2834 return Err(refuse("section_hole", "the section has a hole, which is not built here".into()));
2835 }
2836 }
2837 }
2838 }
2839 let mut slots: Vec<Option<Piece>> = pieces.into_iter().map(Some).collect();
2840 Ok(orders
2841 .into_iter()
2842 .map(|order| {
2843 order
2844 .into_iter()
2845 .map(|(k, reversed)| {
2846 let mut piece = slots[k].take().expect("each piece once");
2847 if reversed {
2848 piece.points.reverse();
2849 piece.sigma = piece.sigma.iter().rev().map(|s| 1.0 - s).collect();
2850 }
2851 piece
2852 })
2853 .collect()
2854 })
2855 .collect())
2856}
2857
2858fn point_in_polygon(engine: &Engine, p: Vec3, polygon: &[Vec3]) -> bool {
2859 let local = |x: Vec3| -> [f64; 2] {
2860 let l = engine.frame.local(x);
2861 match engine.class {
2862 Class::Turn => [l[0].hypot(l[1]), l[2]],
2863 _ => [l[0], l[1]],
2864 }
2865 };
2866 let q = local(p);
2867 let mut inside = false;
2868 for k in 0..polygon.len() {
2869 let a = local(polygon[k]);
2870 let b = local(polygon[(k + 1) % polygon.len()]);
2871 if (a[1] > q[1]) != (b[1] > q[1]) {
2872 let x = a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
2873 if x > q[0] {
2874 inside = !inside;
2875 }
2876 }
2877 }
2878 inside
2879}
2880
2881fn build_body(engine: &Engine, pieces: &[Piece]) -> Result<BrepSolid, KernelRefusal> {
2883 let clip = engine.clip;
2884 let mut profile: Vec<NurbsCurve> = Vec::new();
2885 let count = pieces.len();
2887 let mut start_points: Vec<Vec3> = pieces
2888 .iter()
2889 .map(|p| reduced_to_plane(engine, p.points[0].0, p.points[0].1))
2890 .collect();
2891 for (index, piece) in pieces.iter().enumerate() {
2894 if piece.level_segment && engine.class != Class::Turn {
2895 let (level, o1) = *piece.points.last().expect("nonempty");
2896 start_points[(index + 1) % count] = reduced_to_plane(engine, level, o1);
2897 }
2898 }
2899 for (index, piece) in pieces.iter().enumerate() {
2900 let start = start_points[index];
2901 let end = start_points[(index + 1) % count];
2902 if piece.level_segment {
2903 let (level, o0) = piece.points[0];
2904 let o1 = piece.points.last().expect("nonempty").1;
2905 let curve = match engine.class {
2906 Class::Turn => make_line(start, end),
2907 Class::Screw(c) => {
2908 let shift = clip.axial[0] / c;
2909 level_arc(engine, level, o0 + shift, o1 + shift)
2910 }
2911 Class::Planar => level_arc(engine, level, o0, o1),
2912 }
2913 .or_refuse(KernelStage::Fragment, "level_segment")?;
2914 profile.push(curve);
2915 continue;
2916 }
2917 let mut points: Vec<Vec3> = piece
2918 .points
2919 .iter()
2920 .map(|p| reduced_to_plane(engine, p.0, p.1))
2921 .collect();
2922 points[0] = start;
2923 let last = points.len() - 1;
2924 points[last] = end;
2925 profile.push(fit_piece(&points)?);
2926 }
2927 match engine.class {
2928 Class::Screw(c) => {
2929 let span = (clip.axial[1] - clip.axial[0]) / c; let per_turn = if engine.crest_gap { 12.0 } else { 64.0 };
2932 let rows = if engine.crest_gap && thread_rows_on() { (span.abs() / TAU / TURNS_PER_ROW).round().max(1.0) as usize } else { 1 };
2935 let span = span / rows as f64;
2936 let sections = ((span.abs() / TAU * per_turn).ceil() as usize).max(8) + 1;
2937 let mut lofted = Vec::with_capacity(sections);
2938 for k in 0..sections {
2939 let angle = span * k as f64 / (sections - 1) as f64;
2940 let lift = engine.frame.ez.scale(c * angle);
2941 let moved: Vec<NurbsCurve> = profile
2942 .iter()
2943 .map(|curve| transform_curve(curve, |x| rotate(x, clip.axis_origin, engine.frame.ez, angle).add(lift)))
2944 .collect::<Result<_, _>>()?;
2945 lofted.push(moved);
2946 }
2947 let mut body = loft_profile_brep(&lofted)?;
2948 if engine.crest_gap {
2949 carry_along_screw(&mut body, engine, c, span, sections - 1)?;
2950 }
2951 if rows > 1 {
2952 body = stack_screw_rows(&body, engine, c, span, sections - 1, rows)?;
2953 }
2954 Ok(body)
2955 }
2956 Class::Turn => {
2957 let angle = clip.angle.map(|a| a[1] - a[0]).unwrap_or(TAU);
2958 revolve_profile_brep(&profile, clip.axis_origin, engine.frame.ez, angle)
2959 .or_refuse(KernelStage::Fragment, "revolve")
2960 }
2961 Class::Planar => extrude_profile_brep(&profile, engine.frame.ez, clip.axial[1] - clip.axial[0]),
2962 }
2963}
2964
2965fn level_arc(engine: &Engine, level: f64, a0: f64, a1: f64) -> Result<NurbsCurve, String> {
2966 let centre = engine.frame.world([0.0, 0.0, match engine.class {
2967 Class::Screw(_) | Class::Planar => engine.clip.axial[0],
2968 Class::Turn => 0.0,
2969 }]);
2970 let (start, end) = if a1 >= a0 { (a0, a1) } else { (a1, a0) };
2971 let arc = make_arc(centre, engine.frame.ex, engine.frame.ey, level, start, end)?;
2972 if a1 >= a0 {
2973 Ok(arc)
2974 } else {
2975 arc.reversed()
2976 }
2977}
2978
2979fn transform_curve(curve: &NurbsCurve, map: impl Fn(Vec3) -> Vec3) -> Result<NurbsCurve, KernelRefusal> {
2980 let control_points = curve
2981 .control_points
2982 .iter()
2983 .map(|p| {
2984 let w = p.w;
2985 let x = map(Vec3::new(p.x / w, p.y / w, p.z / w));
2986 crate::Vec4 { x: x.x * w, y: x.y * w, z: x.z * w, w }
2987 })
2988 .collect();
2989 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points).or_refuse(KernelStage::Fragment, "curve_new")
2990}
2991
2992fn check_ends_clear(engine: &Engine) -> Result<(), KernelRefusal> {
2994 let clip = engine.clip;
2995 let mut samples: Vec<[f64; 3]> = Vec::new();
2997 for (index, source) in engine.sources.iter().enumerate() {
2998 if let Some(grid) = &engine.grids[index] {
2999 for i in 0..grid.nu {
3000 for j in 0..grid.nv {
3001 let (u, v) = Engine::grid_param(grid, i, j);
3002 samples.push(engine.frame.local(engine.source_point(*source, u, v)));
3003 }
3004 }
3005 }
3006 }
3007 let margin = 1e-6 * engine.scale;
3008 for t in [engine.motion.from, engine.motion.to] {
3009 for p in &samples {
3010 let x = engine.frame.local(engine.motion_apply(t, engine.frame.world(*p)));
3011 let level = x[0].hypot(x[1]);
3012 if level > clip.radius[1] + margin || level < clip.radius[0] - margin {
3013 continue;
3014 }
3015 if x[2] < clip.axial[0] - margin || x[2] > clip.axial[1] + margin {
3016 continue;
3017 }
3018 if let Some([a0, a1]) = clip.angle {
3019 let angle = x[1].atan2(x[0]);
3020 let mid = 0.5 * (a0 + a1);
3021 let rel = wrap_pi(angle - mid);
3022 if rel.abs() > 0.5 * (a1 - a0) + 1e-9 {
3023 continue;
3024 }
3025 }
3026 return Err(refuse(
3027 "ends_reach_clip",
3028 format!(
3029 "the tool reaches the clip region at the motion's {} (t = {t}); the sweep is \
3030 not invariant there, so the region would be wrong",
3031 if t == engine.motion.from { "start" } else { "end" }
3032 ),
3033 ));
3034 }
3035 }
3036 Ok(())
3037}
3038