1use crate::solid_sweep::outline_signed_distance;
50use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord};
51use crate::{make_line, make_revolution, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, NurbsSurface, OrRefuse, OutlinePiece, SolidMotion, Vec3, Vec4};
52use std::collections::HashMap;
53use std::f64::consts::{PI, TAU};
54
55#[derive(Clone, Debug)]
57pub enum SolidTool {
58 Polyhedron { vertices: Vec<Vec3>, faces: Vec<Vec<usize>> },
61 Revolved { axis_origin: Vec3, axis_direction: Vec3, radial: Vec3, meridian: Vec<OutlinePiece> },
66}
67
68const PCURVE_SAMPLES: usize = 576;
73
74pub const SOLID_SWEEP_WHOLE_REFUSAL: &str = "sweepSolid";
76
77fn refuse(what: &'static str, message: String) -> KernelRefusal {
78 KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
79}
80
81fn refused_as(error: &KernelRefusal) -> Option<&str> {
83 match &error.class {
84 crate::RefusalClass::UnsupportedGeometry { what } => Some(what.as_str()),
85 _ => None,
86 }
87}
88
89fn input(what: &'static str, message: String) -> KernelRefusal {
90 KernelRefusal::input(KernelStage::Collect, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
91}
92
93fn frag<T>(result: Result<T, String>, what: &'static str) -> Result<T, KernelRefusal> {
94 result.or_refuse(KernelStage::Fragment, what)
95}
96
97#[derive(Clone, Copy, Debug)]
100struct Screw {
101 a: Vec3,
102 e: Vec3,
103 omega: f64,
104 c: f64,
105 c2: f64,
106 t0: f64,
107 t1: f64,
108 frame: Option<Frame>,
112 turns: Option<Turns>,
114 frozen: Option<f64>,
116 extent: f64,
118}
119
120#[derive(Clone, Copy, Debug)]
123struct Turns {
124 points: [Vec3; 2],
125 directions: [Vec3; 2],
126 rates: [f64; 2],
127}
128
129impl Turns {
130 fn apply(&self, t: f64, x: Vec3) -> Vec3 {
131 let y = self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t));
132 self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t))
133 }
134 fn invert(&self, t: f64, y: Vec3) -> Vec3 {
135 let x = self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], -self.rates[1] * t));
136 self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], -self.rates[0] * t))
137 }
138 fn velocity(&self, t: f64, x: Vec3) -> Vec3 {
140 let first = x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t);
141 let y = self.points[0].add(first);
142 let moved = y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t);
143 let inner = self.directions[0].cross(first).scale(self.rates[0]);
144 let world = self.directions[1].cross(moved).scale(self.rates[1]).add(inner.rotate_about(self.directions[1], self.rates[1] * t));
145 world.rotate_about(self.directions[1], -self.rates[1] * t).rotate_about(self.directions[0], -self.rates[0] * t)
146 }
147}
148
149#[derive(Clone, Copy, Debug)]
152struct Frame {
153 control: [Vec3; 4],
154 up: Vec3,
155 start_tangent: Vec3,
157}
158
159impl Frame {
160 fn point(&self, s: f64) -> Vec3 {
161 let [p0, p1, p2, p3] = self.control;
162 let r = 1.0 - s;
163 p0.scale(r * r * r).add(p1.scale(3.0 * r * r * s)).add(p2.scale(3.0 * r * s * s)).add(p3.scale(s * s * s))
164 }
165 fn first(&self, s: f64) -> Vec3 {
166 let [p0, p1, p2, p3] = self.control;
167 let r = 1.0 - s;
168 p1.sub(p0).scale(3.0 * r * r).add(p2.sub(p1).scale(6.0 * r * s)).add(p3.sub(p2).scale(3.0 * s * s))
169 }
170 fn second(&self, s: f64) -> Vec3 {
171 let [p0, p1, p2, p3] = self.control;
172 p2.sub(p1.scale(2.0)).add(p0).scale(6.0 * (1.0 - s)).add(p3.sub(p2.scale(2.0)).add(p1).scale(6.0 * s))
173 }
174 fn angle(&self, s: f64) -> f64 {
176 let t = self.first(s);
177 self.up.dot(self.start_tangent.cross(t)).atan2(self.start_tangent.dot(t))
178 }
179 fn angle_rate(&self, s: f64) -> f64 {
180 let (d1, d2) = (self.first(s), self.second(s));
181 self.up.dot(d1.cross(d2)) / d1.dot(d1)
182 }
183}
184
185#[derive(Clone, Copy, Debug)]
190pub struct RisingTurn {
191 pub axis_origin: Vec3,
192 pub axis_direction: Vec3,
193 pub rise: [f64; 2],
194 pub from: f64,
195 pub to: f64,
196}
197
198impl Screw {
199 fn from_motion(motion: &SolidMotion) -> Result<Self, KernelRefusal> {
200 let [step] = motion.steps.as_slice() else {
201 return Err(refuse("motion_steps", "only a single screw, turn or slide is swept whole here".into()));
202 };
203 let e = step
204 .axis_direction
205 .normalized()
206 .map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
207 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
208 return Err(input("range", "the motion's range must run forward".into()));
209 }
210 if step.turn == 0.0 && step.advance == 0.0 {
211 return Err(input("still", "the motion does not move".into()));
212 }
213 Ok(Self { a: step.axis_origin, e, omega: step.turn, c: step.advance, c2: 0.0, t0: motion.from, t1: motion.to, frame: None, turns: None, frozen: None, extent: 0.0 })
214 }
215
216 fn from_rising(motion: &RisingTurn) -> Result<Self, KernelRefusal> {
217 let e = motion.axis_direction.normalized().map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
218 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
219 return Err(input("range", "the motion's range must run forward".into()));
220 }
221 let rate = |t: f64| motion.rise[0] + 2.0 * motion.rise[1] * t;
223 if rate(motion.from) * rate(motion.to) <= 0.0 {
224 return Err(refuse("rise_turns", "the rise stops or turns back within the motion".into()));
225 }
226 Ok(Self { a: motion.axis_origin, e, omega: 1.0, c: motion.rise[0], c2: motion.rise[1], t0: motion.from, t1: motion.to, frame: None, turns: None, frozen: None, extent: 0.0 })
227 }
228
229 fn advance(&self, t: f64) -> f64 {
230 self.c * t + self.c2 * t * t
231 }
232
233 fn rate_bound(&self) -> f64 {
235 (self.c + 2.0 * self.c2 * self.t0).abs().max((self.c + 2.0 * self.c2 * self.t1).abs())
236 }
237
238 fn apply(&self, t: f64, x: Vec3) -> Vec3 {
239 if let Some(turns) = &self.turns {
240 return turns.apply(t, x);
241 }
242 if let Some(frame) = &self.frame {
243 return frame.point(t).add(x.sub(self.a).rotate_about(frame.up, frame.angle(t)));
244 }
245 self.a.add(x.sub(self.a).rotate_about(self.e, self.omega * t)).add(self.e.scale(self.advance(t)))
246 }
247
248 fn invert(&self, t: f64, y: Vec3) -> Vec3 {
249 if let Some(turns) = &self.turns {
250 return turns.invert(t, y);
251 }
252 if let Some(frame) = &self.frame {
253 return self.a.add(y.sub(frame.point(t)).rotate_about(frame.up, -frame.angle(t)));
254 }
255 self.a.add(y.sub(self.e.scale(self.advance(t))).sub(self.a).rotate_about(self.e, -self.omega * t))
256 }
257
258 fn velocity(&self, x: Vec3) -> Vec3 {
262 self.velocity_at(self.frozen.unwrap_or(0.5 * (self.t0 + self.t1)), x)
263 }
264
265 fn velocity_at(&self, t: f64, x: Vec3) -> Vec3 {
266 if let Some(turns) = &self.turns {
267 return turns.velocity(t, x);
268 }
269 if let Some(frame) = &self.frame {
270 return frame.start_tangent.scale(frame.first(t).length()).add(frame.up.cross(x.sub(self.a)).scale(frame.angle_rate(t)));
273 }
274 self.e.cross(x.sub(self.a)).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t))
275 }
276
277 fn jet(&self, t: f64, x: Vec3) -> (Vec3, Vec3, Vec3) {
279 let r = x.sub(self.a).rotate_about(self.e, self.omega * t);
280 let axial = self.e.scale(r.dot(self.e));
281 let radial = r.sub(axial);
282 let value = self.a.add(r).add(self.e.scale(self.advance(t)));
283 let first = self.e.cross(radial).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t));
284 let second = radial.scale(-self.omega * self.omega).add(self.e.scale(2.0 * self.c2));
285 (value, first, second)
286 }
287
288 fn move_point(&self, t: f64, p: &Vec4) -> Result<Vec4, KernelRefusal> {
289 let x = frag(p.point(), "control_point")?;
290 Ok(Vec4::from_point(self.apply(t, x), p.w))
291 }
292
293 fn move_curve(&self, t: f64, curve: &NurbsCurve) -> Result<NurbsCurve, KernelRefusal> {
294 let points = curve.control_points.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?;
295 frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "moved_curve")
296 }
297
298 fn move_surface(&self, t: f64, surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
299 let mut rows = Vec::with_capacity(surface.control_points.len());
300 for row in &surface.control_points {
301 rows.push(row.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?);
302 }
303 frag(
304 NurbsSurface::new(surface.degree_u, surface.degree_v, surface.knots_u.clone(), surface.knots_v.clone(), rows),
305 "moved_surface",
306 )
307 }
308}
309
310fn row_times(screw: &Screw) -> Vec<f64> {
313 let turned = (screw.omega * (screw.t1 - screw.t0)).abs();
314 let rows = ((turned / (0.5 * PI)).ceil() as usize).max(1);
315 let at = |k: usize| match k {
319 0 => screw.t0,
320 k if k == rows => screw.t1,
321 k => screw.t0 + (screw.t1 - screw.t0) * (k as f64 - 0.381966011250105) / rows as f64,
322 };
323 (0..=rows).map(at).collect()
324}
325
326fn carry_segments(screw: &Screw, ta: f64, tb: f64) -> usize {
329 let turned = (screw.omega * (tb - ta)).abs();
330 ((turned / TAU * 48.0).ceil() as usize).max(4)
331}
332
333fn carry_knots(segments: usize) -> Vec<f64> {
335 let mut knots = vec![0.0; 6];
336 for k in 1..segments {
337 knots.extend([k as f64 / segments as f64; 3]);
338 }
339 knots.extend([1.0; 6]);
340 knots
341}
342
343fn carry_points(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Vec<Vec3> {
347 let h = (tb - ta) / segments as f64;
348 let at = |k: usize| screw.jet(ta + h * k as f64, x);
349 let piece = |k: usize| {
350 let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
351 [
352 f0,
353 f0.add(d0.scale(h / 5.0)),
354 f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
355 f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
356 f1.sub(d1.scale(h / 5.0)),
357 f1,
358 ]
359 };
360 let mut points = Vec::with_capacity(3 * segments + 3);
361 let first = piece(0);
362 points.extend_from_slice(&first[..3]);
363 for k in 0..segments - 1 {
364 let (here, next) = (piece(k), piece(k + 1));
365 points.push(here[3]);
366 points.push(here[4].scale(2.0).sub(here[3]));
367 points.push(next[2]);
368 }
369 let last = piece(segments - 1);
370 points.extend_from_slice(&last[3..]);
371 points
372}
373
374fn carry_curve(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Result<NurbsCurve, KernelRefusal> {
375 frag(
376 NurbsCurve::new(5, carry_knots(segments), carry_points(screw, x, ta, tb, segments).into_iter().map(|p| Vec4::from_point(p, 1.0)).collect()),
377 "carry_curve",
378 )
379}
380
381struct ToolCurve {
384 curve: NurbsCurve,
385 start: usize,
386 end: usize,
387 degenerate: bool,
388 samples: Option<Vec<Vec3>>,
392}
393
394const LOFT_SAMPLES: usize = 128;
396
397struct Cap {
401 surface: NurbsSurface,
402 front: bool,
403 coedges: Vec<(usize, bool, NurbsCurve)>,
404 chart: Option<Chart>,
407}
408
409#[derive(Clone, Copy, Debug)]
411enum Chart {
412 Plane { origin: Vec3, u: Vec3, v: Vec3 },
414 Revolution { origin: Vec3, w: Vec3, u1: Vec3, u2: Vec3, alpha: f64, theta: f64, spans_u: usize, meridian: MeridianChart },
417}
418
419#[derive(Clone, Copy, Debug)]
420enum MeridianChart {
421 Line { m0: [f64; 2], m1: [f64; 2] },
423 Arc { centre: [f64; 2], start: f64, sweep: f64, spans: usize },
426}
427
428impl Chart {
429 fn invert(&self, x: Vec3) -> [f64; 2] {
430 match *self {
431 Chart::Plane { origin, u, v } => {
432 let d = x.sub(origin);
433 [d.dot(u), d.dot(v)]
434 }
435 Chart::Revolution { origin, w, u1, u2, alpha, theta, spans_u, meridian } => {
436 let r = x.sub(origin);
437 let z = r.dot(w);
438 let rho = r.sub(w.scale(z)).length();
439 let mid = alpha + 0.5 * theta;
440 let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
441 let u = crate::circle_angle_to_parameter(spans_u, theta, psi - alpha);
442 let v = match meridian {
443 MeridianChart::Line { m0, m1 } => {
444 let (a, b) = (m1[0] - m0[0], m1[1] - m0[1]);
445 ((rho - m0[0]) * a + (z - m0[1]) * b) / (a * a + b * b)
446 }
447 MeridianChart::Arc { centre, start, sweep, spans } => {
448 let mid = start + 0.5 * sweep;
449 let angle = mid + ((z - centre[1]).atan2(rho - centre[0]) - mid + PI).rem_euclid(TAU) - PI;
450 crate::circle_angle_to_parameter(spans, sweep, angle - start)
451 }
452 };
453 [u, v]
454 }
455 }
456 }
457}
458
459fn spans_of(surface_knots_u: &[f64]) -> usize {
461 (surface_knots_u.len() - 6) / 2 + 1
462}
463
464struct Split {
466 points: Vec<Vec3>,
467 curves: Vec<ToolCurve>,
468 caps: Vec<Cap>,
469 scale: f64,
470 shape: Shape,
471}
472
473enum Shape {
475 Planes(Vec<(Vec3, f64)>),
476 Revolved { origin: Vec3, axis: Vec3, meridian: Vec<OutlinePiece> },
477}
478
479impl Split {
480 fn distance(&self, x: Vec3) -> f64 {
482 match &self.shape {
483 Shape::Planes(planes) => planes.iter().map(|(n, d)| n.dot(x) - d).fold(f64::NEG_INFINITY, f64::max),
484 Shape::Revolved { origin, axis, meridian } => {
485 let r = x.sub(*origin);
486 let z = r.dot(*axis);
487 let rho = r.sub(axis.scale(z)).length();
488 outline_signed_distance(meridian, [rho, z])
489 }
490 }
491 }
492
493 fn grazing(&self) -> Result<Vec<(usize, bool)>, KernelRefusal> {
497 let mut uses: HashMap<usize, Vec<(bool, bool)>> = HashMap::new();
498 for cap in &self.caps {
499 for &(curve, forward, _) in &cap.coedges {
500 uses.entry(curve).or_default().push((forward, cap.front));
501 }
502 }
503 let mut grazing = Vec::new();
504 for (curve, list) in uses {
505 if self.curves[curve].degenerate {
506 continue;
507 }
508 match list.as_slice() {
509 [(fa, front_a), (fb, front_b)] => {
510 if fa == fb {
511 return Err(refuse("split_topology", "two caps run one boundary curve the same way".into()));
512 }
513 if front_a != front_b {
514 grazing.push((curve, if *front_a { *fb } else { *fa }));
515 }
516 }
517 _ => return Err(refuse("split_topology", format!("a boundary curve is used {} times", list.len()))),
518 }
519 }
520 grazing.sort_unstable();
521 Ok(grazing)
522 }
523}
524
525fn uv_line(a: [f64; 2], b: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
526 frag(make_line(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0)), "pcurve")
527}
528
529fn plane_patch(origin: Vec3, u: Vec3, v: Vec3, points: &[Vec3]) -> Result<NurbsSurface, KernelRefusal> {
532 let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
533 for p in points {
534 let q = [p.sub(origin).dot(u), p.sub(origin).dot(v)];
535 for k in 0..2 {
536 lo[k] = lo[k].min(q[k]);
537 hi[k] = hi[k].max(q[k]);
538 }
539 }
540 let at = |s: f64, t: f64| Vec4::from_point(origin.add(u.scale(s)).add(v.scale(t)), 1.0);
541 frag(
542 NurbsSurface::new(
543 1,
544 1,
545 vec![lo[0], lo[0], hi[0], hi[0]],
546 vec![lo[1], lo[1], hi[1], hi[1]],
547 vec![vec![at(lo[0], lo[1]), at(lo[0], hi[1])], vec![at(hi[0], lo[1]), at(hi[0], hi[1])]],
548 ),
549 "plane_surface",
550 )
551}
552
553fn plane_pcurve(origin: Vec3, u: Vec3, v: Vec3, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
556 let mut points = Vec::with_capacity(curve.control_points.len());
557 for p in &curve.control_points {
558 let x = frag(p.point(), "control_point")?.sub(origin);
559 points.push(Vec4::from_point(Vec3::new(x.dot(u), x.dot(v), 0.0), p.w));
560 }
561 let pcurve = frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "plane_pcurve")?;
562 if forward { Ok(pcurve) } else { frag(pcurve.reversed(), "plane_pcurve") }
563}
564
565fn split_polyhedron(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<Split, KernelRefusal> {
566 if vertices.len() < 4 || faces.len() < 4 {
567 return Err(input("polyhedron", "a polyhedron needs at least four corners and four faces".into()));
568 }
569 let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &p| s.add(p)).scale(1.0 / vertices.len() as f64);
570 let scale = vertices.iter().map(|p| p.sub(centroid).length()).fold(0.0f64, f64::max).max(1e-300);
571 let tolerance = 1e-12 * scale.max(screw.a.sub(centroid).length());
572 let mut loops: Vec<Vec<usize>> = Vec::with_capacity(faces.len());
573 let mut planes = Vec::with_capacity(faces.len());
574 for face in faces {
575 if face.len() < 3 || face.iter().any(|&i| i >= vertices.len()) {
576 return Err(input("face", "a face needs three or more existing corners".into()));
577 }
578 let mut normal = Vec3::new(0.0, 0.0, 0.0);
579 for k in 0..face.len() {
580 let (p, q) = (vertices[face[k]], vertices[face[(k + 1) % face.len()]]);
581 normal = normal.add(p.sub(centroid).cross(q.sub(centroid)));
582 }
583 let mut normal = normal.normalized().map_err(|_| input("face", "a face has no area".into()))?;
584 let mut face = face.clone();
585 if normal.dot(vertices[face[0]].sub(centroid)) < 0.0 {
586 normal = normal.scale(-1.0);
587 face.reverse();
588 }
589 let offset = normal.dot(vertices[face[0]]);
590 for p in vertices {
591 if normal.dot(*p) - offset > 1e-9 * scale {
592 return Err(refuse("not_convex", "the polyhedron is not convex".into()));
593 }
594 }
595 for &i in &face {
596 if (normal.dot(vertices[i]) - offset).abs() > 1e-9 * scale {
597 return Err(input("face", "a face's corners are not in one plane".into()));
598 }
599 }
600 loops.push(face);
601 planes.push((normal, offset));
602 }
603 let g = |face: usize, x: Vec3| -> f64 {
605 let value = screw.velocity(x).dot(planes[face].0);
606 if value.abs() <= tolerance * screw.velocity(x).length().max(1.0) { 0.0 } else { value }
607 };
608 let mut owner: HashMap<(usize, usize), usize> = HashMap::new();
609 for (index, face) in loops.iter().enumerate() {
610 for k in 0..face.len() {
611 owner.insert((face[k], face[(k + 1) % face.len()]), index);
612 }
613 }
614 for &(i, j) in owner.keys() {
615 if !owner.contains_key(&(j, i)) {
616 return Err(input("closed", "the polyhedron's faces do not close".into()));
617 }
618 }
619 let mut points: Vec<Vec3> = vertices.to_vec();
621 let mut edge_splits: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
622 let mut keys: Vec<(usize, usize)> = owner.keys().copied().filter(|&(i, j)| i < j).collect();
623 keys.sort_unstable();
624 for (i, j) in keys {
625 let mut params: Vec<f64> = Vec::new();
626 for f in [owner[&(i, j)], owner[&(j, i)]] {
627 let (gi, gj) = (g(f, vertices[i]), g(f, vertices[j]));
628 if gi * gj < 0.0 {
629 params.push(gi / (gi - gj));
630 }
631 }
632 params.sort_by(|a, b| a.total_cmp(b));
633 params.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
634 let mut list = Vec::new();
635 for s in params {
636 points.push(vertices[i].add(vertices[j].sub(vertices[i]).scale(s)));
637 list.push(points.len() - 1);
638 }
639 edge_splits.insert((i, j), list);
640 }
641 let splits_along = |i: usize, j: usize| -> Vec<usize> {
642 if i < j {
643 edge_splits[&(i, j)].clone()
644 } else {
645 edge_splits[&(j, i)].iter().rev().copied().collect()
646 }
647 };
648 let mut rings: Vec<(Vec<usize>, usize, bool)> = Vec::new();
650 for (index, face) in loops.iter().enumerate() {
651 let mut ring: Vec<usize> = Vec::new();
652 for k in 0..face.len() {
653 ring.push(face[k]);
654 ring.extend(splits_along(face[k], face[(k + 1) % face.len()]));
655 }
656 let values: Vec<f64> = ring.iter().map(|&p| g(index, points[p])).collect();
657 let positive = values.iter().any(|&v| v > 0.0);
658 let negative = values.iter().any(|&v| v < 0.0);
659 if !positive && !negative {
660 return Err(refuse("face_in_own_plane", format!("face {index} moves in its own plane (its sweep is flat)")));
661 }
662 if !(positive && negative) {
663 rings.push((ring, index, positive));
664 continue;
665 }
666 let zeros: Vec<usize> = (0..ring.len()).filter(|&k| values[k] == 0.0).collect();
667 if zeros.len() != 2 {
668 return Err(refuse("grazing_split", format!("face {index} meets its grazing line {} times", zeros.len())));
669 }
670 let (za, zb) = (zeros[0], zeros[1]);
671 if za + 1 == zb || (zb + 1) % ring.len() == za {
672 return Err(refuse("grazing_split", format!("face {index} is split along one of its own edges")));
673 }
674 let first: Vec<usize> = (za..=zb).map(|k| ring[k]).collect();
675 let second: Vec<usize> = (zb..ring.len()).chain(0..=za).map(|k| ring[k]).collect();
676 let first_front = values[za + 1] > 0.0;
677 rings.push((first, index, first_front));
678 rings.push((second, index, !first_front));
679 }
680 if screw.c2 != 0.0 {
683 for (ring, face, front) in &rings {
684 for &p in ring {
685 for t in [screw.t0, screw.t1] {
686 let value = screw.velocity_at(t, points[p]).dot(planes[*face].0);
687 let slack = tolerance * screw.velocity_at(t, points[p]).length().max(1.0);
688 if (*front && value < -slack) || (!*front && value > slack) {
689 return Err(refuse("rising_sides", format!("face {face} changes side as the rise grows")));
690 }
691 }
692 }
693 }
694 }
695 let mut curves: Vec<ToolCurve> = Vec::new();
697 let mut by_pair: HashMap<(usize, usize), usize> = HashMap::new();
698 let mut caps = Vec::with_capacity(rings.len());
699 for (ring, face, front) in rings {
700 let normal = planes[face].0;
701 let origin = points[ring[0]];
702 let u = frag(points[ring[1]].sub(origin).normalized(), "plane_axis")?;
703 let v = normal.cross(u);
704 let corners: Vec<Vec3> = ring.iter().map(|&p| points[p]).collect();
705 let surface = plane_patch(origin, u, v, &corners)?;
706 let mut coedges = Vec::with_capacity(ring.len());
707 for k in 0..ring.len() {
708 let (p, q) = (ring[k], ring[(k + 1) % ring.len()]);
709 let key = (p.min(q), p.max(q));
710 let index = match by_pair.get(&key) {
711 Some(&index) => index,
712 None => {
713 curves.push(ToolCurve {
714 curve: frag(make_line(points[key.0], points[key.1]), "line")?,
715 start: key.0,
716 end: key.1,
717 degenerate: false,
718 samples: None,
719 });
720 by_pair.insert(key, curves.len() - 1);
721 curves.len() - 1
722 }
723 };
724 let forward = p == key.0;
725 coedges.push((index, forward, plane_pcurve(origin, u, v, &curves[index].curve, forward)?));
726 }
727 caps.push(Cap { surface, front, coedges, chart: Some(Chart::Plane { origin, u, v }) });
728 }
729 Ok(Split { points, curves, caps, scale, shape: Shape::Planes(planes) })
730}
731
732fn harmonic(g: impl Fn(f64) -> f64) -> (f64, f64, f64) {
735 let (g0, g90, g180) = (g(0.0), g(0.5 * PI), g(PI));
736 let c = 0.5 * (g0 + g180);
737 (0.5 * (g0 - g180), g90 - c, c)
738}
739
740fn piece_point(piece: &OutlinePiece, s: f64) -> [f64; 2] {
741 match *piece {
742 OutlinePiece::Line { from, to } => [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s],
743 OutlinePiece::Arc { centre, radius, start, end } => {
744 let angle = start + (end - start) * s;
745 [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
746 }
747 }
748}
749
750fn split_revolved(
751 screw: &Screw,
752 origin: Vec3,
753 axis: Vec3,
754 radial: Vec3,
755 meridian: &[OutlinePiece],
756) -> Result<Split, KernelRefusal> {
757 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
758 let u1 = radial.sub(w.scale(radial.dot(w))).normalized().map_err(|_| input("radial", "the radial direction lies along the axis".into()))?;
759 let u2 = w.cross(u1);
760 if let [OutlinePiece::Arc { centre, radius, start, end }] = meridian {
761 if ((end - start).abs() - TAU).abs() < 1e-12 && centre[0] > *radius {
762 return split_torus(screw, origin, w, u1, u2, *centre, *radius, meridian);
763 }
764 }
765 let at = |rho: f64, z: f64, psi: f64| origin.add(w.scale(z)).add(u1.scale(rho * psi.cos())).add(u2.scale(rho * psi.sin()));
766 let scale = meridian
767 .iter()
768 .flat_map(|p| [piece_point(p, 0.0), piece_point(p, 1.0)])
769 .map(|q| q[0].abs().max(q[1].abs()))
770 .fold(0.0f64, f64::max)
771 .max(1e-300);
772 let on_axis = |q: [f64; 2]| q[0].abs() <= 1e-12 * scale;
773 let mut area = 0.0;
775 for piece in meridian {
776 for k in 0..16 {
777 let (p, q) = (piece_point(piece, k as f64 / 16.0), piece_point(piece, (k + 1) as f64 / 16.0));
778 area += p[0] * q[1] - q[0] * p[1];
779 }
780 }
781 let reversed_meridian: Vec<OutlinePiece>;
783 let meridian: &[OutlinePiece] = if area > 0.0 {
784 meridian
785 } else {
786 reversed_meridian = meridian
787 .iter()
788 .rev()
789 .map(|piece| match *piece {
790 OutlinePiece::Line { from, to } => OutlinePiece::Line { from: to, to: from },
791 OutlinePiece::Arc { centre, radius, start, end } => OutlinePiece::Arc { centre, radius, start: end, end: start },
792 })
793 .collect();
794 &reversed_meridian
795 };
796 let ccw = true;
797 let normal = |piece: &OutlinePiece, s: f64| -> [f64; 2] {
799 let (p, q) = (piece_point(piece, (s - 1e-6).max(0.0)), piece_point(piece, (s + 1e-6).min(1.0)));
800 let (dx, dz) = (q[0] - p[0], q[1] - p[1]);
801 let length = dx.hypot(dz);
802 if ccw { [dz / length, -dx / length] } else { [-dz / length, dx / length] }
803 };
804 let g = |piece: &OutlinePiece, s: f64, psi: f64| -> f64 {
805 let m = piece_point(piece, s);
806 let n = normal(piece, s);
807 let x = at(m[0], m[1], psi);
808 let normal = w.scale(n[1]).add(u1.scale(n[0] * psi.cos())).add(u2.scale(n[0] * psi.sin()));
809 screw.velocity(x).dot(normal)
810 };
811 let faces: Vec<usize> = (0..meridian.len())
813 .filter(|&i| !(matches!(meridian[i], OutlinePiece::Line { .. }) && on_axis(piece_point(&meridian[i], 0.0)) && on_axis(piece_point(&meridian[i], 1.0))))
814 .collect();
815 enum Kind {
818 Ruled(Vec<f64>),
821 Moving,
824 Sphere { centre: Vec3, pole_at_start: bool },
827 Ball { centre: Vec3, radius: f64 },
830 TorusZone { psi_a: f64, psi_b: f64, at_end: bool },
835 }
836 let branches = |piece: &OutlinePiece, s: f64| -> Option<(f64, f64)> {
839 let reference = {
840 let (a, b, _) = harmonic(|psi| g(piece, 0.0, psi));
841 b.atan2(a)
842 };
843 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
844 let r = a.hypot(b);
845 if !(c.abs() < r * (1.0 - 1e-9)) {
846 return None;
847 }
848 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
849 let spread = (-c / r).acos();
850 Some((base - spread, base + spread))
851 };
852 let mut kinds: Vec<Kind> = Vec::with_capacity(faces.len());
853 for &i in &faces {
854 let piece = &meridian[i];
855 match *piece {
856 OutlinePiece::Line { .. } => {
857 let roots = |s: f64| -> Vec<f64> {
858 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
859 let r = a.hypot(b);
860 if c.abs() >= r * (1.0 - 1e-12) {
861 return Vec::new();
862 }
863 let base = b.atan2(a);
864 let spread = (-c / r).acos();
865 let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
866 list.sort_by(|x, y| x.total_cmp(y));
867 list
868 };
869 let alive = |s: f64| {
873 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
874 a.hypot(b).max(c.abs()) > 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound())
875 };
876 let readings: Vec<Vec<f64>> = [0.0, 0.5, 1.0].into_iter().filter(|&t| alive(t)).map(roots).collect();
877 if readings.is_empty() {
878 return Err(refuse("revolved_grazing", format!("meridian piece {i} grazes all over")));
879 }
880 let r0 = readings[0].clone();
881 let still = readings.iter().all(|r| r.len() == r0.len() && r.iter().zip(&r0).all(|(x, y)| (x - y).abs() < 1e-10));
882 if still {
883 kinds.push(Kind::Ruled(r0));
884 continue;
885 }
886 let mut previous: Option<(f64, f64)> = None;
887 for k in 0..=64 {
888 let Some(pair) = branches(piece, k as f64 / 64.0) else {
889 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} do not run rim to rim")));
890 };
891 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
892 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} turn too fast")));
893 }
894 previous = Some(pair);
895 }
896 kinds.push(Kind::Moving);
897 }
898 OutlinePiece::Arc { centre, radius, start, end } => {
899 let (p0, p1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
900 let span = (end - start).abs();
901 let centre_on_axis = on_axis(centre);
902 let pole_at_start = on_axis(p0);
903 let equator = |p: [f64; 2]| (p[1] - centre[1]).abs() <= 1e-12 * scale && (p[0] - radius).abs() <= 1e-12 * scale;
904 if centre_on_axis && (span - PI).abs() < 1e-12 && pole_at_start && on_axis(p1) {
905 kinds.push(Kind::Ball { centre: origin.add(w.scale(centre[1])), radius });
906 continue;
907 }
908 if centre[0] > radius * (1.0 + 1e-9) && end > start && span < PI {
909 let at_rim = |v: f64| [centre[0] + radius * v.cos(), centre[1] + radius * v.sin()];
910 let g_tube = |psi: f64, v: f64| -> f64 {
911 let m = at_rim(v);
912 let x = at(m[0], m[1], psi);
913 let normal = w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
914 screw.velocity(x).dot(normal)
915 };
916 let in_zone = |psi: f64| -> Result<Option<f64>, KernelRefusal> {
917 let (a, b, c) = harmonic(|v| g_tube(psi, v));
918 let r = a.hypot(b);
919 if !(c.abs() < r) {
920 return Ok(None);
921 }
922 let base = b.atan2(a);
923 let spread = (-c / r).acos();
924 let inside: Vec<f64> = [base - spread, base + spread]
925 .into_iter()
926 .map(|v| start + (v - start).rem_euclid(TAU))
927 .filter(|&v| v <= end)
928 .collect();
929 match inside.as_slice() {
930 [] => Ok(None),
931 [v] => Ok(Some(*v)),
932 _ => Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner twice at one angle"))),
933 }
934 };
935 let count = 720;
936 let has: Vec<bool> = (0..count).map(|k| in_zone(TAU * k as f64 / count as f64).map(|r| r.is_some())).collect::<Result<_, _>>()?;
937 let rises: Vec<usize> = (0..count).filter(|&k| has[k] && !has[(k + count - 1) % count]).collect();
938 let falls: Vec<usize> = (0..count).filter(|&k| !has[k] && has[(k + count - 1) % count]).collect();
939 let ([rise], [fall]) = (rises.as_slice(), falls.as_slice()) else {
940 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve does not cross the rounded corner once")));
941 };
942 let estimate = |k: usize| TAU * (k as f64 - 0.5) / count as f64;
944 let (ea, eb) = (estimate(*rise), estimate(*fall));
945 let sample = |k: usize| TAU * (k % count) as f64 / count as f64;
946 let probe_v = in_zone(sample(*rise))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
947 let at_end = (probe_v - end).abs() < (probe_v - start).abs();
948 let rim_v = if at_end { end } else { start };
949 let (a, b, c) = harmonic(|psi| g_tube(psi, rim_v));
950 let r = a.hypot(b);
951 let base = b.atan2(a);
952 let spread = (-c / r).acos();
953 let roots = [base - spread, base + spread];
954 let nearest = |e: f64| roots.iter().copied().min_by(|x, y| ((x - e + PI).rem_euclid(TAU) - PI).abs().total_cmp(&((y - e + PI).rem_euclid(TAU) - PI).abs())).expect("two");
955 let (psi_a, psi_b) = (nearest(ea).rem_euclid(TAU), nearest(eb).rem_euclid(TAU));
956 let psi_b = if psi_b <= psi_a { psi_b + TAU } else { psi_b };
957 let leave_v = in_zone(sample(*fall + count - 1))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
958 if ((leave_v - end).abs() < (leave_v - start).abs()) != at_end {
959 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner from rim to rim")));
960 }
961 kinds.push(Kind::TorusZone { psi_a, psi_b, at_end });
962 continue;
963 }
964 if !(centre_on_axis && (span - 0.5 * PI).abs() < 1e-12 && (pole_at_start && equator(p1) || on_axis(p1) && equator(p0))) {
965 return Err(refuse("revolved_arc", format!("meridian piece {i}: only a quarter circle from a pole to an equator is swept here")));
966 }
967 kinds.push(Kind::Sphere { centre: origin.add(w.scale(centre[1])), pole_at_start });
968 }
969 }
970 }
971 let count = meridian.len();
973 let junction = |k: usize| piece_point(&meridian[k], 1.0); let face_of = |piece: usize| faces.iter().position(|&f| f == piece);
975 let mut rim_angles: HashMap<usize, Vec<f64>> = HashMap::new();
976 for k in 0..count {
977 if on_axis(junction(k)) {
978 continue;
979 }
980 let mut angles: Vec<f64> = Vec::new();
981 for (piece, s_end) in [(k, 1.0), ((k + 1) % count, 0.0)] {
982 let Some(f) = face_of(piece) else { continue };
983 match &kinds[f] {
984 Kind::Ruled(roots) => angles.extend(roots.iter().copied()),
985 Kind::Ball { .. } => {}
986 Kind::TorusZone { psi_a, psi_b, .. } => angles.extend([psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)]),
987 Kind::Moving => {
988 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
989 angles.extend([lower.rem_euclid(TAU), upper.rem_euclid(TAU)]);
990 }
991 Kind::Sphere { centre, .. } => {
992 let d = screw.velocity(*centre).cross(w);
993 let base = d.dot(u2).atan2(d.dot(u1)).rem_euclid(TAU);
994 angles.extend([base, (base + PI).rem_euclid(TAU)]);
995 }
996 }
997 }
998 angles.sort_by(|a, b| a.total_cmp(b));
999 let mut merged: Vec<f64> = Vec::new();
1000 for a in angles {
1001 if merged.last().is_some_and(|&b| (a - b).abs() < 1e-9) || merged.first().is_some_and(|&b| (b + TAU - a).abs() < 1e-9) {
1002 continue;
1003 }
1004 merged.push(a);
1005 }
1006 rim_angles.insert(k, merged);
1007 }
1008 for (f, &piece) in faces.iter().enumerate() {
1010 for (k, s_end) in [((piece + count - 1) % count, 0.0), (piece, 1.0)] {
1011 let own: Vec<f64> = match &kinds[f] {
1012 Kind::Ruled(roots) => roots.clone(),
1013 Kind::Moving => {
1014 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
1015 let mut list = vec![lower.rem_euclid(TAU), upper.rem_euclid(TAU)];
1016 list.sort_by(|a, b| a.total_cmp(b));
1017 list
1018 }
1019 Kind::TorusZone { psi_a, psi_b, .. } => {
1020 let mut list = vec![psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)];
1021 list.sort_by(|a, b| a.total_cmp(b));
1022 list
1023 }
1024 Kind::Sphere { .. } | Kind::Ball { .. } => continue,
1025 };
1026 if let Some(angles) = rim_angles.get(&k) {
1027 if own.is_empty() {
1028 continue;
1029 }
1030 if angles.len() != own.len() || angles.iter().zip(&own).any(|(a, b)| (a - b).abs() > 1e-9) {
1031 return Err(refuse("revolved_rims", format!("meridian piece {piece} splits at other angles than its rim")));
1032 }
1033 }
1034 }
1035 }
1036 let mut points: Vec<Vec3> = Vec::new();
1037 let mut curves: Vec<ToolCurve> = Vec::new();
1038 let mut caps: Vec<Cap> = Vec::new();
1039 let mut rim_points: HashMap<(usize, usize), usize> = HashMap::new();
1041 let mut rim_arcs: HashMap<(usize, usize), usize> = HashMap::new();
1042 let mut rim_keys: Vec<usize> = rim_angles.keys().copied().collect();
1043 rim_keys.sort_unstable();
1044 for &k in &rim_keys {
1045 let q = junction(k);
1046 let angles = rim_angles[&k].clone();
1047 let angles = if angles.is_empty() { vec![0.0] } else { angles };
1048 for (j, &a) in angles.iter().enumerate() {
1049 points.push(at(q[0], q[1], a));
1050 rim_points.insert((k, j), points.len() - 1);
1051 }
1052 for j in 0..angles.len() {
1053 let a = angles[j];
1054 let b = if j + 1 < angles.len() { angles[j + 1] } else { angles[0] + TAU };
1055 let base = at(q[0], q[1], a);
1056 let surface = frag(make_revolution(origin, w, &frag(make_line(base, base.add(w)), "line")?, b - a), "rim")?;
1057 curves.push(ToolCurve {
1058 curve: frag(surface.iso_curve_v(0.0), "rim_arc")?,
1059 start: rim_points[&(k, j)],
1060 end: rim_points[&(k, (j + 1) % angles.len())],
1061 degenerate: false,
1062 samples: Some((0..=LOFT_SAMPLES).map(|i| at(q[0], q[1], a + (b - a) * i as f64 / LOFT_SAMPLES as f64)).collect()),
1063 });
1064 rim_arcs.insert((k, j), curves.len() - 1);
1065 }
1066 rim_angles.insert(k, angles);
1067 }
1068 let arc_index = |k: usize, angle: f64| -> Result<usize, KernelRefusal> {
1069 rim_angles[&k]
1070 .iter()
1071 .position(|&a| ((a - angle).rem_euclid(TAU)).min((angle - a).rem_euclid(TAU)) < 1e-9)
1072 .ok_or_else(|| refuse("revolved_rims", "a patch corner is not on its rim's split".into()))
1073 };
1074 let sign_at = |piece: &OutlinePiece, psi: f64| g(piece, 0.5, psi) > 0.0;
1075 for (f, &piece_index) in faces.iter().enumerate() {
1076 let piece = &meridian[piece_index];
1077 let below = (piece_index + count - 1) % count; let above = piece_index; let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
1080 match &kinds[f] {
1081 Kind::Ruled(roots) if roots.is_empty() => {
1082 let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
1084 if !flat || !(on_axis(m0) || on_axis(m1)) {
1085 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a whole side or ring is not swept here")));
1086 }
1087 let rim = if on_axis(m0) { above } else { below };
1088 let front = sign_at(piece, 0.0);
1089 if (0..64).any(|k| sign_at(piece, TAU * k as f64 / 64.0) != front) {
1090 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a flat end meets its grazing line")));
1091 }
1092 let n = normal(piece, 0.5);
1093 let outward = w.scale(n[1].signum());
1094 let centre = origin.add(w.scale(m0[1]));
1095 let (u, v) = (u1, outward.cross(u1));
1096 let arcs = rim_angles[&rim].len();
1097 let mut coedges = Vec::with_capacity(arcs);
1098 let mut corners = Vec::new();
1099 let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
1101 for j in order {
1102 let c = rim_arcs[&(rim, j)];
1103 let forward = n[1] > 0.0;
1104 coedges.push((c, forward, plane_pcurve(centre, u, v, &curves[c].curve, forward)?));
1105 corners.push(points[curves[c].start]);
1106 }
1107 let r = junction(rim)[0];
1108 corners.extend([centre.add(u.scale(r)).add(v.scale(r)), centre.sub(u.scale(r)).sub(v.scale(r))]);
1109 caps.push(Cap { surface: plane_patch(centre, u, v, &corners)?, front, coedges, chart: Some(Chart::Plane { origin: centre, u, v }) });
1110 }
1111 Kind::Ruled(roots) => {
1112 if on_axis(m0) || on_axis(m1) {
1113 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
1114 }
1115 let mut generators = Vec::with_capacity(roots.len());
1117 for &root in roots {
1118 let (a, b) = (rim_points[&(below, arc_index(below, root)?)], rim_points[&(above, arc_index(above, root)?)]);
1119 let line_samples = (0..=LOFT_SAMPLES).map(|i| points[a].add(points[b].sub(points[a]).scale(i as f64 / LOFT_SAMPLES as f64))).collect();
1120 curves.push(ToolCurve { curve: frag(make_line(points[a], points[b]), "generator")?, start: a, end: b, degenerate: false, samples: Some(line_samples) });
1121 generators.push(curves.len() - 1);
1122 }
1123 for j in 0..roots.len() {
1124 let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1125 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1126 let surface = frag(make_revolution(origin, w, &generatrix, beta - alpha), "patch")?;
1127 let front = sign_at(piece, 0.5 * (alpha + beta));
1128 let (lower, upper) = (rim_arcs[&(below, arc_index(below, alpha)?)], rim_arcs[&(above, arc_index(above, alpha)?)]);
1129 let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1130 let coedges = vec![
1131 (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1132 (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1133 (upper, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1134 (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1135 ];
1136 let chart = Chart::Revolution { origin, w, u1, u2, alpha, theta: beta - alpha, spans_u: spans_of(&surface.knots_u), meridian: MeridianChart::Line { m0, m1 } };
1137 caps.push(Cap { surface, front, coedges, chart: Some(chart) });
1138 }
1139 }
1140 Kind::Moving => {
1141 if on_axis(m0) || on_axis(m1) {
1142 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
1143 }
1144 let n = 128;
1145 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1146 let pairs: Vec<(f64, f64)> = params.iter().map(|&t| branches(piece, t).expect("checked")).collect();
1147 let pick = |pair: (f64, f64), which: usize| if which == 0 { pair.0 } else { pair.1 };
1148 let mut branch_curves = [0usize; 2];
1150 for which in 0..2 {
1151 let (psi0, psi1) = (pick(pairs[0], which), pick(pairs[n], which));
1152 let start = rim_points[&(below, arc_index(below, psi0)?)];
1153 let end = rim_points[&(above, arc_index(above, psi1)?)];
1154 let mut samples: Vec<Vec3> = params
1155 .iter()
1156 .zip(&pairs)
1157 .map(|(&t, &pair)| {
1158 let m = piece_point(piece, t);
1159 at(m[0], m[1], pick(pair, which))
1160 })
1161 .collect();
1162 samples[0] = points[start];
1163 samples[n] = points[end];
1164 let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES)
1165 .map(|i| {
1166 let t = i as f64 / LOFT_SAMPLES as f64;
1167 let m = piece_point(piece, t);
1168 at(m[0], m[1], pick(branches(piece, t).expect("checked"), which))
1169 })
1170 .collect();
1171 exact[0] = points[start];
1172 exact[LOFT_SAMPLES] = points[end];
1173 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&samples, 5, ¶ms), "branch")?, start, end, degenerate: false, samples: Some(exact) });
1174 branch_curves[which] = curves.len() - 1;
1175 }
1176 for (lo_which, hi_which, hi_turn) in [(0usize, 1usize, 0.0), (1, 0, TAU)] {
1178 let lo = |i: usize| pick(pairs[i], lo_which);
1179 let hi = |i: usize| pick(pairs[i], hi_which) + hi_turn;
1180 let alpha = (0..=n).map(lo).fold(f64::INFINITY, f64::min) - 1e-3;
1181 let theta = (0..=n).map(hi).fold(f64::NEG_INFINITY, f64::max) + 1e-3 - alpha;
1182 if theta >= TAU {
1183 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a side patch wraps all the way round")));
1184 }
1185 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1186 let surface = frag(make_revolution(origin, w, &generatrix, theta), "patch")?;
1187 let spans = (surface.knots_u.len() - 6) / 2 + 1;
1188 let u_of = |psi: f64| crate::circle_angle_to_parameter(spans, theta, psi - alpha);
1189 let angle_near = |x: Vec3, guess: f64| -> f64 {
1190 let r = x.sub(origin);
1191 let a = r.dot(u2).atan2(r.dot(u1));
1192 guess + (a - guess + PI).rem_euclid(TAU) - PI
1193 };
1194 let front = sign_at(piece, 0.5 * (lo(n / 2) + hi(n / 2)));
1195 let rim_pcurve = |arc: usize, v: f64| -> Result<NurbsCurve, KernelRefusal> {
1197 let curve = &curves[arc].curve;
1198 let [d0, d1] = frag(curve.domain(), "domain")?;
1199 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1200 let mut uv = Vec::with_capacity(ts.len());
1201 for &t in &ts {
1202 let x = frag(curve.evaluate(t), "rim")?;
1203 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), v, 0.0));
1204 }
1205 frag(crate::interpolate_curve(&uv, 5, &ts), "rim_pcurve")
1206 };
1207 let branch_pcurve = |which: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> {
1211 let curve = &curves[branch_curves[which]].curve;
1212 let [d0, d1] = frag(curve.domain(), "domain")?;
1213 let (span_rho, span_z) = (m1[0] - m0[0], m1[1] - m0[1]);
1214 let length2 = span_rho * span_rho + span_z * span_z;
1215 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1216 let mut uv = Vec::with_capacity(ts.len());
1217 for &t in &ts {
1218 let x = frag(curve.evaluate(t), "branch")?;
1219 let r = x.sub(origin);
1220 let z = r.dot(w);
1221 let rho = r.sub(w.scale(z)).length();
1222 let along = ((rho - m0[0]) * span_rho + (z - m0[1]) * span_z) / length2;
1223 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), along, 0.0));
1224 }
1225 frag(crate::interpolate_curve(&uv, 5, &ts), "branch_pcurve")
1226 };
1227 let bottom = rim_arcs[&(below, arc_index(below, lo(0))?)];
1228 let top = rim_arcs[&(above, arc_index(above, lo(n))?)];
1229 let coedges = vec![
1230 (bottom, true, rim_pcurve(bottom, 0.0)?),
1231 (branch_curves[hi_which], true, branch_pcurve(hi_which, hi_turn)?),
1232 (top, false, frag(rim_pcurve(top, 1.0)?.reversed(), "reversed")?),
1233 (branch_curves[lo_which], false, frag(branch_pcurve(lo_which, 0.0)?.reversed(), "reversed")?),
1234 ];
1235 let chart_spans = spans_of(&surface.knots_u);
1236 caps.push(Cap { surface, front, coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha, theta, spans_u: chart_spans, meridian: MeridianChart::Line { m0, m1 } }) });
1237 }
1238 }
1239 Kind::TorusZone { psi_a, psi_b, at_end } => {
1240 let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1241 let (psi_a, psi_b, at_end) = (*psi_a, *psi_b, *at_end);
1242 let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1243 let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1244 let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1245 let normal_at = |psi: f64, v: f64| w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
1246 let root = |psi: f64| -> Option<f64> {
1247 let (a, b, c) = harmonic(|v| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)));
1248 let r = a.hypot(b);
1249 if !(c.abs() < r) {
1250 return None;
1251 }
1252 let base = b.atan2(a);
1253 let spread = (-c / r).acos();
1254 [base - spread, base + spread].into_iter().map(|v| start + (v - start).rem_euclid(TAU)).find(|&v| v <= end + 1e-12)
1255 };
1256 let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1258 let from = tube_point(psi, start);
1259 let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1260 frag(surface.iso_curve_v(0.0), "tube")
1261 };
1262 let (rim_k, other_k) = if at_end { (above, below) } else { (below, above) };
1263 let ka = rim_points[&(rim_k, arc_index(rim_k, psi_a)?)];
1264 let kb = rim_points[&(rim_k, arc_index(rim_k, psi_b)?)];
1265 let n = 256;
1267 let params: Vec<f64> = (0..=n).map(|j| j as f64 / n as f64).collect();
1268 let psi_of = |t: f64| psi_a + (psi_b - psi_a) * 0.5 * (1.0 - (PI * t).cos());
1272 let mut xyz = Vec::with_capacity(n + 1);
1273 for j in 0..=n {
1274 let psi = psi_of(params[j]);
1275 let v = if j == 0 || j == n { if at_end { end } else { start } } else {
1276 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1277 };
1278 xyz.push(tube_point(psi, v));
1279 }
1280 xyz[0] = points[ka];
1281 xyz[n] = points[kb];
1282 let mut exact = Vec::with_capacity(LOFT_SAMPLES + 1);
1283 for i in 0..=LOFT_SAMPLES {
1284 let psi = psi_of(i as f64 / LOFT_SAMPLES as f64);
1285 let v = if i == 0 || i == LOFT_SAMPLES { if at_end { end } else { start } } else {
1286 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1287 };
1288 exact.push(tube_point(psi, v));
1289 }
1290 exact[0] = points[ka];
1291 exact[LOFT_SAMPLES] = points[kb];
1292 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "torus_k")?, start: ka, end: kb, degenerate: false, samples: Some(exact) });
1293 let k_curve = curves.len() - 1;
1294 let mut cut = [0usize; 2];
1296 for (slot, psi) in [(0, psi_a), (1, psi_b)] {
1297 let (lo, hi) = (rim_points[&(below, arc_index(below, psi)?)], rim_points[&(above, arc_index(above, psi)?)]);
1298 curves.push(ToolCurve { curve: generatrix(psi)?, start: lo, end: hi, degenerate: false, samples: None });
1299 cut[slot] = curves.len() - 1;
1300 }
1301 let label = |psi: f64, v: f64| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)) > 0.0;
1302 let patch = |alpha: f64, beta: f64| -> Result<(NurbsSurface, usize, usize), KernelRefusal> {
1304 let g = generatrix(alpha)?;
1305 let surface = frag(make_revolution(origin, w, &g, beta - alpha), "torus_patch")?;
1306 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1307 let spans_v = (g.knots.len() - 6) / 2 + 1;
1308 Ok((surface, spans_u, spans_v))
1309 };
1310 let inverse_pcurve = |curve: &NurbsCurve, alpha: f64, beta: f64, spans_u: usize, spans_v: usize, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
1311 let [d0, d1] = frag(curve.domain(), "domain")?;
1312 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1313 let mut uv = Vec::with_capacity(ts.len());
1314 for &t in &ts {
1315 let x = frag(curve.evaluate(t), "point")?;
1316 let r = x.sub(origin);
1317 let mid = 0.5 * (alpha + beta);
1318 let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
1319 let q = x.sub(tube_centre(psi));
1320 let radial = u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1321 let vmid = 0.5 * (start + end);
1322 let v = vmid + (q.dot(w).atan2(q.dot(radial)) - vmid + PI).rem_euclid(TAU) - PI;
1323 uv.push(Vec3::new(
1324 crate::circle_angle_to_parameter(spans_u, beta - alpha, psi - alpha),
1325 crate::circle_angle_to_parameter(spans_v, end - start, v - start),
1326 0.0,
1327 ));
1328 }
1329 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "torus_pcurve")?;
1330 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
1331 };
1332 {
1334 let (surface, _, band_spans_v) = patch(psi_b, psi_a + TAU)?;
1335 let (bottom, top) = (rim_arcs[&(below, arc_index(below, psi_b)?)], rim_arcs[&(above, arc_index(above, psi_b)?)]);
1336 let coedges = vec![
1337 (bottom, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1338 (cut[0], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1339 (top, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1340 (cut[1], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1341 ];
1342 let mid = psi_b + 0.5 * (psi_a + TAU - psi_b);
1343 let chart_spans = spans_of(&surface.knots_u);
1344 caps.push(Cap { surface, front: label(mid, 0.5 * (start + end)), coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_b, theta: psi_a + TAU - psi_b, spans_u: chart_spans, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: band_spans_v } }) });
1345 }
1346 let (surface, spans_u, spans_v) = patch(psi_a, psi_b)?;
1348 let rim_arc = rim_arcs[&(rim_k, arc_index(rim_k, psi_a)?)];
1349 let other_arc = rim_arcs[&(other_k, arc_index(other_k, psi_a)?)];
1350 let mid = 0.5 * (psi_a + psi_b);
1351 let v_k = root(mid).ok_or_else(|| refuse("torus_zone", "no grazing point mid-way".into()))?;
1352 let k_pc = |forward: bool| inverse_pcurve(&curves[k_curve].curve, psi_a, psi_b, spans_u, spans_v, forward);
1353 let lens_v = if at_end { 0.5 * (v_k + end) } else { 0.5 * (v_k + start) };
1354 let rest_v = if at_end { 0.5 * (v_k + start) } else { 0.5 * (v_k + end) };
1355 if at_end {
1356 let lens = vec![(k_curve, true, k_pc(true)?), (rim_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?)];
1358 caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1359 let rest = vec![
1360 (other_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1361 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1362 (k_curve, false, k_pc(false)?),
1363 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1364 ];
1365 caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1366 } else {
1367 let lens = vec![(rim_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?), (k_curve, false, k_pc(false)?)];
1368 caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1369 let rest = vec![
1370 (k_curve, true, k_pc(true)?),
1371 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1372 (other_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1373 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1374 ];
1375 caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1376 }
1377 }
1378 Kind::Ball { centre, radius } => {
1379 let m = frag(screw.velocity(*centre).normalized(), "ball_still")?;
1380 let d = frag(w.cross(m).normalized().or_else(|_| u1.cross(m).normalized()), "ball_axis")?;
1381 let (e1, e2) = (centre.add(d.scale(*radius)), centre.sub(d.scale(*radius)));
1382 points.push(e1);
1383 points.push(e2);
1384 let (i1, i2) = (points.len() - 2, points.len() - 1);
1385 let half = frag(make_revolution(*centre, m, &frag(make_line(e1, e1.add(m)), "line")?, PI), "great")?;
1386 let k_a = frag(half.iso_curve_v(0.0), "great")?;
1387 curves.push(ToolCurve { curve: k_a.clone(), start: i1, end: i2, degenerate: false, samples: None });
1388 let k_a_index = curves.len() - 1;
1389 let [a0, a1] = frag(k_a.domain(), "domain")?;
1390 let h = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?.sub(*centre);
1391 let mut k_b_index: Option<usize> = None;
1392 for front in [true, false] {
1393 let toward = if front { m } else { m.scale(-1.0) };
1394 let da = if d.cross(h).dot(toward) > 0.0 { d } else { d.scale(-1.0) };
1395 let mut generatrix = k_a.clone();
1396 let mut surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1397 let outward = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1398 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1399 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1400 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1401 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1402 };
1403 let mut forward = true;
1404 if !outward(&surface)? {
1405 generatrix = frag(generatrix.reversed(), "reversed")?;
1406 surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1407 forward = false;
1408 }
1409 let (start, end) = if forward { (i1, i2) } else { (i2, i1) };
1410 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1411 let k_b = match k_b_index {
1412 Some(index) => index,
1413 None => {
1414 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1415 k_b_index = Some(curves.len() - 1);
1416 curves.len() - 1
1417 }
1418 };
1419 let k_b_forward = curves[k_b].start == start;
1420 let k_b_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1421 let poles = [start, end].map(|p| {
1422 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1423 curves.len() - 1
1424 });
1425 let coedges = vec![
1426 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1427 (k_b, k_b_forward, k_b_pcurve),
1428 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1429 (k_a_index, !forward, uv_line([0.0, g1], [0.0, g0])?),
1430 ];
1431 caps.push(Cap { surface, front, coedges, chart: None });
1432 }
1433 }
1434 Kind::Sphere { centre, pole_at_start } => {
1435 let rim = if *pole_at_start { above } else { below };
1436 let angles = rim_angles[&rim].clone();
1437 if angles.len() != 2 {
1438 return Err(refuse("revolved_rims", "a hemisphere's rim is not split in two".into()));
1439 }
1440 let pole_side = w.scale(if *pole_at_start { m0[1] - m1[1] } else { m1[1] - m0[1] }.signum());
1441 let m = screw.velocity(*centre);
1442 let e1 = points[rim_points[&(rim, 0)]];
1443 let d = frag(e1.sub(*centre).normalized(), "lune_axis")?;
1444 let mut great: Option<usize> = None;
1445 for j in 0..2 {
1446 let arc = rim_arcs[&(rim, j)];
1447 let [a0, a1] = frag(curves[arc].curve.domain(), "domain")?;
1448 let middle = frag(curves[arc].curve.evaluate(0.5 * (a0 + a1)), "middle")?;
1449 let hv = middle.sub(*centre);
1450 let da = if d.cross(hv).dot(pole_side) > 0.0 { d } else { d.scale(-1.0) };
1451 let theta = (-m.dot(hv)).atan2(m.dot(da.cross(hv))).rem_euclid(TAU);
1453 let theta = if theta > PI { theta - PI } else { theta };
1454 if !(theta > 1e-9 && theta < PI - 1e-9) {
1455 return Err(refuse("revolved_lune", "the great circle meets the rim's plane edge-on".into()));
1456 }
1457 let mut generatrix = curves[arc].curve.clone();
1458 let mut surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1459 let probe = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1461 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1462 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1463 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1464 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1465 };
1466 let mut forward = true;
1467 if !probe(&surface)? {
1468 generatrix = frag(generatrix.reversed(), "reversed")?;
1469 surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1470 forward = false;
1471 }
1472 let (start, end) = if forward { (curves[arc].start, curves[arc].end) } else { (curves[arc].end, curves[arc].start) };
1473 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1474 let k_curve = match great {
1477 Some(index) => index,
1478 None => {
1479 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1480 great = Some(curves.len() - 1);
1481 curves.len() - 1
1482 }
1483 };
1484 let k_forward = curves[k_curve].start == start;
1485 let poles = [start, end].map(|p| {
1486 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1487 curves.len() - 1
1488 });
1489 let k_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1490 let coedges = vec![
1491 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1492 (k_curve, k_forward, k_pcurve),
1493 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1494 (arc, !forward, uv_line([0.0, g1], [0.0, g0])?),
1495 ];
1496 let mid_psi = if j == 0 { 0.5 * (angles[0] + angles[1]) } else { 0.5 * (angles[1] + angles[0] + TAU) };
1497 let front = m.dot(at(1.0, 0.0, mid_psi).sub(origin)) > 0.0;
1498 caps.push(Cap { surface, front, coedges, chart: None });
1499 }
1500 }
1501 }
1502 }
1503 Ok(Split {
1504 points,
1505 curves,
1506 caps,
1507 scale,
1508 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1509 })
1510}
1511
1512#[allow(clippy::too_many_arguments)]
1519fn split_torus(
1520 screw: &Screw,
1521 origin: Vec3,
1522 w: Vec3,
1523 u1: Vec3,
1524 u2: Vec3,
1525 centre: [f64; 2],
1526 radius: f64,
1527 meridian: &[OutlinePiece],
1528) -> Result<Split, KernelRefusal> {
1529 let (rho_c, z_c) = (centre[0], centre[1]);
1530 let radial = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1531 let tube_centre = |psi: f64| origin.add(w.scale(z_c)).add(radial(psi).scale(rho_c));
1532 let point = |psi: f64, v: f64| tube_centre(psi).add(radial(psi).scale(radius * v.cos())).add(w.scale(radius * v.sin()));
1533 let g = |psi: f64, v: f64| screw.velocity(point(psi, v)).dot(radial(psi).scale(v.cos()).add(w.scale(v.sin())));
1534 let reference = {
1535 let (a, b, _) = harmonic(|v| g(0.0, v));
1536 b.atan2(a)
1537 };
1538 let branches = |psi: f64| -> Option<(f64, f64)> {
1540 let (a, b, c) = harmonic(|v| g(psi, v));
1541 let r = a.hypot(b);
1542 if !(c.abs() < r * (1.0 - 1e-9)) {
1543 return None;
1544 }
1545 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
1546 let spread = (-c / r).acos();
1547 Some((base - spread, base + spread))
1548 };
1549 let n = 128;
1550 let halves = [(0.0, PI), (PI, TAU)];
1551 let mut samples: Vec<Vec<(f64, f64, f64)>> = Vec::new(); for &(a, b) in &halves {
1553 let mut list = Vec::with_capacity(n + 1);
1554 let mut previous: Option<(f64, f64)> = None;
1555 for i in 0..=n {
1556 let psi = a + (b - a) * i as f64 / n as f64;
1557 let Some(pair) = branches(psi) else {
1558 return Err(refuse("ring_grazing", "the ring's grazing curves do not run all the way round it".into()));
1559 };
1560 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
1561 return Err(refuse("ring_grazing", "the ring's grazing curves turn too fast".into()));
1562 }
1563 previous = Some(pair);
1564 list.push((psi, pair.0, pair.1));
1565 }
1566 samples.push(list);
1567 }
1568 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1569 let mut points: Vec<Vec3> = Vec::new();
1570 let mut curves: Vec<ToolCurve> = Vec::new();
1571 let mut corner: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1574 let (psi, (lower, upper)) = if seam == 0 { (0.0, (samples[0][0].1, samples[0][0].2)) } else { (PI, (samples[1][0].1, samples[1][0].2)) };
1575 for (branch, v) in [(0, lower), (1, upper)] {
1576 points.push(point(psi, v));
1577 corner.insert((seam, branch), points.len() - 1);
1578 }
1579 }
1580 let seam_of_end = |half: usize, at_end: bool| -> usize { if at_end { 1 - half } else { half } };
1581 let mut branch_curve: HashMap<(usize, usize), usize> = HashMap::new(); for half in 0..2 {
1584 for branch in 0..2 {
1585 let mut xyz: Vec<Vec3> = samples[half].iter().map(|&(psi, lo, hi)| point(psi, if branch == 0 { lo } else { hi })).collect();
1586 let (start, end) = (corner[&(seam_of_end(half, false), branch)], corner[&(seam_of_end(half, true), branch)]);
1587 xyz[0] = points[start];
1588 xyz[n] = points[end];
1589 let exact = (n == LOFT_SAMPLES).then(|| xyz.clone());
1590 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "ring_branch")?, start, end, degenerate: false, samples: exact });
1591 branch_curve.insert((half, branch), curves.len() - 1);
1592 }
1593 }
1594 let tube_arc = |psi: f64, v0: f64, v1: f64| -> Result<NurbsCurve, KernelRefusal> {
1597 let start = point(psi, v0);
1598 let axis = radial(psi).cross(w);
1599 let surface = frag(make_revolution(tube_centre(psi), axis, &frag(make_line(start, start.add(axis)), "line")?, v1 - v0), "tube_arc")?;
1600 frag(surface.iso_curve_v(0.0), "tube_arc")
1601 };
1602 let mut seam_arc: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1604 let psi = if seam == 0 { 0.0 } else { PI };
1605 let (lower, upper) = (samples[seam][0].1, samples[seam][0].2);
1606 let (lo_point, hi_point) = (corner[&(seam, 0)], corner[&(seam, 1)]);
1607 curves.push(ToolCurve { curve: tube_arc(psi, lower, upper)?, start: lo_point, end: hi_point, degenerate: false, samples: None });
1608 seam_arc.insert((seam, 0), curves.len() - 1);
1609 curves.push(ToolCurve { curve: tube_arc(psi, upper, lower + TAU)?, start: hi_point, end: lo_point, degenerate: false, samples: None });
1610 seam_arc.insert((seam, 1), curves.len() - 1);
1611 }
1612 let mut caps = Vec::new();
1613 for half in 0..2 {
1614 let (psi_a, psi_b) = halves[half];
1615 for band in 0..2 {
1616 let (lo_branch, hi_branch, hi_turn) = if band == 0 { (0usize, 1usize, 0.0) } else { (1, 0, TAU) };
1618 let pick = |s: &(f64, f64, f64), b: usize| if b == 0 { s.1 } else { s.2 };
1619 let low = samples[half].iter().map(|s| pick(s, lo_branch)).fold(f64::INFINITY, f64::min);
1622 let high = samples[half].iter().map(|s| pick(s, hi_branch) + hi_turn).fold(f64::NEG_INFINITY, f64::max);
1623 let margin = (0.25 * (TAU - (high - low))).clamp(1e-3, 0.25);
1624 let (v_low, v_high) = (low - margin, high + margin);
1625 let theta_v = v_high - v_low;
1626 if theta_v >= TAU {
1627 return Err(refuse("ring_grazing", "a band of the ring wraps all the way round its tube".into()));
1628 }
1629 let generatrix = tube_arc(psi_a, v_low, v_high)?;
1630 let surface = frag(make_revolution(origin, w, &generatrix, psi_b - psi_a), "ring_band")?;
1631 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1632 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
1633 let uv_of = |psi: f64, v: f64| {
1634 Vec3::new(
1635 crate::circle_angle_to_parameter(spans_u, psi_b - psi_a, psi - psi_a),
1636 crate::circle_angle_to_parameter(spans_v, theta_v, v - v_low),
1637 0.0,
1638 )
1639 };
1640 let angles_of = |x: Vec3| -> (f64, f64) {
1642 let r = x.sub(origin);
1643 let psi = r.dot(u2).atan2(r.dot(u1));
1644 let psi = 0.5 * (psi_a + psi_b) + (psi - 0.5 * (psi_a + psi_b) + PI).rem_euclid(TAU) - PI;
1645 let q = x.sub(tube_centre(psi));
1646 let v = q.dot(w).atan2(q.dot(radial(psi)));
1647 let mid = 0.5 * (v_low + v_high);
1648 (psi, mid + (v - mid + PI).rem_euclid(TAU) - PI)
1649 };
1650 let inverse_pcurve = |curve: &NurbsCurve| -> Result<NurbsCurve, KernelRefusal> {
1651 let [d0, d1] = frag(curve.domain(), "domain")?;
1652 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1653 let mut uv = Vec::with_capacity(ts.len());
1654 for &t in &ts {
1655 let (psi, v) = angles_of(frag(curve.evaluate(t), "point")?);
1656 uv.push(uv_of(psi, v));
1657 }
1658 frag(crate::interpolate_curve(&uv, 5, &ts), "ring_pcurve")
1659 };
1660 let branch_pcurve = |b: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> { inverse_pcurve(&curves[branch_curve[&(half, b)]].curve) };
1661 let lower_edge = branch_curve[&(half, lo_branch)];
1662 let upper_edge = branch_curve[&(half, hi_branch)];
1663 let right = seam_arc[&(seam_of_end(half, true), band)];
1664 let left = seam_arc[&(seam_of_end(half, false), band)];
1665 let coedges = vec![
1666 (lower_edge, true, branch_pcurve(lo_branch, 0.0)?),
1667 (right, true, inverse_pcurve(&curves[right].curve)?),
1668 (upper_edge, false, frag(branch_pcurve(hi_branch, hi_turn)?.reversed(), "reversed")?),
1669 (left, false, frag(inverse_pcurve(&curves[left].curve)?.reversed(), "reversed")?),
1670 ];
1671 let mid = &samples[half][n / 2];
1672 let front = g(mid.0, 0.5 * (pick(mid, lo_branch) + pick(mid, hi_branch) + hi_turn)) > 0.0;
1673 caps.push(Cap { surface, front, coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre: [rho_c, z_c], start: v_low, sweep: theta_v, spans: spans_v } }) });
1674 }
1675 }
1676 Ok(Split {
1677 points,
1678 curves,
1679 caps,
1680 scale: rho_c + radius,
1681 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1682 })
1683}
1684
1685struct Shell {
1687 vertices: Vec<VertexRecord>,
1688 edges: Vec<EdgeRecord>,
1689 faces: Vec<FaceRecord>,
1690 next: u64,
1691}
1692
1693impl Shell {
1694 fn new() -> Self {
1695 Self { vertices: Vec::new(), edges: Vec::new(), faces: Vec::new(), next: 1 }
1696 }
1697
1698 fn id(&mut self) -> u64 {
1699 self.next += 1;
1700 self.next
1701 }
1702
1703 fn vertex(&mut self, point: Vec3) -> u64 {
1704 let id = self.id();
1705 self.vertices.push(VertexRecord { id, point });
1706 id
1707 }
1708
1709 fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
1710 let domain = frag(curve.domain(), "edge_domain")?;
1711 let id = self.id();
1712 self.edges.push(EdgeRecord { id, curve, t0: domain[0], t1: domain[1], start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
1713 Ok(id)
1714 }
1715
1716 fn face(&mut self, surface: NurbsSurface, coedges: Vec<(u64, bool, NurbsCurve)>) {
1719 let mut records = Vec::with_capacity(coedges.len());
1720 for (edge, forward, pcurve) in coedges {
1721 let id = self.id();
1722 records.push(CoedgeRecord { id, edge_id: edge, forward, pcurve });
1723 }
1724 let loop_id = self.id();
1725 let id = self.id();
1726 self.faces.push(FaceRecord { id, surface, same_sense: true, loops: vec![LoopRecord { id: loop_id, coedges: records }], name: None });
1727 }
1728
1729 fn solid(mut self) -> BrepSolid {
1730 let shell = self.id();
1731 let id = self.id();
1732 let edges = self.edges.iter().filter(|e| !e.degenerate).count() as i64;
1735 let euler = self.vertices.len() as i64 - edges + self.faces.len() as i64;
1736 let genus = (2 - euler) / 2;
1737 BrepSolid {
1738 mass_properties_cache: Default::default(),
1739 id,
1740 vertices: self.vertices,
1741 edges: self.edges,
1742 shells: vec![ShellRecord { id: shell, faces: self.faces }],
1743 genus,
1744 kind: Default::default(),
1745 cells: Default::default(),
1746 }
1747 }
1748}
1749
1750pub fn sweep_solid(tool: &SolidTool, motion: &SolidMotion) -> Result<BrepSolid, KernelRefusal> {
1753 sweep_with(&Screw::from_motion(motion)?, tool)
1754}
1755
1756fn sweep_with(screw: &Screw, tool: &SolidTool) -> Result<BrepSolid, KernelRefusal> {
1757 let screw = *screw;
1758 if let SolidTool::Revolved { axis_origin, axis_direction, meridian, .. } = tool {
1759 if let Some(body) = ball_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
1760 return Ok(body);
1761 }
1762 }
1763 let split = match tool {
1764 SolidTool::Polyhedron { vertices, faces } => split_polyhedron(&screw, vertices, faces)?,
1765 SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } => {
1766 split_revolved(&screw, *axis_origin, *axis_direction, *radial, meridian)?
1767 }
1768 };
1769 let grazing = split.grazing()?;
1770 if grazing.is_empty() {
1771 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
1772 }
1773 let times = row_times(&screw);
1774 let last = times.len() - 1;
1775 let mut shell = Shell::new();
1776 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
1779 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
1780 let mut at_time = |shell: &mut Shell, j: usize, p: usize| -> u64 {
1781 *vertex_at.entry((j, p)).or_insert_with(|| shell.vertex(screw.apply(times[j], split.points[p])))
1782 };
1783 let mut time_edge = |shell: &mut Shell, j: usize, c: usize, vertex: &mut dyn FnMut(&mut Shell, usize, usize) -> u64| -> Result<u64, KernelRefusal> {
1784 if let Some(&edge) = edge_at.get(&(j, c)) {
1785 return Ok(edge);
1786 }
1787 let curve = &split.curves[c];
1788 let (a, b) = (vertex(shell, j, curve.start), vertex(shell, j, curve.end));
1789 let edge = shell.edge(screw.move_curve(times[j], &curve.curve)?, a, b, curve.degenerate)?;
1790 edge_at.insert((j, c), edge);
1791 Ok(edge)
1792 };
1793 for cap in &split.caps {
1795 let j = if cap.front { last } else { 0 };
1796 let surface = screw.move_surface(times[j], &cap.surface)?;
1797 let mut coedges = Vec::with_capacity(cap.coedges.len());
1798 for (c, forward, pcurve) in &cap.coedges {
1799 let edge = time_edge(&mut shell, j, *c, &mut at_time)?;
1800 coedges.push((edge, *forward, pcurve.clone()));
1801 }
1802 shell.face(surface, coedges);
1803 }
1804 for row in 0..last {
1809 let (ta, tb) = (times[row], times[row + 1]);
1810 let segments = carry_segments(&screw, ta, tb);
1811 let knots_v = carry_knots(segments);
1812 let mut orbits: HashMap<usize, u64> = HashMap::new();
1813 for &(c, _) in &grazing {
1814 for r in [split.curves[c].start, split.curves[c].end] {
1815 if orbits.contains_key(&r) {
1816 continue;
1817 }
1818 let (v0, v1) = (at_time(&mut shell, row, r), at_time(&mut shell, row + 1, r));
1819 let edge = shell.edge(carry_curve(&screw, split.points[r], ta, tb, segments)?, v0, v1, false)?;
1820 orbits.insert(r, edge);
1821 }
1822 }
1823 for &(c, back_forward) in &grazing {
1824 let tool_curve = &split.curves[c];
1825 let along = if back_forward { frag(tool_curve.curve.reversed(), "reversed")? } else { tool_curve.curve.clone() };
1826 let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
1827 let mut rows = Vec::with_capacity(along.control_points.len());
1828 for p in &along.control_points {
1829 let x = frag(p.point(), "control_point")?;
1830 rows.push(carry_points(&screw, x, ta, tb, segments).into_iter().map(|q| Vec4::from_point(q, p.w)).collect::<Vec<_>>());
1831 }
1832 let surface = frag(NurbsSurface::new(along.degree, 5, along.knots.clone(), knots_v.clone(), rows), "wall")?;
1833 let [u0, u1] = frag(along.domain(), "domain")?;
1834 let bottom = time_edge(&mut shell, row, c, &mut at_time)?;
1835 let top = time_edge(&mut shell, row + 1, c, &mut at_time)?;
1836 let coedges = vec![
1837 (bottom, !back_forward, uv_line([u0, 0.0], [u1, 0.0])?),
1838 (orbits[&to], true, uv_line([u1, 0.0], [u1, 1.0])?),
1839 (top, back_forward, uv_line([u1, 1.0], [u0, 1.0])?),
1840 (orbits[&from], false, uv_line([u0, 1.0], [u0, 0.0])?),
1841 ];
1842 shell.face(surface, coedges);
1843 }
1844 }
1845 let body = shell.solid();
1846 let issues = body.validate();
1847 if !issues.is_empty() {
1848 return Err(refuse("assembly", format!("the assembled sweep is not valid: {:?}", issues.first())));
1849 }
1850 check_single_pass(&screw, &split, &body)?;
1851 Ok(body)
1852}
1853
1854pub fn sweep_solid_clipped(tool: &SolidTool, motion: &SolidMotion, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
1865 let screw = Screw::from_motion(motion)?;
1866 if let (SolidTool::Polyhedron { vertices, faces }, Some(wedge)) = (tool, clip.angle) {
1867 let axis = clip.axis_direction.normalized().map_err(|_| input("clip_axis", "the clip's axis has no direction".into()))?;
1868 let same_axis = screw.c == 0.0 && screw.c2 == 0.0
1869 && screw.e.cross(axis).length() < 1e-12
1870 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
1871 if same_axis {
1872 return turn_shadow(&screw, vertices, faces, clip, axis, wedge);
1873 }
1874 }
1875 if let SolidTool::Polyhedron { vertices, faces } = tool {
1876 if let Some((vertices, faces)) = trim_to_clip(&screw, vertices, faces, clip)? {
1877 return sweep_solid(&SolidTool::Polyhedron { vertices, faces }, motion);
1878 }
1879 }
1880 sweep_solid(tool, motion)
1881}
1882
1883pub fn sweep_solid_rising(tool: &SolidTool, motion: &RisingTurn, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
1887 let screw = Screw::from_rising(motion)?;
1888 let SolidTool::Polyhedron { vertices, faces } = tool else {
1889 return Err(refuse("rising_tool", "a growing rise sweeps a polyhedron only".into()));
1890 };
1891 let (vertices, faces) = trim_to_clip(&screw, vertices, faces, clip)?.unwrap_or_else(|| (vertices.clone(), faces.clone()));
1892 sweep_with(&screw, &SolidTool::Polyhedron { vertices, faces })
1893}
1894
1895pub fn sweep_solid_parts(tool: &SolidTool, motion: &SolidMotion) -> Result<Vec<BrepSolid>, KernelRefusal> {
1903 let folded = match sweep_solid(tool, motion) {
1904 Ok(body) => return Ok(vec![body]),
1905 Err(error) if refused_as(&error) == Some("folds") => error,
1906 Err(error) => return Err(error),
1907 };
1908 let SolidTool::Polyhedron { vertices, faces } = tool else { return Err(folded) };
1909 let screw = Screw::from_motion(motion)?;
1910 if screw.omega == 0.0 {
1911 return Err(folded);
1912 }
1913 let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |a, &p| a.add(p)).scale(1.0 / vertices.len() as f64);
1914 let mut planes: Vec<(Vec3, f64)> = Vec::new();
1915 for face in faces {
1916 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
1917 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
1918 if n.dot(p.sub(centroid)) < 0.0 {
1919 n = n.scale(-1.0);
1920 }
1921 planes.push((n, n.dot(p)));
1922 }
1923 for (n, d) in planes.clone() {
1927 if n.dot(screw.e).abs() > 1e-12 {
1928 continue;
1929 }
1930 let foot = screw.a.add(n.scale(d - n.dot(screw.a)));
1931 let radial = foot.sub(screw.a).sub(screw.e.scale(foot.sub(screw.a).dot(screw.e)));
1932 if radial.dot(n) <= 0.0 {
1933 continue; }
1935 let cut = match screw.e.cross(radial).normalized() {
1936 Ok(m) => m,
1937 Err(_) => continue,
1938 };
1939 let offset = cut.dot(screw.a);
1940 let mut parts = Vec::with_capacity(2);
1941 for sense in [1.0, -1.0] {
1942 let mut half = planes.clone();
1943 half.push((cut.scale(sense), offset * sense));
1944 let (corners, loops) = polyhedron_of_planes(&half)?;
1945 if corners.len() < 4 || loops.len() < 4 {
1946 parts.clear();
1947 break;
1948 }
1949 match sweep_solid(&SolidTool::Polyhedron { vertices: corners, faces: loops }, motion) {
1950 Ok(body) => parts.push(body),
1951 Err(_) => {
1952 parts.clear();
1953 break;
1954 }
1955 }
1956 }
1957 if parts.len() == 2 {
1958 return Ok(parts);
1959 }
1960 }
1961 Err(folded)
1962}
1963
1964fn trim_to_clip(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip) -> Result<Option<(Vec<Vec3>, Vec<Vec<usize>>)>, KernelRefusal> {
1973 let Ok(axis) = clip.axis_direction.normalized() else { return Ok(None) };
1974 let same_axis = screw.omega != 0.0
1975 && screw.e.cross(axis).length() < 1e-12
1976 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
1977 if !same_axis || !clip.radius[1].is_finite() {
1978 return Ok(None);
1979 }
1980 let ez = axis;
1981 let ex = Vec3::new(1.0, 0.0, 0.0).sub(ez.scale(ez.x)).normalized().or_else(|_| Vec3::new(0.0, 1.0, 0.0).sub(ez.scale(ez.y)).normalized()).map_err(|e| input("clip_axis", e))?;
1982 let ey = ez.cross(ex);
1983 let flat = |p: Vec3| {
1984 let r = p.sub(clip.axis_origin);
1985 [r.dot(ex), r.dot(ey)]
1986 };
1987 let radius = clip.radius[1] * 1.02;
1988 let shadow: Vec<[f64; 2]> = vertices.iter().map(|&p| flat(p)).collect();
1989 if shadow.iter().all(|q| q[0].hypot(q[1]) <= radius) {
1990 return Ok(None);
1991 }
1992 let mut points = shadow.clone();
1994 points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
1995 let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
1996 let mut hull: Vec<[f64; 2]> = Vec::new();
1997 for pass in 0..2 {
1998 let start = hull.len();
1999 let ordered: Vec<[f64; 2]> = if pass == 0 { points.clone() } else { points.iter().rev().copied().collect() };
2000 for p in ordered {
2001 while hull.len() >= start + 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
2002 hull.pop();
2003 }
2004 hull.push(p);
2005 }
2006 hull.pop();
2007 }
2008 let m = hull.len();
2010 let inside = |q: [f64; 2]| q[0].hypot(q[1]) <= radius;
2011 let mut nearest = f64::INFINITY;
2013 let mut wraps = true;
2014 for k in 0..m {
2015 let (a, b) = (hull[k], hull[(k + 1) % m]);
2016 let d = [b[0] - a[0], b[1] - a[1]];
2017 let t = (-(a[0] * d[0] + a[1] * d[1]) / (d[0] * d[0] + d[1] * d[1])).clamp(0.0, 1.0);
2018 nearest = nearest.min((a[0] + d[0] * t).hypot(a[1] + d[1] * t));
2019 if cross(a, b, [0.0, 0.0]) < 0.0 {
2020 wraps = false;
2021 }
2022 }
2023 if wraps || nearest >= radius || (0..m).all(|k| inside(hull[k])) {
2024 return Ok(None);
2025 }
2026 let mut leave: Vec<f64> = Vec::new();
2027 let mut enter: Vec<f64> = Vec::new();
2028 for k in 0..m {
2029 let (a, b) = (hull[k], hull[(k + 1) % m]);
2030 let d = [b[0] - a[0], b[1] - a[1]];
2031 let (qa, qb, qc) = (d[0] * d[0] + d[1] * d[1], 2.0 * (a[0] * d[0] + a[1] * d[1]), a[0] * a[0] + a[1] * a[1] - radius * radius);
2032 let disc = qb * qb - 4.0 * qa * qc;
2033 if disc <= 0.0 {
2034 continue;
2035 }
2036 for t in [(-qb - disc.sqrt()) / (2.0 * qa), (-qb + disc.sqrt()) / (2.0 * qa)] {
2037 if t > 0.0 && t < 1.0 {
2038 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2039 let outward = 2.0 * (q[0] * d[0] + q[1] * d[1]) > 0.0;
2040 (if outward { &mut leave } else { &mut enter }).push(q[1].atan2(q[0]));
2041 }
2042 }
2043 }
2044 let ([a], [b]) = (leave.as_slice(), enter.as_slice()) else {
2045 return Err(refuse("clip_trim", "the tool's shadow leaves the clip's disc more than once".into()));
2046 };
2047 let width = (b - a).rem_euclid(TAU);
2048 if width >= 0.9 * PI {
2049 return Err(refuse("clip_trim", "the tool's shadow spans too much of the clip's circle to be trimmed".into()));
2050 }
2051 let mut planes: Vec<(Vec3, f64)> = Vec::new();
2053 let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &p| s.add(p)).scale(1.0 / vertices.len() as f64);
2054 for face in faces {
2055 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
2056 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
2057 if n.dot(p.sub(centroid)) < 0.0 {
2058 n = n.scale(-1.0);
2059 }
2060 planes.push((n, n.dot(p)));
2061 }
2062 for angle in [*a, *b] {
2063 let n = ex.scale(angle.cos()).add(ey.scale(angle.sin()));
2064 planes.push((n, n.dot(clip.axis_origin) + radius));
2065 }
2066 Ok(Some(polyhedron_of_planes(&planes)?))
2067}
2068
2069fn polyhedron_of_planes(planes: &[(Vec3, f64)]) -> Result<(Vec<Vec3>, Vec<Vec<usize>>), KernelRefusal> {
2072 let k = planes.len();
2073 let mut corners: Vec<Vec3> = Vec::new();
2074 let scale = planes.iter().map(|p| p.1.abs()).fold(1.0f64, f64::max);
2075 for i in 0..k {
2076 for j in i + 1..k {
2077 for l in j + 1..k {
2078 let (a, b, c) = (planes[i].0, planes[j].0, planes[l].0);
2079 let det = a.dot(b.cross(c));
2080 if det.abs() < 1e-12 {
2081 continue;
2082 }
2083 let p = b.cross(c).scale(planes[i].1).add(c.cross(a).scale(planes[j].1)).add(a.cross(b).scale(planes[l].1)).scale(1.0 / det);
2084 if planes.iter().all(|(n, d)| n.dot(p) - d <= 1e-9 * scale) && !corners.iter().any(|q| q.sub(p).length() <= 1e-9 * scale) {
2085 corners.push(p);
2086 }
2087 }
2088 }
2089 }
2090 let mut faces = Vec::new();
2091 for (n, d) in planes {
2092 let on: Vec<usize> = (0..corners.len()).filter(|&c| (n.dot(corners[c]) - d).abs() <= 1e-9 * scale).collect();
2093 if on.len() < 3 {
2094 continue;
2095 }
2096 let middle = on.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &c| s.add(corners[c])).scale(1.0 / on.len() as f64);
2097 let u = frag(corners[on[0]].sub(middle).normalized(), "face_axis")?;
2098 let v = n.cross(u);
2099 let mut ordered = on.clone();
2100 ordered.sort_by(|&p, &q| {
2101 let (rp, rq) = (corners[p].sub(middle), corners[q].sub(middle));
2102 rp.dot(v).atan2(rp.dot(u)).total_cmp(&rq.dot(v).atan2(rq.dot(u)))
2103 });
2104 faces.push(ordered);
2105 }
2106 Ok((corners, faces))
2107}
2108
2109fn turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip, axis: Vec3, wedge: [f64; 2]) -> Result<BrepSolid, KernelRefusal> {
2110 let ez = axis;
2111 let ex = clip.reference.sub(ez.scale(clip.reference.dot(ez))).normalized().map_err(|_| input("clip_reference", "the clip's reference lies along its axis".into()))?;
2112 let ey = ez.cross(ex);
2113 let rate = screw.omega * screw.e.dot(ez).signum();
2115 let local = |p: Vec3| {
2116 let r = p.sub(clip.axis_origin);
2117 [r.dot(ex), r.dot(ey), r.dot(ez)]
2118 };
2119 let corners: Vec<[f64; 3]> = vertices.iter().map(|&p| local(p)).collect();
2120 let mid = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
2123 let mid_angle = mid[1].atan2(mid[0]);
2124 let angles: Vec<f64> = corners.iter().map(|c| mid_angle + (c[1].atan2(c[0]) - mid_angle + PI).rem_euclid(TAU) - PI).collect();
2125 let (low, high) = (angles.iter().copied().fold(f64::INFINITY, f64::min), angles.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2126 if high - low >= PI {
2127 return Err(refuse("turn_shadow", "the tool reaches round the turn's axis".into()));
2128 }
2129 let (start, end) = (rate * screw.t0, rate * screw.t1);
2130 let (first, last) = if rate > 0.0 { (start, end) } else { (end, start) };
2131 let reference = 0.5 * (wedge[0] + wedge[1]);
2132 let near = |a: f64| reference + (a - reference + PI).rem_euclid(TAU) - PI;
2133 let clear = near(high + first) < wedge[0] - 1e-9 && near(low + last) > wedge[1] + 1e-9 && (last - first) < TAU;
2135 if !clear {
2136 return Err(refuse("turn_shadow", "the turned tool's ends reach the clip's wedge; its sweep there is not its turned shadow".into()));
2137 }
2138 let profile = polyhedron_shadow(&corners, faces)?;
2139 let (c0, s0) = (wedge[0].cos(), wedge[0].sin());
2141 let world = |rho: f64, z: f64| clip.axis_origin.add(ez.scale(z)).add(ex.scale(rho * c0)).add(ey.scale(rho * s0));
2142 let mut curves = Vec::with_capacity(profile.len());
2143 for piece in &profile {
2144 let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2145 let curve = if points.len() == 2 {
2146 frag(make_line(frag(points[0].point(), "point")?, frag(points[1].point(), "point")?), "line")?
2147 } else {
2148 frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")?
2149 };
2150 curves.push(curve);
2151 }
2152 frag(crate::revolve_profile_brep(&curves, clip.axis_origin, ez, wedge[1] - wedge[0]), "turn_shadow")
2153}
2154
2155type ShadowPiece = Vec<(f64, f64, f64)>;
2158
2159fn polyhedron_shadow(corners: &[[f64; 3]], faces: &[Vec<usize>]) -> Result<Vec<ShadowPiece>, KernelRefusal> {
2165 let zs: Vec<f64> = corners.iter().map(|c| c[2]).collect();
2166 let (z_low, z_high) = (zs.iter().copied().fold(f64::INFINITY, f64::min), zs.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2167 let scale = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(1.0f64, f64::max);
2168 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
2170 for (f, face) in faces.iter().enumerate() {
2171 for k in 0..face.len() {
2172 let (i, j) = (face[k], face[(k + 1) % face.len()]);
2173 edge_faces.entry((i.min(j), i.max(j))).or_default().push(f);
2174 }
2175 }
2176 let mut edges: Vec<(usize, usize)> = edge_faces.keys().copied().collect();
2177 edges.sort_unstable();
2178 let planes: Vec<([f64; 3], f64)> = faces
2180 .iter()
2181 .map(|face| {
2182 let (a, b, c) = (corners[face[0]], corners[face[1]], corners[face[2]]);
2183 let (u, v) = ([b[0] - a[0], b[1] - a[1], b[2] - a[2]], [c[0] - a[0], c[1] - a[1], c[2] - a[2]]);
2184 let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
2185 (n, n[0] * a[0] + n[1] * a[1] + n[2] * a[2])
2186 })
2187 .collect();
2188 #[derive(Clone, Copy, PartialEq, Debug)]
2189 enum Source {
2190 Edge(usize),
2192 Face(usize),
2194 }
2195 let edge_at = |k: usize, z: f64| -> [f64; 2] {
2196 let (a, b) = (corners[edges[k].0], corners[edges[k].1]);
2197 let t = (z - a[2]) / (b[2] - a[2]);
2198 [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
2199 };
2200 let crossing = |z: f64| -> Vec<usize> {
2201 (0..edges.len()).filter(|&k| (corners[edges[k].0][2] - z) * (corners[edges[k].1][2] - z) < 0.0).collect()
2202 };
2203 let farthest = |z: f64| -> (Source, f64) {
2204 let mut best = (Source::Edge(0), f64::NEG_INFINITY);
2205 for k in crossing(z) {
2206 let p = edge_at(k, z);
2207 let r = p[0].hypot(p[1]);
2208 if r > best.1 {
2209 best = (Source::Edge(k), r);
2210 }
2211 }
2212 best
2213 };
2214 let nearest = |z: f64| -> Result<(Source, f64), KernelRefusal> {
2216 let list = crossing(z);
2217 let points: Vec<[f64; 2]> = list.iter().map(|&k| edge_at(k, z)).collect();
2218 let m = points.len();
2219 if m < 3 {
2220 return Err(refuse("turn_shadow", "a section of the tool is not a polygon".into()));
2221 }
2222 let centre = points.iter().fold([0.0, 0.0], |s, p| [s[0] + p[0] / m as f64, s[1] + p[1] / m as f64]);
2223 let mut order: Vec<usize> = (0..m).collect();
2224 order.sort_by(|&a, &b| {
2225 let (pa, pb) = (points[a], points[b]);
2226 (pa[1] - centre[1]).atan2(pa[0] - centre[0]).total_cmp(&(pb[1] - centre[1]).atan2(pb[0] - centre[0]))
2227 });
2228 let mut inside = true;
2229 let mut best = (Source::Edge(0), f64::INFINITY);
2230 for k in 0..m {
2231 let (ia, ib) = (order[k], order[(k + 1) % m]);
2232 let (a, b) = (points[ia], points[ib]);
2233 let d = [b[0] - a[0], b[1] - a[1]];
2234 if d[0] * (0.0 - a[1]) - d[1] * (0.0 - a[0]) < 0.0 {
2235 inside = false;
2236 }
2237 let length2 = d[0] * d[0] + d[1] * d[1];
2238 let t = -(a[0] * d[0] + a[1] * d[1]) / length2;
2239 let (source, r) = if t > 0.0 && t < 1.0 {
2240 let (ea, eb) = (edges[list[ia]], edges[list[ib]]);
2241 let shared = edge_faces[&ea].iter().find(|f| edge_faces[&eb].contains(f)).copied();
2242 let Some(face) = shared else {
2243 return Err(refuse("turn_shadow", "a section side does not lie on one face".into()));
2244 };
2245 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2246 (Source::Face(face), q[0].hypot(q[1]))
2247 } else {
2248 let q = if t <= 0.0 { a } else { b };
2249 (Source::Edge(list[if t <= 0.0 { ia } else { ib }]), q[0].hypot(q[1]))
2250 };
2251 if r < best.1 {
2252 best = (source, r);
2253 }
2254 }
2255 if inside {
2256 return Err(refuse("turn_shadow", "the turn's axis passes through the tool".into()));
2257 }
2258 Ok(best)
2259 };
2260 let value = |source: Source, z: f64| -> f64 {
2262 match source {
2263 Source::Edge(k) => {
2264 let p = edge_at(k, z);
2265 p[0].hypot(p[1])
2266 }
2267 Source::Face(f) => {
2268 let (n, d) = planes[f];
2269 (d - n[2] * z).abs() / n[0].hypot(n[1])
2270 }
2271 }
2272 };
2273 let mut heights: Vec<f64> = zs.clone();
2275 heights.sort_by(|a, b| a.total_cmp(b));
2276 heights.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * scale);
2277 let mut pieces_far: Vec<(f64, f64, Source)> = Vec::new();
2278 let mut pieces_near: Vec<(f64, f64, Source)> = Vec::new();
2279 for window in heights.windows(2) {
2280 let (za, zb) = (window[0], window[1]);
2281 let samples = 400;
2282 let inner = |k: usize| za + (zb - za) * (k as f64 + 0.5) / samples as f64;
2283 for near in [false, true] {
2284 let source_at = |z: f64| -> Result<Source, KernelRefusal> { Ok(if near { nearest(z)?.0 } else { farthest(z).0 }) };
2285 let mut runs: Vec<(f64, Source)> = vec![(za, source_at(inner(0))?)];
2286 for k in 1..samples {
2287 let (z0, z1) = (inner(k - 1), inner(k));
2288 let (s0, s1) = (source_at(z0)?, source_at(z1)?);
2289 if s0 == s1 {
2290 continue;
2291 }
2292 let (mut lo, mut hi) = (z0, z1);
2295 for _ in 0..200 {
2296 let midz = 0.5 * (lo + hi);
2297 if source_at(midz)? == s0 { lo = midz } else { hi = midz }
2298 }
2299 runs.push((0.5 * (lo + hi), s1));
2300 }
2301 let list = if near { &mut pieces_near } else { &mut pieces_far };
2302 for k in 0..runs.len() {
2303 let z_end = if k + 1 < runs.len() { runs[k + 1].0 } else { zb };
2304 list.push((runs[k].0, z_end, runs[k].1));
2305 }
2306 }
2307 }
2308 let piece = |source: Source, za: f64, zb: f64| -> Vec<ShadowPiece> {
2310 let (ra, rb) = (value(source, za), value(source, zb));
2311 if let Source::Face(..) = source {
2312 return vec![vec![(ra, za, 1.0), (rb, zb, 1.0)]];
2313 }
2314 let rho = |z: f64| value(source, z);
2316 let slope = |z: f64| {
2317 let h = 1e-6 * (zb - za).abs().max(1e-300);
2318 (rho(z + h) - rho(z - h)) / (2.0 * h)
2319 };
2320 let parts = 4;
2321 let mut out = Vec::new();
2322 for k in 0..parts {
2323 let (z0, z1) = (za + (zb - za) * k as f64 / parts as f64, za + (zb - za) * (k + 1) as f64 / parts as f64);
2324 let (a, b) = ((rho(z0), z0), (rho(z1), z1));
2325 let (ta, tb) = ((slope(z0), 1.0), (slope(z1), 1.0));
2326 let det = ta.0 * (-tb.1) - ta.1 * (-tb.0);
2328 if det.abs() < 1e-12 {
2329 out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
2330 continue;
2331 }
2332 let rhs = (b.0 - a.0, b.1 - a.1);
2333 let s = (rhs.0 * (-tb.1) - rhs.1 * (-tb.0)) / det;
2334 let c = (a.0 + ta.0 * s, a.1 + ta.1 * s);
2335 let m = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
2337 let on = |u: f64| {
2338 let p = (m.0 + (c.0 - m.0) * u, m.1 + (c.1 - m.1) * u);
2339 p.0 - rho(p.1)
2340 };
2341 let (mut lo, mut hi) = (0.0, 1.0);
2342 let sign_lo = on(lo);
2343 for _ in 0..200 {
2344 let mid = 0.5 * (lo + hi);
2345 if (on(mid) > 0.0) == (sign_lo > 0.0) { lo = mid } else { hi = mid }
2346 }
2347 let u = 0.5 * (lo + hi);
2348 let weight = u / (1.0 - u);
2349 out.push(vec![(a.0, a.1, 1.0), (c.0, c.1, weight), (b.0, b.1, 1.0)]);
2350 }
2351 out
2352 };
2353 let mut outline: Vec<ShadowPiece> = Vec::new();
2355 for &(za, zb, source) in &pieces_near {
2356 outline.extend(piece(source, za, zb));
2357 }
2358 let (near_top, far_top) = (value(pieces_near.last().expect("near").2, z_high), value(pieces_far.last().expect("far").2, z_high));
2359 if far_top - near_top > 1e-12 * scale {
2360 outline.push(vec![(near_top, z_high, 1.0), (far_top, z_high, 1.0)]);
2361 }
2362 for &(za, zb, source) in pieces_far.iter().rev() {
2363 for mut arc in piece(source, za, zb).into_iter().rev() {
2364 arc.reverse();
2365 outline.push(arc);
2366 }
2367 }
2368 let (near_bottom, far_bottom) = (value(pieces_near[0].2, z_low), value(pieces_far[0].2, z_low));
2369 if far_bottom - near_bottom > 1e-12 * scale {
2370 outline.push(vec![(far_bottom, z_low, 1.0), (near_bottom, z_low, 1.0)]);
2371 }
2372 for k in 0..outline.len() {
2374 let next = (k + 1) % outline.len();
2375 let end = *outline[k].last().expect("piece");
2376 outline[next][0] = (end.0, end.1, 1.0);
2377 }
2378 Ok(outline)
2379}
2380
2381fn ball_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2391 let [OutlinePiece::Arc { centre, radius, start, end }, OutlinePiece::Line { .. }] = meridian else { return Ok(None) };
2392 let (centre, r) = (*centre, *radius);
2393 if centre[0].abs() > 1e-12 * r || ((end - start).abs() - PI).abs() > 1e-12 || screw.c != 0.0 || screw.c2 != 0.0 || screw.omega == 0.0 {
2394 return Ok(None);
2395 }
2396 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
2397 let centre_tool = origin.add(w.scale(centre[1]));
2398 let turned = screw.omega * (screw.t1 - screw.t0);
2399 if turned.abs() >= TAU - 1e-9 {
2400 return Ok(None);
2401 }
2402 let e = screw.e.scale(screw.omega.signum()); let c0 = screw.apply(screw.t0, centre_tool);
2404 let c1 = screw.apply(screw.t1, centre_tool);
2405 let foot = |c: Vec3| screw.a.add(e.scale(c.sub(screw.a).dot(e)));
2406 let rho = c0.sub(foot(c0)).length();
2407 if rho >= r * (1.0 - 1e-9) || rho <= 1e-9 * r {
2408 return Ok(None);
2409 }
2410 let h0 = frag(c0.sub(foot(c0)).normalized(), "radial")?;
2411 let height = (r * r - rho * rho).sqrt();
2412 let (north, south) = (foot(c0).add(e.scale(height)), foot(c0).sub(e.scale(height)));
2413 let arc = |about: Vec3, k: Vec3, from: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
2415 let surface = frag(make_revolution(about, k, &frag(make_line(from, from.add(k)), "line")?, angle), "arc")?;
2416 frag(surface.iso_curve_v(0.0), "arc")
2417 };
2418 let alpha = height.atan2(-rho);
2421 let outer = arc(c0, h0.cross(e), south, 2.0 * alpha)?;
2422 let wall = frag(make_revolution(screw.a, e, &outer, turned.abs()), "wall")?;
2423 let outer_end = frag(wall.iso_curve_u(1.0), "wall_end")?;
2424 let mid = c0.add(c1).scale(0.5);
2426 let towards = frag(mid.sub(foot(mid)).normalized(), "bisector")?;
2427 let rho_mid = mid.sub(foot(mid)).length();
2428 let beta = height.atan2(rho_mid); let meet = arc(mid, e.cross(towards), south, 2.0 * beta)?;
2432 {
2433 let [d0, d1] = frag(meet.domain(), "domain")?;
2435 let middle = frag(meet.evaluate(0.5 * (d0 + d1)), "middle")?;
2436 if middle.sub(foot(middle)).dot(towards) > 0.0 {
2437 return Err(refuse("ball_across_axis", "the spheres' common circle runs the wrong way round".into()));
2438 }
2439 }
2440 let cap = |c: Vec3, inward: Vec3, side: Vec3, other: Vec3| -> Result<(NurbsSurface, Box<dyn Fn(Vec3) -> Vec3>), KernelRefusal> {
2442 let toward_south = frag(inward.add(side.scale(-0.3)).normalized(), "pole")?;
2446 let polar = toward_south.scale(-1.0);
2447 let seam = frag(side.sub(polar.scale(side.dot(polar))).normalized(), "seam")?;
2448 for k in 0..=64 {
2449 let a = PI * k as f64 / 64.0;
2452 let q = c.add(polar.scale(-r * a.cos())).add(seam.scale(r * a.sin()));
2453 if q.sub(c).dot(side) < 0.0 && q.sub(other).length() >= r * (1.0 - 1e-9) {
2454 return Err(refuse("ball_across_axis", "no seam place for a cap".into()));
2455 }
2456 }
2457 let south_pole = c.sub(polar.scale(r));
2458 if south_pole.sub(other).length() >= r * (1.0 - 1e-6) {
2459 return Err(refuse("ball_across_axis", "no pole place for a cap".into()));
2460 }
2461 let generatrix = arc(c, polar.scale(-1.0).cross(seam), south_pole, PI)?;
2462 let surface = frag(make_revolution(c, polar, &generatrix, TAU), "cap")?;
2463 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
2464 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
2465 let around = polar.cross(seam);
2466 let inverse = move |x: Vec3| -> Vec3 {
2467 let d = x.sub(c).scale(1.0 / r);
2468 let latitude = d.dot(polar).clamp(-1.0, 1.0).asin();
2469 let longitude = d.dot(around).atan2(d.dot(seam)).rem_euclid(TAU);
2470 Vec3::new(
2471 crate::circle_angle_to_parameter(spans_u, TAU, longitude),
2472 crate::circle_angle_to_parameter(spans_v, PI, latitude + 0.5 * PI),
2473 0.0,
2474 )
2475 };
2476 Ok((surface, Box::new(inverse)))
2477 };
2478 let moving0 = e.cross(h0);
2479 let h1 = frag(c1.sub(foot(c1)).normalized(), "radial")?;
2480 let moving1 = e.cross(h1);
2481 let (back, back_inverse) = cap(c0, h0.scale(-1.0), moving0.scale(1.0), c1)?;
2484 let (front, front_inverse) = cap(c1, h1.scale(-1.0), moving1.scale(-1.0), c0)?;
2485 let pcurve = |inverse: &dyn Fn(Vec3) -> Vec3, curve: &NurbsCurve, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
2486 let [d0, d1] = frag(curve.domain(), "domain")?;
2487 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
2488 let uv: Vec<Vec3> = ts.iter().map(|&t| curve.evaluate(t).map(|x| inverse(x))).collect::<Result<_, _>>().map_err(|e| refuse("pcurve", e))?;
2489 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "cap_pcurve")?;
2490 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
2491 };
2492 let mut shell = Shell::new();
2493 let (vn, vs) = (shell.vertex(north), shell.vertex(south));
2494 let k0 = shell.edge(outer.clone(), vs, vn, false)?;
2495 let k1 = shell.edge(outer_end.clone(), vs, vn, false)?;
2496 let c = shell.edge(meet.clone(), vs, vn, false)?;
2497 let ds = shell.edge(frag(make_line(south, south), "point")?, vs, vs, true)?;
2498 let dn = shell.edge(frag(make_line(north, north), "point")?, vn, vn, true)?;
2499 let wall_outward = {
2502 let p = frag(wall.evaluate(0.5, 0.5), "probe")?;
2503 let su = frag(wall.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(wall.evaluate(0.5 - 1e-6, 0.5), "probe")?);
2504 let sv = frag(wall.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(wall.evaluate(0.5, 0.5 - 1e-6), "probe")?);
2505 su.cross(sv).dot(p.sub(foot(p))) > 0.0
2506 };
2507 let [g0, g1] = frag(outer.domain(), "domain")?;
2508 let wall_loop = vec![
2509 (ds, true, uv_line([0.0, g0], [1.0, g0])?),
2510 (k1, true, uv_line([1.0, g0], [1.0, g1])?),
2511 (dn, true, uv_line([1.0, g1], [0.0, g1])?),
2512 (k0, false, uv_line([0.0, g1], [0.0, g0])?),
2513 ];
2514 let reversed = |list: Vec<(u64, bool, NurbsCurve)>| -> Result<Vec<(u64, bool, NurbsCurve)>, KernelRefusal> {
2515 list.into_iter().rev().map(|(e, f, p)| Ok((e, !f, frag(p.reversed(), "reversed")?))).collect()
2516 };
2517 shell.face(wall.clone(), if wall_outward { wall_loop } else { reversed(wall_loop)? });
2518 for (surface, inverse, k_edge, k_curve) in [(back, &back_inverse, k0, &outer), (front, &front_inverse, k1, &outer_end)] {
2521 let mut lp = vec![(k_edge, true, pcurve(inverse.as_ref(), k_curve, true)?), (c, false, pcurve(inverse.as_ref(), &meet, false)?)];
2522 let mut area = 0.0;
2523 for (_, _, pc) in &lp {
2524 let [d0, d1] = frag(pc.domain(), "domain")?;
2525 for j in 0..64 {
2526 let (a, b) = (frag(pc.evaluate(d0 + (d1 - d0) * j as f64 / 64.0), "uv")?, frag(pc.evaluate(d0 + (d1 - d0) * (j + 1) as f64 / 64.0), "uv")?);
2527 area += a.x * b.y - b.x * a.y;
2528 }
2529 }
2530 if area < 0.0 {
2531 lp = reversed(lp)?;
2532 }
2533 shell.face(surface, lp);
2534 }
2535 let body = shell.solid();
2536 let issues = body.validate();
2537 if !issues.is_empty() {
2538 return Err(refuse("assembly", format!("the turned ball's sweep is not valid: {:?}", issues.first())));
2539 }
2540 let probe = Split { points: Vec::new(), curves: Vec::new(), caps: Vec::new(), scale: r + rho, shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() } };
2541 check_single_pass(screw, &probe, &body)?;
2542 Ok(Some(body))
2543}
2544
2545#[derive(Clone, Copy, Debug)]
2550pub struct FrameCurve {
2551 pub control: [Vec3; 4],
2552 pub up: Vec3,
2553}
2554
2555pub fn sweep_solid_along(tool: &SolidTool, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
2564 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
2565 let base = path.control[0];
2566 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
2567 return Err(input("plane", "the curve is not in a plane square to up".into()));
2568 }
2569 let tangent = path.control[1].sub(base);
2570 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
2571 let start_tangent = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
2572 let frame = Frame { control: path.control, up, start_tangent };
2573 let (mut speed, mut rate, mut extent) = (0.0f64, 0.0f64, 0.0f64);
2574 let mut previous = 0.0;
2575 for k in 0..=512 {
2576 let t = k as f64 / 512.0;
2577 let first = frame.first(t);
2578 if first.length() <= 1e-12 {
2579 return Err(input("tangent", "the curve stops (zero tangent)".into()));
2580 }
2581 speed = speed.max(first.length());
2582 rate = rate.max(frame.angle_rate(t).abs());
2583 extent = extent.max(frame.point(t).sub(base).length());
2584 let angle = frame.angle(t);
2585 if (angle - previous).abs() > 0.5 {
2586 return Err(refuse("frame_turns", "the frame turns half way round".into()));
2587 }
2588 previous = angle;
2589 }
2590 let screw = Screw { a: base, e: up, omega: rate, c: speed, c2: 0.0, t0: 0.0, t1: 1.0, frame: Some(frame), turns: None, frozen: None, extent };
2591 let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
2592 return Err(refuse("along_tool", "a frame along a curve carries a solid of revolution only".into()));
2593 };
2594 let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
2595 let mut last = None;
2599 for k in 0..8 {
2600 let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
2601 match lofted(&screw, *axis_origin, axis, reference, meridian) {
2602 Err(error) if refused_as(&error) == Some("loft_structure") => last = Some(error),
2603 other => return other,
2604 }
2605 }
2606 Err(last.expect("tried"))
2607}
2608
2609pub fn sweep_prism_along(outline: &[[f64; 2]], from: f64, to: f64, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
2620 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
2621 let base = path.control[0];
2622 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
2623 return Err(input("plane", "the curve is not in a plane square to up".into()));
2624 }
2625 let tangent = path.control[1].sub(base);
2626 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
2627 let e1 = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
2628 let e2 = up.cross(e1);
2629 let frame = Frame { control: path.control, up, start_tangent: e1 };
2630 if outline.len() < 3 || !(to > from) {
2631 return Err(input("prism", "a prism needs three corners and a positive height".into()));
2632 }
2633 let n = outline.len();
2635 let mut corners = outline.to_vec();
2636 let area: f64 = (0..n).map(|k| corners[k][0] * corners[(k + 1) % n][1] - corners[(k + 1) % n][0] * corners[k][1]).sum();
2637 if area < 0.0 {
2638 corners.reverse();
2639 }
2640 for k in 0..n {
2641 let (a, b, c) = (corners[k], corners[(k + 1) % n], corners[(k + 2) % n]);
2642 if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) <= 0.0 {
2643 return Err(input("prism", "the outline is not convex".into()));
2644 }
2645 }
2646 let scale = corners.iter().map(|c| c[0].hypot(c[1])).fold(1.0f64, f64::max);
2647 let flat = |p: Vec3| [p.sub(base).dot(e1), p.sub(base).dot(e2)];
2649 let place = |s: f64, q: [f64; 2]| -> [f64; 2] {
2650 let o = flat(frame.point(s));
2651 let (sn, cs) = frame.angle(s).sin_cos();
2652 [o[0] + cs * q[0] - sn * q[1], o[1] + sn * q[0] + cs * q[1]]
2653 };
2654 let local = |s: f64, p: [f64; 2]| -> [f64; 2] {
2655 let o = flat(frame.point(s));
2656 let (sn, cs) = frame.angle(s).sin_cos();
2657 let d = [p[0] - o[0], p[1] - o[1]];
2658 [cs * d[0] + sn * d[1], -sn * d[0] + cs * d[1]]
2659 };
2660 let edges: Vec<([f64; 2], f64)> = (0..n)
2662 .map(|k| {
2663 let (a, b) = (corners[k], corners[(k + 1) % n]);
2664 let d = [b[0] - a[0], b[1] - a[1]];
2665 let length = d[0].hypot(d[1]);
2666 let normal = [d[1] / length, -d[0] / length];
2667 (normal, normal[0] * a[0] + normal[1] * a[1])
2668 })
2669 .collect();
2670 let inside = |q: [f64; 2]| edges.iter().map(|(nn, d)| nn[0] * q[0] + nn[1] * q[1] - d).fold(f64::NEG_INFINITY, f64::max);
2671 let depth = |p: [f64; 2]| -> f64 {
2673 let samples = 2000;
2674 let f = |s: f64| inside(local(s, p));
2675 let mut best = (f64::INFINITY, 0usize);
2676 let values: Vec<f64> = (0..=samples).map(|k| f(k as f64 / samples as f64)).collect();
2677 for (k, &v) in values.iter().enumerate() {
2678 if v < best.0 {
2679 best = (v, k);
2680 }
2681 }
2682 let mut least = best.0;
2684 for k in 0..=samples {
2685 let left = if k == 0 { f64::INFINITY } else { values[k - 1] };
2686 let right = if k == samples { f64::INFINITY } else { values[k + 1] };
2687 if values[k] <= left && values[k] <= right && values[k] < best.0 + 1e-3 * scale {
2688 let (mut lo, mut hi) = (((k as f64) - 1.0).max(0.0) / samples as f64, ((k as f64) + 1.0).min(samples as f64) / samples as f64);
2689 for _ in 0..80 {
2690 let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
2691 if f(m1) < f(m2) { hi = m2 } else { lo = m1 }
2692 }
2693 least = least.min(f(0.5 * (lo + hi)));
2694 }
2695 }
2696 least
2697 };
2698 #[derive(Clone, Copy)]
2700 enum Path {
2701 Corner(usize),
2702 Graze(usize, f64, f64),
2704 End(usize, bool),
2706 }
2707 let speed_ratio = |s: f64| frame.angle_rate(s); let graze_point = |k: usize, s: f64| -> Option<f64> {
2709 let (a, b) = (corners[k], corners[(k + 1) % n]);
2711 let (nn, _) = edges[k];
2712 let kappa = speed_ratio(s) / frame.first(s).length();
2713 let rot = |x: [f64; 2]| -x[1] * nn[0] + x[0] * nn[1]; let (ga, gb) = if nn[0].abs() < 1e-15 { (rot(a), rot(b)) } else { (nn[0] + kappa * rot(a), nn[0] + kappa * rot(b)) };
2717 if ga == gb {
2718 return None;
2719 }
2720 let u = ga / (ga - gb);
2721 (u > 0.0 && u < 1.0).then_some(u)
2722 };
2723 let mut paths: Vec<Path> = (0..n).map(Path::Corner).collect();
2724 for k in 0..n {
2725 let steps = 2000;
2727 let mut run: Option<f64> = None;
2728 for j in 0..=steps {
2729 let s = j as f64 / steps as f64;
2730 let on = graze_point(k, s).is_some();
2731 match (on, run) {
2732 (true, None) => run = Some(s),
2733 (false, Some(s0)) => {
2734 paths.push(Path::Graze(k, s0, s - 1.0 / steps as f64));
2735 run = None;
2736 }
2737 _ => {}
2738 }
2739 }
2740 if let Some(s0) = run {
2741 paths.push(Path::Graze(k, s0, 1.0));
2742 }
2743 paths.push(Path::End(k, false));
2744 paths.push(Path::End(k, true));
2745 }
2746 let eval = |path: Path, t: f64| -> [f64; 2] {
2747 match path {
2748 Path::Corner(k) => place(t, corners[k]),
2749 Path::Graze(k, s0, s1) => {
2750 let s = s0 + (s1 - s0) * t;
2751 let u = graze_point(k, s).unwrap_or(if t < 0.5 { 0.0 } else { 1.0 });
2752 let (a, b) = (corners[k], corners[(k + 1) % n]);
2753 place(s, [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u])
2754 }
2755 Path::End(k, at_end) => {
2756 let (a, b) = (corners[k], corners[(k + 1) % n]);
2757 place(if at_end { 1.0 } else { 0.0 }, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])
2758 }
2759 }
2760 };
2761 let m = 400;
2764 let polylines: Vec<Vec<[f64; 2]>> = paths.iter().map(|&p| (0..=m).map(|i| eval(p, i as f64 / m as f64)).collect()).collect();
2765 let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; paths.len()];
2766 let tol = 1e-10 * scale;
2767 for a in 0..paths.len() {
2768 for b in a + 1..paths.len() {
2769 for i in 0..m {
2770 for j in 0..m {
2771 let (p0, p1, q0, q1) = (polylines[a][i], polylines[a][i + 1], polylines[b][j], polylines[b][j + 1]);
2772 let d1 = [p1[0] - p0[0], p1[1] - p0[1]];
2773 let d2 = [q1[0] - q0[0], q1[1] - q0[1]];
2774 let det = d1[0] * d2[1] - d1[1] * d2[0];
2775 if det.abs() < 1e-300 {
2776 continue;
2777 }
2778 let r = [q0[0] - p0[0], q0[1] - p0[1]];
2779 let (x, y) = ((r[0] * d2[1] - r[1] * d2[0]) / det, (r[0] * d1[1] - r[1] * d1[0]) / det);
2780 if !(-1e-9..=1.0 + 1e-9).contains(&x) || !(-1e-9..=1.0 + 1e-9).contains(&y) {
2781 continue;
2782 }
2783 let (mut ta, mut tb) = ((i as f64 + x) / m as f64, (j as f64 + y) / m as f64);
2785 for _ in 0..50 {
2786 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
2787 let h = 1e-7;
2788 let da = { let q = eval(paths[a], (ta + h).min(1.0)); let p = eval(paths[a], (ta - h).max(0.0)); let w = (ta + h).min(1.0) - (ta - h).max(0.0); [(q[0] - p[0]) / w, (q[1] - p[1]) / w] };
2789 let db = { let q = eval(paths[b], (tb + h).min(1.0)); let p = eval(paths[b], (tb - h).max(0.0)); let w = (tb + h).min(1.0) - (tb - h).max(0.0); [(q[0] - p[0]) / w, (q[1] - p[1]) / w] };
2790 let f = [pa[0] - pb[0], pa[1] - pb[1]];
2791 let det = -da[0] * db[1] + da[1] * db[0];
2792 if det.abs() < 1e-300 {
2793 break;
2794 }
2795 let dta = (-f[0] * db[1] + f[1] * db[0]) / det;
2796 let dtb = (da[0] * f[1] - da[1] * f[0]) / det;
2797 ta = (ta - dta).clamp(0.0, 1.0);
2798 tb = (tb - dtb).clamp(0.0, 1.0);
2799 if dta.abs() + dtb.abs() < 1e-15 {
2800 break;
2801 }
2802 }
2803 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
2804 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < tol {
2805 cuts[a].push(ta);
2806 cuts[b].push(tb);
2807 }
2808 }
2809 }
2810 }
2811 }
2812 for c in &mut cuts {
2813 c.sort_by(|x, y| x.total_cmp(y));
2814 c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
2815 }
2816 struct Piece {
2818 path: usize,
2819 t0: f64,
2820 t1: f64,
2821 }
2822 let eps = 1e-7 * scale;
2823 let mut pieces: Vec<Piece> = Vec::new();
2824 for (index, c) in cuts.iter().enumerate() {
2825 for w in c.windows(2) {
2826 let (t0, t1) = (w[0], w[1]);
2827 if t1 - t0 < 1e-9 {
2828 continue;
2829 }
2830 let (pa, pb) = (eval(paths[index], t0), eval(paths[index], t1));
2831 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < 1e-9 * scale {
2832 continue;
2833 }
2834 let tm = 0.5 * (t0 + t1);
2835 let (p, q) = (eval(paths[index], tm), eval(paths[index], (tm + 1e-6).min(1.0)));
2836 let d = [q[0] - p[0], q[1] - p[1]];
2837 let length = d[0].hypot(d[1]);
2838 if length == 0.0 {
2839 continue;
2840 }
2841 let left = [p[0] - d[1] / length * eps, p[1] + d[0] / length * eps];
2842 let right = [p[0] + d[1] / length * eps, p[1] - d[0] / length * eps];
2843 let (l, r) = (depth(left) < -1e-12 * scale, depth(right) < -1e-12 * scale);
2844 match (l, r) {
2845 (true, false) => pieces.push(Piece { path: index, t0, t1 }),
2846 (false, true) => pieces.push(Piece { path: index, t0: t1, t1: t0 }),
2847 _ => {}
2848 }
2849 }
2850 }
2851 if pieces.is_empty() {
2853 return Err(refuse("prism_along", "no boundary found".into()));
2854 }
2855 let mut order = vec![0usize];
2856 let mut used = vec![false; pieces.len()];
2857 used[0] = true;
2858 loop {
2859 let last = &pieces[*order.last().expect("order")];
2860 let end = eval(paths[last.path], last.t1);
2861 let start = eval(paths[pieces[order[0]].path], pieces[order[0]].t0);
2862 if order.len() > 1 && (end[0] - start[0]).hypot(end[1] - start[1]) < 1e-8 * scale {
2863 break;
2864 }
2865 let next = (0..pieces.len()).filter(|&k| !used[k]).min_by(|&x, &y| {
2866 let (px, py) = (eval(paths[pieces[x].path], pieces[x].t0), eval(paths[pieces[y].path], pieces[y].t0));
2867 (px[0] - end[0]).hypot(px[1] - end[1]).total_cmp(&(py[0] - end[0]).hypot(py[1] - end[1]))
2868 });
2869 let Some(next) = next else {
2870 return Err(refuse("prism_along", "the boundary does not close".into()));
2871 };
2872 let p = eval(paths[pieces[next].path], pieces[next].t0);
2873 if (p[0] - end[0]).hypot(p[1] - end[1]) > 1e-8 * scale {
2874 return Err(refuse("prism_along", format!("the boundary does not close (gap {:e})", (p[0] - end[0]).hypot(p[1] - end[1]))));
2875 }
2876 used[next] = true;
2877 order.push(next);
2878 }
2879 if used.iter().any(|u| !u) {
2880 return Err(refuse("prism_along", "the swept region is not one simple loop".into()));
2881 }
2882 let world = |q: [f64; 2], h: f64| base.add(e1.scale(q[0])).add(e2.scale(q[1])).add(up.scale(h));
2884 let mut loop_points: Vec<[f64; 2]> = order.iter().map(|&k| eval(paths[pieces[k].path], pieces[k].t0)).collect();
2885 loop_points.push(loop_points[0]);
2886 let count = order.len();
2887 let mut shell = Shell::new();
2888 let (bottom_v, top_v): (Vec<u64>, Vec<u64>) = (0..count).map(|k| (shell.vertex(world(loop_points[k], from)), shell.vertex(world(loop_points[k], to)))).unzip();
2889 let mut bottom_e = Vec::with_capacity(count);
2890 let mut top_e = Vec::with_capacity(count);
2891 let mut curves2d = Vec::with_capacity(count);
2892 for (slot, &k) in order.iter().enumerate() {
2893 let piece = &pieces[k];
2894 let straight = matches!(paths[piece.path], Path::End(..));
2895 let next = (slot + 1) % count;
2896 let curve_at = |h: f64| -> Result<NurbsCurve, KernelRefusal> {
2897 if straight {
2898 return frag(make_line(world(loop_points[slot], h), world(loop_points[next], h)), "line");
2899 }
2900 let samples = 192;
2901 let ts: Vec<f64> = (0..=samples).map(|i| i as f64 / samples as f64).collect();
2902 let mut points: Vec<Vec3> = ts.iter().map(|&t| world(eval(paths[piece.path], piece.t0 + (piece.t1 - piece.t0) * t), h)).collect();
2903 points[0] = world(loop_points[slot], h);
2904 points[samples] = world(loop_points[next], h);
2905 frag(crate::interpolate_curve(&points, 5, &ts), "plane_path")
2906 };
2907 let (cb, ct) = (curve_at(from)?, curve_at(to)?);
2908 bottom_e.push(shell.edge(cb.clone(), bottom_v[slot], bottom_v[next], false)?);
2909 top_e.push(shell.edge(ct.clone(), top_v[slot], top_v[next], false)?);
2910 curves2d.push((cb, ct));
2911 }
2912 let verticals: Vec<u64> = (0..count).map(|k| shell.edge(make_line(world(loop_points[k], from), world(loop_points[k], to)).expect("line"), bottom_v[k], top_v[k], false)).collect::<Result<_, _>>()?;
2913 let top_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, to)).collect();
2915 let top_surface = plane_patch(world([0.0, 0.0], to), e1, e2, &top_points)?;
2916 let mut coedges = Vec::with_capacity(count);
2917 for k in 0..count {
2918 coedges.push((top_e[k], true, plane_pcurve(world([0.0, 0.0], to), e1, e2, &curves2d[k].1, true)?));
2919 }
2920 shell.face(top_surface, coedges);
2921 let bottom_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, from)).collect();
2922 let bottom_surface = plane_patch(world([0.0, 0.0], from), e2, e1, &bottom_points)?;
2923 let mut coedges = Vec::with_capacity(count);
2924 for k in (0..count).rev() {
2925 coedges.push((bottom_e[k], false, plane_pcurve(world([0.0, 0.0], from), e2, e1, &curves2d[k].0, false)?));
2926 }
2927 shell.face(bottom_surface, coedges);
2928 for k in 0..count {
2930 let next = (k + 1) % count;
2931 let (cb, _) = &curves2d[k];
2932 let rows: Vec<Vec<Vec4>> = cb
2933 .control_points
2934 .iter()
2935 .map(|p| {
2936 let x = p.point().expect("point");
2937 vec![Vec4::from_point(x, p.w), Vec4::from_point(x.add(up.scale(to - from)), p.w)]
2938 })
2939 .collect();
2940 let surface = frag(NurbsSurface::new(cb.degree, 1, cb.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], rows), "side")?;
2941 let [u0, u1] = frag(cb.domain(), "domain")?;
2942 let coedges = vec![
2943 (bottom_e[k], true, uv_line([u0, 0.0], [u1, 0.0])?),
2944 (verticals[next], true, uv_line([u1, 0.0], [u1, 1.0])?),
2945 (top_e[k], false, uv_line([u1, 1.0], [u0, 1.0])?),
2946 (verticals[k], false, uv_line([u0, 1.0], [u0, 0.0])?),
2947 ];
2948 shell.face(surface, coedges);
2949 }
2950 let body = shell.solid();
2951 let issues = body.validate();
2952 if !issues.is_empty() {
2953 return Err(refuse("assembly", format!("the swept prism is not valid: {:?}", issues.first())));
2954 }
2955 Ok(body)
2956}
2957
2958#[derive(Clone, Copy, Debug)]
2962pub struct TwoTurns {
2963 pub axes: [(Vec3, Vec3); 2],
2964 pub rates: [f64; 2],
2965 pub from: f64,
2966 pub to: f64,
2967}
2968
2969pub fn sweep_solid_tumbled(tool: &SolidTool, motion: &TwoTurns) -> Result<BrepSolid, KernelRefusal> {
2976 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
2977 return Err(input("range", "the motion's range must run forward".into()));
2978 }
2979 let mut points = [Vec3::new(0.0, 0.0, 0.0); 2];
2980 let mut directions = [Vec3::new(0.0, 0.0, 1.0); 2];
2981 for k in 0..2 {
2982 points[k] = motion.axes[k].0;
2983 directions[k] = motion.axes[k].1.normalized().map_err(|_| input("axis", "a turn's axis has no direction".into()))?;
2984 }
2985 let turns = Turns { points, directions, rates: motion.rates };
2986 let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
2987 return Err(refuse("tumble_tool", "a tumble sweeps a solid of revolution only".into()));
2988 };
2989 let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
2990 let base = Screw {
2991 a: points[0],
2992 e: directions[0],
2993 omega: motion.rates[0].abs() + motion.rates[1].abs(),
2994 c: 0.0,
2995 c2: 0.0,
2996 t0: motion.from,
2997 t1: motion.to,
2998 frame: None,
2999 turns: Some(turns),
3000 frozen: None,
3001 extent: points[1].sub(points[0]).length(),
3002 };
3003 let span = |t0: f64, t1: f64| -> Result<BrepSolid, KernelRefusal> {
3004 let mut screw = base;
3005 screw.t0 = t0;
3006 screw.t1 = t1;
3007 let mut last = None;
3008 for k in 0..8 {
3009 let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
3010 match lofted(&screw, *axis_origin, axis, reference, meridian) {
3011 Err(error) if matches!(refused_as(&error), Some("loft_structure") | Some("end_trim_corner")) => last = Some(error),
3012 other => return other,
3013 }
3014 }
3015 Err(last.expect("tried"))
3016 };
3017 let mut pending = vec![(motion.from, motion.to, 0usize)];
3019 let mut spans: Vec<(f64, BrepSolid)> = Vec::new();
3020 while let Some((t0, t1, depth)) = pending.pop() {
3021 match span(t0, t1) {
3022 Ok(body) => spans.push((t0, body)),
3023 Err(error) if refused_as(&error) == Some("folds") && depth < 6 => {
3024 let middle = 0.5 * (t0 + t1);
3025 pending.push((middle, t1, depth + 1));
3026 pending.push((t0, middle, depth + 1));
3027 }
3028 Err(error) => return Err(error),
3029 }
3030 }
3031 spans.sort_by(|a, b| a.0.total_cmp(&b.0));
3032 let mut bodies: Vec<BrepSolid> = spans.into_iter().map(|(_, body)| body).collect();
3033 if bodies.len() == 1 {
3034 return Ok(bodies.pop().expect("one"));
3035 }
3036 crate::boolean_operation_nary(&bodies, crate::BooleanOperation::Union)
3037}
3038
3039fn charted_pcurve(chart: &Chart, screw: &Screw, t: f64, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3042 let [d0, d1] = frag(curve.domain(), "domain")?;
3043 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3044 let mut uv = Vec::with_capacity(ts.len());
3045 for &tt in &ts {
3046 let x = screw.invert(t, frag(curve.evaluate(tt), "point")?);
3047 let [u, v] = chart.invert(x);
3048 uv.push(Vec3::new(u, v, 0.0));
3049 }
3050 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "charted_pcurve")?;
3051 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3052}
3053
3054fn projected_pcurve(surface: &NurbsSurface, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3056 let [d0, d1] = frag(curve.domain(), "domain")?;
3057 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3058 let mut uv = Vec::with_capacity(ts.len());
3059 for &t in &ts {
3060 let x = frag(curve.evaluate(t), "point")?;
3061 let foot = frag(crate::project_point_to_surface(surface, x), "project")?;
3062 if foot.distance > 1e-6 * (1.0 + x.length()) {
3063 return Err(refuse("loft_cap", format!("a lofted edge leaves its cap by {} at {x:?}", foot.distance)));
3064 }
3065 uv.push(Vec3::new(foot.u, foot.v, 0.0));
3066 }
3067 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "projected_pcurve")?;
3068 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3069}
3070
3071type Signature = (Vec<(usize, usize, bool)>, Vec<(bool, Vec<(usize, bool)>)>, Vec<(usize, bool)>);
3072
3073fn signature(split: &Split) -> Result<Signature, KernelRefusal> {
3074 Ok((
3075 split.curves.iter().map(|c| (c.start, c.end, c.degenerate)).collect(),
3076 split.caps.iter().map(|cap| (cap.front, cap.coedges.iter().map(|&(c, f, _)| (c, f)).collect())).collect(),
3077 split.grazing()?,
3078 ))
3079}
3080
3081fn lofted(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<BrepSolid, KernelRefusal> {
3082 let split_at = |t: f64| -> Result<Split, KernelRefusal> {
3083 let mut frozen = *screw;
3084 frozen.frozen = Some(t);
3085 split_revolved(&frozen, origin, axis, radial, meridian)
3086 };
3087 let lambdas: Vec<f64> = (0..=LOFT_SAMPLES).map(|i| i as f64 / LOFT_SAMPLES as f64).collect();
3088 let samples_of = |split: &Split, c: usize, back_forward: bool, t: f64| -> Result<Vec<Vec3>, KernelRefusal> {
3090 let Some(list) = &split.curves[c].samples else {
3091 return Err(refuse("loft_samples", "a grazing curve has no exact samples to loft".into()));
3092 };
3093 let mut world: Vec<Vec3> = list.iter().map(|&x| screw.apply(t, x)).collect();
3094 if back_forward {
3095 world.reverse();
3096 }
3097 Ok(world)
3098 };
3099 let mut fractions: Vec<f64> = (0..=63).map(|j| j as f64 / 63.0).collect();
3103 loop {
3104 let count = fractions.len() - 1;
3105 let times: Vec<f64> = fractions.iter().map(|&f| if f == 1.0 { screw.t1 } else { screw.t0 + (screw.t1 - screw.t0) * f }).collect();
3107 let splits: Vec<Split> = times.iter().map(|&t| split_at(t)).collect::<Result<_, _>>()?;
3108 let shape = signature(&splits[0])?;
3109 for split in &splits[1..] {
3110 if signature(split)? != shape {
3111 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3112 }
3113 }
3114 let grazing = shape.2.clone();
3115 if grazing.is_empty() {
3116 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
3117 }
3118 let scale = splits[0].scale + screw.extent;
3119 let target = 5e-10 * scale;
3120 let mut walls = Vec::with_capacity(grazing.len());
3122 for &(c, back_forward) in &grazing {
3123 let mut rows: Vec<NurbsCurve> = Vec::with_capacity(times.len());
3124 for (j, split) in splits.iter().enumerate() {
3125 let points = samples_of(split, c, back_forward, times[j])?;
3126 rows.push(frag(crate::interpolate_curve(&points, 5, &lambdas), "loft_row")?);
3127 }
3128 let knots_u = rows[0].knots.clone();
3129 let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows[0].control_points.len());
3130 let mut knots_v = Vec::new();
3131 for k in 0..rows[0].control_points.len() {
3132 let column: Vec<Vec3> = rows.iter().map(|row| row.control_points[k].point()).collect::<Result<_, _>>().map_err(|e| refuse("loft", e))?;
3133 let across = frag(crate::interpolate_curve(&column, 5, &fractions), "loft_column")?;
3134 knots_v = across.knots.clone();
3135 net.push(across.control_points.clone());
3136 }
3137 let surface = frag(NurbsSurface::new(5, 5, knots_u, knots_v, net), "loft_wall")?;
3138 walls.push((c, back_forward, surface, rows.first().cloned().expect("rows"), rows.last().cloned().expect("rows")));
3139 }
3140 let mut misses = vec![0.0f64; count];
3142 for j in 0..count {
3143 let t = 0.5 * (times[j] + times[j + 1]);
3144 let fraction = 0.5 * (fractions[j] + fractions[j + 1]);
3145 let split = split_at(t)?;
3146 if signature(&split)? != shape {
3147 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3148 }
3149 for (c, back_forward, surface, _, _) in &walls {
3150 let exact = samples_of(&split, *c, *back_forward, t)?;
3151 for (i, x) in exact.iter().enumerate() {
3152 misses[j] = misses[j].max(frag(surface.evaluate(lambdas[i], fraction), "loft_check")?.sub(*x).length());
3153 }
3154 }
3155 }
3156 let worst = misses.iter().copied().fold(0.0f64, f64::max);
3157 if worst > target {
3158 if count > 2000 {
3159 return Err(refuse("loft_accuracy", format!("the lofted wall stays {worst:e} off the grazing loop")));
3160 }
3161 let mut refined = Vec::with_capacity(2 * fractions.len());
3164 for j in 0..count {
3165 let pieces = if misses[j] > 0.5 * target { ((misses[j] / (0.25 * target)).powf(1.0 / 6.0).ceil() as usize).max(2) } else { 1 };
3166 for k in 0..pieces {
3167 refined.push(fractions[j] + (fractions[j + 1] - fractions[j]) * k as f64 / pieces as f64);
3168 }
3169 }
3170 refined.push(1.0);
3171 fractions = refined;
3172 continue;
3173 }
3174 let mut paths: HashMap<usize, Vec<Vec3>> = HashMap::new();
3176 for &(c, _) in &grazing {
3177 for r in [splits[0].curves[c].start, splits[0].curves[c].end] {
3178 paths.entry(r).or_insert_with(|| splits.iter().zip(×).map(|(split, &t)| screw.apply(t, split.points[r])).collect());
3179 }
3180 }
3181 return assemble_lofted(screw, &splits[0], &splits[count], &walls, &fractions, &paths);
3182 }
3183}
3184
3185fn wall_regular(surface: &NurbsSurface, fractions: &[f64], removed: &[&[[f64; 2]]]) -> Result<(), KernelRefusal> {
3193 const COLUMNS: usize = 4 * LOFT_SAMPLES;
3194 let mut rows: Vec<f64> = Vec::with_capacity(2 * fractions.len());
3195 for j in 0..fractions.len() {
3196 rows.push(fractions[j]);
3197 if j + 1 < fractions.len() {
3198 rows.push(0.5 * (fractions[j] + fractions[j + 1]));
3199 }
3200 }
3201 let normal = |u: f64, v: f64| -> Result<Option<Vec3>, KernelRefusal> {
3202 let h = 1e-6;
3203 let (ua, ub, va, vb) = ((u - h).max(0.0), (u + h).min(1.0), (v - h).max(0.0), (v + h).min(1.0));
3204 let du = frag(surface.evaluate(ub, v), "regular")?.sub(frag(surface.evaluate(ua, v), "regular")?).scale(1.0 / (ub - ua));
3205 let dv = frag(surface.evaluate(u, vb), "regular")?.sub(frag(surface.evaluate(u, va), "regular")?).scale(1.0 / (vb - va));
3206 Ok(du.cross(dv).normalized().ok())
3207 };
3208 let mut previous: Option<Vec<Option<Vec3>>> = None;
3209 for &v in &rows {
3210 let row: Vec<Option<Vec3>> = (0..=COLUMNS)
3212 .map(|i| {
3213 let u = i as f64 / COLUMNS as f64;
3214 if removed.iter().any(|ring| inside_ring(ring, [u, v]) || near_ring(ring, [u, v], 4.0 / COLUMNS as f64)) { Ok(None) } else { normal(u, v) }
3215 })
3216 .collect::<Result<_, _>>()?;
3217 for i in 0..COLUMNS {
3218 if let (Some(a), Some(b)) = (row[i], row[i + 1]) {
3219 if a.dot(b) < -0.5 {
3220 return Err(refuse("wall_folds", format!("a lofted wall turns over (an edge of regression near parameter ({:.4}, {v:.4})); the sweep folds onto itself", i as f64 / COLUMNS as f64)));
3221 }
3222 }
3223 if let Some(Some(Some(a))) = previous.as_ref().map(|p| p.get(i).copied()) {
3224 if let Some(b) = row[i] {
3225 if a.dot(b) < -0.5 {
3226 return Err(refuse("wall_folds", format!("a lofted wall turns over (an edge of regression near parameter ({:.4}, {v:.4})); the sweep folds onto itself", i as f64 / COLUMNS as f64)));
3227 }
3228 }
3229 }
3230 }
3231 previous = Some(row);
3232 }
3233 Ok(())
3234}
3235
3236struct EndTrim {
3243 lambdas: [f64; 2],
3244 points: [Vec3; 2],
3245 cut: NurbsCurve,
3246 before: NurbsCurve,
3247 after: NurbsCurve,
3248 chain: Vec<(TrimPiece, bool, NurbsCurve)>,
3249 slot: Option<(NurbsCurve, Vec3)>,
3250 removed: Vec<[f64; 2]>,
3251}
3252
3253#[derive(Clone, Copy, PartialEq, Eq, Debug)]
3256enum TrimPiece {
3257 Cut,
3258 Slot,
3259}
3260
3261fn end_trim(screw: &Screw, split: &Split, t: f64, wall: &NurbsSurface, end: usize) -> Result<Option<EndTrim>, KernelRefusal> {
3272 let v_of = |e: f64| if end == 1 { 1.0 - e } else { e };
3273 let at = |u: f64, e: f64| -> Result<Vec3, KernelRefusal> { frag(wall.evaluate(u.clamp(0.0, 1.0), v_of(e)), "wall") };
3274 let f = |u: f64, e: f64| -> Result<f64, KernelRefusal> { Ok(split.distance(screw.invert(t, at(u, e)?)) / (e * e)) };
3275 let floor = 1e-3;
3277 let window = 0.25;
3278 let noise = 1e-9 * split.scale / (floor * floor);
3279 let n = 2000;
3281 let edge: Vec<f64> = (0..=n).map(|i| f(i as f64 / n as f64, floor)).collect::<Result<_, _>>()?;
3282 let inside: Vec<bool> = edge.iter().map(|&x| x < -noise).collect();
3283 if !inside.iter().any(|&x| x) {
3284 return pierced(&f, floor, window, noise, None);
3286 }
3287 if inside[0] || inside[n] || inside[1] || inside[n - 1] {
3288 return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
3289 }
3290 let mut crossings: Vec<f64> = Vec::new();
3292 for i in 0..n {
3293 if inside[i] != inside[i + 1] {
3294 let (mut a, mut b) = (i as f64 / n as f64, (i + 1) as f64 / n as f64);
3295 let a_inside = inside[i];
3296 for _ in 0..60 {
3297 let mid = 0.5 * (a + b);
3298 if (f(mid, floor)? < 0.0) == a_inside { a = mid } else { b = mid }
3299 }
3300 crossings.push(0.5 * (a + b));
3301 }
3302 }
3303 if crossings.len() != 2 {
3304 return Err(refuse("end_trim", format!("an end pose bulges past a wall in {} places", crossings.len() / 2)));
3305 }
3306 let gradient = |u: f64, e: f64| -> Result<[f64; 2], KernelRefusal> {
3309 let h = 1e-6;
3310 Ok([(f(u + h, e)? - f(u - h, e)?) / (2.0 * h), (f(u, e + h)? - f(u, e - h)?) / (2.0 * h)])
3311 };
3312 let settle = |mut p: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
3313 for _ in 0..30 {
3314 let value = f(p[0], p[1])?;
3315 let g = gradient(p[0], p[1])?;
3316 let norm = g[0] * g[0] + g[1] * g[1];
3317 if norm == 0.0 {
3318 return Err(refuse("end_trim", "the cut of an end pose has a flat point".into()));
3319 }
3320 p = [p[0] - value * g[0] / norm, p[1] - value * g[1] / norm];
3321 if value.abs() * p[1] * p[1] < 1e-14 * split.scale {
3322 break;
3323 }
3324 }
3325 Ok(p)
3326 };
3327 let step = 1e-3;
3328 let mut points: Vec<[f64; 2]> = vec![[crossings[0], floor]];
3329 let mut heading = [0.0, 1.0];
3330 loop {
3331 let last = *points.last().expect("points");
3332 let g = gradient(last[0], last[1])?;
3333 let length = g[0].hypot(g[1]);
3334 let mut tangent = [-g[1] / length, g[0] / length];
3335 if tangent[0] * heading[0] + tangent[1] * heading[1] < 0.0 {
3336 tangent = [-tangent[0], -tangent[1]];
3337 }
3338 let next = settle([last[0] + step * tangent[0], last[1] + step * tangent[1]])?;
3339 if next[1] < floor {
3340 break;
3341 }
3342 if next[0] <= 1e-3 || next[0] >= 1.0 - 1e-3 {
3343 return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
3344 }
3345 if next[1] > window {
3346 return Err(refuse("end_trim", "an end pose holds a deep part of a wall".into()));
3347 }
3348 if points.len() > 20_000 {
3349 return Err(refuse("end_trim", "the cut of an end pose does not close".into()));
3350 }
3351 heading = [next[0] - last[0], next[1] - last[1]];
3352 points.push(next);
3353 }
3354 let back = *points.last().expect("points");
3355 if (back[0] - crossings[1]).abs() > 2.0 * step {
3356 return Err(refuse("end_trim", "the cut of an end pose does not come back where it should".into()));
3357 }
3358 points.push([crossings[1], floor]);
3359 let to_end = |p: [f64; 2], q: [f64; 2]| -> [f64; 2] {
3361 let slope = (q[0] - p[0]) / (q[1] - p[1]);
3362 [p[0] - slope * p[1], 0.0]
3363 };
3364 let (first, last) = (to_end(points[0], points[1]), to_end(points[points.len() - 1], points[points.len() - 2]));
3365 points.insert(0, first);
3366 points.push(last);
3367 if points[0][0] > points[points.len() - 1][0] {
3368 points.reverse();
3369 }
3370 pierced(&f, floor, window, noise, Some(&points))?;
3371 let chart = |p: [f64; 2]| [p[0], v_of(p[1])];
3372 let chart_lengths = |chart_points: &[[f64; 2]]| -> Vec<f64> {
3377 let mut params = vec![0.0];
3378 for k in 1..chart_points.len() {
3379 let gap = (chart_points[k][0] - chart_points[k - 1][0]).hypot(chart_points[k][1] - chart_points[k - 1][1]);
3380 params.push(params[k - 1] + gap.max(1e-15));
3381 }
3382 params
3383 };
3384 let curve_through = |chart_points: &[[f64; 2]], params: &[f64], what: &'static str| -> Result<(NurbsCurve, NurbsCurve), KernelRefusal> {
3385 let total = *params.last().expect("params");
3386 let params: Vec<f64> = params.iter().map(|x| x / total).collect();
3387 let world: Vec<Vec3> = chart_points.iter().map(|p| at(p[0], p[1])).collect::<Result<_, _>>()?;
3388 let uv: Vec<Vec3> = chart_points.iter().map(|p| Vec3::new(p[0], v_of(p[1]), 0.0)).collect();
3389 let (curve, pcurve) = (frag(crate::interpolate_curve(&world, 5, ¶ms), what)?, frag(crate::interpolate_curve(&uv, 5, ¶ms), what)?);
3390 let mut worst = 0.0f64;
3392 for k in 0..params.len() - 1 {
3393 let s = 0.5 * (params[k] + params[k + 1]);
3394 let q = frag(pcurve.evaluate(s), "pcurve")?;
3395 let e = if end == 1 { 1.0 - q.y } else { q.y };
3396 worst = worst.max(at(q.x, e)?.sub(frag(curve.evaluate(s), "curve")?).length());
3397 }
3398 if worst > 1e-7 * split.scale {
3399 return Err(refuse("end_trim", format!("a trim curve's pcurve leaves it by {worst:e} between its points")));
3400 }
3401 Ok((curve, pcurve))
3402 };
3403 let piece = |a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
3404 let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
3405 let along: Vec<Vec3> = ts.iter().map(|&s| at(a + (b - a) * s, 0.0)).collect::<Result<_, _>>()?;
3406 frag(crate::interpolate_curve(&along, 5, &ts), "loop_piece")
3407 };
3408 if let Some((k, ua, b)) = double_start(&at, &points, floor)? {
3413 let (a_side, b_side) = double_curve(&at, [ua, 0.0], b, window)?;
3414 let low = ua < points[0][0];
3417 if !low && ua <= points[points.len() - 1][0] {
3418 return Err(refuse("end_trim", "the double curve of a wall leaves its end inside the cut".into()));
3419 }
3420 let mut kept: Vec<[f64; 2]> = if low { points[k..].to_vec() } else { points[..=k].to_vec() };
3421 if low { kept[0] = b } else { *kept.last_mut().expect("kept") = b }
3422 let (cut, cut_pcurve) = curve_through(&kept, &chart_lengths(&kept), "cut")?;
3423 let mut sigma = vec![0.0];
3426 for k in 1..a_side.len() {
3427 let step = (a_side[k][0] - a_side[k - 1][0]).hypot(a_side[k][1] - a_side[k - 1][1]).hypot((b_side[k][0] - b_side[k - 1][0]).hypot(b_side[k][1] - b_side[k - 1][1]));
3428 sigma.push(sigma[k - 1] + step.max(1e-15));
3429 }
3430 let (slot, pa) = curve_through(&a_side, &sigma, "double_curve")?;
3431 let (_, pb) = curve_through(&b_side, &sigma, "double_curve")?;
3432 let tip = at(a_side[a_side.len() - 1][0], a_side[a_side.len() - 1][1])?;
3433 let chain = if low {
3435 vec![(TrimPiece::Slot, true, pa), (TrimPiece::Slot, false, frag(pb.reversed(), "reversed")?), (TrimPiece::Cut, true, cut_pcurve)]
3436 } else {
3437 vec![(TrimPiece::Cut, true, cut_pcurve), (TrimPiece::Slot, true, pb), (TrimPiece::Slot, false, frag(pa.reversed(), "reversed")?)]
3438 };
3439 let lambdas = if low { [ua, points[points.len() - 1][0]] } else { [points[0][0], ua] };
3440 let mut removed: Vec<[f64; 2]> = Vec::new();
3441 if low {
3442 removed.extend(a_side.iter().map(|&p| chart(p)));
3443 removed.extend(b_side.iter().rev().map(|&p| chart(p)));
3444 removed.extend(kept.iter().map(|&p| chart(p)));
3445 } else {
3446 removed.extend(kept.iter().map(|&p| chart(p)));
3447 removed.extend(b_side.iter().map(|&p| chart(p)));
3448 removed.extend(a_side.iter().rev().map(|&p| chart(p)));
3449 }
3450 let dropped: Vec<[f64; 2]> = if low { points[..k].to_vec() } else { points[k + 1..].to_vec() };
3452 for p in dropped.iter().filter(|p| p[1] > 2.0 * floor && (p[0] - b[0]).hypot(p[1] - b[1]) > 5e-3) {
3453 if !inside_ring(&removed, chart(*p)) {
3454 return Err(refuse("end_trim", "the cut of an end pose leaves the part of the wall its double curve takes away".into()));
3455 }
3456 }
3457 let points3 = [at(lambdas[0], 0.0)?, at(lambdas[1], 0.0)?];
3458 return Ok(Some(EndTrim { lambdas, points: points3, cut, before: piece(0.0, lambdas[0])?, after: piece(lambdas[1], 1.0)?, chain, slot: Some((slot, tip)), removed }));
3459 }
3460 let (cut, wall_pcurve) = curve_through(&points, &chart_lengths(&points), "cut")?;
3461 let (la, lb) = (points[0][0], points[points.len() - 1][0]);
3462 let removed: Vec<[f64; 2]> = points.iter().map(|&p| chart(p)).collect();
3463 Ok(Some(EndTrim {
3464 lambdas: [la, lb],
3465 points: [at(la, 0.0)?, at(lb, 0.0)?],
3466 cut,
3467 before: piece(0.0, la)?,
3468 after: piece(lb, 1.0)?,
3469 chain: vec![(TrimPiece::Cut, true, wall_pcurve)],
3470 slot: None,
3471 removed,
3472 }))
3473}
3474
3475fn near_ring(ring: &[[f64; 2]], q: [f64; 2], reach: f64) -> bool {
3477 (0..ring.len()).any(|k| {
3478 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
3479 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
3480 let length = dx * dx + dy * dy;
3481 let s = if length > 0.0 { (((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / length).clamp(0.0, 1.0) } else { 0.0 };
3482 (q[0] - a[0] - s * dx).hypot(q[1] - a[1] - s * dy) < reach
3483 })
3484}
3485
3486fn inside_ring(ring: &[[f64; 2]], q: [f64; 2]) -> bool {
3488 let mut crossings = 0;
3489 for k in 0..ring.len() {
3490 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
3491 if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
3492 crossings += 1;
3493 }
3494 }
3495 crossings % 2 == 1
3496}
3497
3498fn double_start(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, points: &[[f64; 2]], floor: f64) -> Result<Option<(usize, f64, [f64; 2])>, KernelRefusal> {
3503 let n = 4000;
3504 let row: Vec<Vec3> = (0..=n).map(|i| at(i as f64 / n as f64, 0.0)).collect::<Result<_, _>>()?;
3505 let scale = row.iter().fold(0.0f64, |m, p| m.max(p.sub(row[0]).length())).max(1e-300);
3506 let mut best: Option<(f64, usize, usize)> = None;
3507 for (k, p) in points.iter().enumerate() {
3508 if p[1] < 4.0 * floor {
3509 continue;
3510 }
3511 let x = at(p[0], p[1])?;
3512 let (i, d) = row.iter().enumerate().map(|(i, q)| (i, q.sub(x).length())).fold((0, f64::INFINITY), |a, b| if b.1 < a.1 { b } else { a });
3513 if best.map_or(true, |b| d / p[1] < b.0 / points[b.1][1]) {
3516 best = Some((d, k, i));
3517 }
3518 }
3519 let Some((distance, k, i)) = best else { return Ok(None) };
3520 if distance > 5e-3 * scale {
3522 return Ok(None);
3523 }
3524 let mut x = [points[k][0], points[k][1], i as f64 / n as f64];
3526 let h = 1e-7;
3527 for _ in 0..40 {
3528 let g = at(x[0], x[1])?.sub(at(x[2], 0.0)?);
3529 if g.length() < 1e-13 * scale {
3530 if x[1] < 2.0 * floor || !(0.0..=1.0).contains(&x[0]) || !(0.0..=1.0).contains(&x[2]) {
3532 return Ok(None);
3533 }
3534 return Ok(Some((k, x[2], [x[0], x[1]])));
3535 }
3536 let c0 = at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h);
3537 let c1 = at(x[0], x[1] + h)?.sub(at(x[0], x[1] - h)?).scale(0.5 / h);
3538 let c2 = at(x[2] + h, 0.0)?.sub(at(x[2] - h, 0.0)?).scale(-0.5 / h);
3539 let det = c0.dot(c1.cross(c2));
3540 if det.abs() < 1e-300 {
3541 break;
3542 }
3543 let r = g.scale(-1.0);
3545 let d = [r.dot(c1.cross(c2)) / det, c0.dot(r.cross(c2)) / det, c0.dot(c1.cross(r)) / det];
3546 x = [x[0] + d[0], x[1] + d[1], x[2] + d[2]];
3547 }
3548 if distance < 1e-6 * scale {
3549 return Err(refuse("end_trim", "an end pose's cut meets its own loop, but not exactly enough to follow".into()));
3550 }
3551 Ok(None)
3552}
3553
3554fn double_curve(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, a: [f64; 2], b: [f64; 2], window: f64) -> Result<(Vec<[f64; 2]>, Vec<[f64; 2]>), KernelRefusal> {
3559 let h = 1e-7;
3560 let scale = at(0.0, 0.0)?.sub(at(1.0, 0.0)?).length().max(at(0.5, 0.0)?.sub(at(0.0, 0.0)?).length()).max(1e-300);
3561 let jacobian = |x: [f64; 4]| -> Result<[Vec3; 4], KernelRefusal> {
3562 Ok([
3563 at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h),
3564 at(x[0], x[1] + h)?.sub(at(x[0], (x[1] - h).max(0.0))?).scale(1.0 / (h + x[1].min(h))),
3565 at(x[2] + h, x[3])?.sub(at(x[2] - h, x[3])?).scale(-0.5 / h),
3566 at(x[2], x[3] + h)?.sub(at(x[2], (x[3] - h).max(0.0))?).scale(-1.0 / (h + x[3].min(h))),
3567 ])
3568 };
3569 let residual = |x: [f64; 4]| -> Result<Vec3, KernelRefusal> { Ok(at(x[0], x[1])?.sub(at(x[2], x[3])?)) };
3570 let null = |j: &[Vec3; 4]| -> [f64; 4] {
3572 let minor = |p: usize, q: usize, r: usize| j[p].dot(j[q].cross(j[r]));
3573 let n = [minor(1, 2, 3), -minor(0, 2, 3), minor(0, 1, 3), -minor(0, 1, 2)];
3574 let length = n.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-300);
3575 [n[0] / length, n[1] / length, n[2] / length, n[3] / length]
3576 };
3577 let correct = |mut x: [f64; 4]| -> Result<Option<[f64; 4]>, KernelRefusal> {
3579 for _ in 0..30 {
3580 let g = residual(x)?;
3581 if g.length() < 1e-13 * scale {
3582 return Ok(Some(x));
3583 }
3584 let j = jacobian(x)?;
3585 let mut m = [[0.0; 3]; 3];
3587 let rows = [[j[0].x, j[1].x, j[2].x, j[3].x], [j[0].y, j[1].y, j[2].y, j[3].y], [j[0].z, j[1].z, j[2].z, j[3].z]];
3588 for p in 0..3 {
3589 for q in 0..3 {
3590 m[p][q] = (0..4).map(|c| rows[p][c] * rows[q][c]).sum();
3591 }
3592 }
3593 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
3594 if det.abs() < 1e-300 {
3595 return Ok(None);
3596 }
3597 let r = [-g.x, -g.y, -g.z];
3598 let solve = |col: usize| -> f64 {
3599 let mut a = m;
3600 for p in 0..3 {
3601 a[p][col] = r[p];
3602 }
3603 (a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0]) + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])) / det
3604 };
3605 let y = [solve(0), solve(1), solve(2)];
3606 for c in 0..4 {
3607 x[c] += (0..3).map(|p| rows[p][c] * y[p]).sum::<f64>();
3608 }
3609 }
3610 Ok(None)
3611 };
3612 let step = 2e-3;
3613 let mut x = [a[0], a[1], b[0], b[1]];
3614 let mut a_side = vec![a];
3615 let mut b_side = vec![b];
3616 let mut heading: Option<[f64; 4]> = None;
3617 loop {
3618 let separation = (x[0] - x[2]).hypot(x[1] - x[3]);
3619 if separation < 3.0 * step && a_side.len() > 2 {
3620 let tip = [0.5 * (x[0] + x[2]), 0.5 * (x[1] + x[3])];
3621 a_side.push(tip);
3622 b_side.push(tip);
3623 return Ok((a_side, b_side));
3624 }
3625 let mut t = null(&jacobian(x)?);
3626 let reference = heading.unwrap_or([0.0, 1.0, 0.0, 0.0]);
3627 if (0..4).map(|c| t[c] * reference[c]).sum::<f64>() < 0.0 {
3628 t = [-t[0], -t[1], -t[2], -t[3]];
3629 }
3630 let mut next = None;
3631 let mut length = step;
3632 for _ in 0..6 {
3633 let guess = [x[0] + length * t[0], x[1] + length * t[1], x[2] + length * t[2], x[3] + length * t[3]];
3634 if let Some(y) = correct(guess)? {
3635 next = Some(y);
3636 break;
3637 }
3638 length *= 0.5;
3639 }
3640 let Some(y) = next else {
3641 return Err(refuse("end_trim", format!("the double curve of a wall cannot be followed (sides {separation:e} apart)")));
3642 };
3643 if [y[0], y[2]].iter().any(|&u| u <= 1e-3 || u >= 1.0 - 1e-3) {
3644 return Err(refuse("end_trim_corner", "the double curve of a wall reaches a corner of its grazing loop".into()));
3645 }
3646 if y[1] > window || y[3] > window || y[1] < 0.0 || y[3] < 0.0 {
3647 return Err(refuse("end_trim", "the double curve of a wall runs deep into the motion or back past its end".into()));
3648 }
3649 if a_side.len() > 20_000 {
3650 return Err(refuse("end_trim", "the double curve of a wall does not close".into()));
3651 }
3652 heading = Some([y[0] - x[0], y[1] - x[1], y[2] - x[2], y[3] - x[3]]);
3653 x = y;
3654 a_side.push([x[0], x[1]]);
3655 b_side.push([x[2], x[3]]);
3656 }
3657}
3658
3659fn pierced(
3662 f: &dyn Fn(f64, f64) -> Result<f64, KernelRefusal>,
3663 floor: f64,
3664 window: f64,
3665 noise: f64,
3666 cut: Option<&[[f64; 2]]>,
3667) -> Result<Option<EndTrim>, KernelRefusal> {
3668 let enclosed = |q: [f64; 2]| -> bool {
3669 let Some(ring) = cut else { return false };
3670 let mut crossings = 0;
3671 for k in 0..ring.len() {
3672 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
3673 if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
3674 crossings += 1;
3675 }
3676 }
3677 crossings % 2 == 1
3678 };
3679 for i in 1..200 {
3680 for j in 1..=40 {
3681 let (u, e) = (i as f64 / 200.0, floor + (window - floor) * (j as f64 / 40.0).powi(2));
3682 if f(u, e)? * e * e < -noise * floor * floor && !enclosed([u, e]) {
3684 return Err(refuse("end_trim", "an end pose holds a part of a wall away from its end".into()));
3685 }
3686 }
3687 }
3688 Ok(None)
3689}
3690
3691#[allow(clippy::too_many_arguments)]
3692fn assemble_lofted(
3693 screw: &Screw,
3694 first: &Split,
3695 last: &Split,
3696 walls: &[(usize, bool, NurbsSurface, NurbsCurve, NurbsCurve)],
3697 times: &[f64],
3698 paths: &HashMap<usize, Vec<Vec3>>,
3699) -> Result<BrepSolid, KernelRefusal> {
3700 let mut shell = Shell::new();
3701 let ends = [(first, screw.t0), (last, screw.t1)];
3702 let mut trims: HashMap<(usize, usize), EndTrim> = HashMap::new();
3706 for (index, (_, _, surface, _, _)) in walls.iter().enumerate() {
3707 for end in 0..2 {
3708 if let Some(trim) = end_trim(screw, ends[end].0, ends[end].1, surface, end)? {
3709 trims.insert((index, end), trim);
3710 }
3711 }
3712 let removed: Vec<&[[f64; 2]]> = (0..2).filter_map(|end| trims.get(&(index, end)).map(|trim| trim.removed.as_slice())).collect();
3713 wall_regular(surface, times, &removed)?;
3714 }
3715 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
3716 let mut vertex = |shell: &mut Shell, end: usize, p: usize| -> u64 {
3717 *vertex_at.entry((end, p)).or_insert_with(|| shell.vertex(screw.apply(ends[end].1, ends[end].0.points[p])))
3718 };
3719 let grazing: HashMap<usize, usize> = walls.iter().enumerate().map(|(index, w)| (w.0, index)).collect();
3720 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
3723 let mut edge_curve: HashMap<(usize, usize), NurbsCurve> = HashMap::new();
3724 let mut trim_edges: HashMap<(usize, usize), [u64; 4]> = HashMap::new();
3725 for end in 0..2 {
3726 let (split, t) = ends[end];
3727 for (c, tool_curve) in split.curves.iter().enumerate() {
3728 let used = split.caps.iter().any(|cap| cap.front == (end == 1) && cap.coedges.iter().any(|e| e.0 == c));
3729 if !used {
3730 continue;
3731 }
3732 if let Some(&index) = grazing.get(&c) {
3733 if let Some(trim) = trims.get(&(index, end)) {
3734 let back_forward = walls[index].1;
3736 let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
3737 let (vf, vt) = (vertex(&mut shell, end, from), vertex(&mut shell, end, to));
3738 let (va, vb) = (shell.vertex(trim.points[0]), shell.vertex(trim.points[1]));
3739 let ka = shell.edge(trim.before.clone(), vf, va, false)?;
3740 let cut = shell.edge(trim.cut.clone(), va, vb, false)?;
3741 let kb = shell.edge(trim.after.clone(), vb, vt, false)?;
3742 let slot = match &trim.slot {
3746 Some((curve, tip)) => {
3747 let from = if trim.chain[0].0 == TrimPiece::Slot { va } else { vb };
3748 let tip = shell.vertex(*tip);
3749 shell.edge(curve.clone(), from, tip, false)?
3750 }
3751 None => u64::MAX,
3752 };
3753 trim_edges.insert((index, end), [ka, cut, kb, slot]);
3754 continue;
3755 }
3756 }
3757 let curve = match grazing.get(&c) {
3758 Some(&index) => {
3759 let (_, back_forward, _, row0, row1) = &walls[index];
3760 let row = if end == 0 { row0 } else { row1 };
3761 if *back_forward { frag(row.reversed(), "reversed")? } else { row.clone() }
3762 }
3763 None => screw.move_curve(t, &tool_curve.curve)?,
3764 };
3765 let (a, b) = (vertex(&mut shell, end, tool_curve.start), vertex(&mut shell, end, tool_curve.end));
3766 let edge = shell.edge(curve.clone(), a, b, tool_curve.degenerate)?;
3767 edge_at.insert((end, c), edge);
3768 edge_curve.insert((end, c), curve);
3769 }
3770 }
3771 for end in 0..2 {
3772 let (split, t) = ends[end];
3773 for cap in split.caps.iter().filter(|cap| cap.front == (end == 1)) {
3774 let surface = screw.move_surface(t, &cap.surface)?;
3775 let mut coedges = Vec::with_capacity(cap.coedges.len());
3776 for (c, forward, pcurve) in &cap.coedges {
3777 if let Some(&index) = grazing.get(c) {
3778 if let (Some(trim), Some(edges)) = (trims.get(&(index, end)), trim_edges.get(&(index, end))) {
3779 let Some(chart) = cap.chart.as_ref() else {
3780 return Err(refuse("end_trim", "an end pose bulges past a wall beside a cap without a chart".into()));
3781 };
3782 let along = *forward != walls[index].1;
3785 let pieces = [(edges[0], &trim.before), (edges[1], &trim.cut), (edges[2], &trim.after)];
3786 let order: Vec<usize> = if along { vec![0, 1, 2] } else { vec![2, 1, 0] };
3787 for k in order {
3788 coedges.push((pieces[k].0, along, charted_pcurve(chart, screw, t, pieces[k].1, along)?));
3789 }
3790 continue;
3791 }
3792 }
3793 let pcurve = if grazing.contains_key(c) && cap.chart.is_some() {
3794 charted_pcurve(cap.chart.as_ref().expect("chart"), screw, t, &edge_curve[&(end, *c)], *forward)?
3795 } else if grazing.contains_key(c) {
3796 match projected_pcurve(&surface, &edge_curve[&(end, *c)], *forward) {
3797 Ok(p) => p,
3798 Err(error) => {
3799 let samples = split.curves[*c].samples.as_ref().map(|list| {
3801 list.iter()
3802 .map(|&x| crate::project_point_to_surface(&surface, screw.apply(t, x)).map(|f| f.distance).unwrap_or(f64::NAN))
3803 .fold(0.0f64, f64::max)
3804 });
3805 let line = split.curves[*c].curve.degree;
3806 return Err(KernelRefusal::unsupported(
3807 KernelStage::Classify,
3808 "loft_cap",
3809 format!("{} (curve {c} of degree {line}, end {end}, its exact samples off the cap by {samples:?}, cap degrees {}x{})", error.message, surface.degree_u, surface.degree_v),
3810 ));
3811 }
3812 }
3813 } else {
3814 pcurve.clone()
3815 };
3816 coedges.push((edge_at[&(end, *c)], *forward, pcurve));
3817 }
3818 shell.face(surface, coedges);
3819 }
3820 }
3821 let mut orbits: HashMap<usize, u64> = HashMap::new();
3824 let mut keys: Vec<usize> = paths.keys().copied().collect();
3825 keys.sort_unstable();
3826 for r in keys {
3827 let curve = frag(crate::interpolate_curve(&paths[&r], 5, times), "orbit")?;
3828 let (a, b) = (vertex(&mut shell, 0, r), vertex(&mut shell, 1, r));
3829 orbits.insert(r, shell.edge(curve, a, b, false)?);
3830 }
3831 for (index, (c, back_forward, surface, _, _)) in walls.iter().enumerate() {
3832 let tool_curve = &first.curves[*c];
3833 let (from, to) = if *back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
3834 let mut coedges = Vec::new();
3835 match (trims.get(&(index, 0)), trim_edges.get(&(index, 0))) {
3836 (Some(trim), Some(edges)) => {
3837 coedges.push((edges[0], true, uv_line([0.0, 0.0], [trim.lambdas[0], 0.0])?));
3838 for (piece, forward, pcurve) in &trim.chain {
3839 let edge = if *piece == TrimPiece::Cut { edges[1] } else { edges[3] };
3840 coedges.push((edge, *forward, pcurve.clone()));
3841 }
3842 coedges.push((edges[2], true, uv_line([trim.lambdas[1], 0.0], [1.0, 0.0])?));
3843 }
3844 _ => coedges.push((edge_at[&(0, *c)], !*back_forward, uv_line([0.0, 0.0], [1.0, 0.0])?)),
3845 }
3846 coedges.push((orbits[&to], true, uv_line([1.0, 0.0], [1.0, 1.0])?));
3847 match (trims.get(&(index, 1)), trim_edges.get(&(index, 1))) {
3848 (Some(trim), Some(edges)) => {
3849 coedges.push((edges[2], false, uv_line([1.0, 1.0], [trim.lambdas[1], 1.0])?));
3850 for (piece, forward, pcurve) in trim.chain.iter().rev() {
3851 let edge = if *piece == TrimPiece::Cut { edges[1] } else { edges[3] };
3852 coedges.push((edge, !*forward, frag(pcurve.reversed(), "reversed")?));
3853 }
3854 coedges.push((edges[0], false, uv_line([trim.lambdas[0], 1.0], [0.0, 1.0])?));
3855 }
3856 _ => coedges.push((edge_at[&(1, *c)], *back_forward, uv_line([1.0, 1.0], [0.0, 1.0])?)),
3857 }
3858 coedges.push((orbits[&from], false, uv_line([0.0, 1.0], [0.0, 0.0])?));
3859 shell.face(surface.clone(), coedges);
3860 }
3861 let body = shell.solid();
3862 let issues = body.validate();
3863 if !issues.is_empty() {
3864 let mut notes = Vec::new();
3867 for face in body.shells.iter().flat_map(|s| &s.faces) {
3868 for coedge in &face.loops[0].coedges {
3869 let edge = body.edges.iter().find(|e| e.id == coedge.edge_id).expect("edge");
3870 let [p0, p1] = frag(coedge.pcurve.domain(), "domain")?;
3871 let a = frag(coedge.pcurve.evaluate(p0), "uv")?;
3872 let on = frag(face.surface.evaluate_extended(a.x, a.y), "on")?;
3873 let at = frag(edge.curve.evaluate(if coedge.forward { edge.t0 } else { edge.t1 }), "at")?;
3874 if on.sub(at).length() > 1e-6 {
3875 notes.push(format!("face {} (degrees {}x{}) coedge on edge {} starts {:?} on the surface, its edge {:?}", face.id, face.surface.degree_u, face.surface.degree_v, edge.id, on, at));
3876 }
3877 let _ = p1;
3878 }
3879 }
3880 return Err(refuse("assembly", format!("the lofted sweep is not valid: {:?}; {}", issues.first(), notes.iter().take(4).cloned().collect::<Vec<_>>().join("; "))));
3881 }
3882 check_single_pass(screw, first, &body)?;
3883 Ok(body)
3884}
3885
3886fn covered(screw: &Screw, split: &Split, y: Vec3, margin: f64) -> Result<bool, KernelRefusal> {
3889 let reach = y.sub(screw.a).length() + split.scale + screw.extent;
3890 let speed = (screw.omega.abs() * reach + screw.rate_bound()).max(1e-300);
3891 let floor = (screw.t1 - screw.t0) * 1e-7;
3892 let mut t = screw.t0;
3893 let mut steps = 0usize;
3894 loop {
3895 let d = split.distance(screw.invert(t, y));
3896 if d < -margin {
3897 return Ok(true);
3898 }
3899 if t >= screw.t1 {
3900 return Ok(false);
3901 }
3902 if steps > 2_000_000 {
3903 return Err(refuse("probe_budget", "the one-pass check could not walk the motion past a point near the sweep".into()));
3904 }
3905 t = (t + ((d.abs() + margin) / speed).max(floor)).min(screw.t1);
3906 steps += 1;
3907 }
3908}
3909
3910fn check_single_pass(screw: &Screw, split: &Split, body: &BrepSolid) -> Result<(), KernelRefusal> {
3912 let step = 1e-6 * split.scale;
3913 let margin = 1e-9 * split.scale;
3914 for face in body.shells.iter().flat_map(|s| &s.faces) {
3915 let surface = &face.surface;
3916 let mut corners: Vec<[f64; 2]> = Vec::new();
3920 for coedge in &face.loops[0].coedges {
3921 let domain = frag(coedge.pcurve.domain(), "domain")?;
3922 for k in 0..4 {
3923 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 4.0), "pcurve")?;
3924 corners.push([p.x, p.y]);
3925 }
3926 }
3927 let middle = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
3928 let middle = [middle[0] / corners.len() as f64, middle[1] / corners.len() as f64];
3929 let mut probes: Vec<[f64; 2]> = vec![middle];
3930 for c in &corners {
3931 for w in [0.25, 0.5, 0.75, 0.9, 0.99] {
3932 probes.push([middle[0] + (c[0] - middle[0]) * w, middle[1] + (c[1] - middle[1]) * w]);
3933 }
3934 }
3935 if surface.degree_v == 5 {
3936 let [u0, u1] = frag(surface.domain_u(), "domain")?;
3937 for i in 1..8 {
3938 for j in 1..64 {
3939 probes.push([u0 + (u1 - u0) * i as f64 / 8.0, j as f64 / 64.0]);
3940 }
3941 }
3942 }
3943 let ([u0, u1], [v0, v1]) = (frag(surface.domain_u(), "domain")?, frag(surface.domain_v(), "domain")?);
3944 let mut ring: Vec<[f64; 2]> = Vec::new();
3947 for coedge in &face.loops[0].coedges {
3948 let domain = frag(coedge.pcurve.domain(), "domain")?;
3949 for k in 0..32 {
3950 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 32.0), "pcurve")?;
3951 ring.push([p.x, p.y]);
3952 }
3953 }
3954 let (lo, hi) = ring.iter().fold(([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]), |(lo, hi), q| ([lo[0].min(q[0]), lo[1].min(q[1])], [hi[0].max(q[0]), hi[1].max(q[1])]));
3955 for i in 1..12 {
3956 for j in 1..12 {
3957 probes.push([lo[0] + (hi[0] - lo[0]) * i as f64 / 12.0, lo[1] + (hi[1] - lo[1]) * j as f64 / 12.0]);
3958 }
3959 }
3960 let inside = |q: [f64; 2]| -> bool {
3961 let mut crossings = 0;
3962 for k in 0..ring.len() {
3963 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
3964 if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
3965 crossings += 1;
3966 }
3967 }
3968 crossings % 2 == 1
3969 };
3970 for [u, v] in probes.into_iter().filter(|&q| inside(q)) {
3971 let p = frag(surface.evaluate(u, v), "probe")?;
3972 let (hu, hv) = (1e-6 * (u1 - u0), 1e-6 * (v1 - v0));
3973 let (ua, ub, va, vb) = ((u - hu).max(u0), (u + hu).min(u1), (v - hv).max(v0), (v + hv).min(v1));
3976 let du = frag(surface.evaluate(ub, v), "probe")?.sub(frag(surface.evaluate(ua, v), "probe")?).scale(1.0 / (ub - ua));
3977 let dv = frag(surface.evaluate(u, vb), "probe")?.sub(frag(surface.evaluate(u, va), "probe")?).scale(1.0 / (vb - va));
3978 let Ok(n) = du.cross(dv).normalized() else { continue };
3979 let n = if face.same_sense { n } else { n.scale(-1.0) };
3980 if covered(screw, split, p.add(n.scale(step)), margin)? {
3981 return Err(refuse("folds", format!("the sweep passes over a point twice (just outside its own face at {p:?}); not one pass")));
3982 }
3983 }
3984 }
3985 Ok(())
3986}
3987