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 loop_samples: usize,
119 extent: f64,
121}
122
123#[derive(Clone, Copy, Debug)]
126struct Turns {
127 points: [Vec3; 2],
128 directions: [Vec3; 2],
129 rates: [f64; 2],
130}
131
132impl Turns {
133 fn apply(&self, t: f64, x: Vec3) -> Vec3 {
134 let y = self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t));
135 self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t))
136 }
137 fn invert(&self, t: f64, y: Vec3) -> Vec3 {
138 let x = self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], -self.rates[1] * t));
139 self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], -self.rates[0] * t))
140 }
141 fn velocity(&self, t: f64, x: Vec3) -> Vec3 {
143 let first = x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t);
144 let y = self.points[0].add(first);
145 let moved = y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t);
146 let inner = self.directions[0].cross(first).scale(self.rates[0]);
147 let world = self.directions[1].cross(moved).scale(self.rates[1]).add(inner.rotate_about(self.directions[1], self.rates[1] * t));
148 world.rotate_about(self.directions[1], -self.rates[1] * t).rotate_about(self.directions[0], -self.rates[0] * t)
149 }
150}
151
152#[derive(Clone, Copy, Debug)]
155struct Frame {
156 control: [Vec3; 4],
157 up: Vec3,
158 start_tangent: Vec3,
160}
161
162impl Frame {
163 fn point(&self, s: f64) -> Vec3 {
164 let [p0, p1, p2, p3] = self.control;
165 let r = 1.0 - s;
166 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))
167 }
168 fn first(&self, s: f64) -> Vec3 {
169 let [p0, p1, p2, p3] = self.control;
170 let r = 1.0 - s;
171 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))
172 }
173 fn second(&self, s: f64) -> Vec3 {
174 let [p0, p1, p2, p3] = self.control;
175 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))
176 }
177 fn angle(&self, s: f64) -> f64 {
179 let t = self.first(s);
180 self.up.dot(self.start_tangent.cross(t)).atan2(self.start_tangent.dot(t))
181 }
182 fn angle_rate(&self, s: f64) -> f64 {
183 let (d1, d2) = (self.first(s), self.second(s));
184 self.up.dot(d1.cross(d2)) / d1.dot(d1)
185 }
186}
187
188#[derive(Clone, Copy, Debug)]
193pub struct RisingTurn {
194 pub axis_origin: Vec3,
195 pub axis_direction: Vec3,
196 pub rise: [f64; 2],
197 pub from: f64,
198 pub to: f64,
199}
200
201impl Screw {
202 fn from_motion(motion: &SolidMotion) -> Result<Self, KernelRefusal> {
203 let [step] = motion.steps.as_slice() else {
204 return Err(refuse("motion_steps", "only a single screw, turn or slide is swept whole here".into()));
205 };
206 let e = step
207 .axis_direction
208 .normalized()
209 .map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
210 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
211 return Err(input("range", "the motion's range must run forward".into()));
212 }
213 if step.turn == 0.0 && step.advance == 0.0 {
214 return Err(input("still", "the motion does not move".into()));
215 }
216 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,
217 loop_samples: LOFT_SAMPLES, extent: 0.0 })
218 }
219
220 fn from_rising(motion: &RisingTurn) -> Result<Self, KernelRefusal> {
221 let e = motion.axis_direction.normalized().map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
222 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
223 return Err(input("range", "the motion's range must run forward".into()));
224 }
225 let rate = |t: f64| motion.rise[0] + 2.0 * motion.rise[1] * t;
227 if rate(motion.from) * rate(motion.to) <= 0.0 {
228 return Err(refuse("rise_turns", "the rise stops or turns back within the motion".into()));
229 }
230 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,
231 loop_samples: LOFT_SAMPLES, extent: 0.0 })
232 }
233
234 fn advance(&self, t: f64) -> f64 {
235 self.c * t + self.c2 * t * t
236 }
237
238 fn rate_bound(&self) -> f64 {
240 (self.c + 2.0 * self.c2 * self.t0).abs().max((self.c + 2.0 * self.c2 * self.t1).abs())
241 }
242
243 fn apply(&self, t: f64, x: Vec3) -> Vec3 {
244 if let Some(turns) = &self.turns {
245 return turns.apply(t, x);
246 }
247 if let Some(frame) = &self.frame {
248 return frame.point(t).add(x.sub(self.a).rotate_about(frame.up, frame.angle(t)));
249 }
250 self.a.add(x.sub(self.a).rotate_about(self.e, self.omega * t)).add(self.e.scale(self.advance(t)))
251 }
252
253 fn invert(&self, t: f64, y: Vec3) -> Vec3 {
254 if let Some(turns) = &self.turns {
255 return turns.invert(t, y);
256 }
257 if let Some(frame) = &self.frame {
258 return self.a.add(y.sub(frame.point(t)).rotate_about(frame.up, -frame.angle(t)));
259 }
260 self.a.add(y.sub(self.e.scale(self.advance(t))).sub(self.a).rotate_about(self.e, -self.omega * t))
261 }
262
263 fn velocity(&self, x: Vec3) -> Vec3 {
267 self.velocity_at(self.frozen.unwrap_or(0.5 * (self.t0 + self.t1)), x)
268 }
269
270 fn velocity_at(&self, t: f64, x: Vec3) -> Vec3 {
271 if let Some(turns) = &self.turns {
272 return turns.velocity(t, x);
273 }
274 if let Some(frame) = &self.frame {
275 return frame.start_tangent.scale(frame.first(t).length()).add(frame.up.cross(x.sub(self.a)).scale(frame.angle_rate(t)));
278 }
279 self.e.cross(x.sub(self.a)).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t))
280 }
281
282 fn jet(&self, t: f64, x: Vec3) -> (Vec3, Vec3, Vec3) {
284 let r = x.sub(self.a).rotate_about(self.e, self.omega * t);
285 let axial = self.e.scale(r.dot(self.e));
286 let radial = r.sub(axial);
287 let value = self.a.add(r).add(self.e.scale(self.advance(t)));
288 let first = self.e.cross(radial).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t));
289 let second = radial.scale(-self.omega * self.omega).add(self.e.scale(2.0 * self.c2));
290 (value, first, second)
291 }
292
293 fn move_point(&self, t: f64, p: &Vec4) -> Result<Vec4, KernelRefusal> {
294 let x = frag(p.point(), "control_point")?;
295 Ok(Vec4::from_point(self.apply(t, x), p.w))
296 }
297
298 fn move_curve(&self, t: f64, curve: &NurbsCurve) -> Result<NurbsCurve, KernelRefusal> {
299 let points = curve.control_points.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?;
300 frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "moved_curve")
301 }
302
303 fn move_surface(&self, t: f64, surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
304 let mut rows = Vec::with_capacity(surface.control_points.len());
305 for row in &surface.control_points {
306 rows.push(row.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?);
307 }
308 frag(
309 NurbsSurface::new(surface.degree_u, surface.degree_v, surface.knots_u.clone(), surface.knots_v.clone(), rows),
310 "moved_surface",
311 )
312 }
313}
314
315fn row_times(screw: &Screw) -> Vec<f64> {
318 let turned = (screw.omega * (screw.t1 - screw.t0)).abs();
319 let rows = ((turned / (0.5 * PI)).ceil() as usize).max(1);
320 let at = |k: usize| match k {
324 0 => screw.t0,
325 k if k == rows => screw.t1,
326 k => screw.t0 + (screw.t1 - screw.t0) * (k as f64 - 0.381966011250105) / rows as f64,
327 };
328 (0..=rows).map(at).collect()
329}
330
331fn carry_segments(screw: &Screw, ta: f64, tb: f64) -> usize {
334 let turned = (screw.omega * (tb - ta)).abs();
335 ((turned / TAU * 48.0).ceil() as usize).max(4)
336}
337
338fn carry_knots(segments: usize) -> Vec<f64> {
340 let mut knots = vec![0.0; 6];
341 for k in 1..segments {
342 knots.extend([k as f64 / segments as f64; 3]);
343 }
344 knots.extend([1.0; 6]);
345 knots
346}
347
348fn carry_points(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Vec<Vec3> {
352 let h = (tb - ta) / segments as f64;
353 let at = |k: usize| screw.jet(ta + h * k as f64, x);
354 let piece = |k: usize| {
355 let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
356 [
357 f0,
358 f0.add(d0.scale(h / 5.0)),
359 f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
360 f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
361 f1.sub(d1.scale(h / 5.0)),
362 f1,
363 ]
364 };
365 let mut points = Vec::with_capacity(3 * segments + 3);
366 let first = piece(0);
367 points.extend_from_slice(&first[..3]);
368 for k in 0..segments - 1 {
369 let (here, next) = (piece(k), piece(k + 1));
370 points.push(here[3]);
371 points.push(here[4].scale(2.0).sub(here[3]));
372 points.push(next[2]);
373 }
374 let last = piece(segments - 1);
375 points.extend_from_slice(&last[3..]);
376 points
377}
378
379fn carry_curve(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Result<NurbsCurve, KernelRefusal> {
380 frag(
381 NurbsCurve::new(5, carry_knots(segments), carry_points(screw, x, ta, tb, segments).into_iter().map(|p| Vec4::from_point(p, 1.0)).collect()),
382 "carry_curve",
383 )
384}
385
386struct ToolCurve {
389 curve: NurbsCurve,
390 start: usize,
391 end: usize,
392 degenerate: bool,
393 samples: Option<Vec<Vec3>>,
397}
398
399const LOFT_SAMPLES: usize = 128;
401
402struct Cap {
406 surface: NurbsSurface,
407 front: bool,
408 coedges: Vec<(usize, bool, NurbsCurve)>,
409 chart: Option<Chart>,
412}
413
414#[derive(Clone, Copy, Debug)]
416enum Chart {
417 Plane { origin: Vec3, u: Vec3, v: Vec3 },
419 Revolution { origin: Vec3, w: Vec3, u1: Vec3, u2: Vec3, alpha: f64, theta: f64, spans_u: usize, meridian: MeridianChart },
422}
423
424#[derive(Clone, Copy, Debug)]
425enum MeridianChart {
426 Line { m0: [f64; 2], m1: [f64; 2] },
428 Arc { centre: [f64; 2], start: f64, sweep: f64, spans: usize },
431}
432
433impl Chart {
434 fn invert(&self, x: Vec3) -> [f64; 2] {
435 match *self {
436 Chart::Plane { origin, u, v } => {
437 let d = x.sub(origin);
438 [d.dot(u), d.dot(v)]
439 }
440 Chart::Revolution { origin, w, u1, u2, alpha, theta, spans_u, meridian } => {
441 let r = x.sub(origin);
442 let z = r.dot(w);
443 let rho = r.sub(w.scale(z)).length();
444 let mid = alpha + 0.5 * theta;
445 let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
446 let u = crate::circle_angle_to_parameter(spans_u, theta, psi - alpha);
447 let v = match meridian {
448 MeridianChart::Line { m0, m1 } => {
449 let (a, b) = (m1[0] - m0[0], m1[1] - m0[1]);
450 ((rho - m0[0]) * a + (z - m0[1]) * b) / (a * a + b * b)
451 }
452 MeridianChart::Arc { centre, start, sweep, spans } => {
453 let mid = start + 0.5 * sweep;
454 let angle = mid + ((z - centre[1]).atan2(rho - centre[0]) - mid + PI).rem_euclid(TAU) - PI;
455 crate::circle_angle_to_parameter(spans, sweep, angle - start)
456 }
457 };
458 [u, v]
459 }
460 }
461 }
462}
463
464fn spans_of(surface_knots_u: &[f64]) -> usize {
466 (surface_knots_u.len() - 6) / 2 + 1
467}
468
469struct Split {
471 points: Vec<Vec3>,
472 curves: Vec<ToolCurve>,
473 caps: Vec<Cap>,
474 slides: Vec<SlideDisc>,
476 slide_sides: Vec<SlideSide>,
478 scale: f64,
479 shape: Shape,
480}
481
482struct SlideDisc {
490 centre: Vec3,
491 u: Vec3,
492 v: Vec3,
493 rim: Vec<(usize, bool)>,
494}
495
496struct SlideSide {
503 origin: Vec3,
504 w: Vec3,
505 patches: Vec<(f64, f64, usize, usize, [usize; 2])>,
506}
507
508enum Shape {
510 Planes(Vec<(Vec3, f64)>),
511 Revolved { origin: Vec3, axis: Vec3, meridian: Vec<OutlinePiece> },
512}
513
514impl Split {
515 fn distance(&self, x: Vec3) -> f64 {
517 match &self.shape {
518 Shape::Planes(planes) => planes.iter().map(|(n, d)| n.dot(x) - d).fold(f64::NEG_INFINITY, f64::max),
519 Shape::Revolved { origin, axis, meridian } => {
520 let r = x.sub(*origin);
521 let z = r.dot(*axis);
522 let rho = r.sub(axis.scale(z)).length();
523 outline_signed_distance(meridian, [rho, z])
524 }
525 }
526 }
527
528 fn grazing(&self) -> Result<Vec<(usize, bool)>, KernelRefusal> {
532 let mut uses: HashMap<usize, Vec<(bool, bool)>> = HashMap::new();
533 for cap in &self.caps {
534 for &(curve, forward, _) in &cap.coedges {
535 uses.entry(curve).or_default().push((forward, cap.front));
536 }
537 }
538 let mut grazing = Vec::new();
539 for (curve, list) in uses {
540 if self.curves[curve].degenerate {
541 continue;
542 }
543 if list.len() == 1
545 && (self.slides.iter().any(|slide| slide.rim.iter().any(|&(c, _)| c == curve))
546 || self.slide_sides.iter().any(|side| side.patches.iter().any(|patch| patch.2 == curve || patch.3 == curve)))
547 {
548 continue;
549 }
550 match list.as_slice() {
551 [(fa, front_a), (fb, front_b)] => {
552 if fa == fb {
553 return Err(refuse("split_topology", "two caps run one boundary curve the same way".into()));
554 }
555 if front_a != front_b {
556 grazing.push((curve, if *front_a { *fb } else { *fa }));
557 }
558 }
559 _ => return Err(refuse("split_topology", format!("a boundary curve is used {} times", list.len()))),
560 }
561 }
562 grazing.sort_unstable();
563 Ok(grazing)
564 }
565}
566
567fn uv_line(a: [f64; 2], b: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
568 frag(make_line(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0)), "pcurve")
569}
570
571fn plane_patch(origin: Vec3, u: Vec3, v: Vec3, points: &[Vec3]) -> Result<NurbsSurface, KernelRefusal> {
574 let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
575 for p in points {
576 let q = [p.sub(origin).dot(u), p.sub(origin).dot(v)];
577 for k in 0..2 {
578 lo[k] = lo[k].min(q[k]);
579 hi[k] = hi[k].max(q[k]);
580 }
581 }
582 let at = |s: f64, t: f64| Vec4::from_point(origin.add(u.scale(s)).add(v.scale(t)), 1.0);
583 frag(
584 NurbsSurface::new(
585 1,
586 1,
587 vec![lo[0], lo[0], hi[0], hi[0]],
588 vec![lo[1], lo[1], hi[1], hi[1]],
589 vec![vec![at(lo[0], lo[1]), at(lo[0], hi[1])], vec![at(hi[0], lo[1]), at(hi[0], hi[1])]],
590 ),
591 "plane_surface",
592 )
593}
594
595fn plane_pcurve(origin: Vec3, u: Vec3, v: Vec3, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
598 let mut points = Vec::with_capacity(curve.control_points.len());
599 for p in &curve.control_points {
600 let x = frag(p.point(), "control_point")?.sub(origin);
601 points.push(Vec4::from_point(Vec3::new(x.dot(u), x.dot(v), 0.0), p.w));
602 }
603 let pcurve = frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "plane_pcurve")?;
604 if forward { Ok(pcurve) } else { frag(pcurve.reversed(), "plane_pcurve") }
605}
606
607fn split_polyhedron(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<Split, KernelRefusal> {
608 if vertices.len() < 4 || faces.len() < 4 {
609 return Err(input("polyhedron", "a polyhedron needs at least four corners and four faces".into()));
610 }
611 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);
612 let scale = vertices.iter().map(|p| p.sub(centroid).length()).fold(0.0f64, f64::max).max(1e-300);
613 let tolerance = 1e-12 * scale.max(screw.a.sub(centroid).length());
614 let mut loops: Vec<Vec<usize>> = Vec::with_capacity(faces.len());
615 let mut planes = Vec::with_capacity(faces.len());
616 for face in faces {
617 if face.len() < 3 || face.iter().any(|&i| i >= vertices.len()) {
618 return Err(input("face", "a face needs three or more existing corners".into()));
619 }
620 let mut normal = Vec3::new(0.0, 0.0, 0.0);
621 for k in 0..face.len() {
622 let (p, q) = (vertices[face[k]], vertices[face[(k + 1) % face.len()]]);
623 normal = normal.add(p.sub(centroid).cross(q.sub(centroid)));
624 }
625 let mut normal = normal.normalized().map_err(|_| input("face", "a face has no area".into()))?;
626 let mut face = face.clone();
627 if normal.dot(vertices[face[0]].sub(centroid)) < 0.0 {
628 normal = normal.scale(-1.0);
629 face.reverse();
630 }
631 let offset = normal.dot(vertices[face[0]]);
632 for p in vertices {
633 if normal.dot(*p) - offset > 1e-9 * scale {
634 return Err(refuse("not_convex", "the polyhedron is not convex".into()));
635 }
636 }
637 for &i in &face {
638 if (normal.dot(vertices[i]) - offset).abs() > 1e-9 * scale {
639 return Err(input("face", "a face's corners are not in one plane".into()));
640 }
641 }
642 loops.push(face);
643 planes.push((normal, offset));
644 }
645 let g = |face: usize, x: Vec3| -> f64 {
647 let value = screw.velocity(x).dot(planes[face].0);
648 if value.abs() <= tolerance * screw.velocity(x).length().max(1.0) { 0.0 } else { value }
649 };
650 let mut owner: HashMap<(usize, usize), usize> = HashMap::new();
651 for (index, face) in loops.iter().enumerate() {
652 for k in 0..face.len() {
653 owner.insert((face[k], face[(k + 1) % face.len()]), index);
654 }
655 }
656 for &(i, j) in owner.keys() {
657 if !owner.contains_key(&(j, i)) {
658 return Err(input("closed", "the polyhedron's faces do not close".into()));
659 }
660 }
661 let mut points: Vec<Vec3> = vertices.to_vec();
663 let mut edge_splits: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
664 let mut keys: Vec<(usize, usize)> = owner.keys().copied().filter(|&(i, j)| i < j).collect();
665 keys.sort_unstable();
666 for (i, j) in keys {
667 let mut params: Vec<f64> = Vec::new();
668 for f in [owner[&(i, j)], owner[&(j, i)]] {
669 let (gi, gj) = (g(f, vertices[i]), g(f, vertices[j]));
670 if gi * gj < 0.0 {
671 params.push(gi / (gi - gj));
672 }
673 }
674 params.sort_by(|a, b| a.total_cmp(b));
675 params.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
676 let mut list = Vec::new();
677 for s in params {
678 points.push(vertices[i].add(vertices[j].sub(vertices[i]).scale(s)));
679 list.push(points.len() - 1);
680 }
681 edge_splits.insert((i, j), list);
682 }
683 let splits_along = |i: usize, j: usize| -> Vec<usize> {
684 if i < j {
685 edge_splits[&(i, j)].clone()
686 } else {
687 edge_splits[&(j, i)].iter().rev().copied().collect()
688 }
689 };
690 let mut rings: Vec<(Vec<usize>, usize, bool)> = Vec::new();
692 for (index, face) in loops.iter().enumerate() {
693 let mut ring: Vec<usize> = Vec::new();
694 for k in 0..face.len() {
695 ring.push(face[k]);
696 ring.extend(splits_along(face[k], face[(k + 1) % face.len()]));
697 }
698 let values: Vec<f64> = ring.iter().map(|&p| g(index, points[p])).collect();
699 let positive = values.iter().any(|&v| v > 0.0);
700 let negative = values.iter().any(|&v| v < 0.0);
701 if !positive && !negative {
702 return Err(refuse("face_in_own_plane", format!("face {index} moves in its own plane (its sweep is flat)")));
703 }
704 if !(positive && negative) {
705 rings.push((ring, index, positive));
706 continue;
707 }
708 let zeros: Vec<usize> = (0..ring.len()).filter(|&k| values[k] == 0.0).collect();
709 if zeros.len() != 2 {
710 return Err(refuse("grazing_split", format!("face {index} meets its grazing line {} times", zeros.len())));
711 }
712 let (za, zb) = (zeros[0], zeros[1]);
713 if za + 1 == zb || (zb + 1) % ring.len() == za {
714 return Err(refuse("grazing_split", format!("face {index} is split along one of its own edges")));
715 }
716 let first: Vec<usize> = (za..=zb).map(|k| ring[k]).collect();
717 let second: Vec<usize> = (zb..ring.len()).chain(0..=za).map(|k| ring[k]).collect();
718 let first_front = values[za + 1] > 0.0;
719 rings.push((first, index, first_front));
720 rings.push((second, index, !first_front));
721 }
722 if screw.c2 != 0.0 {
725 for (ring, face, front) in &rings {
726 for &p in ring {
727 for t in [screw.t0, screw.t1] {
728 let value = screw.velocity_at(t, points[p]).dot(planes[*face].0);
729 let slack = tolerance * screw.velocity_at(t, points[p]).length().max(1.0);
730 if (*front && value < -slack) || (!*front && value > slack) {
731 return Err(refuse("rising_sides", format!("face {face} changes side as the rise grows")));
732 }
733 }
734 }
735 }
736 }
737 let mut curves: Vec<ToolCurve> = Vec::new();
739 let mut by_pair: HashMap<(usize, usize), usize> = HashMap::new();
740 let mut caps = Vec::with_capacity(rings.len());
741 for (ring, face, front) in rings {
742 let normal = planes[face].0;
743 let origin = points[ring[0]];
744 let u = frag(points[ring[1]].sub(origin).normalized(), "plane_axis")?;
745 let v = normal.cross(u);
746 let corners: Vec<Vec3> = ring.iter().map(|&p| points[p]).collect();
747 let surface = plane_patch(origin, u, v, &corners)?;
748 let mut coedges = Vec::with_capacity(ring.len());
749 for k in 0..ring.len() {
750 let (p, q) = (ring[k], ring[(k + 1) % ring.len()]);
751 let key = (p.min(q), p.max(q));
752 let index = match by_pair.get(&key) {
753 Some(&index) => index,
754 None => {
755 curves.push(ToolCurve {
756 curve: frag(make_line(points[key.0], points[key.1]), "line")?,
757 start: key.0,
758 end: key.1,
759 degenerate: false,
760 samples: None,
761 });
762 by_pair.insert(key, curves.len() - 1);
763 curves.len() - 1
764 }
765 };
766 let forward = p == key.0;
767 coedges.push((index, forward, plane_pcurve(origin, u, v, &curves[index].curve, forward)?));
768 }
769 caps.push(Cap { surface, front, coedges, chart: Some(Chart::Plane { origin, u, v }) });
770 }
771 Ok(Split { points, curves, caps, slides: Vec::new(), slide_sides: Vec::new(), scale, shape: Shape::Planes(planes) })
772}
773
774fn harmonic(g: impl Fn(f64) -> f64) -> (f64, f64, f64) {
777 let (g0, g90, g180) = (g(0.0), g(0.5 * PI), g(PI));
778 let c = 0.5 * (g0 + g180);
779 (0.5 * (g0 - g180), g90 - c, c)
780}
781
782fn piece_point(piece: &OutlinePiece, s: f64) -> [f64; 2] {
783 match *piece {
784 OutlinePiece::Line { from, to } => [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s],
785 OutlinePiece::Arc { centre, radius, start, end } => {
786 let angle = start + (end - start) * s;
787 [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
788 }
789 }
790}
791
792fn split_revolved(
793 screw: &Screw,
794 origin: Vec3,
795 axis: Vec3,
796 radial: Vec3,
797 meridian: &[OutlinePiece],
798) -> Result<Split, KernelRefusal> {
799 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
800 let u1 = radial.sub(w.scale(radial.dot(w))).normalized().map_err(|_| input("radial", "the radial direction lies along the axis".into()))?;
801 let u2 = w.cross(u1);
802 if let [OutlinePiece::Arc { centre, radius, start, end }] = meridian {
803 if ((end - start).abs() - TAU).abs() < 1e-12 && centre[0] > *radius {
804 return split_torus(screw, origin, w, u1, u2, *centre, *radius, meridian);
805 }
806 }
807 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()));
808 let scale = meridian
809 .iter()
810 .flat_map(|p| [piece_point(p, 0.0), piece_point(p, 1.0)])
811 .map(|q| q[0].abs().max(q[1].abs()))
812 .fold(0.0f64, f64::max)
813 .max(1e-300);
814 let on_axis = |q: [f64; 2]| q[0].abs() <= 1e-12 * scale;
815 let mut area = 0.0;
817 for piece in meridian {
818 for k in 0..16 {
819 let (p, q) = (piece_point(piece, k as f64 / 16.0), piece_point(piece, (k + 1) as f64 / 16.0));
820 area += p[0] * q[1] - q[0] * p[1];
821 }
822 }
823 let reversed_meridian: Vec<OutlinePiece>;
825 let meridian: &[OutlinePiece] = if area > 0.0 {
826 meridian
827 } else {
828 reversed_meridian = meridian
829 .iter()
830 .rev()
831 .map(|piece| match *piece {
832 OutlinePiece::Line { from, to } => OutlinePiece::Line { from: to, to: from },
833 OutlinePiece::Arc { centre, radius, start, end } => OutlinePiece::Arc { centre, radius, start: end, end: start },
834 })
835 .collect();
836 &reversed_meridian
837 };
838 let ccw = true;
839 let normal = |piece: &OutlinePiece, s: f64| -> [f64; 2] {
841 let (p, q) = (piece_point(piece, (s - 1e-6).max(0.0)), piece_point(piece, (s + 1e-6).min(1.0)));
842 let (dx, dz) = (q[0] - p[0], q[1] - p[1]);
843 let length = dx.hypot(dz);
844 if ccw { [dz / length, -dx / length] } else { [-dz / length, dx / length] }
845 };
846 let g = |piece: &OutlinePiece, s: f64, psi: f64| -> f64 {
847 let m = piece_point(piece, s);
848 let n = normal(piece, s);
849 let x = at(m[0], m[1], psi);
850 let normal = w.scale(n[1]).add(u1.scale(n[0] * psi.cos())).add(u2.scale(n[0] * psi.sin()));
851 screw.velocity(x).dot(normal)
852 };
853 let faces: Vec<usize> = (0..meridian.len())
855 .filter(|&i| !(matches!(meridian[i], OutlinePiece::Line { .. }) && on_axis(piece_point(&meridian[i], 0.0)) && on_axis(piece_point(&meridian[i], 1.0))))
856 .collect();
857 enum Kind {
860 Ruled(Vec<f64>),
863 Moving,
866 Sphere { centre: Vec3, pole_at_start: bool },
869 Ball { centre: Vec3, radius: f64 },
872 TorusZone { psi_a: f64, psi_b: f64, at_end: bool },
877 Slide,
879 SlideSide,
881 RuledArc(Vec<f64>),
885 }
886 let branches = |piece: &OutlinePiece, s: f64| -> Option<(f64, f64)> {
889 let reference = {
890 let (a, b, _) = harmonic(|psi| g(piece, 0.0, psi));
891 b.atan2(a)
892 };
893 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
894 let r = a.hypot(b);
895 if !(c.abs() < r * (1.0 - 1e-9)) {
896 return None;
897 }
898 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
899 let spread = (-c / r).acos();
900 Some((base - spread, base + spread))
901 };
902 let mut kinds: Vec<Kind> = Vec::with_capacity(faces.len());
903 for &i in &faces {
904 let piece = &meridian[i];
905 match *piece {
906 OutlinePiece::Line { .. } => {
907 let roots = |s: f64| -> Vec<f64> {
908 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
909 let r = a.hypot(b);
910 if c.abs() >= r * (1.0 - 1e-12) {
911 return Vec::new();
912 }
913 let base = b.atan2(a);
914 let spread = (-c / r).acos();
915 let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
916 list.sort_by(|x, y| x.total_cmp(y));
917 list
918 };
919 let alive = |s: f64| {
923 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
924 a.hypot(b).max(c.abs()) > 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound())
925 };
926 let readings: Vec<Vec<f64>> = [0.0, 0.5, 1.0].into_iter().filter(|&t| alive(t)).map(roots).collect();
927 if readings.is_empty() {
928 let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
929 let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
930 if flat && (on_axis(m0) || on_axis(m1)) {
931 kinds.push(Kind::Slide);
932 continue;
933 }
934 let upright = (m0[0] - m1[0]).abs() <= 1e-12 * scale;
935 if upright && screw.omega == 0.0 && screw.turns.is_none() && screw.frame.is_none() {
936 kinds.push(Kind::SlideSide);
937 continue;
938 }
939 return Err(refuse("revolved_grazing", format!("meridian piece {i} grazes all over")));
940 }
941 let r0 = readings[0].clone();
942 let still = readings.iter().all(|r| r.len() == r0.len() && r.iter().zip(&r0).all(|(x, y)| (x - y).abs() < 1e-10));
943 if still {
944 let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
948 let disc = (m0[1] - m1[1]).abs() <= 1e-12 * scale && (on_axis(m0) || on_axis(m1));
949 kinds.push(Kind::Ruled(if r0.is_empty() && !disc { vec![0.0, PI] } else { r0 }));
950 continue;
951 }
952 let mut previous: Option<(f64, f64)> = None;
953 for k in 0..=64 {
954 let Some(pair) = branches(piece, k as f64 / 64.0) else {
955 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} do not run rim to rim")));
956 };
957 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
958 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} turn too fast")));
959 }
960 previous = Some(pair);
961 }
962 kinds.push(Kind::Moving);
963 }
964 OutlinePiece::Arc { centre, radius, start, end } => {
965 let (p0, p1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
966 let span = (end - start).abs();
967 let centre_on_axis = on_axis(centre);
968 let pole_at_start = on_axis(p0);
969 let equator = |p: [f64; 2]| (p[1] - centre[1]).abs() <= 1e-12 * scale && (p[0] - radius).abs() <= 1e-12 * scale;
970 if centre_on_axis && (span - PI).abs() < 1e-12 && pole_at_start && on_axis(p1) {
971 kinds.push(Kind::Ball { centre: origin.add(w.scale(centre[1])), radius });
972 continue;
973 }
974 if centre[0] > radius * (1.0 + 1e-9) && end > start && span <= PI + 1e-12 {
975 let at_rim = |v: f64| [centre[0] + radius * v.cos(), centre[1] + radius * v.sin()];
976 let g_tube = |psi: f64, v: f64| -> f64 {
977 let m = at_rim(v);
978 let x = at(m[0], m[1], psi);
979 let normal = w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
980 screw.velocity(x).dot(normal)
981 };
982 {
984 let roots_at = |v: f64| -> Option<Vec<f64>> {
985 let (a, b, c) = harmonic(|psi| g_tube(psi, v));
986 let r = a.hypot(b);
987 let floor = 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound());
988 if r.max(c.abs()) <= floor || c.abs() >= r * (1.0 - 1e-12) {
989 return None;
990 }
991 let (base, spread) = (b.atan2(a), (-c / r).acos());
992 let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
993 list.sort_by(|x, y| x.total_cmp(y));
994 Some(list)
995 };
996 let readings: Vec<Option<Vec<f64>>> = [0.25, 0.5, 0.75].into_iter().map(|t| roots_at(start + (end - start) * t)).collect();
997 if let [Some(r0), Some(r1), Some(r2)] = readings.as_slice() {
998 if r0.iter().zip(r1).chain(r0.iter().zip(r2)).all(|(x, y)| (x - y).abs() < 1e-10) {
999 kinds.push(Kind::RuledArc(r0.clone()));
1000 continue;
1001 }
1002 }
1003 }
1004 if span >= PI {
1005 return Err(refuse("revolved_arc", format!("meridian piece {i}: a half-round rim is swept only where its grazing curves keep their angle")));
1006 }
1007 let in_zone = |psi: f64| -> Result<Option<f64>, KernelRefusal> {
1008 let (a, b, c) = harmonic(|v| g_tube(psi, v));
1009 let r = a.hypot(b);
1010 if !(c.abs() < r) {
1011 return Ok(None);
1012 }
1013 let base = b.atan2(a);
1014 let spread = (-c / r).acos();
1015 let inside: Vec<f64> = [base - spread, base + spread]
1016 .into_iter()
1017 .map(|v| start + (v - start).rem_euclid(TAU))
1018 .filter(|&v| v <= end)
1019 .collect();
1020 match inside.as_slice() {
1021 [] => Ok(None),
1022 [v] => Ok(Some(*v)),
1023 _ => Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner twice at one angle"))),
1024 }
1025 };
1026 let count = 720;
1027 let has: Vec<bool> = (0..count).map(|k| in_zone(TAU * k as f64 / count as f64).map(|r| r.is_some())).collect::<Result<_, _>>()?;
1028 let rises: Vec<usize> = (0..count).filter(|&k| has[k] && !has[(k + count - 1) % count]).collect();
1029 let falls: Vec<usize> = (0..count).filter(|&k| !has[k] && has[(k + count - 1) % count]).collect();
1030 let ([rise], [fall]) = (rises.as_slice(), falls.as_slice()) else {
1031 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve does not cross the rounded corner once")));
1032 };
1033 let estimate = |k: usize| TAU * (k as f64 - 0.5) / count as f64;
1035 let (ea, eb) = (estimate(*rise), estimate(*fall));
1036 let sample = |k: usize| TAU * (k % count) as f64 / count as f64;
1037 let probe_v = in_zone(sample(*rise))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
1038 let at_end = (probe_v - end).abs() < (probe_v - start).abs();
1039 let rim_v = if at_end { end } else { start };
1040 let (a, b, c) = harmonic(|psi| g_tube(psi, rim_v));
1041 let r = a.hypot(b);
1042 let base = b.atan2(a);
1043 let spread = (-c / r).acos();
1044 let roots = [base - spread, base + spread];
1045 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");
1046 let (psi_a, psi_b) = (nearest(ea).rem_euclid(TAU), nearest(eb).rem_euclid(TAU));
1047 let psi_b = if psi_b <= psi_a { psi_b + TAU } else { psi_b };
1048 let leave_v = in_zone(sample(*fall + count - 1))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
1049 if ((leave_v - end).abs() < (leave_v - start).abs()) != at_end {
1050 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner from rim to rim")));
1051 }
1052 kinds.push(Kind::TorusZone { psi_a, psi_b, at_end });
1053 continue;
1054 }
1055 if !(centre_on_axis && (span - 0.5 * PI).abs() < 1e-12 && (pole_at_start && equator(p1) || on_axis(p1) && equator(p0))) {
1056 if centre_on_axis && end > start && span < PI && (pole_at_start != on_axis(p1)) {
1061 let roots_at = |s: f64| -> Option<Vec<f64>> {
1062 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
1063 let r = a.hypot(b);
1064 let floor = 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound());
1065 if r.max(c.abs()) <= floor || c.abs() >= r * (1.0 - 1e-12) {
1066 return None;
1067 }
1068 let (base, spread) = (b.atan2(a), (-c / r).acos());
1069 let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
1070 list.sort_by(|x, y| x.total_cmp(y));
1071 Some(list)
1072 };
1073 if let [Some(r0), Some(r1), Some(r2)] = [0.25, 0.5, 0.75].map(roots_at) {
1074 if r0.iter().zip(&r1).chain(r0.iter().zip(&r2)).all(|(x, y)| (x - y).abs() < 1e-10) {
1075 kinds.push(Kind::RuledArc(r0));
1076 continue;
1077 }
1078 }
1079 }
1080 return Err(refuse("revolved_arc", format!("meridian piece {i}: only a quarter circle from a pole to an equator is swept here")));
1081 }
1082 kinds.push(Kind::Sphere { centre: origin.add(w.scale(centre[1])), pole_at_start });
1083 }
1084 }
1085 }
1086 let count = meridian.len();
1088 let junction = |k: usize| piece_point(&meridian[k], 1.0); let face_of = |piece: usize| faces.iter().position(|&f| f == piece);
1090 let mut rim_angles: HashMap<usize, Vec<f64>> = HashMap::new();
1091 for k in 0..count {
1092 if on_axis(junction(k)) {
1093 continue;
1094 }
1095 let mut angles: Vec<f64> = Vec::new();
1096 for (piece, s_end) in [(k, 1.0), ((k + 1) % count, 0.0)] {
1097 let Some(f) = face_of(piece) else { continue };
1098 match &kinds[f] {
1099 Kind::Ruled(roots) | Kind::RuledArc(roots) => angles.extend(roots.iter().copied()),
1100 Kind::Ball { .. } | Kind::Slide => {}
1101 Kind::SlideSide => angles.extend([0.0, PI]),
1102 Kind::TorusZone { psi_a, psi_b, .. } => angles.extend([psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)]),
1103 Kind::Moving => {
1104 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
1105 angles.extend([lower.rem_euclid(TAU), upper.rem_euclid(TAU)]);
1106 }
1107 Kind::Sphere { centre, .. } => {
1108 let d = screw.velocity(*centre).cross(w);
1109 let base = d.dot(u2).atan2(d.dot(u1)).rem_euclid(TAU);
1110 angles.extend([base, (base + PI).rem_euclid(TAU)]);
1111 }
1112 }
1113 }
1114 angles.sort_by(|a, b| a.total_cmp(b));
1115 let mut merged: Vec<f64> = Vec::new();
1116 for a in angles {
1117 if merged.last().is_some_and(|&b| (a - b).abs() < 1e-9) || merged.first().is_some_and(|&b| (b + TAU - a).abs() < 1e-9) {
1118 continue;
1119 }
1120 merged.push(a);
1121 }
1122 rim_angles.insert(k, merged);
1123 }
1124 for (f, &piece) in faces.iter().enumerate() {
1126 for (k, s_end) in [((piece + count - 1) % count, 0.0), (piece, 1.0)] {
1127 let own: Vec<f64> = match &kinds[f] {
1128 Kind::Ruled(roots) | Kind::RuledArc(roots) => roots.clone(),
1129 Kind::Moving => {
1130 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
1131 let mut list = vec![lower.rem_euclid(TAU), upper.rem_euclid(TAU)];
1132 list.sort_by(|a, b| a.total_cmp(b));
1133 list
1134 }
1135 Kind::TorusZone { psi_a, psi_b, .. } => {
1136 let mut list = vec![psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)];
1137 list.sort_by(|a, b| a.total_cmp(b));
1138 list
1139 }
1140 Kind::Sphere { .. } | Kind::Ball { .. } | Kind::Slide => continue,
1141 Kind::SlideSide => vec![0.0, PI],
1142 };
1143 if let Some(angles) = rim_angles.get(&k) {
1144 if own.is_empty() {
1145 continue;
1146 }
1147 if angles.len() != own.len() || angles.iter().zip(&own).any(|(a, b)| (a - b).abs() > 1e-9) {
1148 return Err(refuse("revolved_rims", format!("meridian piece {piece} splits at other angles than its rim")));
1149 }
1150 }
1151 }
1152 }
1153 let mut points: Vec<Vec3> = Vec::new();
1154 let mut curves: Vec<ToolCurve> = Vec::new();
1155 let mut caps: Vec<Cap> = Vec::new();
1156 let mut slides: Vec<SlideDisc> = Vec::new();
1157 let mut slide_sides: Vec<SlideSide> = Vec::new();
1158 let mut rim_points: HashMap<(usize, usize), usize> = HashMap::new();
1160 let mut rim_arcs: HashMap<(usize, usize), usize> = HashMap::new();
1161 let mut rim_keys: Vec<usize> = rim_angles.keys().copied().collect();
1162 rim_keys.sort_unstable();
1163 for &k in &rim_keys {
1164 let q = junction(k);
1165 let angles = rim_angles[&k].clone();
1166 let angles = if angles.is_empty() { vec![0.0] } else { angles };
1167 for (j, &a) in angles.iter().enumerate() {
1168 points.push(at(q[0], q[1], a));
1169 rim_points.insert((k, j), points.len() - 1);
1170 }
1171 for j in 0..angles.len() {
1172 let a = angles[j];
1173 let b = if j + 1 < angles.len() { angles[j + 1] } else { angles[0] + TAU };
1174 let base = at(q[0], q[1], a);
1175 let surface = frag(make_revolution(origin, w, &frag(make_line(base, base.add(w)), "line")?, b - a), "rim")?;
1176 curves.push(ToolCurve {
1177 curve: frag(surface.iso_curve_v(0.0), "rim_arc")?,
1178 start: rim_points[&(k, j)],
1179 end: rim_points[&(k, (j + 1) % angles.len())],
1180 degenerate: false,
1181 samples: Some((0..=LOFT_SAMPLES).map(|i| at(q[0], q[1], a + (b - a) * i as f64 / LOFT_SAMPLES as f64)).collect()),
1182 });
1183 rim_arcs.insert((k, j), curves.len() - 1);
1184 }
1185 rim_angles.insert(k, angles);
1186 }
1187 let arc_index = |k: usize, angle: f64| -> Result<usize, KernelRefusal> {
1188 rim_angles[&k]
1189 .iter()
1190 .position(|&a| ((a - angle).rem_euclid(TAU)).min((angle - a).rem_euclid(TAU)) < 1e-9)
1191 .ok_or_else(|| refuse("revolved_rims", "a patch corner is not on its rim's split".into()))
1192 };
1193 let sign_at = |piece: &OutlinePiece, psi: f64| g(piece, 0.5, psi) > 0.0;
1194 for (f, &piece_index) in faces.iter().enumerate() {
1195 let piece = &meridian[piece_index];
1196 let below = (piece_index + count - 1) % count; let above = piece_index; let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
1199 match &kinds[f] {
1200 Kind::SlideSide => {
1201 if on_axis(m0) || on_axis(m1) {
1202 return Err(refuse("revolved_grazing", format!("meridian piece {piece_index} grazes all over")));
1203 }
1204 let seams = [0.0, PI];
1205 let mut patches = Vec::with_capacity(2);
1206 for j in 0..2 {
1207 let (alpha, theta) = (seams[j], PI);
1208 let (ib, ia) = (arc_index(below, alpha)?, arc_index(above, alpha)?);
1209 patches.push((alpha, theta, rim_arcs[&(below, ib)], rim_arcs[&(above, ia)], [rim_points[&(below, ib)], rim_points[&(above, ia)]]));
1210 }
1211 slide_sides.push(SlideSide { origin, w, patches });
1212 }
1213 Kind::RuledArc(roots) => {
1214 let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1215 let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1216 let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1217 let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1218 let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1220 let from = tube_point(psi, start);
1221 let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1222 frag(surface.iso_curve_v(0.0), "tube")
1223 };
1224 let apex = if on_axis(m0) || on_axis(m1) {
1227 let m = if on_axis(m0) { m0 } else { m1 };
1228 points.push(at(0.0, m[1], 0.0));
1229 Some(points.len() - 1)
1230 } else {
1231 None
1232 };
1233 let rim_point = |rim: usize, on: bool, angle: f64| -> Result<usize, KernelRefusal> {
1234 match (on, apex) {
1235 (true, Some(p)) => Ok(p),
1236 _ => Ok(rim_points[&(rim, arc_index(rim, angle)?)]),
1237 }
1238 };
1239 let mut generators = Vec::with_capacity(roots.len());
1240 for &root in roots {
1241 let (a, b) = (rim_point(below, on_axis(m0), root)?, rim_point(above, on_axis(m1), root)?);
1242 let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES).map(|i| tube_point(root, start + (end - start) * i as f64 / LOFT_SAMPLES as f64)).collect();
1243 exact[0] = points[a];
1244 exact[LOFT_SAMPLES] = points[b];
1245 curves.push(ToolCurve { curve: generatrix(root)?, start: a, end: b, degenerate: false, samples: Some(exact) });
1246 generators.push(curves.len() - 1);
1247 }
1248 for j in 0..roots.len() {
1249 let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1250 let surface = frag(make_revolution(origin, w, &generatrix(alpha)?, beta - alpha), "patch")?;
1251 let (mid_psi, mid_v) = (0.5 * (alpha + beta), 0.5 * (start + end));
1252 let mid_normal = w.scale(mid_v.sin()).add(u1.scale(mid_v.cos() * mid_psi.cos())).add(u2.scale(mid_v.cos() * mid_psi.sin()));
1253 let front = screw.velocity(tube_point(mid_psi, mid_v)).dot(mid_normal) > 0.0;
1254 let mut pole = |curves: &mut Vec<ToolCurve>| -> usize {
1255 let p = apex.expect("apex");
1256 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1257 curves.len() - 1
1258 };
1259 let lower = if on_axis(m0) { pole(&mut curves) } else { rim_arcs[&(below, arc_index(below, alpha)?)] };
1260 let upper = if on_axis(m1) { pole(&mut curves) } else { rim_arcs[&(above, arc_index(above, alpha)?)] };
1261 let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1262 let coedges = vec![
1263 (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1264 (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1265 (upper, on_axis(m1), uv_line([1.0, 1.0], [0.0, 1.0])?),
1266 (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1267 ];
1268 let chart = Chart::Revolution {
1269 origin,
1270 w,
1271 u1,
1272 u2,
1273 alpha,
1274 theta: beta - alpha,
1275 spans_u: spans_of(&surface.knots_u),
1276 meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_of(&surface.knots_v) },
1277 };
1278 caps.push(Cap { surface, front, coedges, chart: Some(chart) });
1279 }
1280 }
1281 Kind::Slide => {
1282 let rim = if on_axis(m0) { above } else { below };
1283 let n = normal(piece, 0.5);
1284 let outward = w.scale(n[1].signum());
1285 let arcs = rim_angles[&rim].len();
1286 let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
1288 slides.push(SlideDisc {
1289 centre: origin.add(w.scale(m0[1])),
1290 u: u1,
1291 v: outward.cross(u1),
1292 rim: order.into_iter().map(|j| (rim_arcs[&(rim, j)], n[1] > 0.0)).collect(),
1293 });
1294 }
1295 Kind::Ruled(roots) if roots.is_empty() => {
1296 let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
1298 if !flat || !(on_axis(m0) || on_axis(m1)) {
1299 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a whole side or ring is not swept here")));
1300 }
1301 let rim = if on_axis(m0) { above } else { below };
1302 let front = sign_at(piece, 0.0);
1303 if (0..64).any(|k| sign_at(piece, TAU * k as f64 / 64.0) != front) {
1304 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a flat end meets its grazing line")));
1305 }
1306 let n = normal(piece, 0.5);
1307 let outward = w.scale(n[1].signum());
1308 let centre = origin.add(w.scale(m0[1]));
1309 let (u, v) = (u1, outward.cross(u1));
1310 let arcs = rim_angles[&rim].len();
1311 let mut coedges = Vec::with_capacity(arcs);
1312 let mut corners = Vec::new();
1313 let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
1315 for j in order {
1316 let c = rim_arcs[&(rim, j)];
1317 let forward = n[1] > 0.0;
1318 coedges.push((c, forward, plane_pcurve(centre, u, v, &curves[c].curve, forward)?));
1319 corners.push(points[curves[c].start]);
1320 }
1321 let r = junction(rim)[0];
1322 corners.extend([centre.add(u.scale(r)).add(v.scale(r)), centre.sub(u.scale(r)).sub(v.scale(r))]);
1323 caps.push(Cap { surface: plane_patch(centre, u, v, &corners)?, front, coedges, chart: Some(Chart::Plane { origin: centre, u, v }) });
1324 }
1325 Kind::Ruled(roots) => {
1326 let apex = if on_axis(m0) || on_axis(m1) {
1329 let m = if on_axis(m0) { m0 } else { m1 };
1330 points.push(at(0.0, m[1], 0.0));
1331 Some(points.len() - 1)
1332 } else {
1333 None
1334 };
1335 let rim_point = |rim: usize, on: bool, angle: f64| -> Result<usize, KernelRefusal> {
1336 match (on, apex) {
1337 (true, Some(p)) => Ok(p),
1338 _ => Ok(rim_points[&(rim, arc_index(rim, angle)?)]),
1339 }
1340 };
1341 let mut generators = Vec::with_capacity(roots.len());
1343 for &root in roots {
1344 let (a, b) = (rim_point(below, on_axis(m0), root)?, rim_point(above, on_axis(m1), root)?);
1345 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();
1346 curves.push(ToolCurve { curve: frag(make_line(points[a], points[b]), "generator")?, start: a, end: b, degenerate: false, samples: Some(line_samples) });
1347 generators.push(curves.len() - 1);
1348 }
1349 for j in 0..roots.len() {
1350 let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1351 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1352 let surface = frag(make_revolution(origin, w, &generatrix, beta - alpha), "patch")?;
1353 let front = sign_at(piece, 0.5 * (alpha + beta));
1354 let mut pole = |curves: &mut Vec<ToolCurve>| -> usize {
1355 let p = apex.expect("apex");
1356 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1357 curves.len() - 1
1358 };
1359 let lower = if on_axis(m0) { pole(&mut curves) } else { rim_arcs[&(below, arc_index(below, alpha)?)] };
1360 let upper = if on_axis(m1) { pole(&mut curves) } else { rim_arcs[&(above, arc_index(above, alpha)?)] };
1361 let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1362 let coedges = vec![
1363 (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1364 (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1365 (upper, on_axis(m1), uv_line([1.0, 1.0], [0.0, 1.0])?),
1366 (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1367 ];
1368 let chart = Chart::Revolution { origin, w, u1, u2, alpha, theta: beta - alpha, spans_u: spans_of(&surface.knots_u), meridian: MeridianChart::Line { m0, m1 } };
1369 caps.push(Cap { surface, front, coedges, chart: Some(chart) });
1370 }
1371 }
1372 Kind::Moving => {
1373 if on_axis(m0) || on_axis(m1) {
1374 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
1375 }
1376 let n = 128;
1377 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1378 let pairs: Vec<(f64, f64)> = params.iter().map(|&t| branches(piece, t).expect("checked")).collect();
1379 let pick = |pair: (f64, f64), which: usize| if which == 0 { pair.0 } else { pair.1 };
1380 let mut branch_curves = [0usize; 2];
1382 for which in 0..2 {
1383 let (psi0, psi1) = (pick(pairs[0], which), pick(pairs[n], which));
1384 let start = rim_points[&(below, arc_index(below, psi0)?)];
1385 let end = rim_points[&(above, arc_index(above, psi1)?)];
1386 let mut samples: Vec<Vec3> = params
1387 .iter()
1388 .zip(&pairs)
1389 .map(|(&t, &pair)| {
1390 let m = piece_point(piece, t);
1391 at(m[0], m[1], pick(pair, which))
1392 })
1393 .collect();
1394 samples[0] = points[start];
1395 samples[n] = points[end];
1396 let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES)
1397 .map(|i| {
1398 let t = i as f64 / LOFT_SAMPLES as f64;
1399 let m = piece_point(piece, t);
1400 at(m[0], m[1], pick(branches(piece, t).expect("checked"), which))
1401 })
1402 .collect();
1403 exact[0] = points[start];
1404 exact[LOFT_SAMPLES] = points[end];
1405 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&samples, 5, ¶ms), "branch")?, start, end, degenerate: false, samples: Some(exact) });
1406 branch_curves[which] = curves.len() - 1;
1407 }
1408 for (lo_which, hi_which, hi_turn) in [(0usize, 1usize, 0.0), (1, 0, TAU)] {
1410 let lo = |i: usize| pick(pairs[i], lo_which);
1411 let hi = |i: usize| pick(pairs[i], hi_which) + hi_turn;
1412 let alpha = (0..=n).map(lo).fold(f64::INFINITY, f64::min) - 1e-3;
1413 let theta = (0..=n).map(hi).fold(f64::NEG_INFINITY, f64::max) + 1e-3 - alpha;
1414 if theta >= TAU {
1415 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a side patch wraps all the way round")));
1416 }
1417 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1418 let surface = frag(make_revolution(origin, w, &generatrix, theta), "patch")?;
1419 let spans = (surface.knots_u.len() - 6) / 2 + 1;
1420 let u_of = |psi: f64| crate::circle_angle_to_parameter(spans, theta, psi - alpha);
1421 let angle_near = |x: Vec3, guess: f64| -> f64 {
1422 let r = x.sub(origin);
1423 let a = r.dot(u2).atan2(r.dot(u1));
1424 guess + (a - guess + PI).rem_euclid(TAU) - PI
1425 };
1426 let front = sign_at(piece, 0.5 * (lo(n / 2) + hi(n / 2)));
1427 let rim_pcurve = |arc: usize, v: f64| -> Result<NurbsCurve, KernelRefusal> {
1429 let curve = &curves[arc].curve;
1430 let [d0, d1] = frag(curve.domain(), "domain")?;
1431 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1432 let mut uv = Vec::with_capacity(ts.len());
1433 for &t in &ts {
1434 let x = frag(curve.evaluate(t), "rim")?;
1435 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), v, 0.0));
1436 }
1437 frag(crate::interpolate_curve(&uv, 5, &ts), "rim_pcurve")
1438 };
1439 let branch_pcurve = |which: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> {
1443 let curve = &curves[branch_curves[which]].curve;
1444 let [d0, d1] = frag(curve.domain(), "domain")?;
1445 let (span_rho, span_z) = (m1[0] - m0[0], m1[1] - m0[1]);
1446 let length2 = span_rho * span_rho + span_z * span_z;
1447 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1448 let mut uv = Vec::with_capacity(ts.len());
1449 for &t in &ts {
1450 let x = frag(curve.evaluate(t), "branch")?;
1451 let r = x.sub(origin);
1452 let z = r.dot(w);
1453 let rho = r.sub(w.scale(z)).length();
1454 let along = ((rho - m0[0]) * span_rho + (z - m0[1]) * span_z) / length2;
1455 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), along, 0.0));
1456 }
1457 frag(crate::interpolate_curve(&uv, 5, &ts), "branch_pcurve")
1458 };
1459 let bottom = rim_arcs[&(below, arc_index(below, lo(0))?)];
1460 let top = rim_arcs[&(above, arc_index(above, lo(n))?)];
1461 let coedges = vec![
1462 (bottom, true, rim_pcurve(bottom, 0.0)?),
1463 (branch_curves[hi_which], true, branch_pcurve(hi_which, hi_turn)?),
1464 (top, false, frag(rim_pcurve(top, 1.0)?.reversed(), "reversed")?),
1465 (branch_curves[lo_which], false, frag(branch_pcurve(lo_which, 0.0)?.reversed(), "reversed")?),
1466 ];
1467 let chart_spans = spans_of(&surface.knots_u);
1468 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 } }) });
1469 }
1470 }
1471 Kind::TorusZone { psi_a, psi_b, at_end } => {
1472 let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1473 let (psi_a, psi_b, at_end) = (*psi_a, *psi_b, *at_end);
1474 let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1475 let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1476 let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1477 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()));
1478 let root = |psi: f64| -> Option<f64> {
1479 let (a, b, c) = harmonic(|v| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)));
1480 let r = a.hypot(b);
1481 if !(c.abs() < r) {
1482 return None;
1483 }
1484 let base = b.atan2(a);
1485 let spread = (-c / r).acos();
1486 [base - spread, base + spread].into_iter().map(|v| start + (v - start).rem_euclid(TAU)).find(|&v| v <= end + 1e-12)
1487 };
1488 let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1490 let from = tube_point(psi, start);
1491 let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1492 frag(surface.iso_curve_v(0.0), "tube")
1493 };
1494 let (rim_k, other_k) = if at_end { (above, below) } else { (below, above) };
1495 let ka = rim_points[&(rim_k, arc_index(rim_k, psi_a)?)];
1496 let kb = rim_points[&(rim_k, arc_index(rim_k, psi_b)?)];
1497 let n = 256;
1499 let params: Vec<f64> = (0..=n).map(|j| j as f64 / n as f64).collect();
1500 let psi_of = |t: f64| psi_a + (psi_b - psi_a) * 0.5 * (1.0 - (PI * t).cos());
1504 let mut xyz = Vec::with_capacity(n + 1);
1505 for j in 0..=n {
1506 let psi = psi_of(params[j]);
1507 let v = if j == 0 || j == n { if at_end { end } else { start } } else {
1508 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1509 };
1510 xyz.push(tube_point(psi, v));
1511 }
1512 xyz[0] = points[ka];
1513 xyz[n] = points[kb];
1514 let mut exact = Vec::with_capacity(LOFT_SAMPLES + 1);
1515 for i in 0..=LOFT_SAMPLES {
1516 let psi = psi_of(i as f64 / LOFT_SAMPLES as f64);
1517 let v = if i == 0 || i == LOFT_SAMPLES { if at_end { end } else { start } } else {
1518 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1519 };
1520 exact.push(tube_point(psi, v));
1521 }
1522 exact[0] = points[ka];
1523 exact[LOFT_SAMPLES] = points[kb];
1524 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "torus_k")?, start: ka, end: kb, degenerate: false, samples: Some(exact) });
1525 let k_curve = curves.len() - 1;
1526 let mut cut = [0usize; 2];
1528 for (slot, psi) in [(0, psi_a), (1, psi_b)] {
1529 let (lo, hi) = (rim_points[&(below, arc_index(below, psi)?)], rim_points[&(above, arc_index(above, psi)?)]);
1530 curves.push(ToolCurve { curve: generatrix(psi)?, start: lo, end: hi, degenerate: false, samples: None });
1531 cut[slot] = curves.len() - 1;
1532 }
1533 let label = |psi: f64, v: f64| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)) > 0.0;
1534 let patch = |alpha: f64, beta: f64| -> Result<(NurbsSurface, usize, usize), KernelRefusal> {
1536 let g = generatrix(alpha)?;
1537 let surface = frag(make_revolution(origin, w, &g, beta - alpha), "torus_patch")?;
1538 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1539 let spans_v = (g.knots.len() - 6) / 2 + 1;
1540 Ok((surface, spans_u, spans_v))
1541 };
1542 let inverse_pcurve = |curve: &NurbsCurve, alpha: f64, beta: f64, spans_u: usize, spans_v: usize, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
1543 let [d0, d1] = frag(curve.domain(), "domain")?;
1544 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1545 let mut uv = Vec::with_capacity(ts.len());
1546 for &t in &ts {
1547 let x = frag(curve.evaluate(t), "point")?;
1548 let r = x.sub(origin);
1549 let mid = 0.5 * (alpha + beta);
1550 let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
1551 let q = x.sub(tube_centre(psi));
1552 let radial = u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1553 let vmid = 0.5 * (start + end);
1554 let v = vmid + (q.dot(w).atan2(q.dot(radial)) - vmid + PI).rem_euclid(TAU) - PI;
1555 uv.push(Vec3::new(
1556 crate::circle_angle_to_parameter(spans_u, beta - alpha, psi - alpha),
1557 crate::circle_angle_to_parameter(spans_v, end - start, v - start),
1558 0.0,
1559 ));
1560 }
1561 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "torus_pcurve")?;
1562 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
1563 };
1564 {
1566 let (surface, _, band_spans_v) = patch(psi_b, psi_a + TAU)?;
1567 let (bottom, top) = (rim_arcs[&(below, arc_index(below, psi_b)?)], rim_arcs[&(above, arc_index(above, psi_b)?)]);
1568 let coedges = vec![
1569 (bottom, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1570 (cut[0], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1571 (top, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1572 (cut[1], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1573 ];
1574 let mid = psi_b + 0.5 * (psi_a + TAU - psi_b);
1575 let chart_spans = spans_of(&surface.knots_u);
1576 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 } }) });
1577 }
1578 let (surface, spans_u, spans_v) = patch(psi_a, psi_b)?;
1580 let rim_arc = rim_arcs[&(rim_k, arc_index(rim_k, psi_a)?)];
1581 let other_arc = rim_arcs[&(other_k, arc_index(other_k, psi_a)?)];
1582 let mid = 0.5 * (psi_a + psi_b);
1583 let v_k = root(mid).ok_or_else(|| refuse("torus_zone", "no grazing point mid-way".into()))?;
1584 let k_pc = |forward: bool| inverse_pcurve(&curves[k_curve].curve, psi_a, psi_b, spans_u, spans_v, forward);
1585 let lens_v = if at_end { 0.5 * (v_k + end) } else { 0.5 * (v_k + start) };
1586 let rest_v = if at_end { 0.5 * (v_k + start) } else { 0.5 * (v_k + end) };
1587 if at_end {
1588 let lens = vec![(k_curve, true, k_pc(true)?), (rim_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?)];
1590 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 } }) });
1591 let rest = vec![
1592 (other_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1593 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1594 (k_curve, false, k_pc(false)?),
1595 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1596 ];
1597 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 } }) });
1598 } else {
1599 let lens = vec![(rim_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?), (k_curve, false, k_pc(false)?)];
1600 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 } }) });
1601 let rest = vec![
1602 (k_curve, true, k_pc(true)?),
1603 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1604 (other_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1605 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1606 ];
1607 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 } }) });
1608 }
1609 }
1610 Kind::Ball { centre, radius } => {
1611 let m = frag(screw.velocity(*centre).normalized(), "ball_still")?;
1612 let d = frag(w.cross(m).normalized().or_else(|_| u1.cross(m).normalized()), "ball_axis")?;
1613 let (e1, e2) = (centre.add(d.scale(*radius)), centre.sub(d.scale(*radius)));
1614 points.push(e1);
1615 points.push(e2);
1616 let (i1, i2) = (points.len() - 2, points.len() - 1);
1617 let half = frag(make_revolution(*centre, m, &frag(make_line(e1, e1.add(m)), "line")?, PI), "great")?;
1618 let k_a = frag(half.iso_curve_v(0.0), "great")?;
1619 let k_a_samples: Vec<Vec3> = {
1622 let [a0, a1] = frag(k_a.domain(), "domain")?;
1623 let middle = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?;
1624 let arm = e1.sub(*centre);
1625 let sense = if arm.rotate_about(m, 0.5 * PI).add(*centre).sub(middle).length() < arm.rotate_about(m, -0.5 * PI).add(*centre).sub(middle).length() { 1.0 } else { -1.0 };
1626 let mut list: Vec<Vec3> = (0..=LOFT_SAMPLES).map(|i| centre.add(arm.rotate_about(m, sense * PI * i as f64 / LOFT_SAMPLES as f64))).collect();
1627 list[0] = e1;
1628 list[LOFT_SAMPLES] = e2;
1629 list
1630 };
1631 curves.push(ToolCurve { curve: k_a.clone(), start: i1, end: i2, degenerate: false, samples: Some(k_a_samples.clone()) });
1632 let k_a_index = curves.len() - 1;
1633 let [a0, a1] = frag(k_a.domain(), "domain")?;
1634 let h = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?.sub(*centre);
1635 let mut k_b_index: Option<usize> = None;
1636 for front in [true, false] {
1637 let toward = if front { m } else { m.scale(-1.0) };
1638 let da = if d.cross(h).dot(toward) > 0.0 { d } else { d.scale(-1.0) };
1639 let mut generatrix = k_a.clone();
1640 let mut surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1641 let outward = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1642 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1643 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1644 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1645 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1646 };
1647 let mut forward = true;
1648 if !outward(&surface)? {
1649 generatrix = frag(generatrix.reversed(), "reversed")?;
1650 surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1651 forward = false;
1652 }
1653 let (start, end) = if forward { (i1, i2) } else { (i2, i1) };
1654 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1655 let k_b = match k_b_index {
1656 Some(index) => index,
1657 None => {
1658 let mut samples: Vec<Vec3> = k_a_samples.iter().map(|&x| centre.add(x.sub(*centre).rotate_about(da, PI))).collect();
1660 if !forward {
1661 samples.reverse();
1662 }
1663 samples[0] = points[start];
1664 samples[LOFT_SAMPLES] = points[end];
1665 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: Some(samples) });
1666 k_b_index = Some(curves.len() - 1);
1667 curves.len() - 1
1668 }
1669 };
1670 let k_b_forward = curves[k_b].start == start;
1671 let k_b_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1672 let poles = [start, end].map(|p| {
1673 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1674 curves.len() - 1
1675 });
1676 let coedges = vec![
1677 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1678 (k_b, k_b_forward, k_b_pcurve),
1679 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1680 (k_a_index, !forward, uv_line([0.0, g1], [0.0, g0])?),
1681 ];
1682 caps.push(Cap { surface, front, coedges, chart: None });
1683 }
1684 }
1685 Kind::Sphere { centre, pole_at_start } => {
1686 let rim = if *pole_at_start { above } else { below };
1687 let angles = rim_angles[&rim].clone();
1688 if angles.len() != 2 {
1689 return Err(refuse("revolved_rims", "a hemisphere's rim is not split in two".into()));
1690 }
1691 let pole_side = w.scale(if *pole_at_start { m0[1] - m1[1] } else { m1[1] - m0[1] }.signum());
1692 let m = screw.velocity(*centre);
1693 let e1 = points[rim_points[&(rim, 0)]];
1694 let d = frag(e1.sub(*centre).normalized(), "lune_axis")?;
1695 let mut great: Option<usize> = None;
1696 for j in 0..2 {
1697 let arc = rim_arcs[&(rim, j)];
1698 let [a0, a1] = frag(curves[arc].curve.domain(), "domain")?;
1699 let middle = frag(curves[arc].curve.evaluate(0.5 * (a0 + a1)), "middle")?;
1700 let hv = middle.sub(*centre);
1701 let da = if d.cross(hv).dot(pole_side) > 0.0 { d } else { d.scale(-1.0) };
1702 let theta = (-m.dot(hv)).atan2(m.dot(da.cross(hv))).rem_euclid(TAU);
1704 let theta = if theta > PI { theta - PI } else { theta };
1705 if !(theta > 1e-9 && theta < PI - 1e-9) {
1706 return Err(refuse("revolved_lune", "the great circle meets the rim's plane edge-on".into()));
1707 }
1708 let mut generatrix = curves[arc].curve.clone();
1709 let mut surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1710 let probe = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1712 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1713 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1714 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1715 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1716 };
1717 let mut forward = true;
1718 if !probe(&surface)? {
1719 generatrix = frag(generatrix.reversed(), "reversed")?;
1720 surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1721 forward = false;
1722 }
1723 let (start, end) = if forward { (curves[arc].start, curves[arc].end) } else { (curves[arc].end, curves[arc].start) };
1724 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1725 let k_curve = match great {
1728 Some(index) => index,
1729 None => {
1730 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1731 great = Some(curves.len() - 1);
1732 curves.len() - 1
1733 }
1734 };
1735 let k_forward = curves[k_curve].start == start;
1736 let poles = [start, end].map(|p| {
1737 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1738 curves.len() - 1
1739 });
1740 let k_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1741 let coedges = vec![
1742 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1743 (k_curve, k_forward, k_pcurve),
1744 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1745 (arc, !forward, uv_line([0.0, g1], [0.0, g0])?),
1746 ];
1747 let mid_psi = if j == 0 { 0.5 * (angles[0] + angles[1]) } else { 0.5 * (angles[1] + angles[0] + TAU) };
1748 let front = m.dot(at(1.0, 0.0, mid_psi).sub(origin)) > 0.0;
1749 caps.push(Cap { surface, front, coedges, chart: None });
1750 }
1751 }
1752 }
1753 }
1754 Ok(Split {
1755 points,
1756 curves,
1757 caps,
1758 slides,
1759 slide_sides,
1760 scale,
1761 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1762 })
1763}
1764
1765#[allow(clippy::too_many_arguments)]
1772fn split_torus(
1773 screw: &Screw,
1774 origin: Vec3,
1775 w: Vec3,
1776 u1: Vec3,
1777 u2: Vec3,
1778 centre: [f64; 2],
1779 radius: f64,
1780 meridian: &[OutlinePiece],
1781) -> Result<Split, KernelRefusal> {
1782 let (rho_c, z_c) = (centre[0], centre[1]);
1783 let radial = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1784 let tube_centre = |psi: f64| origin.add(w.scale(z_c)).add(radial(psi).scale(rho_c));
1785 let point = |psi: f64, v: f64| tube_centre(psi).add(radial(psi).scale(radius * v.cos())).add(w.scale(radius * v.sin()));
1786 let g = |psi: f64, v: f64| screw.velocity(point(psi, v)).dot(radial(psi).scale(v.cos()).add(w.scale(v.sin())));
1787 let reference = {
1788 let (a, b, _) = harmonic(|v| g(0.0, v));
1789 b.atan2(a)
1790 };
1791 let branches = |psi: f64| -> Option<(f64, f64)> {
1793 let (a, b, c) = harmonic(|v| g(psi, v));
1794 let r = a.hypot(b);
1795 if !(c.abs() < r * (1.0 - 1e-9)) {
1796 return None;
1797 }
1798 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
1799 let spread = (-c / r).acos();
1800 Some((base - spread, base + spread))
1801 };
1802 let n = screw.loop_samples;
1803 let halves = [(0.0, PI), (PI, TAU)];
1804 let mut samples: Vec<Vec<(f64, f64, f64)>> = Vec::new(); for &(a, b) in &halves {
1806 let mut list = Vec::with_capacity(n + 1);
1807 let mut previous: Option<(f64, f64)> = None;
1808 for i in 0..=n {
1809 let psi = a + (b - a) * i as f64 / n as f64;
1810 let Some(pair) = branches(psi) else {
1811 return Err(refuse("ring_grazing", "the ring's grazing curves do not run all the way round it".into()));
1812 };
1813 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
1814 return Err(refuse("ring_grazing", "the ring's grazing curves turn too fast".into()));
1815 }
1816 previous = Some(pair);
1817 list.push((psi, pair.0, pair.1));
1818 }
1819 samples.push(list);
1820 }
1821 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1822 let mut points: Vec<Vec3> = Vec::new();
1823 let mut curves: Vec<ToolCurve> = Vec::new();
1824 let mut corner: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1827 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)) };
1828 for (branch, v) in [(0, lower), (1, upper)] {
1829 points.push(point(psi, v));
1830 corner.insert((seam, branch), points.len() - 1);
1831 }
1832 }
1833 let seam_of_end = |half: usize, at_end: bool| -> usize { if at_end { 1 - half } else { half } };
1834 let mut branch_curve: HashMap<(usize, usize), usize> = HashMap::new(); for half in 0..2 {
1837 for branch in 0..2 {
1838 let mut xyz: Vec<Vec3> = samples[half].iter().map(|&(psi, lo, hi)| point(psi, if branch == 0 { lo } else { hi })).collect();
1839 let (start, end) = (corner[&(seam_of_end(half, false), branch)], corner[&(seam_of_end(half, true), branch)]);
1840 xyz[0] = points[start];
1841 xyz[n] = points[end];
1842 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "ring_branch")?, start, end, degenerate: false, samples: Some(xyz.clone()) });
1843 branch_curve.insert((half, branch), curves.len() - 1);
1844 }
1845 }
1846 let tube_arc = |psi: f64, v0: f64, v1: f64| -> Result<NurbsCurve, KernelRefusal> {
1849 let start = point(psi, v0);
1850 let axis = radial(psi).cross(w);
1851 let surface = frag(make_revolution(tube_centre(psi), axis, &frag(make_line(start, start.add(axis)), "line")?, v1 - v0), "tube_arc")?;
1852 frag(surface.iso_curve_v(0.0), "tube_arc")
1853 };
1854 let mut seam_arc: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1856 let psi = if seam == 0 { 0.0 } else { PI };
1857 let (lower, upper) = (samples[seam][0].1, samples[seam][0].2);
1858 let (lo_point, hi_point) = (corner[&(seam, 0)], corner[&(seam, 1)]);
1859 curves.push(ToolCurve { curve: tube_arc(psi, lower, upper)?, start: lo_point, end: hi_point, degenerate: false, samples: None });
1860 seam_arc.insert((seam, 0), curves.len() - 1);
1861 curves.push(ToolCurve { curve: tube_arc(psi, upper, lower + TAU)?, start: hi_point, end: lo_point, degenerate: false, samples: None });
1862 seam_arc.insert((seam, 1), curves.len() - 1);
1863 }
1864 let mut caps = Vec::new();
1865 for half in 0..2 {
1866 let (psi_a, psi_b) = halves[half];
1867 for band in 0..2 {
1868 let (lo_branch, hi_branch, hi_turn) = if band == 0 { (0usize, 1usize, 0.0) } else { (1, 0, TAU) };
1870 let pick = |s: &(f64, f64, f64), b: usize| if b == 0 { s.1 } else { s.2 };
1871 let low = samples[half].iter().map(|s| pick(s, lo_branch)).fold(f64::INFINITY, f64::min);
1874 let high = samples[half].iter().map(|s| pick(s, hi_branch) + hi_turn).fold(f64::NEG_INFINITY, f64::max);
1875 let margin = (0.25 * (TAU - (high - low))).clamp(1e-3, 0.25);
1876 let (v_low, v_high) = (low - margin, high + margin);
1877 let theta_v = v_high - v_low;
1878 if theta_v >= TAU {
1879 return Err(refuse("ring_grazing", "a band of the ring wraps all the way round its tube".into()));
1880 }
1881 let generatrix = tube_arc(psi_a, v_low, v_high)?;
1882 let surface = frag(make_revolution(origin, w, &generatrix, psi_b - psi_a), "ring_band")?;
1883 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1884 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
1885 let uv_of = |psi: f64, v: f64| {
1886 Vec3::new(
1887 crate::circle_angle_to_parameter(spans_u, psi_b - psi_a, psi - psi_a),
1888 crate::circle_angle_to_parameter(spans_v, theta_v, v - v_low),
1889 0.0,
1890 )
1891 };
1892 let angles_of = |x: Vec3| -> (f64, f64) {
1894 let r = x.sub(origin);
1895 let psi = r.dot(u2).atan2(r.dot(u1));
1896 let psi = 0.5 * (psi_a + psi_b) + (psi - 0.5 * (psi_a + psi_b) + PI).rem_euclid(TAU) - PI;
1897 let q = x.sub(tube_centre(psi));
1898 let v = q.dot(w).atan2(q.dot(radial(psi)));
1899 let mid = 0.5 * (v_low + v_high);
1900 (psi, mid + (v - mid + PI).rem_euclid(TAU) - PI)
1901 };
1902 let inverse_pcurve = |curve: &NurbsCurve| -> Result<NurbsCurve, KernelRefusal> {
1903 let [d0, d1] = frag(curve.domain(), "domain")?;
1904 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1905 let mut uv = Vec::with_capacity(ts.len());
1906 for &t in &ts {
1907 let (psi, v) = angles_of(frag(curve.evaluate(t), "point")?);
1908 uv.push(uv_of(psi, v));
1909 }
1910 frag(crate::interpolate_curve(&uv, 5, &ts), "ring_pcurve")
1911 };
1912 let branch_pcurve = |b: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> { inverse_pcurve(&curves[branch_curve[&(half, b)]].curve) };
1913 let lower_edge = branch_curve[&(half, lo_branch)];
1914 let upper_edge = branch_curve[&(half, hi_branch)];
1915 let right = seam_arc[&(seam_of_end(half, true), band)];
1916 let left = seam_arc[&(seam_of_end(half, false), band)];
1917 let coedges = vec![
1918 (lower_edge, true, branch_pcurve(lo_branch, 0.0)?),
1919 (right, true, inverse_pcurve(&curves[right].curve)?),
1920 (upper_edge, false, frag(branch_pcurve(hi_branch, hi_turn)?.reversed(), "reversed")?),
1921 (left, false, frag(inverse_pcurve(&curves[left].curve)?.reversed(), "reversed")?),
1922 ];
1923 let mid = &samples[half][n / 2];
1924 let front = g(mid.0, 0.5 * (pick(mid, lo_branch) + pick(mid, hi_branch) + hi_turn)) > 0.0;
1925 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 } }) });
1926 }
1927 }
1928 Ok(Split {
1929 points,
1930 curves,
1931 caps,
1932 slides: Vec::new(),
1933 slide_sides: Vec::new(),
1934 scale: rho_c + radius,
1935 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1936 })
1937}
1938
1939struct Shell {
1941 vertices: Vec<VertexRecord>,
1942 edges: Vec<EdgeRecord>,
1943 faces: Vec<FaceRecord>,
1944 next: u64,
1945}
1946
1947impl Shell {
1948 fn new() -> Self {
1949 Self { vertices: Vec::new(), edges: Vec::new(), faces: Vec::new(), next: 1 }
1950 }
1951
1952 fn id(&mut self) -> u64 {
1953 self.next += 1;
1954 self.next
1955 }
1956
1957 fn vertex(&mut self, point: Vec3) -> u64 {
1958 let id = self.id();
1959 self.vertices.push(VertexRecord { id, point });
1960 id
1961 }
1962
1963 fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
1964 let domain = frag(curve.domain(), "edge_domain")?;
1965 let id = self.id();
1966 self.edges.push(EdgeRecord { id, curve, t0: domain[0], t1: domain[1], start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
1967 Ok(id)
1968 }
1969
1970 fn face(&mut self, surface: NurbsSurface, coedges: Vec<(u64, bool, NurbsCurve)>) -> u64 {
1973 let mut records = Vec::with_capacity(coedges.len());
1974 for (edge, forward, pcurve) in coedges {
1975 let id = self.id();
1976 records.push(CoedgeRecord { id, edge_id: edge, forward, pcurve });
1977 }
1978 let loop_id = self.id();
1979 let id = self.id();
1980 self.faces.push(FaceRecord { id, surface, same_sense: true, loops: vec![LoopRecord { id: loop_id, coedges: records }], name: None });
1981 id
1982 }
1983
1984 fn solid(mut self) -> BrepSolid {
1985 let shell = self.id();
1986 let id = self.id();
1987 let edges = self.edges.iter().filter(|e| !e.degenerate).count() as i64;
1990 let euler = self.vertices.len() as i64 - edges + self.faces.len() as i64;
1991 let genus = (2 - euler) / 2;
1992 BrepSolid {
1993 mass_properties_cache: Default::default(),
1994 id,
1995 vertices: self.vertices,
1996 edges: self.edges,
1997 shells: vec![ShellRecord { id: shell, faces: self.faces }],
1998 genus,
1999 kind: Default::default(),
2000 cells: Default::default(),
2001 }
2002 }
2003}
2004
2005pub fn sweep_solid(tool: &SolidTool, motion: &SolidMotion) -> Result<BrepSolid, KernelRefusal> {
2008 sweep_with(&Screw::from_motion(motion)?, tool)
2009}
2010
2011fn sweep_with(screw: &Screw, tool: &SolidTool) -> Result<BrepSolid, KernelRefusal> {
2012 let screw = *screw;
2013 if let SolidTool::Revolved { axis_origin, axis_direction, meridian, .. } = tool {
2014 if let Some(body) = ball_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
2015 return Ok(body);
2016 }
2017 }
2018 if let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool {
2019 if let Some(body) = flat_ring(&screw, *axis_origin, *axis_direction, *radial, meridian)? {
2020 return Ok(body);
2021 }
2022 if let Some(body) = rod_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
2023 return Ok(body);
2024 }
2025 }
2026 if let SolidTool::Polyhedron { vertices, faces } = tool {
2027 let whole = screw.omega != 0.0 && screw.c == 0.0 && screw.c2 == 0.0 && screw.frame.is_none() && screw.turns.is_none() && screw.omega.abs() * (screw.t1 - screw.t0) >= TAU * (1.0 - 1e-12);
2028 if whole {
2029 return whole_turn_shadow(&screw, vertices, faces);
2030 }
2031 }
2032 let split = match tool {
2033 SolidTool::Polyhedron { vertices, faces } => split_polyhedron(&screw, vertices, faces)?,
2034 SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } => {
2035 split_revolved(&screw, *axis_origin, *axis_direction, *radial, meridian)?
2036 }
2037 };
2038 let grazing = split.grazing()?;
2039 if grazing.is_empty() && split.slide_sides.is_empty() {
2042 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
2043 }
2044 let times = row_times(&screw);
2045 let last = times.len() - 1;
2046 let mut shell = Shell::new();
2047 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
2050 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
2051 let mut at_time = |shell: &mut Shell, j: usize, p: usize| -> u64 {
2052 *vertex_at.entry((j, p)).or_insert_with(|| shell.vertex(screw.apply(times[j], split.points[p])))
2053 };
2054 let mut time_edge = |shell: &mut Shell, j: usize, c: usize, vertex: &mut dyn FnMut(&mut Shell, usize, usize) -> u64| -> Result<u64, KernelRefusal> {
2055 if let Some(&edge) = edge_at.get(&(j, c)) {
2056 return Ok(edge);
2057 }
2058 let curve = &split.curves[c];
2059 let (a, b) = (vertex(shell, j, curve.start), vertex(shell, j, curve.end));
2060 let edge = shell.edge(screw.move_curve(times[j], &curve.curve)?, a, b, curve.degenerate)?;
2061 edge_at.insert((j, c), edge);
2062 Ok(edge)
2063 };
2064 for cap in &split.caps {
2066 let j = if cap.front { last } else { 0 };
2067 let surface = screw.move_surface(times[j], &cap.surface)?;
2068 let mut coedges = Vec::with_capacity(cap.coedges.len());
2069 for (c, forward, pcurve) in &cap.coedges {
2070 let edge = time_edge(&mut shell, j, *c, &mut at_time)?;
2071 coedges.push((edge, *forward, pcurve.clone()));
2072 }
2073 shell.face(surface, coedges);
2074 }
2075 let mut orbit_at: HashMap<(usize, usize), u64> = HashMap::new();
2081 for row in 0..last {
2082 let (ta, tb) = (times[row], times[row + 1]);
2083 let segments = carry_segments(&screw, ta, tb);
2084 let knots_v = carry_knots(segments);
2085 let mut orbits: HashMap<usize, u64> = HashMap::new();
2086 for &(c, _) in &grazing {
2087 for r in [split.curves[c].start, split.curves[c].end] {
2088 if orbits.contains_key(&r) {
2089 continue;
2090 }
2091 let (v0, v1) = (at_time(&mut shell, row, r), at_time(&mut shell, row + 1, r));
2092 let edge = shell.edge(carry_curve(&screw, split.points[r], ta, tb, segments)?, v0, v1, false)?;
2093 orbits.insert(r, edge);
2094 orbit_at.insert((row, r), edge);
2095 }
2096 }
2097 for &(c, back_forward) in &grazing {
2098 let tool_curve = &split.curves[c];
2099 let along = if back_forward { frag(tool_curve.curve.reversed(), "reversed")? } else { tool_curve.curve.clone() };
2100 let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
2101 let mut rows = Vec::with_capacity(along.control_points.len());
2102 for p in &along.control_points {
2103 let x = frag(p.point(), "control_point")?;
2104 rows.push(carry_points(&screw, x, ta, tb, segments).into_iter().map(|q| Vec4::from_point(q, p.w)).collect::<Vec<_>>());
2105 }
2106 let surface = frag(NurbsSurface::new(along.degree, 5, along.knots.clone(), knots_v.clone(), rows), "wall")?;
2107 let [u0, u1] = frag(along.domain(), "domain")?;
2108 let bottom = time_edge(&mut shell, row, c, &mut at_time)?;
2109 let top = time_edge(&mut shell, row + 1, c, &mut at_time)?;
2110 let coedges = vec![
2111 (bottom, !back_forward, uv_line([u0, 0.0], [u1, 0.0])?),
2112 (orbits[&to], true, uv_line([u1, 0.0], [u1, 1.0])?),
2113 (top, back_forward, uv_line([u1, 1.0], [u0, 1.0])?),
2114 (orbits[&from], false, uv_line([u0, 1.0], [u0, 0.0])?),
2115 ];
2116 shell.face(surface, coedges);
2117 }
2118 }
2119 let mut sliding: std::collections::HashSet<u64> = std::collections::HashSet::new();
2123 let side_of: HashMap<usize, bool> = split.caps.iter().flat_map(|cap| cap.coedges.iter().map(move |&(c, _, _)| (c, cap.front))).collect();
2124 for slide in &split.slides {
2125 let centre = screw.apply(times[0], slide.centre);
2126 let u = screw.apply(times[0], slide.centre.add(slide.u)).sub(centre);
2127 let v = screw.apply(times[0], slide.centre.add(slide.v)).sub(centre);
2128 let row_of = |c: usize| -> Result<usize, KernelRefusal> {
2129 match side_of.get(&c) {
2130 Some(true) => Ok(last),
2131 Some(false) => Ok(0),
2132 None => Err(refuse("revolved_grazing", "a sliding end has no face beside its rim".into())),
2133 }
2134 };
2135 let mut coedges = Vec::new();
2136 let mut corners: Vec<Vec3> = Vec::new();
2137 let mut push = |coedges: &mut Vec<(u64, bool, NurbsCurve)>, corners: &mut Vec<Vec3>, edge: u64, forward: bool, curve: &NurbsCurve| -> Result<(), KernelRefusal> {
2138 let [d0, d1] = frag(curve.domain(), "domain")?;
2139 for k in 0..=8 {
2140 corners.push(frag(curve.evaluate(d0 + (d1 - d0) * k as f64 / 8.0), "slide")?);
2141 }
2142 coedges.push((edge, forward, plane_pcurve(centre, u, v, curve, forward)?));
2143 Ok(())
2144 };
2145 for (index, &(c, forward)) in slide.rim.iter().enumerate() {
2146 let j = row_of(c)?;
2147 let edge = time_edge(&mut shell, j, c, &mut at_time)?;
2148 push(&mut coedges, &mut corners, edge, forward, &screw.move_curve(times[j], &split.curves[c].curve)?)?;
2149 let (next, _) = slide.rim[(index + 1) % slide.rim.len()];
2151 let next_j = row_of(next)?;
2152 if next_j == j {
2153 continue;
2154 }
2155 let point = if forward { split.curves[c].end } else { split.curves[c].start };
2156 let rows: Vec<usize> = if next_j > j { (j..next_j).collect() } else { (next_j..j).rev().collect() };
2157 for row in rows {
2158 let Some(&orbit) = orbit_at.get(&(row, point)) else {
2159 return Err(refuse("revolved_grazing", "a sliding end's rim changes side where no grazing curve meets it".into()));
2160 };
2161 let (ta, tb) = (times[row], times[row + 1]);
2162 let curve = carry_curve(&screw, split.points[point], ta, tb, carry_segments(&screw, ta, tb))?;
2163 push(&mut coedges, &mut corners, orbit, next_j > j, &curve)?;
2164 }
2165 }
2166 sliding.insert(shell.face(plane_patch(centre, u, v, &corners)?, coedges));
2167 }
2168 for side in &split.slide_sides {
2171 let row_of = |c: usize| -> Result<usize, KernelRefusal> {
2172 match side_of.get(&c) {
2173 Some(true) => Ok(last),
2174 Some(false) => Ok(0),
2175 None => Err(refuse("revolved_grazing", "a sliding side has no face beside its rim".into())),
2176 }
2177 };
2178 let origin = screw.apply(times[0], side.origin);
2179 let w = screw.apply(times[0], side.origin.add(side.w)).sub(origin);
2180 let rows: Vec<[usize; 2]> = side.patches.iter().map(|patch| Ok([row_of(patch.2)?, row_of(patch.3)?])).collect::<Result<_, KernelRefusal>>()?;
2181 if rows.iter().any(|r| *r != rows[0]) {
2182 return Err(refuse("revolved_grazing", "a sliding side's rim changes side along it".into()));
2183 }
2184 let [jb, ja] = rows[0];
2185 let mut seams = Vec::with_capacity(side.patches.len());
2187 for patch in &side.patches {
2188 let (a, b) = (at_time(&mut shell, jb, patch.4[0]), at_time(&mut shell, ja, patch.4[1]));
2189 let line = frag(make_line(screw.apply(times[jb], split.points[patch.4[0]]), screw.apply(times[ja], split.points[patch.4[1]])), "seam")?;
2190 seams.push((shell.edge(line.clone(), a, b, false)?, line));
2191 }
2192 for (k, patch) in side.patches.iter().enumerate() {
2193 let surface = frag(make_revolution(origin, w, &seams[k].1, patch.1), "slide_side")?;
2194 let lower = time_edge(&mut shell, jb, patch.2, &mut at_time)?;
2195 let upper = time_edge(&mut shell, ja, patch.3, &mut at_time)?;
2196 let next = (k + 1) % side.patches.len();
2197 let face = shell.face(
2198 surface,
2199 vec![
2200 (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
2201 (seams[next].0, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
2202 (upper, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
2203 (seams[k].0, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
2204 ],
2205 );
2206 sliding.insert(face);
2207 }
2208 }
2209 let body = shell.solid();
2210 let issues = body.validate();
2211 if !issues.is_empty() {
2212 return Err(refuse("assembly", format!("the assembled sweep is not valid: {:?}", issues.first())));
2213 }
2214 check_single_pass(&screw, &split, &body, &sliding)?;
2215 Ok(body)
2216}
2217
2218fn rod_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2236 if screw.frame.is_some() || screw.turns.is_some() || screw.omega == 0.0 || screw.c != 0.0 || screw.c2 != 0.0 {
2237 return Ok(None);
2238 }
2239 let Ok(w) = axis.normalized() else { return Ok(None) };
2240 let mut corners: Vec<[f64; 2]> = Vec::new();
2242 for piece in meridian {
2243 let OutlinePiece::Line { from, to } = piece else { return Ok(None) };
2244 corners.push(*from);
2245 corners.push(*to);
2246 }
2247 if meridian.len() != 4 {
2248 return Ok(None);
2249 }
2250 let r = corners.iter().map(|c| c[0]).fold(0.0f64, f64::max);
2251 let (z0, z1) = corners.iter().map(|c| c[1]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), z| (lo.min(z), hi.max(z)));
2252 let scale = r.max(z0.abs()).max(z1.abs());
2253 let on = |a: f64, b: f64| (a - b).abs() <= 1e-12 * scale;
2254 if !(r > 0.0 && z1 > z0) || !corners.iter().all(|c| (on(c[0], 0.0) || on(c[0], r)) && (on(c[1], z0) || on(c[1], z1))) {
2255 return Ok(None);
2256 }
2257 if w.dot(screw.e).abs() > 1e-12 {
2259 return Ok(None);
2260 }
2261 let offset = screw.a.sub(origin);
2262 let along = offset.dot(w);
2263 let foot = origin.add(w.scale(along));
2264 let apart = screw.a.sub(foot);
2265 if apart.sub(screw.e.scale(apart.dot(screw.e))).length() > 1e-12 * (scale + offset.length()) {
2266 return Ok(None);
2267 }
2268 if !(along > z0 + 1e-9 * scale && along < z1 - 1e-9 * scale) {
2269 return Ok(None);
2270 }
2271 let theta = screw.omega.abs() * (screw.t1 - screw.t0);
2272 let arms = [z1 - along, along - z0];
2273 let gap = arms.iter().map(|l| (l / l.hypot(r)).acos()).fold(0.0f64, f64::max);
2274 if !(theta > 2.0 * gap * (1.0 + 1e-9) && theta < TAU * (1.0 - 1e-9)) {
2275 return Err(refuse("rod_across", "a rod turned about a line through it is swept only for a turn wider than its ends' gaps and short of a whole turn".into()));
2276 }
2277 let arc = |centre: Vec3, from: Vec3, normal: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
2278 let surface = frag(make_revolution(centre, normal, &frag(make_line(from, from.add(normal)), "line")?, angle), "rod_across")?;
2279 frag(surface.iso_curve_v(0.0), "rod_across")
2280 };
2281 let line = |a: Vec3, b: Vec3| frag(make_line(a, b), "rod_across");
2282 let filled_at = |t: f64| -> Result<BrepSolid, KernelRefusal> {
2284 let o = screw.apply(t, foot);
2285 let x = screw.apply(t, foot.add(w)).sub(o);
2286 let up = screw.e;
2287 let at = |rho: f64, s: f64| o.add(up.scale(rho)).add(x.scale(s));
2288 let n = up.cross(x);
2289 let (far_plus, far_minus) = (arms[0].hypot(r), arms[1].hypot(r));
2290 let profile = [
2291 arc(o, at(0.0, -far_minus), n, r.atan2(arms[1]))?,
2292 line(at(r, -arms[1]), at(r, arms[0]))?,
2293 arc(o, at(r, arms[0]), n, r.atan2(arms[0]))?,
2294 line(at(0.0, far_plus), at(0.0, -far_minus))?,
2295 ];
2296 frag(crate::revolve_profile_brep(&profile, o, x, TAU), "rod_across")
2297 };
2298 let o = screw.apply(screw.t0, foot);
2299 let x = screw.apply(screw.t0, foot.add(w)).sub(o);
2300 let turn_axis = screw.e.scale(screw.omega.signum());
2301 let section = |direction: Vec3, length: f64| -> Result<BrepSolid, KernelRefusal> {
2302 let at = |rho: f64, z: f64| o.add(direction.scale(rho)).add(screw.e.scale(z));
2303 let half = (r / length.hypot(r)).asin();
2304 let profile = [
2305 line(at(0.0, -r), at(length, -r))?,
2306 arc(o, at(length, -r), direction.cross(screw.e), 2.0 * half)?,
2307 line(at(length, r), at(0.0, r))?,
2308 line(at(0.0, r), at(0.0, -r))?,
2309 ];
2310 frag(crate::revolve_profile_brep(&profile, o, turn_axis, theta), "rod_across")
2311 };
2312 let parts = [filled_at(screw.t0)?, filled_at(screw.t1)?, section(x, arms[0])?, section(x.scale(-1.0), arms[1])?];
2313 crate::boolean_operation_nary(&parts, crate::BooleanOperation::Union).map(Some)
2314}
2315
2316fn flat_ring(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2330 if screw.frame.is_some() || screw.turns.is_some() || screw.c2 != 0.0 {
2331 return Ok(None);
2332 }
2333 let Ok(w) = axis.normalized() else { return Ok(None) };
2334 let flat = if screw.omega != 0.0 { screw.e.cross(w).length() <= 1e-12 && screw.c == 0.0 } else { screw.e.dot(w).abs() <= 1e-12 };
2335 if !flat {
2336 return Ok(None);
2337 }
2338 let (big, level, torus, filled_meridian): (f64, f64, Option<f64>, Vec<OutlinePiece>) = match meridian {
2343 [OutlinePiece::Arc { centre, radius, start, end }] => {
2344 let (big, tube, level) = (centre[0], *radius, centre[1]);
2345 if ((end - start).abs() - TAU).abs() >= 1e-12 || big <= tube {
2346 return Ok(None);
2347 }
2348 (
2349 big,
2350 level,
2351 Some(tube),
2352 vec![
2353 OutlinePiece::Line { from: [0.0, level - tube], to: [big, level - tube] },
2354 OutlinePiece::Arc { centre: [big, level], radius: tube, start: -0.5 * PI, end: 0.5 * PI },
2355 OutlinePiece::Line { from: [big, level + tube], to: [0.0, level + tube] },
2356 OutlinePiece::Line { from: [0.0, level + tube], to: [0.0, level - tube] },
2357 ],
2358 )
2359 }
2360 pieces => {
2361 let mut ends: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(pieces.len());
2362 for piece in pieces {
2363 let OutlinePiece::Line { from, to } = piece else { return Ok(None) };
2364 ends.push((*from, *to));
2365 }
2366 let scale = ends.iter().flat_map(|(a, b)| [a[0].abs(), a[1].abs(), b[0].abs(), b[1].abs()]).fold(0.0f64, f64::max).max(1e-300);
2367 let inner = ends.iter().flat_map(|(a, b)| [a[0], b[0]]).fold(f64::INFINITY, f64::min);
2368 let (low, high) = ends.iter().flat_map(|(a, b)| [a[1], b[1]]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), z| (lo.min(z), hi.max(z)));
2369 if ends.len() < 3 || inner <= 1e-9 * scale {
2370 return Ok(None);
2371 }
2372 let walls: Vec<usize> = (0..ends.len())
2373 .filter(|&k| {
2374 let (a, b) = ends[k];
2375 (a[0] - inner).abs() <= 1e-12 * scale && (b[0] - inner).abs() <= 1e-12 * scale && (a[1].min(b[1]) - low).abs() <= 1e-12 * scale && (a[1].max(b[1]) - high).abs() <= 1e-12 * scale
2376 })
2377 .collect();
2378 let [wall] = walls.as_slice() else { return Ok(None) };
2379 let (from, to) = ends[*wall];
2380 let mut filled = vec![OutlinePiece::Line { from: [0.0, to[1]], to }];
2383 for k in 1..pieces.len() {
2384 filled.push(pieces[(wall + k) % pieces.len()].clone());
2385 }
2386 filled.push(OutlinePiece::Line { from, to: [0.0, from[1]] });
2387 filled.push(OutlinePiece::Line { from: [0.0, from[1]], to: [0.0, to[1]] });
2388 (inner, 0.5 * (low + high), None, filled)
2389 }
2390 };
2391 let centre0 = origin.add(w.scale(level));
2392 let filled = SolidTool::Revolved { axis_origin: origin, axis_direction: w, radial, meridian: filled_meridian };
2393 let short = || refuse("ring_flat", "a ring lying flat to its motion travels less than its hole is wide: the hole is not swept shut".into());
2394 if screw.omega == 0.0 {
2395 if screw.c.abs() * (screw.t1 - screw.t0) <= 2.0 * big * (1.0 + 1e-9) {
2396 let Some(tube) = torus else { return Err(short()) };
2397 let hole = |t: f64| -> Result<BrepSolid, KernelRefusal> {
2402 let o = screw.apply(t, centre0);
2403 let u = frag(radial.sub(w.scale(radial.dot(w))).normalized(), "ring_flat")?;
2404 let at = |rho: f64, z: f64| o.add(u.scale(rho)).add(w.scale(z));
2405 let n = w.cross(u);
2406 let from = at(big, -tube);
2407 let inner = frag(frag(make_revolution(at(big, 0.0), n, &frag(make_line(from, from.add(n)), "line")?, PI), "ring_flat")?.iso_curve_v(0.0), "ring_flat")?;
2408 let profile = [
2409 frag(make_line(at(0.0, -tube), at(big, -tube)), "ring_flat")?,
2410 inner,
2411 frag(make_line(at(big, tube), at(0.0, tube)), "ring_flat")?,
2412 frag(make_line(at(0.0, tube), at(0.0, -tube)), "ring_flat")?,
2413 ];
2414 frag(crate::revolve_profile_brep(&profile, o, w, TAU), "ring_flat")
2415 };
2416 let lens = crate::boolean_operation_nary(&[hole(screw.t0)?, hole(screw.t1)?], crate::BooleanOperation::Intersect)?;
2417 let whole = sweep_with(screw, &filled)?;
2418 return crate::boolean_operation_nary(&[whole, lens], crate::BooleanOperation::Subtract).map(Some);
2419 }
2420 return sweep_with(screw, &filled).map(Some);
2421 }
2422 let start_centre = screw.apply(screw.t0, centre0);
2424 let from_axis = start_centre.sub(screw.a);
2425 let across = from_axis.sub(screw.e.scale(from_axis.dot(screw.e)));
2426 let reach = across.length();
2427 let theta = screw.omega.abs() * (screw.t1 - screw.t0);
2428 if theta >= TAU * (1.0 - 1e-12) {
2429 let Some(tube) = torus else {
2430 return Err(refuse("ring_flat", "a whole turn of a holed tool in its own plane is swept for a ring only".into()));
2431 };
2432 if reach <= (big + tube) * (1.0 + 1e-9) {
2433 return Err(refuse("ring_flat", "a ring turned a whole turn in its own plane reaches the turn's axis".into()));
2434 }
2435 let out = frag(across.normalized(), "ring_flat")?;
2436 let foot = start_centre.sub(across);
2437 let at = |rho: f64, z: f64| foot.add(out.scale(rho)).add(screw.e.scale(z));
2438 let half_round = |centre: Vec3, from: Vec3| -> Result<NurbsCurve, KernelRefusal> {
2439 let n = out.cross(screw.e);
2440 let surface = frag(make_revolution(centre, n, &frag(make_line(from, from.add(n)), "line")?, PI), "ring_flat")?;
2441 frag(surface.iso_curve_v(0.0), "ring_flat")
2442 };
2443 let profile = [
2444 frag(make_line(at(reach - big, -tube), at(reach + big, -tube)), "ring_flat")?,
2445 half_round(at(reach + big, 0.0), at(reach + big, -tube))?,
2446 frag(make_line(at(reach + big, tube), at(reach - big, tube)), "ring_flat")?,
2447 half_round(at(reach - big, 0.0), at(reach - big, tube))?,
2448 ];
2449 return frag(crate::revolve_profile_brep(&profile, foot, screw.e, TAU), "ring_flat").map(Some);
2450 }
2451 let covered = if theta >= PI { big < reach } else { reach * (0.5 * theta).sin() > big * (1.0 + 1e-9) };
2452 if !covered {
2453 return Err(short());
2454 }
2455 sweep_with(screw, &filled).map(Some)
2456}
2457
2458pub fn sweep_solid_clipped(tool: &SolidTool, motion: &SolidMotion, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
2469 let screw = Screw::from_motion(motion)?;
2470 if let (SolidTool::Polyhedron { vertices, faces }, Some(wedge)) = (tool, clip.angle) {
2471 let axis = clip.axis_direction.normalized().map_err(|_| input("clip_axis", "the clip's axis has no direction".into()))?;
2472 let same_axis = screw.c == 0.0 && screw.c2 == 0.0
2473 && screw.e.cross(axis).length() < 1e-12
2474 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
2475 if same_axis {
2476 return turn_shadow(&screw, vertices, faces, clip, axis, wedge);
2477 }
2478 }
2479 if let SolidTool::Polyhedron { vertices, faces } = tool {
2480 if let Some((vertices, faces)) = trim_to_clip(&screw, vertices, faces, clip)? {
2481 return sweep_solid(&SolidTool::Polyhedron { vertices, faces }, motion);
2482 }
2483 }
2484 sweep_solid(tool, motion)
2485}
2486
2487pub fn sweep_solid_rising(tool: &SolidTool, motion: &RisingTurn, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
2491 let screw = Screw::from_rising(motion)?;
2492 let SolidTool::Polyhedron { vertices, faces } = tool else {
2493 return Err(refuse("rising_tool", "a growing rise sweeps a polyhedron only".into()));
2494 };
2495 let (vertices, faces) = trim_to_clip(&screw, vertices, faces, clip)?.unwrap_or_else(|| (vertices.clone(), faces.clone()));
2496 sweep_with(&screw, &SolidTool::Polyhedron { vertices, faces })
2497}
2498
2499pub fn sweep_solid_parts(tool: &SolidTool, motion: &SolidMotion) -> Result<Vec<BrepSolid>, KernelRefusal> {
2507 let folded = match sweep_solid(tool, motion) {
2508 Ok(body) => return Ok(vec![body]),
2509 Err(error) if refused_as(&error) == Some("folds") => error,
2510 Err(error) => return Err(error),
2511 };
2512 let SolidTool::Polyhedron { vertices, faces } = tool else { return Err(folded) };
2513 let screw = Screw::from_motion(motion)?;
2514 if screw.omega == 0.0 {
2515 return Err(folded);
2516 }
2517 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);
2518 let mut planes: Vec<(Vec3, f64)> = Vec::new();
2519 for face in faces {
2520 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
2521 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
2522 if n.dot(p.sub(centroid)) < 0.0 {
2523 n = n.scale(-1.0);
2524 }
2525 planes.push((n, n.dot(p)));
2526 }
2527 for (n, d) in planes.clone() {
2531 if n.dot(screw.e).abs() > 1e-12 {
2532 continue;
2533 }
2534 let foot = screw.a.add(n.scale(d - n.dot(screw.a)));
2535 let radial = foot.sub(screw.a).sub(screw.e.scale(foot.sub(screw.a).dot(screw.e)));
2536 if radial.dot(n) <= 0.0 {
2537 continue; }
2539 let cut = match screw.e.cross(radial).normalized() {
2540 Ok(m) => m,
2541 Err(_) => continue,
2542 };
2543 let offset = cut.dot(screw.a);
2544 let mut parts = Vec::with_capacity(2);
2545 for sense in [1.0, -1.0] {
2546 let mut half = planes.clone();
2547 half.push((cut.scale(sense), offset * sense));
2548 let (corners, loops) = polyhedron_of_planes(&half)?;
2549 if corners.len() < 4 || loops.len() < 4 {
2550 parts.clear();
2551 break;
2552 }
2553 match sweep_solid(&SolidTool::Polyhedron { vertices: corners, faces: loops }, motion) {
2554 Ok(body) => parts.push(body),
2555 Err(_) => {
2556 parts.clear();
2557 break;
2558 }
2559 }
2560 }
2561 if parts.len() == 2 {
2562 return Ok(parts);
2563 }
2564 }
2565 Err(folded)
2566}
2567
2568fn trim_to_clip(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip) -> Result<Option<(Vec<Vec3>, Vec<Vec<usize>>)>, KernelRefusal> {
2577 let Ok(axis) = clip.axis_direction.normalized() else { return Ok(None) };
2578 let same_axis = screw.omega != 0.0
2579 && screw.e.cross(axis).length() < 1e-12
2580 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
2581 if !same_axis || !clip.radius[1].is_finite() {
2582 return Ok(None);
2583 }
2584 let ez = axis;
2585 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))?;
2586 let ey = ez.cross(ex);
2587 let flat = |p: Vec3| {
2588 let r = p.sub(clip.axis_origin);
2589 [r.dot(ex), r.dot(ey)]
2590 };
2591 let radius = clip.radius[1] * 1.02;
2592 let shadow: Vec<[f64; 2]> = vertices.iter().map(|&p| flat(p)).collect();
2593 if shadow.iter().all(|q| q[0].hypot(q[1]) <= radius) {
2594 return Ok(None);
2595 }
2596 let mut points = shadow.clone();
2598 points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
2599 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]);
2600 let mut hull: Vec<[f64; 2]> = Vec::new();
2601 for pass in 0..2 {
2602 let start = hull.len();
2603 let ordered: Vec<[f64; 2]> = if pass == 0 { points.clone() } else { points.iter().rev().copied().collect() };
2604 for p in ordered {
2605 while hull.len() >= start + 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
2606 hull.pop();
2607 }
2608 hull.push(p);
2609 }
2610 hull.pop();
2611 }
2612 let m = hull.len();
2614 let inside = |q: [f64; 2]| q[0].hypot(q[1]) <= radius;
2615 let mut nearest = f64::INFINITY;
2617 let mut wraps = true;
2618 for k in 0..m {
2619 let (a, b) = (hull[k], hull[(k + 1) % m]);
2620 let d = [b[0] - a[0], b[1] - a[1]];
2621 let t = (-(a[0] * d[0] + a[1] * d[1]) / (d[0] * d[0] + d[1] * d[1])).clamp(0.0, 1.0);
2622 nearest = nearest.min((a[0] + d[0] * t).hypot(a[1] + d[1] * t));
2623 if cross(a, b, [0.0, 0.0]) < 0.0 {
2624 wraps = false;
2625 }
2626 }
2627 if wraps || nearest >= radius || (0..m).all(|k| inside(hull[k])) {
2628 return Ok(None);
2629 }
2630 let mut leave: Vec<f64> = Vec::new();
2631 let mut enter: Vec<f64> = Vec::new();
2632 for k in 0..m {
2633 let (a, b) = (hull[k], hull[(k + 1) % m]);
2634 let d = [b[0] - a[0], b[1] - a[1]];
2635 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);
2636 let disc = qb * qb - 4.0 * qa * qc;
2637 if disc <= 0.0 {
2638 continue;
2639 }
2640 for t in [(-qb - disc.sqrt()) / (2.0 * qa), (-qb + disc.sqrt()) / (2.0 * qa)] {
2641 if t > 0.0 && t < 1.0 {
2642 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2643 let outward = 2.0 * (q[0] * d[0] + q[1] * d[1]) > 0.0;
2644 (if outward { &mut leave } else { &mut enter }).push(q[1].atan2(q[0]));
2645 }
2646 }
2647 }
2648 let ([a], [b]) = (leave.as_slice(), enter.as_slice()) else {
2649 return Err(refuse("clip_trim", "the tool's shadow leaves the clip's disc more than once".into()));
2650 };
2651 let width = (b - a).rem_euclid(TAU);
2652 if width >= 0.9 * PI {
2653 return Err(refuse("clip_trim", "the tool's shadow spans too much of the clip's circle to be trimmed".into()));
2654 }
2655 let mut planes: Vec<(Vec3, f64)> = Vec::new();
2657 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);
2658 for face in faces {
2659 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
2660 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
2661 if n.dot(p.sub(centroid)) < 0.0 {
2662 n = n.scale(-1.0);
2663 }
2664 planes.push((n, n.dot(p)));
2665 }
2666 for angle in [*a, *b] {
2667 let n = ex.scale(angle.cos()).add(ey.scale(angle.sin()));
2668 planes.push((n, n.dot(clip.axis_origin) + radius));
2669 }
2670 Ok(Some(polyhedron_of_planes(&planes)?))
2671}
2672
2673fn polyhedron_of_planes(planes: &[(Vec3, f64)]) -> Result<(Vec<Vec3>, Vec<Vec<usize>>), KernelRefusal> {
2676 let k = planes.len();
2677 let mut corners: Vec<Vec3> = Vec::new();
2678 let scale = planes.iter().map(|p| p.1.abs()).fold(1.0f64, f64::max);
2679 for i in 0..k {
2680 for j in i + 1..k {
2681 for l in j + 1..k {
2682 let (a, b, c) = (planes[i].0, planes[j].0, planes[l].0);
2683 let det = a.dot(b.cross(c));
2684 if det.abs() < 1e-12 {
2685 continue;
2686 }
2687 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);
2688 if planes.iter().all(|(n, d)| n.dot(p) - d <= 1e-9 * scale) && !corners.iter().any(|q| q.sub(p).length() <= 1e-9 * scale) {
2689 corners.push(p);
2690 }
2691 }
2692 }
2693 }
2694 let mut faces = Vec::new();
2695 for (n, d) in planes {
2696 let on: Vec<usize> = (0..corners.len()).filter(|&c| (n.dot(corners[c]) - d).abs() <= 1e-9 * scale).collect();
2697 if on.len() < 3 {
2698 continue;
2699 }
2700 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);
2701 let u = frag(corners[on[0]].sub(middle).normalized(), "face_axis")?;
2702 let v = n.cross(u);
2703 let mut ordered = on.clone();
2704 ordered.sort_by(|&p, &q| {
2705 let (rp, rq) = (corners[p].sub(middle), corners[q].sub(middle));
2706 rp.dot(v).atan2(rp.dot(u)).total_cmp(&rq.dot(v).atan2(rq.dot(u)))
2707 });
2708 faces.push(ordered);
2709 }
2710 Ok((corners, faces))
2711}
2712
2713fn turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip, axis: Vec3, wedge: [f64; 2]) -> Result<BrepSolid, KernelRefusal> {
2714 let ez = axis;
2715 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()))?;
2716 let ey = ez.cross(ex);
2717 let rate = screw.omega * screw.e.dot(ez).signum();
2719 let local = |p: Vec3| {
2720 let r = p.sub(clip.axis_origin);
2721 [r.dot(ex), r.dot(ey), r.dot(ez)]
2722 };
2723 let corners: Vec<[f64; 3]> = vertices.iter().map(|&p| local(p)).collect();
2724 let mid = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
2727 let mid_angle = mid[1].atan2(mid[0]);
2728 let angles: Vec<f64> = corners.iter().map(|c| mid_angle + (c[1].atan2(c[0]) - mid_angle + PI).rem_euclid(TAU) - PI).collect();
2729 let (low, high) = (angles.iter().copied().fold(f64::INFINITY, f64::min), angles.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2730 if high - low >= PI {
2731 return Err(refuse("turn_shadow", "the tool reaches round the turn's axis".into()));
2732 }
2733 let (start, end) = (rate * screw.t0, rate * screw.t1);
2734 let (first, last) = if rate > 0.0 { (start, end) } else { (end, start) };
2735 let reference = 0.5 * (wedge[0] + wedge[1]);
2736 let near = |a: f64| reference + (a - reference + PI).rem_euclid(TAU) - PI;
2737 let clear = near(high + first) < wedge[0] - 1e-9 && near(low + last) > wedge[1] + 1e-9 && (last - first) < TAU;
2739 if !clear {
2740 return Err(refuse("turn_shadow", "the turned tool's ends reach the clip's wedge; its sweep there is not its turned shadow".into()));
2741 }
2742 let profile = polyhedron_shadow(&corners, faces, false)?;
2743 let (c0, s0) = (wedge[0].cos(), wedge[0].sin());
2745 let world = |rho: f64, z: f64| clip.axis_origin.add(ez.scale(z)).add(ex.scale(rho * c0)).add(ey.scale(rho * s0));
2746 let mut curves = Vec::with_capacity(profile.len());
2747 for piece in &profile {
2748 let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2749 let curve = if points.len() == 2 {
2750 frag(make_line(frag(points[0].point(), "point")?, frag(points[1].point(), "point")?), "line")?
2751 } else {
2752 frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")?
2753 };
2754 curves.push(curve);
2755 }
2756 frag(crate::revolve_profile_brep(&curves, clip.axis_origin, ez, wedge[1] - wedge[0]), "turn_shadow")
2757}
2758
2759fn whole_turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<BrepSolid, KernelRefusal> {
2764 let ez = screw.e;
2765 let ex = frag(ez.perpendicular(), "turn_shadow")?;
2766 let ex = frag(ex.sub(ez.scale(ex.dot(ez))).normalized(), "turn_shadow")?;
2767 let ey = ez.cross(ex);
2768 let corners: Vec<[f64; 3]> = vertices
2769 .iter()
2770 .map(|&p| {
2771 let r = screw.apply(screw.t0, p).sub(screw.a);
2772 [r.dot(ex), r.dot(ey), r.dot(ez)]
2773 })
2774 .collect();
2775 let profile = polyhedron_shadow(&corners, faces, true)?;
2776 let reach = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(0.0f64, f64::max);
2778 let world = |rho: f64, z: f64| screw.a.add(ez.scale(z)).add(ex.scale(if rho.abs() <= 1e-10 * reach { 0.0 } else { rho }));
2779 let mut curves = Vec::with_capacity(profile.len());
2780 let on_axis = |piece: &ShadowPiece| piece.iter().all(|&(rho, _, _)| rho.abs() <= 1e-10 * reach);
2783 let mut along: Option<(f64, f64)> = None;
2784 for piece in &profile {
2785 if on_axis(piece) {
2786 let (za, zb) = (piece[0].1, piece[piece.len() - 1].1);
2787 along = Some(match along {
2788 Some((from, _)) => (from, zb),
2789 None => (za, zb),
2790 });
2791 continue;
2792 }
2793 if let Some((from, to)) = along.take() {
2794 curves.push(frag(make_line(world(0.0, from), world(0.0, to)), "line")?);
2795 }
2796 let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2797 let (first, last) = (frag(points[0].point(), "point")?, frag(points[points.len() - 1].point(), "point")?);
2798 if first.sub(last).length() <= 1e-12 * (1.0 + first.length()) {
2799 continue;
2800 }
2801 let curve = if points.len() == 2 { frag(make_line(first, last), "line")? } else { frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")? };
2802 curves.push(curve);
2803 }
2804 if let Some((from, to)) = along.take() {
2805 curves.push(frag(make_line(world(0.0, from), world(0.0, to)), "line")?);
2806 }
2807 frag(crate::revolve_profile_brep(&curves, screw.a, ez, TAU), "turn_shadow")
2808}
2809
2810type ShadowPiece = Vec<(f64, f64, f64)>;
2813
2814fn polyhedron_shadow(corners: &[[f64; 3]], faces: &[Vec<usize>], through: bool) -> Result<Vec<ShadowPiece>, KernelRefusal> {
2823 let zs: Vec<f64> = corners.iter().map(|c| c[2]).collect();
2824 let (z_low, z_high) = (zs.iter().copied().fold(f64::INFINITY, f64::min), zs.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2825 let scale = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(1.0f64, f64::max);
2826 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
2828 for (f, face) in faces.iter().enumerate() {
2829 for k in 0..face.len() {
2830 let (i, j) = (face[k], face[(k + 1) % face.len()]);
2831 edge_faces.entry((i.min(j), i.max(j))).or_default().push(f);
2832 }
2833 }
2834 let mut edges: Vec<(usize, usize)> = edge_faces.keys().copied().collect();
2835 edges.sort_unstable();
2836 let planes: Vec<([f64; 3], f64)> = faces
2838 .iter()
2839 .map(|face| {
2840 let (a, b, c) = (corners[face[0]], corners[face[1]], corners[face[2]]);
2841 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]]);
2842 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]];
2843 (n, n[0] * a[0] + n[1] * a[1] + n[2] * a[2])
2844 })
2845 .collect();
2846 #[derive(Clone, Copy, PartialEq, Debug)]
2847 enum Source {
2848 Edge(usize),
2850 Face(usize),
2852 Axis,
2854 }
2855 let edge_at = |k: usize, z: f64| -> [f64; 2] {
2856 let (a, b) = (corners[edges[k].0], corners[edges[k].1]);
2857 let t = (z - a[2]) / (b[2] - a[2]);
2858 [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
2859 };
2860 let crossing = |z: f64| -> Vec<usize> {
2861 (0..edges.len()).filter(|&k| (corners[edges[k].0][2] - z) * (corners[edges[k].1][2] - z) < 0.0).collect()
2862 };
2863 let farthest = |z: f64| -> (Source, f64) {
2864 let mut best = (Source::Edge(0), f64::NEG_INFINITY);
2865 for k in crossing(z) {
2866 let p = edge_at(k, z);
2867 let r = p[0].hypot(p[1]);
2868 if r > best.1 {
2869 best = (Source::Edge(k), r);
2870 }
2871 }
2872 best
2873 };
2874 let nearest = |z: f64| -> Result<(Source, f64), KernelRefusal> {
2876 let list = crossing(z);
2877 let points: Vec<[f64; 2]> = list.iter().map(|&k| edge_at(k, z)).collect();
2878 let m = points.len();
2879 if m < 3 {
2880 return Err(refuse("turn_shadow", "a section of the tool is not a polygon".into()));
2881 }
2882 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]);
2883 let mut order: Vec<usize> = (0..m).collect();
2884 order.sort_by(|&a, &b| {
2885 let (pa, pb) = (points[a], points[b]);
2886 (pa[1] - centre[1]).atan2(pa[0] - centre[0]).total_cmp(&(pb[1] - centre[1]).atan2(pb[0] - centre[0]))
2887 });
2888 let mut inside = true;
2889 let mut best = (Source::Edge(0), f64::INFINITY);
2890 for k in 0..m {
2891 let (ia, ib) = (order[k], order[(k + 1) % m]);
2892 let (a, b) = (points[ia], points[ib]);
2893 let d = [b[0] - a[0], b[1] - a[1]];
2894 if d[0] * (0.0 - a[1]) - d[1] * (0.0 - a[0]) < 0.0 {
2895 inside = false;
2896 }
2897 let length2 = d[0] * d[0] + d[1] * d[1];
2898 let t = -(a[0] * d[0] + a[1] * d[1]) / length2;
2899 let (source, r) = if t > 0.0 && t < 1.0 {
2900 let (ea, eb) = (edges[list[ia]], edges[list[ib]]);
2901 let shared = edge_faces[&ea].iter().find(|f| edge_faces[&eb].contains(f)).copied();
2902 let Some(face) = shared else {
2903 return Err(refuse("turn_shadow", "a section side does not lie on one face".into()));
2904 };
2905 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2906 (Source::Face(face), q[0].hypot(q[1]))
2907 } else {
2908 let q = if t <= 0.0 { a } else { b };
2909 (Source::Edge(list[if t <= 0.0 { ia } else { ib }]), q[0].hypot(q[1]))
2910 };
2911 if r < best.1 {
2912 best = (source, r);
2913 }
2914 }
2915 if inside {
2916 if !through {
2917 return Err(refuse("turn_shadow", "the turn's axis passes through the tool".into()));
2918 }
2919 return Ok((Source::Axis, 0.0));
2920 }
2921 Ok(best)
2922 };
2923 let value = |source: Source, z: f64| -> f64 {
2925 match source {
2926 Source::Edge(k) => {
2927 let p = edge_at(k, z);
2928 p[0].hypot(p[1])
2929 }
2930 Source::Face(f) => {
2931 let (n, d) = planes[f];
2932 (d - n[2] * z).abs() / n[0].hypot(n[1])
2933 }
2934 Source::Axis => 0.0,
2935 }
2936 };
2937 let mut heights: Vec<f64> = zs.clone();
2939 heights.sort_by(|a, b| a.total_cmp(b));
2940 heights.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * scale);
2941 let mut pieces_far: Vec<(f64, f64, Source)> = Vec::new();
2942 let mut pieces_near: Vec<(f64, f64, Source)> = Vec::new();
2943 for window in heights.windows(2) {
2944 let (za, zb) = (window[0], window[1]);
2945 let samples = 400;
2946 let inner = |k: usize| za + (zb - za) * (k as f64 + 0.5) / samples as f64;
2947 for near in [false, true] {
2948 let source_at = |z: f64| -> Result<Source, KernelRefusal> { Ok(if near { nearest(z)?.0 } else { farthest(z).0 }) };
2949 let mut runs: Vec<(f64, Source)> = vec![(za, source_at(inner(0))?)];
2950 for k in 1..samples {
2951 let (z0, z1) = (inner(k - 1), inner(k));
2952 let (s0, s1) = (source_at(z0)?, source_at(z1)?);
2953 let tied = [z0, 0.5 * (z0 + z1), z1].iter().all(|&z| (value(s0, z) - value(s1, z)).abs() <= 1e-12 * scale);
2956 if s0 == s1 || tied {
2957 continue;
2958 }
2959 let (mut lo, mut hi) = (z0, z1);
2962 for _ in 0..200 {
2963 let midz = 0.5 * (lo + hi);
2964 if source_at(midz)? == s0 { lo = midz } else { hi = midz }
2965 }
2966 runs.push((0.5 * (lo + hi), s1));
2967 }
2968 let list = if near { &mut pieces_near } else { &mut pieces_far };
2969 for k in 0..runs.len() {
2970 let z_end = if k + 1 < runs.len() { runs[k + 1].0 } else { zb };
2971 list.push((runs[k].0, z_end, runs[k].1));
2972 }
2973 }
2974 }
2975 let piece = |source: Source, za: f64, zb: f64| -> Vec<ShadowPiece> {
2977 let (ra, rb) = (value(source, za), value(source, zb));
2978 if matches!(source, Source::Face(..) | Source::Axis) {
2979 return vec![vec![(ra, za, 1.0), (rb, zb, 1.0)]];
2980 }
2981 let rho = |z: f64| value(source, z);
2983 let (z_min, z_max) = (za.min(zb), za.max(zb));
2986 let slope = |z: f64| {
2987 let h = 1e-6 * (zb - za).abs().max(1e-300);
2988 let (zp, zm) = ((z + h).min(z_max), (z - h).max(z_min));
2989 (rho(zp) - rho(zm)) / (zp - zm)
2990 };
2991 let parts = 4;
2992 let mut out = Vec::new();
2993 for k in 0..parts {
2994 let (z0, z1) = (za + (zb - za) * k as f64 / parts as f64, za + (zb - za) * (k + 1) as f64 / parts as f64);
2995 let (a, b) = ((rho(z0), z0), (rho(z1), z1));
2996 let bend = [0.25, 0.5, 0.75].iter().map(|&t| (rho(z0 + (z1 - z0) * t) - (a.0 + (b.0 - a.0) * t)).abs()).fold(0.0f64, f64::max);
2998 if bend <= 1e-12 * scale {
2999 out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
3000 continue;
3001 }
3002 let (ta, tb) = ((slope(z0), 1.0), (slope(z1), 1.0));
3003 let det = ta.0 * (-tb.1) - ta.1 * (-tb.0);
3005 if det.abs() < 1e-12 {
3006 out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
3007 continue;
3008 }
3009 let rhs = (b.0 - a.0, b.1 - a.1);
3010 let s = (rhs.0 * (-tb.1) - rhs.1 * (-tb.0)) / det;
3011 let c = (a.0 + ta.0 * s, a.1 + ta.1 * s);
3012 let m = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
3014 let on = |u: f64| {
3015 let p = (m.0 + (c.0 - m.0) * u, m.1 + (c.1 - m.1) * u);
3016 p.0 - rho(p.1)
3017 };
3018 let (mut lo, mut hi) = (0.0, 1.0);
3019 let sign_lo = on(lo);
3020 for _ in 0..200 {
3021 let mid = 0.5 * (lo + hi);
3022 if (on(mid) > 0.0) == (sign_lo > 0.0) { lo = mid } else { hi = mid }
3023 }
3024 let u = 0.5 * (lo + hi);
3025 let weight = u / (1.0 - u);
3026 out.push(vec![(a.0, a.1, 1.0), (c.0, c.1, weight), (b.0, b.1, 1.0)]);
3027 }
3028 out
3029 };
3030 let mut outline: Vec<ShadowPiece> = Vec::new();
3032 for &(za, zb, source) in &pieces_near {
3033 outline.extend(piece(source, za, zb));
3034 }
3035 let (near_top, far_top) = (value(pieces_near.last().expect("near").2, z_high), value(pieces_far.last().expect("far").2, z_high));
3036 if far_top - near_top > 1e-12 * scale {
3037 outline.push(vec![(near_top, z_high, 1.0), (far_top, z_high, 1.0)]);
3038 }
3039 for &(za, zb, source) in pieces_far.iter().rev() {
3040 for mut arc in piece(source, za, zb).into_iter().rev() {
3041 arc.reverse();
3042 outline.push(arc);
3043 }
3044 }
3045 let (near_bottom, far_bottom) = (value(pieces_near[0].2, z_low), value(pieces_far[0].2, z_low));
3046 if far_bottom - near_bottom > 1e-12 * scale {
3047 outline.push(vec![(far_bottom, z_low, 1.0), (near_bottom, z_low, 1.0)]);
3048 }
3049 for k in 0..outline.len() {
3051 let next = (k + 1) % outline.len();
3052 let end = *outline[k].last().expect("piece");
3053 outline[next][0] = (end.0, end.1, 1.0);
3054 }
3055 Ok(outline)
3056}
3057
3058fn ball_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
3068 let [OutlinePiece::Arc { centre, radius, start, end }, OutlinePiece::Line { .. }] = meridian else { return Ok(None) };
3069 let (centre, r) = (*centre, *radius);
3070 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 {
3071 return Ok(None);
3072 }
3073 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3074 let centre_tool = origin.add(w.scale(centre[1]));
3075 let turned = screw.omega * (screw.t1 - screw.t0);
3076 if turned.abs() >= TAU - 1e-9 {
3077 return Ok(None);
3078 }
3079 let e = screw.e.scale(screw.omega.signum()); let c0 = screw.apply(screw.t0, centre_tool);
3081 let c1 = screw.apply(screw.t1, centre_tool);
3082 let foot = |c: Vec3| screw.a.add(e.scale(c.sub(screw.a).dot(e)));
3083 let rho = c0.sub(foot(c0)).length();
3084 if rho >= r * (1.0 - 1e-9) || rho <= 1e-9 * r {
3085 return Ok(None);
3086 }
3087 let h0 = frag(c0.sub(foot(c0)).normalized(), "radial")?;
3088 let height = (r * r - rho * rho).sqrt();
3089 let (north, south) = (foot(c0).add(e.scale(height)), foot(c0).sub(e.scale(height)));
3090 let arc = |about: Vec3, k: Vec3, from: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
3092 let surface = frag(make_revolution(about, k, &frag(make_line(from, from.add(k)), "line")?, angle), "arc")?;
3093 frag(surface.iso_curve_v(0.0), "arc")
3094 };
3095 let alpha = height.atan2(-rho);
3098 let outer = arc(c0, h0.cross(e), south, 2.0 * alpha)?;
3099 let wall = frag(make_revolution(screw.a, e, &outer, turned.abs()), "wall")?;
3100 let outer_end = frag(wall.iso_curve_u(1.0), "wall_end")?;
3101 let mid = c0.add(c1).scale(0.5);
3103 let towards = frag(mid.sub(foot(mid)).normalized(), "bisector")?;
3104 let rho_mid = mid.sub(foot(mid)).length();
3105 let beta = height.atan2(rho_mid); let meet = arc(mid, e.cross(towards), south, 2.0 * beta)?;
3109 {
3110 let [d0, d1] = frag(meet.domain(), "domain")?;
3112 let middle = frag(meet.evaluate(0.5 * (d0 + d1)), "middle")?;
3113 if middle.sub(foot(middle)).dot(towards) > 0.0 {
3114 return Err(refuse("ball_across_axis", "the spheres' common circle runs the wrong way round".into()));
3115 }
3116 }
3117 let cap = |c: Vec3, inward: Vec3, side: Vec3, other: Vec3| -> Result<(NurbsSurface, Box<dyn Fn(Vec3) -> Vec3>), KernelRefusal> {
3119 let toward_south = frag(inward.add(side.scale(-0.3)).normalized(), "pole")?;
3123 let polar = toward_south.scale(-1.0);
3124 let seam = frag(side.sub(polar.scale(side.dot(polar))).normalized(), "seam")?;
3125 for k in 0..=64 {
3126 let a = PI * k as f64 / 64.0;
3129 let q = c.add(polar.scale(-r * a.cos())).add(seam.scale(r * a.sin()));
3130 if q.sub(c).dot(side) < 0.0 && q.sub(other).length() >= r * (1.0 - 1e-9) {
3131 return Err(refuse("ball_across_axis", "no seam place for a cap".into()));
3132 }
3133 }
3134 let south_pole = c.sub(polar.scale(r));
3135 if south_pole.sub(other).length() >= r * (1.0 - 1e-6) {
3136 return Err(refuse("ball_across_axis", "no pole place for a cap".into()));
3137 }
3138 let generatrix = arc(c, polar.scale(-1.0).cross(seam), south_pole, PI)?;
3139 let surface = frag(make_revolution(c, polar, &generatrix, TAU), "cap")?;
3140 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
3141 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
3142 let around = polar.cross(seam);
3143 let inverse = move |x: Vec3| -> Vec3 {
3144 let d = x.sub(c).scale(1.0 / r);
3145 let latitude = d.dot(polar).clamp(-1.0, 1.0).asin();
3146 let longitude = d.dot(around).atan2(d.dot(seam)).rem_euclid(TAU);
3147 Vec3::new(
3148 crate::circle_angle_to_parameter(spans_u, TAU, longitude),
3149 crate::circle_angle_to_parameter(spans_v, PI, latitude + 0.5 * PI),
3150 0.0,
3151 )
3152 };
3153 Ok((surface, Box::new(inverse)))
3154 };
3155 let moving0 = e.cross(h0);
3156 let h1 = frag(c1.sub(foot(c1)).normalized(), "radial")?;
3157 let moving1 = e.cross(h1);
3158 let (back, back_inverse) = cap(c0, h0.scale(-1.0), moving0.scale(1.0), c1)?;
3161 let (front, front_inverse) = cap(c1, h1.scale(-1.0), moving1.scale(-1.0), c0)?;
3162 let pcurve = |inverse: &dyn Fn(Vec3) -> Vec3, curve: &NurbsCurve, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
3163 let [d0, d1] = frag(curve.domain(), "domain")?;
3164 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3165 let uv: Vec<Vec3> = ts.iter().map(|&t| curve.evaluate(t).map(|x| inverse(x))).collect::<Result<_, _>>().map_err(|e| refuse("pcurve", e))?;
3166 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "cap_pcurve")?;
3167 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3168 };
3169 let mut shell = Shell::new();
3170 let (vn, vs) = (shell.vertex(north), shell.vertex(south));
3171 let k0 = shell.edge(outer.clone(), vs, vn, false)?;
3172 let k1 = shell.edge(outer_end.clone(), vs, vn, false)?;
3173 let c = shell.edge(meet.clone(), vs, vn, false)?;
3174 let ds = shell.edge(frag(make_line(south, south), "point")?, vs, vs, true)?;
3175 let dn = shell.edge(frag(make_line(north, north), "point")?, vn, vn, true)?;
3176 let wall_outward = {
3179 let p = frag(wall.evaluate(0.5, 0.5), "probe")?;
3180 let su = frag(wall.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(wall.evaluate(0.5 - 1e-6, 0.5), "probe")?);
3181 let sv = frag(wall.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(wall.evaluate(0.5, 0.5 - 1e-6), "probe")?);
3182 su.cross(sv).dot(p.sub(foot(p))) > 0.0
3183 };
3184 let [g0, g1] = frag(outer.domain(), "domain")?;
3185 let wall_loop = vec![
3186 (ds, true, uv_line([0.0, g0], [1.0, g0])?),
3187 (k1, true, uv_line([1.0, g0], [1.0, g1])?),
3188 (dn, true, uv_line([1.0, g1], [0.0, g1])?),
3189 (k0, false, uv_line([0.0, g1], [0.0, g0])?),
3190 ];
3191 let reversed = |list: Vec<(u64, bool, NurbsCurve)>| -> Result<Vec<(u64, bool, NurbsCurve)>, KernelRefusal> {
3192 list.into_iter().rev().map(|(e, f, p)| Ok((e, !f, frag(p.reversed(), "reversed")?))).collect()
3193 };
3194 shell.face(wall.clone(), if wall_outward { wall_loop } else { reversed(wall_loop)? });
3195 for (surface, inverse, k_edge, k_curve) in [(back, &back_inverse, k0, &outer), (front, &front_inverse, k1, &outer_end)] {
3198 let mut lp = vec![(k_edge, true, pcurve(inverse.as_ref(), k_curve, true)?), (c, false, pcurve(inverse.as_ref(), &meet, false)?)];
3199 let mut area = 0.0;
3200 for (_, _, pc) in &lp {
3201 let [d0, d1] = frag(pc.domain(), "domain")?;
3202 for j in 0..64 {
3203 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")?);
3204 area += a.x * b.y - b.x * a.y;
3205 }
3206 }
3207 if area < 0.0 {
3208 lp = reversed(lp)?;
3209 }
3210 shell.face(surface, lp);
3211 }
3212 let body = shell.solid();
3213 let issues = body.validate();
3214 if !issues.is_empty() {
3215 return Err(refuse("assembly", format!("the turned ball's sweep is not valid: {:?}", issues.first())));
3216 }
3217 let probe = Split { points: Vec::new(), curves: Vec::new(), caps: Vec::new(), slides: Vec::new(), slide_sides: Vec::new(), scale: r + rho, shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() } };
3218 check_single_pass(screw, &probe, &body, &std::collections::HashSet::new())?;
3219 Ok(Some(body))
3220}
3221
3222#[derive(Clone, Copy, Debug)]
3227pub struct FrameCurve {
3228 pub control: [Vec3; 4],
3229 pub up: Vec3,
3230}
3231
3232pub fn sweep_solid_along(tool: &SolidTool, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
3241 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
3242 let base = path.control[0];
3243 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
3244 return Err(input("plane", "the curve is not in a plane square to up".into()));
3245 }
3246 let tangent = path.control[1].sub(base);
3247 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
3248 let start_tangent = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
3249 let frame = Frame { control: path.control, up, start_tangent };
3250 let (mut speed, mut rate, mut extent) = (0.0f64, 0.0f64, 0.0f64);
3251 let mut previous = 0.0;
3252 for k in 0..=512 {
3253 let t = k as f64 / 512.0;
3254 let first = frame.first(t);
3255 if first.length() <= 1e-12 {
3256 return Err(input("tangent", "the curve stops (zero tangent)".into()));
3257 }
3258 speed = speed.max(first.length());
3259 rate = rate.max(frame.angle_rate(t).abs());
3260 extent = extent.max(frame.point(t).sub(base).length());
3261 let angle = frame.angle(t);
3262 if (angle - previous).abs() > 0.5 {
3263 return Err(refuse("frame_turns", "the frame turns half way round".into()));
3264 }
3265 previous = angle;
3266 }
3267 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,
3268 loop_samples: LOFT_SAMPLES, extent };
3269 let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
3270 return Err(refuse("along_tool", "a frame along a curve carries a solid of revolution only".into()));
3271 };
3272 let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3273 let mut last = None;
3277 for k in 0..8 {
3278 let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
3279 match lofted(&screw, *axis_origin, axis, reference, meridian) {
3280 Err(error) if refused_as(&error) == Some("loft_structure") => last = Some(error),
3281 other => return other,
3282 }
3283 }
3284 Err(last.expect("tried"))
3285}
3286
3287pub fn sweep_prism_along(outline: &[[f64; 2]], from: f64, to: f64, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
3298 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
3299 let base = path.control[0];
3300 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
3301 return Err(input("plane", "the curve is not in a plane square to up".into()));
3302 }
3303 let tangent = path.control[1].sub(base);
3304 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
3305 let e1 = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
3306 let e2 = up.cross(e1);
3307 let frame = Frame { control: path.control, up, start_tangent: e1 };
3308 if outline.len() < 3 || !(to > from) {
3309 return Err(input("prism", "a prism needs three corners and a positive height".into()));
3310 }
3311 let n = outline.len();
3313 let mut corners = outline.to_vec();
3314 let area: f64 = (0..n).map(|k| corners[k][0] * corners[(k + 1) % n][1] - corners[(k + 1) % n][0] * corners[k][1]).sum();
3315 if area < 0.0 {
3316 corners.reverse();
3317 }
3318 for k in 0..n {
3319 let (a, b, c) = (corners[k], corners[(k + 1) % n], corners[(k + 2) % n]);
3320 if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) <= 0.0 {
3321 return Err(input("prism", "the outline is not convex".into()));
3322 }
3323 }
3324 let scale = corners.iter().map(|c| c[0].hypot(c[1])).fold(1.0f64, f64::max);
3325 let flat = |p: Vec3| [p.sub(base).dot(e1), p.sub(base).dot(e2)];
3327 let place = |s: f64, q: [f64; 2]| -> [f64; 2] {
3328 let o = flat(frame.point(s));
3329 let (sn, cs) = frame.angle(s).sin_cos();
3330 [o[0] + cs * q[0] - sn * q[1], o[1] + sn * q[0] + cs * q[1]]
3331 };
3332 let local = |s: f64, p: [f64; 2]| -> [f64; 2] {
3333 let o = flat(frame.point(s));
3334 let (sn, cs) = frame.angle(s).sin_cos();
3335 let d = [p[0] - o[0], p[1] - o[1]];
3336 [cs * d[0] + sn * d[1], -sn * d[0] + cs * d[1]]
3337 };
3338 let edges: Vec<([f64; 2], f64)> = (0..n)
3340 .map(|k| {
3341 let (a, b) = (corners[k], corners[(k + 1) % n]);
3342 let d = [b[0] - a[0], b[1] - a[1]];
3343 let length = d[0].hypot(d[1]);
3344 let normal = [d[1] / length, -d[0] / length];
3345 (normal, normal[0] * a[0] + normal[1] * a[1])
3346 })
3347 .collect();
3348 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);
3349 let depth = |p: [f64; 2]| -> f64 {
3351 let samples = 2000;
3352 let f = |s: f64| inside(local(s, p));
3353 let mut best = (f64::INFINITY, 0usize);
3354 let values: Vec<f64> = (0..=samples).map(|k| f(k as f64 / samples as f64)).collect();
3355 for (k, &v) in values.iter().enumerate() {
3356 if v < best.0 {
3357 best = (v, k);
3358 }
3359 }
3360 let mut least = best.0;
3362 for k in 0..=samples {
3363 let left = if k == 0 { f64::INFINITY } else { values[k - 1] };
3364 let right = if k == samples { f64::INFINITY } else { values[k + 1] };
3365 if values[k] <= left && values[k] <= right && values[k] < best.0 + 1e-3 * scale {
3366 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);
3367 for _ in 0..80 {
3368 let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3369 if f(m1) < f(m2) { hi = m2 } else { lo = m1 }
3370 }
3371 least = least.min(f(0.5 * (lo + hi)));
3372 }
3373 }
3374 least
3375 };
3376 #[derive(Clone, Copy)]
3378 enum Path {
3379 Corner(usize),
3380 Graze(usize, f64, f64),
3382 End(usize, bool),
3384 }
3385 let speed_ratio = |s: f64| frame.angle_rate(s); let graze_point = |k: usize, s: f64| -> Option<f64> {
3387 let (a, b) = (corners[k], corners[(k + 1) % n]);
3389 let (nn, _) = edges[k];
3390 let kappa = speed_ratio(s) / frame.first(s).length();
3391 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)) };
3395 if ga == gb {
3396 return None;
3397 }
3398 let u = ga / (ga - gb);
3399 (u > 0.0 && u < 1.0).then_some(u)
3400 };
3401 let mut paths: Vec<Path> = (0..n).map(Path::Corner).collect();
3402 for k in 0..n {
3403 let steps = 2000;
3405 let mut run: Option<f64> = None;
3406 for j in 0..=steps {
3407 let s = j as f64 / steps as f64;
3408 let on = graze_point(k, s).is_some();
3409 match (on, run) {
3410 (true, None) => run = Some(s),
3411 (false, Some(s0)) => {
3412 paths.push(Path::Graze(k, s0, s - 1.0 / steps as f64));
3413 run = None;
3414 }
3415 _ => {}
3416 }
3417 }
3418 if let Some(s0) = run {
3419 paths.push(Path::Graze(k, s0, 1.0));
3420 }
3421 paths.push(Path::End(k, false));
3422 paths.push(Path::End(k, true));
3423 }
3424 let eval = |path: Path, t: f64| -> [f64; 2] {
3425 match path {
3426 Path::Corner(k) => place(t, corners[k]),
3427 Path::Graze(k, s0, s1) => {
3428 let s = s0 + (s1 - s0) * t;
3429 let u = graze_point(k, s).unwrap_or(if t < 0.5 { 0.0 } else { 1.0 });
3430 let (a, b) = (corners[k], corners[(k + 1) % n]);
3431 place(s, [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u])
3432 }
3433 Path::End(k, at_end) => {
3434 let (a, b) = (corners[k], corners[(k + 1) % n]);
3435 place(if at_end { 1.0 } else { 0.0 }, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])
3436 }
3437 }
3438 };
3439 let m = 400;
3442 let polylines: Vec<Vec<[f64; 2]>> = paths.iter().map(|&p| (0..=m).map(|i| eval(p, i as f64 / m as f64)).collect()).collect();
3443 let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; paths.len()];
3444 let tol = 1e-10 * scale;
3445 for a in 0..paths.len() {
3446 for b in a + 1..paths.len() {
3447 for i in 0..m {
3448 for j in 0..m {
3449 let (p0, p1, q0, q1) = (polylines[a][i], polylines[a][i + 1], polylines[b][j], polylines[b][j + 1]);
3450 let d1 = [p1[0] - p0[0], p1[1] - p0[1]];
3451 let d2 = [q1[0] - q0[0], q1[1] - q0[1]];
3452 let det = d1[0] * d2[1] - d1[1] * d2[0];
3453 if det.abs() < 1e-300 {
3454 continue;
3455 }
3456 let r = [q0[0] - p0[0], q0[1] - p0[1]];
3457 let (x, y) = ((r[0] * d2[1] - r[1] * d2[0]) / det, (r[0] * d1[1] - r[1] * d1[0]) / det);
3458 if !(-1e-9..=1.0 + 1e-9).contains(&x) || !(-1e-9..=1.0 + 1e-9).contains(&y) {
3459 continue;
3460 }
3461 let (mut ta, mut tb) = ((i as f64 + x) / m as f64, (j as f64 + y) / m as f64);
3463 for _ in 0..50 {
3464 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
3465 let h = 1e-7;
3466 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] };
3467 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] };
3468 let f = [pa[0] - pb[0], pa[1] - pb[1]];
3469 let det = -da[0] * db[1] + da[1] * db[0];
3470 if det.abs() < 1e-300 {
3471 break;
3472 }
3473 let dta = (-f[0] * db[1] + f[1] * db[0]) / det;
3474 let dtb = (da[0] * f[1] - da[1] * f[0]) / det;
3475 ta = (ta - dta).clamp(0.0, 1.0);
3476 tb = (tb - dtb).clamp(0.0, 1.0);
3477 if dta.abs() + dtb.abs() < 1e-15 {
3478 break;
3479 }
3480 }
3481 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
3482 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < tol {
3483 cuts[a].push(ta);
3484 cuts[b].push(tb);
3485 }
3486 }
3487 }
3488 }
3489 }
3490 for c in &mut cuts {
3491 c.sort_by(|x, y| x.total_cmp(y));
3492 c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
3493 }
3494 struct Piece {
3496 path: usize,
3497 t0: f64,
3498 t1: f64,
3499 }
3500 let eps = 1e-7 * scale;
3501 let mut pieces: Vec<Piece> = Vec::new();
3502 for (index, c) in cuts.iter().enumerate() {
3503 for w in c.windows(2) {
3504 let (t0, t1) = (w[0], w[1]);
3505 if t1 - t0 < 1e-9 {
3506 continue;
3507 }
3508 let (pa, pb) = (eval(paths[index], t0), eval(paths[index], t1));
3509 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < 1e-9 * scale {
3510 continue;
3511 }
3512 let tm = 0.5 * (t0 + t1);
3513 let (p, q) = (eval(paths[index], tm), eval(paths[index], (tm + 1e-6).min(1.0)));
3514 let d = [q[0] - p[0], q[1] - p[1]];
3515 let length = d[0].hypot(d[1]);
3516 if length == 0.0 {
3517 continue;
3518 }
3519 let left = [p[0] - d[1] / length * eps, p[1] + d[0] / length * eps];
3520 let right = [p[0] + d[1] / length * eps, p[1] - d[0] / length * eps];
3521 let (l, r) = (depth(left) < -1e-12 * scale, depth(right) < -1e-12 * scale);
3522 match (l, r) {
3523 (true, false) => pieces.push(Piece { path: index, t0, t1 }),
3524 (false, true) => pieces.push(Piece { path: index, t0: t1, t1: t0 }),
3525 _ => {}
3526 }
3527 }
3528 }
3529 if pieces.is_empty() {
3531 return Err(refuse("prism_along", "no boundary found".into()));
3532 }
3533 let mut order = vec![0usize];
3534 let mut used = vec![false; pieces.len()];
3535 used[0] = true;
3536 loop {
3537 let last = &pieces[*order.last().expect("order")];
3538 let end = eval(paths[last.path], last.t1);
3539 let start = eval(paths[pieces[order[0]].path], pieces[order[0]].t0);
3540 if order.len() > 1 && (end[0] - start[0]).hypot(end[1] - start[1]) < 1e-8 * scale {
3541 break;
3542 }
3543 let next = (0..pieces.len()).filter(|&k| !used[k]).min_by(|&x, &y| {
3544 let (px, py) = (eval(paths[pieces[x].path], pieces[x].t0), eval(paths[pieces[y].path], pieces[y].t0));
3545 (px[0] - end[0]).hypot(px[1] - end[1]).total_cmp(&(py[0] - end[0]).hypot(py[1] - end[1]))
3546 });
3547 let Some(next) = next else {
3548 return Err(refuse("prism_along", "the boundary does not close".into()));
3549 };
3550 let p = eval(paths[pieces[next].path], pieces[next].t0);
3551 if (p[0] - end[0]).hypot(p[1] - end[1]) > 1e-8 * scale {
3552 return Err(refuse("prism_along", format!("the boundary does not close (gap {:e})", (p[0] - end[0]).hypot(p[1] - end[1]))));
3553 }
3554 used[next] = true;
3555 order.push(next);
3556 }
3557 if used.iter().any(|u| !u) {
3558 return Err(refuse("prism_along", "the swept region is not one simple loop".into()));
3559 }
3560 let world = |q: [f64; 2], h: f64| base.add(e1.scale(q[0])).add(e2.scale(q[1])).add(up.scale(h));
3562 let mut loop_points: Vec<[f64; 2]> = order.iter().map(|&k| eval(paths[pieces[k].path], pieces[k].t0)).collect();
3563 loop_points.push(loop_points[0]);
3564 let count = order.len();
3565 let mut shell = Shell::new();
3566 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();
3567 let mut bottom_e = Vec::with_capacity(count);
3568 let mut top_e = Vec::with_capacity(count);
3569 let mut curves2d = Vec::with_capacity(count);
3570 for (slot, &k) in order.iter().enumerate() {
3571 let piece = &pieces[k];
3572 let straight = matches!(paths[piece.path], Path::End(..));
3573 let next = (slot + 1) % count;
3574 let curve_at = |h: f64| -> Result<NurbsCurve, KernelRefusal> {
3575 if straight {
3576 return frag(make_line(world(loop_points[slot], h), world(loop_points[next], h)), "line");
3577 }
3578 let samples = 192;
3579 let ts: Vec<f64> = (0..=samples).map(|i| i as f64 / samples as f64).collect();
3580 let mut points: Vec<Vec3> = ts.iter().map(|&t| world(eval(paths[piece.path], piece.t0 + (piece.t1 - piece.t0) * t), h)).collect();
3581 points[0] = world(loop_points[slot], h);
3582 points[samples] = world(loop_points[next], h);
3583 frag(crate::interpolate_curve(&points, 5, &ts), "plane_path")
3584 };
3585 let (cb, ct) = (curve_at(from)?, curve_at(to)?);
3586 bottom_e.push(shell.edge(cb.clone(), bottom_v[slot], bottom_v[next], false)?);
3587 top_e.push(shell.edge(ct.clone(), top_v[slot], top_v[next], false)?);
3588 curves2d.push((cb, ct));
3589 }
3590 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<_, _>>()?;
3591 let top_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, to)).collect();
3593 let top_surface = plane_patch(world([0.0, 0.0], to), e1, e2, &top_points)?;
3594 let mut coedges = Vec::with_capacity(count);
3595 for k in 0..count {
3596 coedges.push((top_e[k], true, plane_pcurve(world([0.0, 0.0], to), e1, e2, &curves2d[k].1, true)?));
3597 }
3598 shell.face(top_surface, coedges);
3599 let bottom_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, from)).collect();
3600 let bottom_surface = plane_patch(world([0.0, 0.0], from), e2, e1, &bottom_points)?;
3601 let mut coedges = Vec::with_capacity(count);
3602 for k in (0..count).rev() {
3603 coedges.push((bottom_e[k], false, plane_pcurve(world([0.0, 0.0], from), e2, e1, &curves2d[k].0, false)?));
3604 }
3605 shell.face(bottom_surface, coedges);
3606 for k in 0..count {
3608 let next = (k + 1) % count;
3609 let (cb, _) = &curves2d[k];
3610 let rows: Vec<Vec<Vec4>> = cb
3611 .control_points
3612 .iter()
3613 .map(|p| {
3614 let x = p.point().expect("point");
3615 vec![Vec4::from_point(x, p.w), Vec4::from_point(x.add(up.scale(to - from)), p.w)]
3616 })
3617 .collect();
3618 let surface = frag(NurbsSurface::new(cb.degree, 1, cb.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], rows), "side")?;
3619 let [u0, u1] = frag(cb.domain(), "domain")?;
3620 let coedges = vec![
3621 (bottom_e[k], true, uv_line([u0, 0.0], [u1, 0.0])?),
3622 (verticals[next], true, uv_line([u1, 0.0], [u1, 1.0])?),
3623 (top_e[k], false, uv_line([u1, 1.0], [u0, 1.0])?),
3624 (verticals[k], false, uv_line([u0, 1.0], [u0, 0.0])?),
3625 ];
3626 shell.face(surface, coedges);
3627 }
3628 let body = shell.solid();
3629 let issues = body.validate();
3630 if !issues.is_empty() {
3631 return Err(refuse("assembly", format!("the swept prism is not valid: {:?}", issues.first())));
3632 }
3633 Ok(body)
3634}
3635
3636#[derive(Clone, Copy, Debug)]
3640pub struct TwoTurns {
3641 pub axes: [(Vec3, Vec3); 2],
3642 pub rates: [f64; 2],
3643 pub from: f64,
3644 pub to: f64,
3645}
3646
3647pub fn sweep_solid_tumbled(tool: &SolidTool, motion: &TwoTurns) -> Result<BrepSolid, KernelRefusal> {
3654 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
3655 return Err(input("range", "the motion's range must run forward".into()));
3656 }
3657 let mut points = [Vec3::new(0.0, 0.0, 0.0); 2];
3658 let mut directions = [Vec3::new(0.0, 0.0, 1.0); 2];
3659 for k in 0..2 {
3660 points[k] = motion.axes[k].0;
3661 directions[k] = motion.axes[k].1.normalized().map_err(|_| input("axis", "a turn's axis has no direction".into()))?;
3662 }
3663 let turns = Turns { points, directions, rates: motion.rates };
3664 let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
3665 return Err(refuse("tumble_tool", "a tumble sweeps a solid of revolution only".into()));
3666 };
3667 let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3668 let base = Screw {
3669 a: points[0],
3670 e: directions[0],
3671 omega: motion.rates[0].abs() + motion.rates[1].abs(),
3672 c: 0.0,
3673 c2: 0.0,
3674 t0: motion.from,
3675 t1: motion.to,
3676 frame: None,
3677 turns: Some(turns),
3678 frozen: None,
3679 loop_samples: LOFT_SAMPLES,
3680 extent: points[1].sub(points[0]).length(),
3681 };
3682 let span = |t0: f64, t1: f64| -> Result<BrepSolid, KernelRefusal> {
3683 let mut screw = base;
3684 screw.t0 = t0;
3685 screw.t1 = t1;
3686 let mut last = None;
3687 'samples: while screw.loop_samples <= 16 * LOFT_SAMPLES {
3690 for k in 0..8 {
3691 let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
3692 match lofted(&screw, *axis_origin, axis, reference, meridian) {
3693 Err(error) if matches!(refused_as(&error), Some("loft_structure") | Some("end_trim_corner") | Some("end_trim_deep")) => last = Some(error),
3696 Err(error) if refused_as(&error) == Some("loft_rows") => {
3697 last = Some(error);
3698 screw.loop_samples *= 2;
3699 continue 'samples;
3700 }
3701 other => return other,
3702 }
3703 }
3704 break;
3705 }
3706 Err(last.expect("tried"))
3707 };
3708 let mut pending = vec![(motion.from, motion.to, 0usize)];
3710 let mut spans: Vec<(f64, BrepSolid)> = Vec::new();
3711 while let Some((t0, t1, depth)) = pending.pop() {
3712 match span(t0, t1) {
3713 Ok(body) => spans.push((t0, body)),
3714 Err(error) if refused_as(&error) == Some("folds") && depth < 6 => {
3715 let middle = 0.5 * (t0 + t1);
3716 pending.push((middle, t1, depth + 1));
3717 pending.push((t0, middle, depth + 1));
3718 }
3719 Err(error) => return Err(error),
3720 }
3721 }
3722 spans.sort_by(|a, b| a.0.total_cmp(&b.0));
3723 let mut bodies: Vec<BrepSolid> = spans.into_iter().map(|(_, body)| body).collect();
3724 if bodies.len() == 1 {
3725 return Ok(bodies.pop().expect("one"));
3726 }
3727 crate::boolean_operation_nary(&bodies, crate::BooleanOperation::Union)
3728}
3729
3730fn charted_pcurve(chart: &Chart, screw: &Screw, t: f64, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3733 let [d0, d1] = frag(curve.domain(), "domain")?;
3734 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3735 let mut uv = Vec::with_capacity(ts.len());
3736 for &tt in &ts {
3737 let x = screw.invert(t, frag(curve.evaluate(tt), "point")?);
3738 let [u, v] = chart.invert(x);
3739 uv.push(Vec3::new(u, v, 0.0));
3740 }
3741 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "charted_pcurve")?;
3742 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3743}
3744
3745fn projected_pcurve(surface: &NurbsSurface, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3747 let [d0, d1] = frag(curve.domain(), "domain")?;
3748 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3749 let mut uv = Vec::with_capacity(ts.len());
3750 for &t in &ts {
3751 let x = frag(curve.evaluate(t), "point")?;
3752 let foot = frag(crate::project_point_to_surface(surface, x), "project")?;
3753 if foot.distance > 1e-6 * (1.0 + x.length()) {
3754 return Err(refuse("loft_cap", format!("a lofted edge leaves its cap by {} at {x:?}", foot.distance)));
3755 }
3756 uv.push(Vec3::new(foot.u, foot.v, 0.0));
3757 }
3758 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "projected_pcurve")?;
3759 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3760}
3761
3762type Signature = (Vec<(usize, usize, bool)>, Vec<(bool, Vec<(usize, bool)>)>, Vec<(usize, bool)>);
3763
3764fn signature(split: &Split) -> Result<Signature, KernelRefusal> {
3765 Ok((
3766 split.curves.iter().map(|c| (c.start, c.end, c.degenerate)).collect(),
3767 split.caps.iter().map(|cap| (cap.front, cap.coedges.iter().map(|&(c, f, _)| (c, f)).collect())).collect(),
3768 split.grazing()?,
3769 ))
3770}
3771
3772fn lofted(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<BrepSolid, KernelRefusal> {
3773 let split_at = |t: f64| -> Result<Split, KernelRefusal> {
3774 let mut frozen = *screw;
3775 frozen.frozen = Some(t);
3776 split_revolved(&frozen, origin, axis, radial, meridian)
3777 };
3778 let mut lambdas: Vec<f64> = (0..=LOFT_SAMPLES).map(|i| i as f64 / LOFT_SAMPLES as f64).collect();
3779 let samples_of = |split: &Split, c: usize, back_forward: bool, t: f64| -> Result<Vec<Vec3>, KernelRefusal> {
3781 let Some(list) = &split.curves[c].samples else {
3782 return Err(refuse("loft_samples", "a grazing curve has no exact samples to loft".into()));
3783 };
3784 let mut world: Vec<Vec3> = list.iter().map(|&x| screw.apply(t, x)).collect();
3785 if back_forward {
3786 world.reverse();
3787 }
3788 Ok(world)
3789 };
3790 let mut fractions: Vec<f64> = (0..=15).map(|j| j as f64 / 15.0).collect();
3794 loop {
3795 let count = fractions.len() - 1;
3796 let times: Vec<f64> = fractions.iter().map(|&f| if f == 1.0 { screw.t1 } else { screw.t0 + (screw.t1 - screw.t0) * f }).collect();
3798 let splits: Vec<Split> = times.iter().map(|&t| split_at(t)).collect::<Result<_, _>>()?;
3799 if splits.iter().any(|split| !split.slides.is_empty() || !split.slide_sides.is_empty()) {
3800 return Err(refuse("loft_structure", "a flat end that slides within its own plane is not lofted".into()));
3801 }
3802 let shape = signature(&splits[0])?;
3803 for split in &splits[1..] {
3804 if signature(split)? != shape {
3805 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3806 }
3807 }
3808 let grazing = shape.2.clone();
3809 if grazing.is_empty() {
3810 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
3811 }
3812 let scale = splits[0].scale + screw.extent;
3813 let target = 5e-10 * scale;
3814 let mut walls = Vec::with_capacity(grazing.len());
3816 let mut sampled: Vec<Vec<Vec<Vec3>>> = Vec::with_capacity(grazing.len());
3818 for &(c, back_forward) in &grazing {
3819 let mut at_times = Vec::with_capacity(times.len());
3820 for (j, split) in splits.iter().enumerate() {
3821 let points = samples_of(split, c, back_forward, times[j])?;
3822 if points.len() != lambdas.len() {
3823 if points.len() < 13 || points.len() % 2 == 0 {
3824 return Err(refuse("loft_samples", "a grazing curve's exact samples cannot be lofted".into()));
3825 }
3826 lambdas = (0..points.len()).map(|i| i as f64 / (points.len() - 1) as f64).collect();
3827 }
3828 at_times.push(points);
3829 }
3830 sampled.push(at_times);
3831 }
3832 let thinned = |stride: usize| -> Result<f64, KernelRefusal> {
3835 let coarse_lambdas: Vec<f64> = lambdas.iter().step_by(stride).copied().collect();
3836 let mut worst = 0.0f64;
3837 for at_times in &sampled {
3838 for points in at_times {
3839 let coarse_points: Vec<Vec3> = points.iter().step_by(stride).copied().collect();
3840 let coarse = frag(crate::interpolate_curve(&coarse_points, 5, &coarse_lambdas), "loft_row")?;
3841 for (i, x) in points.iter().enumerate().filter(|(i, _)| i % stride != 0) {
3842 worst = worst.max(frag(coarse.evaluate(lambdas[i]), "loft_row")?.sub(*x).length());
3843 }
3844 }
3845 }
3846 Ok(worst)
3847 };
3848 let spans = lambdas.len() - 1;
3854 let mut kept: Vec<Vec<usize>> = Vec::with_capacity(sampled.len());
3855 for at_times in &sampled {
3856 let coarsest = if spans % 16 == 0 && spans >= 96 { 16 } else { 1 };
3857 let mut subset: Vec<usize> = (0..=spans).step_by(coarsest).collect();
3858 loop {
3859 let subset_lambdas: Vec<f64> = subset.iter().map(|&i| lambdas[i]).collect();
3860 let mut misses = vec![0.0f64; subset.len() - 1];
3861 for points in at_times {
3862 let row_points: Vec<Vec3> = subset.iter().map(|&i| points[i]).collect();
3863 let row = frag(crate::interpolate_curve(&row_points, 5, &subset_lambdas), "loft_row")?;
3864 for k in 0..subset.len() - 1 {
3865 for i in subset[k] + 1..subset[k + 1] {
3866 misses[k] = misses[k].max(frag(row.evaluate(lambdas[i]), "loft_row")?.sub(points[i]).length());
3867 }
3868 }
3869 }
3870 let mut refined = Vec::with_capacity(2 * subset.len());
3871 for k in 0..subset.len() - 1 {
3872 refined.push(subset[k]);
3873 if misses[k] > 0.25 * target {
3874 refined.push((subset[k] + subset[k + 1]) / 2);
3875 }
3876 }
3877 refined.push(spans);
3878 if refined.len() == subset.len() {
3879 break;
3880 }
3881 subset = refined;
3882 }
3883 kept.push(subset);
3884 }
3885 let dense = kept.iter().any(|subset| subset.windows(2).any(|pair| pair[1] == pair[0] + 1));
3891 let between = if dense { thinned(2)? / 64.0 } else { 0.0 };
3892 for (index, &(c, back_forward)) in grazing.iter().enumerate() {
3893 let row_lambdas: Vec<f64> = kept[index].iter().map(|&i| lambdas[i]).collect();
3894 let mut rows: Vec<NurbsCurve> = Vec::with_capacity(times.len());
3895 for points in &sampled[index] {
3896 let row_points: Vec<Vec3> = kept[index].iter().map(|&i| points[i]).collect();
3897 rows.push(frag(crate::interpolate_curve(&row_points, 5, &row_lambdas), "loft_row")?);
3898 }
3899 let knots_u = rows[0].knots.clone();
3900 let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows[0].control_points.len());
3901 let mut knots_v = Vec::new();
3902 for k in 0..rows[0].control_points.len() {
3903 let column: Vec<Vec3> = rows.iter().map(|row| row.control_points[k].point()).collect::<Result<_, _>>().map_err(|e| refuse("loft", e))?;
3904 let across = frag(crate::interpolate_curve(&column, 5, &fractions), "loft_column")?;
3905 knots_v = across.knots.clone();
3906 net.push(across.control_points.clone());
3907 }
3908 let surface = frag(NurbsSurface::new(5, 5, knots_u, knots_v, net), "loft_wall")?;
3909 walls.push((c, back_forward, surface, rows.first().cloned().expect("rows"), rows.last().cloned().expect("rows")));
3910 }
3911 if between > target {
3912 return Err(refuse("loft_rows", format!("the lofted wall's rows stay about {between:e} off the grazing loop between its samples")));
3913 }
3914 let mut misses = vec![0.0f64; count];
3916 for j in 0..count {
3917 let t = 0.5 * (times[j] + times[j + 1]);
3918 let fraction = 0.5 * (fractions[j] + fractions[j + 1]);
3919 let split = split_at(t)?;
3920 if signature(&split)? != shape {
3921 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3922 }
3923 for (c, back_forward, surface, _, _) in &walls {
3924 let exact = samples_of(&split, *c, *back_forward, t)?;
3925 for (i, x) in exact.iter().enumerate() {
3926 misses[j] = misses[j].max(frag(surface.evaluate(lambdas[i], fraction), "loft_check")?.sub(*x).length());
3927 }
3928 }
3929 }
3930 let worst = misses.iter().copied().fold(0.0f64, f64::max);
3931 if worst > target {
3932 if count > 2000 {
3933 return Err(refuse("loft_accuracy", format!("the lofted wall stays {worst:e} off the grazing loop")));
3934 }
3935 let mut refined = Vec::with_capacity(2 * fractions.len());
3938 for j in 0..count {
3939 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 };
3940 for k in 0..pieces {
3941 refined.push(fractions[j] + (fractions[j + 1] - fractions[j]) * k as f64 / pieces as f64);
3942 }
3943 }
3944 refined.push(1.0);
3945 fractions = refined;
3946 continue;
3947 }
3948 let mut paths: HashMap<usize, Vec<Vec3>> = HashMap::new();
3950 for &(c, _) in &grazing {
3951 for r in [splits[0].curves[c].start, splits[0].curves[c].end] {
3952 paths.entry(r).or_insert_with(|| splits.iter().zip(×).map(|(split, &t)| screw.apply(t, split.points[r])).collect());
3953 }
3954 }
3955 return assemble_lofted(screw, &splits[0], &splits[count], &walls, &fractions, &paths);
3956 }
3957}
3958
3959fn wall_regular(surface: &NurbsSurface, fractions: &[f64], removed: &[&[[f64; 2]]]) -> Result<(), KernelRefusal> {
3967 const COLUMNS: usize = 4 * LOFT_SAMPLES;
3968 let mut rows: Vec<f64> = Vec::with_capacity(2 * fractions.len());
3969 for j in 0..fractions.len() {
3970 rows.push(fractions[j]);
3971 if j + 1 < fractions.len() {
3972 rows.push(0.5 * (fractions[j] + fractions[j + 1]));
3973 }
3974 }
3975 let normal = |u: f64, v: f64| -> Result<Option<Vec3>, KernelRefusal> {
3976 let h = 1e-6;
3977 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));
3978 let du = frag(surface.evaluate(ub, v), "regular")?.sub(frag(surface.evaluate(ua, v), "regular")?).scale(1.0 / (ub - ua));
3979 let dv = frag(surface.evaluate(u, vb), "regular")?.sub(frag(surface.evaluate(u, va), "regular")?).scale(1.0 / (vb - va));
3980 Ok(du.cross(dv).normalized().ok())
3981 };
3982 let mut previous: Option<Vec<Option<Vec3>>> = None;
3983 for &v in &rows {
3984 let row: Vec<Option<Vec3>> = (0..=COLUMNS)
3986 .map(|i| {
3987 let u = i as f64 / COLUMNS as f64;
3988 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) }
3989 })
3990 .collect::<Result<_, _>>()?;
3991 for i in 0..COLUMNS {
3992 if let (Some(a), Some(b)) = (row[i], row[i + 1]) {
3993 if a.dot(b) < -0.5 {
3994 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)));
3995 }
3996 }
3997 if let Some(Some(Some(a))) = previous.as_ref().map(|p| p.get(i).copied()) {
3998 if let Some(b) = row[i] {
3999 if a.dot(b) < -0.5 {
4000 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)));
4001 }
4002 }
4003 }
4004 }
4005 previous = Some(row);
4006 }
4007 Ok(())
4008}
4009
4010fn row_piece(wall: &NurbsSurface, v: f64, a: f64, b: f64) -> Result<NurbsCurve, KernelRefusal> {
4012 let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4013 let along: Vec<Vec3> = ts.iter().map(|&s| frag(wall.evaluate((a + (b - a) * s).clamp(0.0, 1.0), v), "wall")).collect::<Result<_, _>>()?;
4014 frag(crate::interpolate_curve(&along, 5, &ts), "loop_piece")
4015}
4016
4017fn row_pieces(wall: &NurbsSurface, v: f64, spans: &[TrimSpan]) -> Result<Vec<NurbsCurve>, KernelRefusal> {
4020 let mut pieces = Vec::with_capacity(spans.len() + 1);
4021 let mut from = 0.0;
4022 for span in spans {
4023 pieces.push(row_piece(wall, v, from, span.lambdas[0])?);
4024 from = span.lambdas[1];
4025 }
4026 pieces.push(row_piece(wall, v, from, 1.0)?);
4027 Ok(pieces)
4028}
4029
4030fn restrict_u(surface: &NurbsSurface, a: f64, b: f64) -> Result<NurbsSurface, KernelRefusal> {
4032 let [u0, u1] = frag(surface.domain_u(), "domain")?;
4033 if a <= u0 + 1e-12 && b >= u1 - 1e-12 {
4034 return Ok(surface.clone());
4035 }
4036 let columns = surface.control_points[0].len();
4037 let mut knots = Vec::new();
4038 let mut net: Vec<Vec<Vec4>> = Vec::new();
4039 for j in 0..columns {
4040 let line: Vec<Vec4> = surface.control_points.iter().map(|row| row[j]).collect();
4041 let mut curve = frag(NurbsCurve::new(surface.degree_u, surface.knots_u.clone(), line), "restrict")?;
4042 if b < u1 - 1e-12 {
4043 curve = frag(curve.split(b), "restrict")?.0;
4044 }
4045 if a > u0 + 1e-12 {
4046 curve = frag(curve.split(a), "restrict")?.1;
4047 }
4048 if j == 0 {
4049 knots = curve.knots.clone();
4050 net = vec![Vec::with_capacity(columns); curve.control_points.len()];
4051 }
4052 for (i, point) in curve.control_points.iter().enumerate() {
4053 net[i].push(*point);
4054 }
4055 }
4056 frag(NurbsSurface::new(surface.degree_u, surface.degree_v, knots, surface.knots_v.clone(), net), "restrict")
4057}
4058
4059struct TrimSpan {
4067 lambdas: [f64; 2],
4068 points: [Vec3; 2],
4069 cut: Option<NurbsCurve>,
4072 chain: Vec<(TrimPiece, bool, NurbsCurve)>,
4073 slot: Option<(NurbsCurve, Vec3)>,
4074 tip: Option<[f64; 2]>,
4077 removed: Vec<[f64; 2]>,
4078}
4079
4080struct EndTrim {
4083 spans: Vec<TrimSpan>,
4084 pieces: Vec<NurbsCurve>,
4085}
4086
4087#[derive(Clone, Copy, PartialEq, Eq, Debug)]
4090enum TrimPiece {
4091 Cut,
4092 Slot,
4093}
4094
4095fn end_trim(screw: &Screw, split: &Split, t: f64, wall: &NurbsSurface, end: usize) -> Result<Option<EndTrim>, KernelRefusal> {
4106 let v_of = |e: f64| if end == 1 { 1.0 - e } else { e };
4107 let at = |u: f64, e: f64| -> Result<Vec3, KernelRefusal> { frag(wall.evaluate(u.clamp(0.0, 1.0), v_of(e)), "wall") };
4108 let f = |u: f64, e: f64| -> Result<f64, KernelRefusal> { Ok(split.distance(screw.invert(t, at(u, e)?)) / (e * e)) };
4109 let floor = 1e-3;
4111 let window = 0.6;
4115 let noise = 1e-9 * split.scale / (floor * floor);
4116 let n = 2000;
4118 let edge: Vec<f64> = (0..=n).map(|i| f(i as f64 / n as f64, floor)).collect::<Result<_, _>>()?;
4119 let inside: Vec<bool> = edge.iter().map(|&x| x < -noise).collect();
4120 if !inside.iter().any(|&x| x) {
4121 return pierced(&f, floor, window, noise, &[]);
4123 }
4124 if inside[0] || inside[n] || inside[1] || inside[n - 1] {
4125 return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4126 }
4127 let mut crossings: Vec<f64> = Vec::new();
4129 for i in 0..n {
4130 if inside[i] != inside[i + 1] {
4131 let (mut a, mut b) = (i as f64 / n as f64, (i + 1) as f64 / n as f64);
4132 let a_inside = inside[i];
4133 for _ in 0..60 {
4134 let mid = 0.5 * (a + b);
4135 if (f(mid, floor)? < 0.0) == a_inside { a = mid } else { b = mid }
4136 }
4137 crossings.push(0.5 * (a + b));
4138 }
4139 }
4140 if crossings.len() % 2 != 0 || crossings.is_empty() {
4141 return Err(refuse("end_trim", "an end pose's bulges past a wall do not pair up".into()));
4142 }
4143 let gradient = |u: f64, e: f64| -> Result<[f64; 2], KernelRefusal> {
4146 let h = 1e-6;
4147 Ok([(f(u + h, e)? - f(u - h, e)?) / (2.0 * h), (f(u, e + h)? - f(u, e - h)?) / (2.0 * h)])
4148 };
4149 let settle = |mut p: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
4150 for _ in 0..30 {
4151 let value = f(p[0], p[1])?;
4152 let g = gradient(p[0], p[1])?;
4153 let norm = g[0] * g[0] + g[1] * g[1];
4154 if norm == 0.0 {
4155 return Err(refuse("end_trim", "the cut of an end pose has a flat point".into()));
4156 }
4157 p = [p[0] - value * g[0] / norm, p[1] - value * g[1] / norm];
4158 if value.abs() * p[1] * p[1] < 1e-14 * split.scale {
4159 break;
4160 }
4161 }
4162 Ok(p)
4163 };
4164 let step = 1e-3;
4165 let mut cuts: Vec<Vec<[f64; 2]>> = Vec::new();
4166 let mut unused: Vec<f64> = crossings.clone();
4169 while !unused.is_empty() {
4170 let start = unused.remove(0);
4171 let mut points: Vec<[f64; 2]> = vec![[start, floor]];
4172 let mut heading = [0.0, 1.0];
4173 loop {
4174 let last = *points.last().expect("points");
4175 let g = gradient(last[0], last[1])?;
4176 let length = g[0].hypot(g[1]);
4177 let mut tangent = [-g[1] / length, g[0] / length];
4178 if tangent[0] * heading[0] + tangent[1] * heading[1] < 0.0 {
4179 tangent = [-tangent[0], -tangent[1]];
4180 }
4181 let next = settle([last[0] + step * tangent[0], last[1] + step * tangent[1]])?;
4182 if next[1] < floor {
4183 break;
4184 }
4185 if next[0] <= 1e-3 || next[0] >= 1.0 - 1e-3 {
4186 return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4187 }
4188 if next[1] > window {
4189 return Err(refuse("end_trim_deep", "an end pose holds a deep part of a wall".into()));
4190 }
4191 if points.len() > 20_000 {
4192 return Err(refuse("end_trim", "the cut of an end pose does not close".into()));
4193 }
4194 heading = [next[0] - last[0], next[1] - last[1]];
4195 points.push(next);
4196 }
4197 let back = *points.last().expect("points");
4198 let Some(at_end) = unused.iter().position(|&c| (back[0] - c).abs() <= 2.0 * step) else {
4199 return Err(refuse("end_trim", "the cut of an end pose does not come back where it should".into()));
4200 };
4201 let back_at = unused.remove(at_end);
4202 if points.len() == 1 && (back_at - start).abs() <= step {
4205 continue;
4206 }
4207 points.push([back_at, floor]);
4208 let to_end = |p: [f64; 2], rest: &[[f64; 2]], reversed: bool| -> Result<[f64; 2], KernelRefusal> {
4211 let g = gradient(p[0], p[1])?;
4214 if g[0].abs() > 1e-3 * g[1].abs() {
4215 return Ok([p[0] + g[1] / g[0] * p[1], 0.0]);
4216 }
4217 let clear = |q: &&[f64; 2]| q[1] - p[1] > 0.25 * step;
4218 let found = if reversed { rest.iter().rev().find(clear) } else { rest.iter().find(clear) };
4219 Ok(match found {
4220 Some(q) => [p[0] - (q[0] - p[0]) / (q[1] - p[1]) * p[1], 0.0],
4221 None => [p[0], 0.0],
4222 })
4223 };
4224 let count = points.len();
4225 let (first, last) = (to_end(points[0], &points[1..], false)?, to_end(points[count - 1], &points[..count - 1], true)?);
4226 if [first[0], last[0]].iter().any(|&u| u <= 1e-3 || u >= 1.0 - 1e-3) {
4227 return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4228 }
4229 points.insert(0, first);
4230 points.push(last);
4231 if points[0][0] > points[points.len() - 1][0] {
4232 points.reverse();
4233 }
4234 cuts.push(points);
4235 }
4236 let rings: Vec<&[[f64; 2]]> = cuts.iter().map(|c| c.as_slice()).collect();
4237 pierced(&f, floor, window, noise, &rings)?;
4238 let chart = |p: [f64; 2]| [p[0], v_of(p[1])];
4239 let chart_lengths = |chart_points: &[[f64; 2]]| -> Vec<f64> {
4244 let mut params = vec![0.0];
4245 for k in 1..chart_points.len() {
4246 let gap = (chart_points[k][0] - chart_points[k - 1][0]).hypot(chart_points[k][1] - chart_points[k - 1][1]);
4247 params.push(params[k - 1] + gap.max(1e-15));
4248 }
4249 params
4250 };
4251 let curve_through = |chart_points: &[[f64; 2]], params: &[f64], what: &'static str| -> Result<(NurbsCurve, NurbsCurve, f64), KernelRefusal> {
4254 let total = *params.last().expect("params");
4255 let params: Vec<f64> = params.iter().map(|x| x / total).collect();
4256 let world: Vec<Vec3> = chart_points.iter().map(|p| at(p[0], p[1])).collect::<Result<_, _>>()?;
4257 let uv: Vec<Vec3> = chart_points.iter().map(|p| Vec3::new(p[0], v_of(p[1]), 0.0)).collect();
4258 let (curve, pcurve) = (frag(crate::interpolate_curve(&world, 5, ¶ms), what)?, frag(crate::interpolate_curve(&uv, 5, ¶ms), what)?);
4259 let mut worst = 0.0f64;
4261 for k in 0..params.len() - 1 {
4262 let s = 0.5 * (params[k] + params[k + 1]);
4263 let q = frag(pcurve.evaluate(s), "pcurve")?;
4264 let e = if end == 1 { 1.0 - q.y } else { q.y };
4265 worst = worst.max(at(q.x, e)?.sub(frag(curve.evaluate(s), "curve")?).length());
4266 }
4267 Ok((curve, pcurve, worst))
4268 };
4269 let (fine, loose) = (1e-7 * split.scale, 1e-7 * split.scale);
4274 let missed = |what: &str| refuse("end_trim", format!("the {what}'s pcurve leaves it between its points however finely it is traced"));
4275 let cut_through = |mut chart_points: Vec<[f64; 2]>| -> Result<(NurbsCurve, NurbsCurve), KernelRefusal> {
4279 let mut best: Option<(NurbsCurve, NurbsCurve, f64)> = None;
4280 for _ in 0..7 {
4281 let fitted = curve_through(&chart_points, &chart_lengths(&chart_points), "cut")?;
4282 let done = fitted.2 <= fine;
4283 if best.as_ref().map_or(true, |b| fitted.2 < b.2) {
4284 best = Some(fitted);
4285 }
4286 if done {
4287 break;
4288 }
4289 let mut finer = Vec::with_capacity(2 * chart_points.len());
4290 for k in 0..chart_points.len() - 1 {
4291 finer.push(chart_points[k]);
4292 let (p, q) = (chart_points[k], chart_points[k + 1]);
4293 let middle = [0.5 * (p[0] + q[0]), 0.5 * (p[1] + q[1])];
4294 finer.push(if p[1] >= floor && q[1] >= floor { settle(middle)? } else { middle });
4296 }
4297 finer.push(chart_points[chart_points.len() - 1]);
4298 chart_points = finer;
4299 }
4300 match best {
4301 Some((curve, pcurve, worst)) if worst <= loose => Ok((curve, pcurve)),
4302 _ => Err(missed("cut")),
4303 }
4304 };
4305 let piece = |a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
4306 let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4307 let along: Vec<Vec3> = ts.iter().map(|&s| at(a + (b - a) * s, 0.0)).collect::<Result<_, _>>()?;
4308 frag(crate::interpolate_curve(&along, 5, &ts), "loop_piece")
4309 };
4310 let span_of = |points: &Vec<[f64; 2]>| -> Result<TrimSpan, KernelRefusal> {
4315 if let Some((k, ua, b)) = double_start(&at, points, floor)? {
4316 let mut traced: Option<(Vec<[f64; 2]>, Vec<[f64; 2]>, NurbsCurve, NurbsCurve, NurbsCurve, f64)> = None;
4318 for round in 0..5 {
4319 let (a_side, b_side) = double_curve(&at, [ua, 0.0], b, window, 2e-3 / f64::from(1u32 << round))?;
4320 let mut sigma = vec![0.0];
4323 for k in 1..a_side.len() {
4324 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]));
4325 sigma.push(sigma[k - 1] + step.max(1e-15));
4326 }
4327 let ((slot, pa, miss_a), (_, pb, miss_b)) = (curve_through(&a_side, &sigma, "double_curve")?, curve_through(&b_side, &sigma, "double_curve")?);
4328 let worst = miss_a.max(miss_b);
4329 if traced.as_ref().map_or(true, |t| worst < t.5) {
4330 traced = Some((a_side, b_side, slot, pa, pb, worst));
4331 }
4332 if worst <= fine {
4333 break;
4334 }
4335 }
4336 let Some((a_side, b_side, slot, pa, pb, _)) = traced.filter(|t| t.5 <= loose) else { return Err(missed("double curve")) };
4337 let low = ua < points[0][0];
4340 if !low && ua <= points[points.len() - 1][0] {
4341 return Err(refuse("end_trim", "the double curve of a wall leaves its end inside the cut".into()));
4342 }
4343 let mut kept: Vec<[f64; 2]> = if low { points[k..].to_vec() } else { points[..=k].to_vec() };
4344 if low { kept[0] = b } else { *kept.last_mut().expect("kept") = b }
4345 let (cut, cut_pcurve) = cut_through(kept.clone())?;
4346 let tip = at(a_side[a_side.len() - 1][0], a_side[a_side.len() - 1][1])?;
4347 let chain = if low {
4349 vec![(TrimPiece::Slot, true, pa), (TrimPiece::Slot, false, frag(pb.reversed(), "reversed")?), (TrimPiece::Cut, true, cut_pcurve)]
4350 } else {
4351 vec![(TrimPiece::Cut, true, cut_pcurve), (TrimPiece::Slot, true, pb), (TrimPiece::Slot, false, frag(pa.reversed(), "reversed")?)]
4352 };
4353 let lambdas = if low { [ua, points[points.len() - 1][0]] } else { [points[0][0], ua] };
4354 let mut removed: Vec<[f64; 2]> = Vec::new();
4355 if low {
4356 removed.extend(a_side.iter().map(|&p| chart(p)));
4357 removed.extend(b_side.iter().rev().map(|&p| chart(p)));
4358 removed.extend(kept.iter().map(|&p| chart(p)));
4359 } else {
4360 removed.extend(kept.iter().map(|&p| chart(p)));
4361 removed.extend(b_side.iter().map(|&p| chart(p)));
4362 removed.extend(a_side.iter().rev().map(|&p| chart(p)));
4363 }
4364 let dropped: Vec<[f64; 2]> = if low { points[..k].to_vec() } else { points[k + 1..].to_vec() };
4366 for p in dropped.iter().filter(|p| p[1] > 2.0 * floor && (p[0] - b[0]).hypot(p[1] - b[1]) > 5e-3) {
4367 if !inside_ring(&removed, chart(*p)) {
4368 return Err(refuse("end_trim", "the cut of an end pose leaves the part of the wall its double curve takes away".into()));
4369 }
4370 }
4371 let points3 = [at(lambdas[0], 0.0)?, at(lambdas[1], 0.0)?];
4372 let tip_chart = chart(a_side[a_side.len() - 1]);
4376 let side = |points: &[[f64; 2]]| points.iter().map(|q| q[0] - tip_chart[0]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| (lo.min(d), hi.max(d)));
4377 let ((a_lo, a_hi), (b_lo, b_hi)) = (side(&a_side), side(&b_side));
4378 let parted = (a_lo >= -1e-9 && b_hi <= 1e-9) || (a_hi <= 1e-9 && b_lo >= -1e-9);
4379 if !parted {
4380 return Err(refuse("end_trim", "the two sides of a wall's double curve do not keep to either side of its end".into()));
4381 }
4382 return Ok(TrimSpan { lambdas, points: points3, cut: Some(cut), chain, slot: Some((slot, tip)), tip: Some(tip_chart), removed });
4383 }
4384 let (cut, wall_pcurve) = cut_through(points.clone())?;
4385 let (la, lb) = (points[0][0], points[points.len() - 1][0]);
4386 let removed: Vec<[f64; 2]> = points.iter().map(|&p| chart(p)).collect();
4387 Ok(TrimSpan { lambdas: [la, lb], points: [at(la, 0.0)?, at(lb, 0.0)?], cut: Some(cut), chain: vec![(TrimPiece::Cut, true, wall_pcurve)], slot: None, tip: None, removed })
4388 };
4389 if cuts.is_empty() {
4390 return pierced(&f, floor, window, noise, &[]);
4391 }
4392 let mut spans = Vec::with_capacity(cuts.len());
4393 for points in &cuts {
4394 spans.push(span_of(points)?);
4395 }
4396 spans.sort_by(|a, b| a.lambdas[0].total_cmp(&b.lambdas[0]));
4397 let edge_row = v_of(0.0);
4400 let within = |inner: &TrimSpan, outer: &TrimSpan| -> bool {
4401 inner.lambdas[0] >= outer.lambdas[0]
4402 && inner.lambdas[1] <= outer.lambdas[1]
4403 && inner.removed.iter().filter(|p| (p[1] - edge_row).abs() > floor).all(|&p| inside_ring(&outer.removed, p) || near_ring(&outer.removed, p, 1e-3))
4404 };
4405 let mut k = 0;
4406 while k < spans.len() {
4407 if (0..spans.len()).any(|j| j != k && within(&spans[k], &spans[j])) {
4408 spans.remove(k);
4409 } else {
4410 k += 1;
4411 }
4412 }
4413 for pair in spans.windows(2) {
4414 if pair[0].lambdas[1] >= pair[1].lambdas[0] {
4415 return Err(refuse("end_trim", "an end pose's trims of a wall overlap".into()));
4416 }
4417 }
4418 let mut pieces = Vec::with_capacity(spans.len() + 1);
4419 let mut from = 0.0;
4420 for span in &spans {
4421 pieces.push(piece(from, span.lambdas[0])?);
4422 from = span.lambdas[1];
4423 }
4424 pieces.push(piece(from, 1.0)?);
4425 Ok(Some(EndTrim { spans, pieces }))
4426}
4427
4428
4429
4430fn near_ring(ring: &[[f64; 2]], q: [f64; 2], reach: f64) -> bool {
4432 (0..ring.len()).any(|k| {
4433 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
4434 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
4435 let length = dx * dx + dy * dy;
4436 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 };
4437 (q[0] - a[0] - s * dx).hypot(q[1] - a[1] - s * dy) < reach
4438 })
4439}
4440
4441fn inside_ring(ring: &[[f64; 2]], q: [f64; 2]) -> bool {
4443 let mut crossings = 0;
4444 for k in 0..ring.len() {
4445 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
4446 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]) {
4447 crossings += 1;
4448 }
4449 }
4450 crossings % 2 == 1
4451}
4452
4453fn double_start(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, points: &[[f64; 2]], floor: f64) -> Result<Option<(usize, f64, [f64; 2])>, KernelRefusal> {
4458 let n = 4000;
4459 let row: Vec<Vec3> = (0..=n).map(|i| at(i as f64 / n as f64, 0.0)).collect::<Result<_, _>>()?;
4460 let scale = row.iter().fold(0.0f64, |m, p| m.max(p.sub(row[0]).length())).max(1e-300);
4461 let mut best: Option<(f64, usize, usize)> = None;
4462 for (k, p) in points.iter().enumerate() {
4463 if p[1] < 4.0 * floor {
4464 continue;
4465 }
4466 let x = at(p[0], p[1])?;
4467 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 });
4468 if best.map_or(true, |b| d / p[1] < b.0 / points[b.1][1]) {
4471 best = Some((d, k, i));
4472 }
4473 }
4474 let Some((distance, k, i)) = best else { return Ok(None) };
4475 if distance > 5e-3 * scale {
4477 return Ok(None);
4478 }
4479 let mut x = [points[k][0], points[k][1], i as f64 / n as f64];
4481 let h = 1e-7;
4482 for _ in 0..40 {
4483 let g = at(x[0], x[1])?.sub(at(x[2], 0.0)?);
4484 if g.length() < 1e-13 * scale {
4485 if x[1] < 2.0 * floor || !(0.0..=1.0).contains(&x[0]) || !(0.0..=1.0).contains(&x[2]) {
4487 return Ok(None);
4488 }
4489 return Ok(Some((k, x[2], [x[0], x[1]])));
4490 }
4491 let c0 = at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h);
4492 let c1 = at(x[0], x[1] + h)?.sub(at(x[0], x[1] - h)?).scale(0.5 / h);
4493 let c2 = at(x[2] + h, 0.0)?.sub(at(x[2] - h, 0.0)?).scale(-0.5 / h);
4494 let det = c0.dot(c1.cross(c2));
4495 if det.abs() < 1e-300 {
4496 break;
4497 }
4498 let r = g.scale(-1.0);
4500 let d = [r.dot(c1.cross(c2)) / det, c0.dot(r.cross(c2)) / det, c0.dot(c1.cross(r)) / det];
4501 x = [x[0] + d[0], x[1] + d[1], x[2] + d[2]];
4502 }
4503 if distance < 1e-6 * scale {
4504 return Err(refuse("end_trim", "an end pose's cut meets its own loop, but not exactly enough to follow".into()));
4505 }
4506 Ok(None)
4507}
4508
4509fn double_curve(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, a: [f64; 2], b: [f64; 2], window: f64, step: f64) -> Result<(Vec<[f64; 2]>, Vec<[f64; 2]>), KernelRefusal> {
4514 let h = 1e-7;
4515 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);
4516 let jacobian = |x: [f64; 4]| -> Result<[Vec3; 4], KernelRefusal> {
4517 Ok([
4518 at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h),
4519 at(x[0], x[1] + h)?.sub(at(x[0], (x[1] - h).max(0.0))?).scale(1.0 / (h + x[1].min(h))),
4520 at(x[2] + h, x[3])?.sub(at(x[2] - h, x[3])?).scale(-0.5 / h),
4521 at(x[2], x[3] + h)?.sub(at(x[2], (x[3] - h).max(0.0))?).scale(-1.0 / (h + x[3].min(h))),
4522 ])
4523 };
4524 let residual = |x: [f64; 4]| -> Result<Vec3, KernelRefusal> { Ok(at(x[0], x[1])?.sub(at(x[2], x[3])?)) };
4525 let null = |j: &[Vec3; 4]| -> [f64; 4] {
4527 let minor = |p: usize, q: usize, r: usize| j[p].dot(j[q].cross(j[r]));
4528 let n = [minor(1, 2, 3), -minor(0, 2, 3), minor(0, 1, 3), -minor(0, 1, 2)];
4529 let length = n.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-300);
4530 [n[0] / length, n[1] / length, n[2] / length, n[3] / length]
4531 };
4532 let correct = |mut x: [f64; 4]| -> Result<Option<[f64; 4]>, KernelRefusal> {
4534 for _ in 0..30 {
4535 let g = residual(x)?;
4536 if g.length() < 1e-13 * scale {
4537 return Ok(Some(x));
4538 }
4539 let j = jacobian(x)?;
4540 let mut m = [[0.0; 3]; 3];
4542 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]];
4543 for p in 0..3 {
4544 for q in 0..3 {
4545 m[p][q] = (0..4).map(|c| rows[p][c] * rows[q][c]).sum();
4546 }
4547 }
4548 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]);
4549 if det.abs() < 1e-300 {
4550 return Ok(None);
4551 }
4552 let r = [-g.x, -g.y, -g.z];
4553 let solve = |col: usize| -> f64 {
4554 let mut a = m;
4555 for p in 0..3 {
4556 a[p][col] = r[p];
4557 }
4558 (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
4559 };
4560 let y = [solve(0), solve(1), solve(2)];
4561 for c in 0..4 {
4562 x[c] += (0..3).map(|p| rows[p][c] * y[p]).sum::<f64>();
4563 }
4564 }
4565 Ok(None)
4566 };
4567 let mut x = [a[0], a[1], b[0], b[1]];
4568 let mut a_side = vec![a];
4569 let mut b_side = vec![b];
4570 let mut heading: Option<[f64; 4]> = None;
4571 loop {
4572 let separation = (x[0] - x[2]).hypot(x[1] - x[3]);
4573 if separation < 3.0 * step && a_side.len() > 2 {
4574 let tip = [0.5 * (x[0] + x[2]), 0.5 * (x[1] + x[3])];
4575 a_side.push(tip);
4576 b_side.push(tip);
4577 return Ok((a_side, b_side));
4578 }
4579 let mut t = null(&jacobian(x)?);
4580 let reference = heading.unwrap_or([0.0, 1.0, 0.0, 0.0]);
4581 if (0..4).map(|c| t[c] * reference[c]).sum::<f64>() < 0.0 {
4582 t = [-t[0], -t[1], -t[2], -t[3]];
4583 }
4584 let mut next = None;
4585 let mut length = step;
4586 for _ in 0..6 {
4587 let guess = [x[0] + length * t[0], x[1] + length * t[1], x[2] + length * t[2], x[3] + length * t[3]];
4588 if let Some(y) = correct(guess)? {
4589 next = Some(y);
4590 break;
4591 }
4592 length *= 0.5;
4593 }
4594 let Some(y) = next else {
4595 if separation < 2e-2 && a_side.len() > 2 {
4598 let tip = [0.5 * (x[0] + x[2]), 0.5 * (x[1] + x[3])];
4599 a_side.push(tip);
4600 b_side.push(tip);
4601 return Ok((a_side, b_side));
4602 }
4603 return Err(refuse("end_trim", format!("the double curve of a wall cannot be followed (sides {separation:e} apart)")));
4604 };
4605 if [y[0], y[2]].iter().any(|&u| u <= 1e-3 || u >= 1.0 - 1e-3) {
4606 return Err(refuse("end_trim_corner", "the double curve of a wall reaches a corner of its grazing loop".into()));
4607 }
4608 if y[1] > window || y[3] > window || y[1] < 0.0 || y[3] < 0.0 {
4609 return Err(refuse("end_trim_deep", format!("the double curve of a wall runs deep into the motion or back past its end (its sides at time fractions {:.4} and {:.4} from the end, {} points in)", y[1], y[3], a_side.len())));
4610 }
4611 if a_side.len() > 20_000 {
4612 return Err(refuse("end_trim", "the double curve of a wall does not close".into()));
4613 }
4614 heading = Some([y[0] - x[0], y[1] - x[1], y[2] - x[2], y[3] - x[3]]);
4615 x = y;
4616 a_side.push([x[0], x[1]]);
4617 b_side.push([x[2], x[3]]);
4618 }
4619}
4620
4621fn pierced(
4624 f: &dyn Fn(f64, f64) -> Result<f64, KernelRefusal>,
4625 floor: f64,
4626 window: f64,
4627 noise: f64,
4628 cuts: &[&[[f64; 2]]],
4629) -> Result<Option<EndTrim>, KernelRefusal> {
4630 let enclosed = |q: [f64; 2]| -> bool { cuts.iter().any(|ring| inside_ring(ring, q)) };
4631 for i in 1..200 {
4632 for j in 1..=40 {
4633 let (u, e) = (i as f64 / 200.0, floor + (window - floor) * (j as f64 / 40.0).powi(2));
4634 if f(u, e)? * e * e < -noise * floor * floor && !enclosed([u, e]) {
4636 return Err(refuse("end_trim_deep", "an end pose holds a part of a wall away from its end".into()));
4637 }
4638 }
4639 }
4640 Ok(None)
4641}
4642
4643#[allow(clippy::too_many_arguments)]
4644fn assemble_lofted(
4645 screw: &Screw,
4646 first: &Split,
4647 last: &Split,
4648 walls: &[(usize, bool, NurbsSurface, NurbsCurve, NurbsCurve)],
4649 times: &[f64],
4650 paths: &HashMap<usize, Vec<Vec3>>,
4651) -> Result<BrepSolid, KernelRefusal> {
4652 let mut shell = Shell::new();
4653 let ends = [(first, screw.t0), (last, screw.t1)];
4654 let mut trims: HashMap<(usize, usize), EndTrim> = HashMap::new();
4658 let mut split_walls: std::collections::HashSet<usize> = std::collections::HashSet::new();
4660 for (index, (_, _, surface, _, _)) in walls.iter().enumerate() {
4661 for end in 0..2 {
4662 if let Some(trim) = end_trim(screw, ends[end].0, ends[end].1, surface, end)? {
4663 trims.insert((index, end), trim);
4664 }
4665 }
4666 let removed: Vec<&[[f64; 2]]> = (0..2).filter_map(|end| trims.get(&(index, end))).flat_map(|trim| trim.spans.iter().map(|span| span.removed.as_slice())).collect();
4667 wall_regular(surface, times, &removed)?;
4668 let tips_at = |end: usize| -> Vec<[f64; 2]> { trims.get(&(index, end)).map(|trim| trim.spans.iter().filter_map(|span| span.tip).collect()).unwrap_or_default() };
4674 let (tips0, tips1) = (tips_at(0), tips_at(1));
4675 if !tips0.is_empty() && !tips1.is_empty() {
4676 return Err(refuse("wall_split", "a wall turns over near both its ends".into()));
4677 }
4678 let (far, tips) = if tips0.is_empty() { (0usize, tips1) } else { (1usize, tips0) };
4679 let straight = |trim: &EndTrim| -> Result<bool, KernelRefusal> {
4685 for (k, span) in trim.spans.iter().enumerate() {
4686 let Some(tip) = span.tip else { continue };
4687 let first_slot = span.chain.iter().position(|piece| piece.0 == TrimPiece::Slot).unwrap_or(0);
4688 for (j, other) in trim.spans.iter().enumerate() {
4689 for (n, (_, _, pcurve)) in other.chain.iter().enumerate() {
4690 let before = j < k || (j == k && n <= first_slot);
4691 let [d0, d1] = frag(pcurve.domain(), "domain")?;
4692 for i in 0..=32 {
4693 let u = frag(pcurve.evaluate(d0 + (d1 - d0) * i as f64 / 32.0), "pcurve")?.x;
4694 if (before && u > tip[0] + 1e-9) || (!before && u < tip[0] - 1e-9) {
4695 return Ok(false);
4696 }
4697 }
4698 }
4699 }
4700 }
4701 Ok(true)
4702 };
4703 let split_here = !tips.is_empty() && trims.get(&(index, 1 - far)).map(straight).transpose()?.unwrap_or(false);
4704 if split_here {
4705 split_walls.insert(index);
4706 let v_far = far as f64;
4707 let mut spans = trims.remove(&(index, far)).map(|trim| trim.spans).unwrap_or_default();
4708 for tip in &tips {
4709 if spans.iter().any(|span| tip[0] > span.lambdas[0] - 1e-9 && tip[0] < span.lambdas[1] + 1e-9) || tip[0] <= 1e-9 || tip[0] >= 1.0 - 1e-9 {
4710 return Err(refuse("wall_split", "a wall's split line lands in a trim of its far end".into()));
4711 }
4712 let point = frag(surface.evaluate(tip[0], v_far), "wall")?;
4713 spans.push(TrimSpan { lambdas: [tip[0], tip[0]], points: [point, point], cut: None, chain: Vec::new(), slot: None, tip: None, removed: Vec::new() });
4714 }
4715 spans.sort_by(|a, b| a.lambdas[0].total_cmp(&b.lambdas[0]));
4716 let pieces = row_pieces(surface, v_far, &spans)?;
4717 trims.insert((index, far), EndTrim { spans, pieces });
4718 }
4719 }
4720 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
4721 let mut vertex = |shell: &mut Shell, end: usize, p: usize| -> u64 {
4722 *vertex_at.entry((end, p)).or_insert_with(|| shell.vertex(screw.apply(ends[end].1, ends[end].0.points[p])))
4723 };
4724 let grazing: HashMap<usize, usize> = walls.iter().enumerate().map(|(index, w)| (w.0, index)).collect();
4725 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
4728 let mut edge_curve: HashMap<(usize, usize), NurbsCurve> = HashMap::new();
4729 let mut trim_edges: HashMap<(usize, usize), (Vec<u64>, Vec<[u64; 2]>)> = HashMap::new();
4732 let mut trim_vertices: HashMap<(usize, usize), Vec<(u64, Option<u64>)>> = HashMap::new();
4735 for end in 0..2 {
4736 let (split, t) = ends[end];
4737 for (c, tool_curve) in split.curves.iter().enumerate() {
4738 let used = split.caps.iter().any(|cap| cap.front == (end == 1) && cap.coedges.iter().any(|e| e.0 == c));
4739 if !used {
4740 continue;
4741 }
4742 if let Some(&index) = grazing.get(&c) {
4743 if let Some(trim) = trims.get(&(index, end)) {
4744 let back_forward = walls[index].1;
4746 let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
4747 let (vf, vt) = (vertex(&mut shell, end, from), vertex(&mut shell, end, to));
4748 let mut piece_edges = Vec::with_capacity(trim.pieces.len());
4749 let mut span_edges = Vec::with_capacity(trim.spans.len());
4750 let mut start = vf;
4751 let mut span_vertices = Vec::with_capacity(trim.spans.len());
4752 for (span, piece) in trim.spans.iter().zip(&trim.pieces) {
4753 let va = shell.vertex(span.points[0]);
4754 piece_edges.push(shell.edge(piece.clone(), start, va, false)?);
4755 let Some(cut_curve) = &span.cut else {
4756 span_edges.push([u64::MAX, u64::MAX]);
4758 span_vertices.push((va, None));
4759 start = va;
4760 continue;
4761 };
4762 let vb = shell.vertex(span.points[1]);
4763 let cut = shell.edge(cut_curve.clone(), va, vb, false)?;
4764 let (slot, tip_vertex) = match &span.slot {
4768 Some((curve, tip)) => {
4769 let from = if span.chain[0].0 == TrimPiece::Slot { va } else { vb };
4770 let tip = shell.vertex(*tip);
4771 (shell.edge(curve.clone(), from, tip, false)?, Some(tip))
4772 }
4773 None => (u64::MAX, None),
4774 };
4775 span_edges.push([cut, slot]);
4776 span_vertices.push((va, tip_vertex));
4777 start = vb;
4778 }
4779 trim_vertices.insert((index, end), span_vertices);
4780 piece_edges.push(shell.edge(trim.pieces[trim.pieces.len() - 1].clone(), start, vt, false)?);
4781 trim_edges.insert((index, end), (piece_edges, span_edges));
4782 continue;
4783 }
4784 }
4785 let curve = match grazing.get(&c) {
4786 Some(&index) => {
4787 let (_, back_forward, _, row0, row1) = &walls[index];
4788 let row = if end == 0 { row0 } else { row1 };
4789 if *back_forward { frag(row.reversed(), "reversed")? } else { row.clone() }
4790 }
4791 None => screw.move_curve(t, &tool_curve.curve)?,
4792 };
4793 let (a, b) = (vertex(&mut shell, end, tool_curve.start), vertex(&mut shell, end, tool_curve.end));
4794 let edge = shell.edge(curve.clone(), a, b, tool_curve.degenerate)?;
4795 edge_at.insert((end, c), edge);
4796 edge_curve.insert((end, c), curve);
4797 }
4798 }
4799 for end in 0..2 {
4800 let (split, t) = ends[end];
4801 for cap in split.caps.iter().filter(|cap| cap.front == (end == 1)) {
4802 let surface = screw.move_surface(t, &cap.surface)?;
4803 let mut coedges = Vec::with_capacity(cap.coedges.len());
4804 for (c, forward, pcurve) in &cap.coedges {
4805 if let Some(&index) = grazing.get(c) {
4806 if let (Some(trim), Some((piece_edges, span_edges))) = (trims.get(&(index, end)), trim_edges.get(&(index, end))) {
4807 let Some(chart) = cap.chart.as_ref() else {
4808 return Err(refuse("end_trim", "an end pose bulges past a wall beside a cap without a chart".into()));
4809 };
4810 let along = *forward != walls[index].1;
4814 let mut pieces: Vec<(u64, &NurbsCurve)> = Vec::with_capacity(2 * trim.pieces.len());
4815 for (k, piece) in trim.pieces.iter().enumerate() {
4816 pieces.push((piece_edges[k], piece));
4817 if let Some(cut) = trim.spans.get(k).and_then(|span| span.cut.as_ref()) {
4818 pieces.push((span_edges[k][0], cut));
4819 }
4820 }
4821 if !along {
4822 pieces.reverse();
4823 }
4824 for (edge, curve) in pieces {
4825 coedges.push((edge, along, charted_pcurve(chart, screw, t, curve, along)?));
4826 }
4827 continue;
4828 }
4829 }
4830 let pcurve = if grazing.contains_key(c) && cap.chart.is_some() {
4831 charted_pcurve(cap.chart.as_ref().expect("chart"), screw, t, &edge_curve[&(end, *c)], *forward)?
4832 } else if grazing.contains_key(c) {
4833 match projected_pcurve(&surface, &edge_curve[&(end, *c)], *forward) {
4834 Ok(p) => p,
4835 Err(error) => {
4836 let samples = split.curves[*c].samples.as_ref().map(|list| {
4838 list.iter()
4839 .map(|&x| crate::project_point_to_surface(&surface, screw.apply(t, x)).map(|f| f.distance).unwrap_or(f64::NAN))
4840 .fold(0.0f64, f64::max)
4841 });
4842 let line = split.curves[*c].curve.degree;
4843 return Err(KernelRefusal::unsupported(
4844 KernelStage::Classify,
4845 "loft_cap",
4846 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),
4847 ));
4848 }
4849 }
4850 } else {
4851 pcurve.clone()
4852 };
4853 coedges.push((edge_at[&(end, *c)], *forward, pcurve));
4854 }
4855 shell.face(surface, coedges);
4856 }
4857 }
4858 let mut orbits: HashMap<usize, u64> = HashMap::new();
4861 let mut keys: Vec<usize> = paths.keys().copied().collect();
4862 keys.sort_unstable();
4863 for r in keys {
4864 let curve = frag(crate::interpolate_curve(&paths[&r], 5, times), "orbit")?;
4865 let (a, b) = (vertex(&mut shell, 0, r), vertex(&mut shell, 1, r));
4866 orbits.insert(r, shell.edge(curve, a, b, false)?);
4867 }
4868 type RowPiece = (u64, bool, NurbsCurve, Option<(f64, f64, u64)>);
4872 for (index, (c, back_forward, surface, _, _)) in walls.iter().enumerate() {
4873 let tool_curve = &first.curves[*c];
4874 let (from, to) = if *back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
4875 let row = |end: usize| -> Result<Vec<RowPiece>, KernelRefusal> {
4876 let v = end as f64;
4877 let (Some(trim), Some((piece_edges, span_edges))) = (trims.get(&(index, end)), trim_edges.get(&(index, end))) else {
4878 return Ok(vec![(edge_at[&(end, *c)], !*back_forward, uv_line([0.0, v], [1.0, v])?, None)]);
4879 };
4880 let vertices = &trim_vertices[&(index, end)];
4881 let mut list: Vec<RowPiece> = Vec::new();
4882 let mut at = 0.0;
4883 for (k, span) in trim.spans.iter().enumerate() {
4884 let broken = if span.cut.is_none() { Some((span.lambdas[0], v, vertices[k].0)) } else { None };
4885 list.push((piece_edges[k], true, uv_line([at, v], [span.lambdas[0], v])?, broken));
4886 for (n, (piece, forward, pcurve)) in span.chain.iter().enumerate() {
4887 let edge = if *piece == TrimPiece::Cut { span_edges[k][0] } else { span_edges[k][1] };
4888 let between = *piece == TrimPiece::Slot && span.chain.get(n + 1).is_some_and(|next| next.0 == TrimPiece::Slot);
4890 let tip = match (span.tip, vertices[k].1) {
4891 (Some(tip), Some(vertex)) if between && split_walls.contains(&index) => Some((tip[0], tip[1], vertex)),
4892 _ => None,
4893 };
4894 list.push((edge, *forward, pcurve.clone(), tip));
4895 }
4896 at = span.lambdas[1];
4897 }
4898 list.push((piece_edges[trim.spans.len()], true, uv_line([at, v], [1.0, v])?, None));
4899 Ok(list)
4900 };
4901 let (bottom, top) = (row(0)?, row(1)?);
4902 let (low, high): (Vec<(f64, f64, u64)>, Vec<(f64, f64, u64)>) = (bottom.iter().filter_map(|piece| piece.3).collect(), top.iter().filter_map(|piece| piece.3).collect());
4903 if low.len() != high.len() || low.iter().zip(&high).any(|(a, b)| (a.0 - b.0).abs() > 1e-9) {
4904 return Err(refuse("wall_split", "a wall's split lines do not run from one end's row to the other's".into()));
4905 }
4906 let mut lines: Vec<(u64, f64, f64, f64)> = Vec::with_capacity(low.len());
4908 for (below, above) in low.iter().zip(&high) {
4909 let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4910 let along: Vec<Vec3> = ts.iter().map(|&s| frag(surface.evaluate(below.0, below.1 + (above.1 - below.1) * s), "wall")).collect::<Result<_, _>>()?;
4911 let edge = shell.edge(frag(crate::interpolate_curve(&along, 5, &ts), "split_line")?, below.2, above.2, false)?;
4912 lines.push((edge, below.0, below.1, above.1));
4913 }
4914 let stretches = |list: Vec<RowPiece>| -> Vec<Vec<(u64, bool, NurbsCurve)>> {
4916 let mut out = vec![Vec::new()];
4917 for (edge, forward, pcurve, cut) in list {
4918 out.last_mut().expect("stretch").push((edge, forward, pcurve));
4919 if cut.is_some() {
4920 out.push(Vec::new());
4921 }
4922 }
4923 out
4924 };
4925 let (bottoms, tops) = (stretches(bottom), stretches(top));
4926 for j in 0..=lines.len() {
4930 let mut coedges = bottoms[j].clone();
4931 match lines.get(j) {
4932 Some(&(edge, u, v0, v1)) => coedges.push((edge, true, uv_line([u, v0], [u, v1])?)),
4933 None => coedges.push((orbits[&to], true, uv_line([1.0, 0.0], [1.0, 1.0])?)),
4934 }
4935 for (edge, forward, pcurve) in tops[j].iter().rev() {
4936 coedges.push((*edge, !*forward, frag(pcurve.reversed(), "reversed")?));
4937 }
4938 match j.checked_sub(1).map(|k| lines[k]) {
4939 Some((edge, u, v0, v1)) => coedges.push((edge, false, uv_line([u, v1], [u, v0])?)),
4940 None => coedges.push((orbits[&from], false, uv_line([0.0, 1.0], [0.0, 0.0])?)),
4941 }
4942 let (ua, ub) = (if j == 0 { 0.0 } else { lines[j - 1].1 }, lines.get(j).map_or(1.0, |line| line.1));
4943 shell.face(restrict_u(surface, ua, ub)?, coedges);
4944 }
4945 }
4946 let body = shell.solid();
4947 let issues = body.validate();
4948 if !issues.is_empty() {
4949 let mut notes = Vec::new();
4952 for face in body.shells.iter().flat_map(|s| &s.faces) {
4953 for coedge in &face.loops[0].coedges {
4954 let edge = body.edges.iter().find(|e| e.id == coedge.edge_id).expect("edge");
4955 let [p0, p1] = frag(coedge.pcurve.domain(), "domain")?;
4956 let a = frag(coedge.pcurve.evaluate(p0), "uv")?;
4957 let on = frag(face.surface.evaluate_extended(a.x, a.y), "on")?;
4958 let at = frag(edge.curve.evaluate(if coedge.forward { edge.t0 } else { edge.t1 }), "at")?;
4959 if on.sub(at).length() > 1e-6 {
4960 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));
4961 }
4962 let _ = p1;
4963 }
4964 }
4965 return Err(refuse("assembly", format!("the lofted sweep is not valid: {:?}; {}", issues.first(), notes.iter().take(4).cloned().collect::<Vec<_>>().join("; "))));
4966 }
4967 check_single_pass(screw, first, &body, &std::collections::HashSet::new())?;
4968 Ok(body)
4969}
4970
4971fn covered(screw: &Screw, split: &Split, y: Vec3, margin: f64) -> Result<bool, KernelRefusal> {
4974 let reach = y.sub(screw.a).length() + split.scale + screw.extent;
4975 let speed = (screw.omega.abs() * reach + screw.rate_bound()).max(1e-300);
4976 let floor = (screw.t1 - screw.t0) * 1e-7;
4977 let mut t = screw.t0;
4978 let mut steps = 0usize;
4979 loop {
4980 let d = split.distance(screw.invert(t, y));
4981 if d < -margin {
4982 return Ok(true);
4983 }
4984 if t >= screw.t1 {
4985 return Ok(false);
4986 }
4987 if steps > 2_000_000 {
4988 return Err(refuse("probe_budget", "the one-pass check could not walk the motion past a point near the sweep".into()));
4989 }
4990 t = (t + ((d.abs() + margin) / speed).max(floor)).min(screw.t1);
4991 steps += 1;
4992 }
4993}
4994
4995fn check_single_pass(screw: &Screw, split: &Split, body: &BrepSolid, sliding: &std::collections::HashSet<u64>) -> Result<(), KernelRefusal> {
5002 let margin = 1e-9 * split.scale;
5003 for face in body.shells.iter().flat_map(|s| &s.faces) {
5004 let step = if sliding.contains(&face.id) { 1e-3 * split.scale } else { 1e-6 * split.scale };
5005 let surface = &face.surface;
5006 let mut corners: Vec<[f64; 2]> = Vec::new();
5010 for coedge in &face.loops[0].coedges {
5011 let domain = frag(coedge.pcurve.domain(), "domain")?;
5012 for k in 0..4 {
5013 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 4.0), "pcurve")?;
5014 corners.push([p.x, p.y]);
5015 }
5016 }
5017 let middle = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
5018 let middle = [middle[0] / corners.len() as f64, middle[1] / corners.len() as f64];
5019 let mut probes: Vec<[f64; 2]> = vec![middle];
5020 for c in &corners {
5021 for w in [0.25, 0.5, 0.75, 0.9, 0.99] {
5022 probes.push([middle[0] + (c[0] - middle[0]) * w, middle[1] + (c[1] - middle[1]) * w]);
5023 }
5024 }
5025 if surface.degree_v == 5 {
5026 let [u0, u1] = frag(surface.domain_u(), "domain")?;
5027 for i in 1..8 {
5028 for j in 1..64 {
5029 probes.push([u0 + (u1 - u0) * i as f64 / 8.0, j as f64 / 64.0]);
5030 }
5031 }
5032 }
5033 let ([u0, u1], [v0, v1]) = (frag(surface.domain_u(), "domain")?, frag(surface.domain_v(), "domain")?);
5034 let mut ring: Vec<[f64; 2]> = Vec::new();
5037 for coedge in &face.loops[0].coedges {
5038 let domain = frag(coedge.pcurve.domain(), "domain")?;
5039 for k in 0..32 {
5040 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 32.0), "pcurve")?;
5041 ring.push([p.x, p.y]);
5042 }
5043 }
5044 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])]));
5045 for i in 1..12 {
5046 for j in 1..12 {
5047 probes.push([lo[0] + (hi[0] - lo[0]) * i as f64 / 12.0, lo[1] + (hi[1] - lo[1]) * j as f64 / 12.0]);
5048 }
5049 }
5050 let inside = |q: [f64; 2]| -> bool {
5051 let mut crossings = 0;
5052 for k in 0..ring.len() {
5053 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
5054 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]) {
5055 crossings += 1;
5056 }
5057 }
5058 crossings % 2 == 1
5059 };
5060 for [u, v] in probes.into_iter().filter(|&q| inside(q)) {
5061 let p = frag(surface.evaluate(u, v), "probe")?;
5062 let (hu, hv) = (1e-6 * (u1 - u0), 1e-6 * (v1 - v0));
5063 let (ua, ub, va, vb) = ((u - hu).max(u0), (u + hu).min(u1), (v - hv).max(v0), (v + hv).min(v1));
5066 let du = frag(surface.evaluate(ub, v), "probe")?.sub(frag(surface.evaluate(ua, v), "probe")?).scale(1.0 / (ub - ua));
5067 let dv = frag(surface.evaluate(u, vb), "probe")?.sub(frag(surface.evaluate(u, va), "probe")?).scale(1.0 / (vb - va));
5068 let Ok(n) = du.cross(dv).normalized() else { continue };
5069 let n = if face.same_sense { n } else { n.scale(-1.0) };
5070 if covered(screw, split, p.add(n.scale(step)), margin)? {
5071 return Err(refuse("folds", format!("the sweep passes over a point twice (just outside its own face at {p:?}); not one pass")));
5072 }
5073 }
5074 }
5075 Ok(())
5076}
5077