1use crate::solid_sweep::outline_signed_distance;
40use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord};
41use crate::{make_line, make_revolution, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, NurbsSurface, OrRefuse, OutlinePiece, SolidMotion, Vec3, Vec4};
42use std::collections::HashMap;
43use std::f64::consts::{PI, TAU};
44
45#[derive(Clone, Debug)]
47pub enum SolidTool {
48 Polyhedron { vertices: Vec<Vec3>, faces: Vec<Vec<usize>> },
51 Revolved { axis_origin: Vec3, axis_direction: Vec3, radial: Vec3, meridian: Vec<OutlinePiece> },
56}
57
58const PCURVE_SAMPLES: usize = 576;
63
64pub const SOLID_SWEEP_WHOLE_REFUSAL: &str = "sweepSolid";
66
67fn refuse(what: &'static str, message: String) -> KernelRefusal {
68 KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
69}
70
71fn input(what: &'static str, message: String) -> KernelRefusal {
72 KernelRefusal::input(KernelStage::Collect, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
73}
74
75fn frag<T>(result: Result<T, String>, what: &'static str) -> Result<T, KernelRefusal> {
76 result.or_refuse(KernelStage::Fragment, what)
77}
78
79#[derive(Clone, Copy, Debug)]
82struct Screw {
83 a: Vec3,
84 e: Vec3,
85 omega: f64,
86 c: f64,
87 c2: f64,
88 t0: f64,
89 t1: f64,
90 frame: Option<Frame>,
94 frozen: Option<f64>,
96 extent: f64,
98}
99
100#[derive(Clone, Copy, Debug)]
103struct Frame {
104 control: [Vec3; 4],
105 up: Vec3,
106 start_tangent: Vec3,
108}
109
110impl Frame {
111 fn point(&self, s: f64) -> Vec3 {
112 let [p0, p1, p2, p3] = self.control;
113 let r = 1.0 - s;
114 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))
115 }
116 fn first(&self, s: f64) -> Vec3 {
117 let [p0, p1, p2, p3] = self.control;
118 let r = 1.0 - s;
119 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))
120 }
121 fn second(&self, s: f64) -> Vec3 {
122 let [p0, p1, p2, p3] = self.control;
123 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))
124 }
125 fn angle(&self, s: f64) -> f64 {
127 let t = self.first(s);
128 self.up.dot(self.start_tangent.cross(t)).atan2(self.start_tangent.dot(t))
129 }
130 fn angle_rate(&self, s: f64) -> f64 {
131 let (d1, d2) = (self.first(s), self.second(s));
132 self.up.dot(d1.cross(d2)) / d1.dot(d1)
133 }
134}
135
136#[derive(Clone, Copy, Debug)]
141pub struct RisingTurn {
142 pub axis_origin: Vec3,
143 pub axis_direction: Vec3,
144 pub rise: [f64; 2],
145 pub from: f64,
146 pub to: f64,
147}
148
149impl Screw {
150 fn from_motion(motion: &SolidMotion) -> Result<Self, KernelRefusal> {
151 let [step] = motion.steps.as_slice() else {
152 return Err(refuse("motion_steps", "only a single screw, turn or slide is swept whole here".into()));
153 };
154 let e = step
155 .axis_direction
156 .normalized()
157 .map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
158 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
159 return Err(input("range", "the motion's range must run forward".into()));
160 }
161 if step.turn == 0.0 && step.advance == 0.0 {
162 return Err(input("still", "the motion does not move".into()));
163 }
164 Ok(Self { a: step.axis_origin, e, omega: step.turn, c: step.advance, c2: 0.0, t0: motion.from, t1: motion.to, frame: None, frozen: None, extent: 0.0 })
165 }
166
167 fn from_rising(motion: &RisingTurn) -> Result<Self, KernelRefusal> {
168 let e = motion.axis_direction.normalized().map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
169 if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
170 return Err(input("range", "the motion's range must run forward".into()));
171 }
172 let rate = |t: f64| motion.rise[0] + 2.0 * motion.rise[1] * t;
174 if rate(motion.from) * rate(motion.to) <= 0.0 {
175 return Err(refuse("rise_turns", "the rise stops or turns back within the motion".into()));
176 }
177 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, frozen: None, extent: 0.0 })
178 }
179
180 fn advance(&self, t: f64) -> f64 {
181 self.c * t + self.c2 * t * t
182 }
183
184 fn rate_bound(&self) -> f64 {
186 (self.c + 2.0 * self.c2 * self.t0).abs().max((self.c + 2.0 * self.c2 * self.t1).abs())
187 }
188
189 fn apply(&self, t: f64, x: Vec3) -> Vec3 {
190 if let Some(frame) = &self.frame {
191 return frame.point(t).add(x.sub(self.a).rotate_about(frame.up, frame.angle(t)));
192 }
193 self.a.add(x.sub(self.a).rotate_about(self.e, self.omega * t)).add(self.e.scale(self.advance(t)))
194 }
195
196 fn invert(&self, t: f64, y: Vec3) -> Vec3 {
197 if let Some(frame) = &self.frame {
198 return self.a.add(y.sub(frame.point(t)).rotate_about(frame.up, -frame.angle(t)));
199 }
200 self.a.add(y.sub(self.e.scale(self.advance(t))).sub(self.a).rotate_about(self.e, -self.omega * t))
201 }
202
203 fn velocity(&self, x: Vec3) -> Vec3 {
207 self.velocity_at(self.frozen.unwrap_or(0.5 * (self.t0 + self.t1)), x)
208 }
209
210 fn velocity_at(&self, t: f64, x: Vec3) -> Vec3 {
211 if let Some(frame) = &self.frame {
212 return frame.start_tangent.scale(frame.first(t).length()).add(frame.up.cross(x.sub(self.a)).scale(frame.angle_rate(t)));
215 }
216 self.e.cross(x.sub(self.a)).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t))
217 }
218
219 fn jet(&self, t: f64, x: Vec3) -> (Vec3, Vec3, Vec3) {
221 let r = x.sub(self.a).rotate_about(self.e, self.omega * t);
222 let axial = self.e.scale(r.dot(self.e));
223 let radial = r.sub(axial);
224 let value = self.a.add(r).add(self.e.scale(self.advance(t)));
225 let first = self.e.cross(radial).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t));
226 let second = radial.scale(-self.omega * self.omega).add(self.e.scale(2.0 * self.c2));
227 (value, first, second)
228 }
229
230 fn move_point(&self, t: f64, p: &Vec4) -> Result<Vec4, KernelRefusal> {
231 let x = frag(p.point(), "control_point")?;
232 Ok(Vec4::from_point(self.apply(t, x), p.w))
233 }
234
235 fn move_curve(&self, t: f64, curve: &NurbsCurve) -> Result<NurbsCurve, KernelRefusal> {
236 let points = curve.control_points.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?;
237 frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "moved_curve")
238 }
239
240 fn move_surface(&self, t: f64, surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
241 let mut rows = Vec::with_capacity(surface.control_points.len());
242 for row in &surface.control_points {
243 rows.push(row.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?);
244 }
245 frag(
246 NurbsSurface::new(surface.degree_u, surface.degree_v, surface.knots_u.clone(), surface.knots_v.clone(), rows),
247 "moved_surface",
248 )
249 }
250}
251
252fn row_times(screw: &Screw) -> Vec<f64> {
255 let turned = (screw.omega * (screw.t1 - screw.t0)).abs();
256 let rows = ((turned / (0.5 * PI)).ceil() as usize).max(1);
257 let at = |k: usize| match k {
261 0 => screw.t0,
262 k if k == rows => screw.t1,
263 k => screw.t0 + (screw.t1 - screw.t0) * (k as f64 - 0.381966011250105) / rows as f64,
264 };
265 (0..=rows).map(at).collect()
266}
267
268fn carry_segments(screw: &Screw, ta: f64, tb: f64) -> usize {
271 let turned = (screw.omega * (tb - ta)).abs();
272 ((turned / TAU * 48.0).ceil() as usize).max(4)
273}
274
275fn carry_knots(segments: usize) -> Vec<f64> {
277 let mut knots = vec![0.0; 6];
278 for k in 1..segments {
279 knots.extend([k as f64 / segments as f64; 3]);
280 }
281 knots.extend([1.0; 6]);
282 knots
283}
284
285fn carry_points(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Vec<Vec3> {
289 let h = (tb - ta) / segments as f64;
290 let at = |k: usize| screw.jet(ta + h * k as f64, x);
291 let piece = |k: usize| {
292 let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
293 [
294 f0,
295 f0.add(d0.scale(h / 5.0)),
296 f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
297 f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
298 f1.sub(d1.scale(h / 5.0)),
299 f1,
300 ]
301 };
302 let mut points = Vec::with_capacity(3 * segments + 3);
303 let first = piece(0);
304 points.extend_from_slice(&first[..3]);
305 for k in 0..segments - 1 {
306 let (here, next) = (piece(k), piece(k + 1));
307 points.push(here[3]);
308 points.push(here[4].scale(2.0).sub(here[3]));
309 points.push(next[2]);
310 }
311 let last = piece(segments - 1);
312 points.extend_from_slice(&last[3..]);
313 points
314}
315
316fn carry_curve(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Result<NurbsCurve, KernelRefusal> {
317 frag(
318 NurbsCurve::new(5, carry_knots(segments), carry_points(screw, x, ta, tb, segments).into_iter().map(|p| Vec4::from_point(p, 1.0)).collect()),
319 "carry_curve",
320 )
321}
322
323struct ToolCurve {
326 curve: NurbsCurve,
327 start: usize,
328 end: usize,
329 degenerate: bool,
330 samples: Option<Vec<Vec3>>,
334}
335
336const LOFT_SAMPLES: usize = 128;
338
339struct Cap {
343 surface: NurbsSurface,
344 front: bool,
345 coedges: Vec<(usize, bool, NurbsCurve)>,
346}
347
348struct Split {
350 points: Vec<Vec3>,
351 curves: Vec<ToolCurve>,
352 caps: Vec<Cap>,
353 scale: f64,
354 shape: Shape,
355}
356
357enum Shape {
359 Planes(Vec<(Vec3, f64)>),
360 Revolved { origin: Vec3, axis: Vec3, meridian: Vec<OutlinePiece> },
361}
362
363impl Split {
364 fn distance(&self, x: Vec3) -> f64 {
366 match &self.shape {
367 Shape::Planes(planes) => planes.iter().map(|(n, d)| n.dot(x) - d).fold(f64::NEG_INFINITY, f64::max),
368 Shape::Revolved { origin, axis, meridian } => {
369 let r = x.sub(*origin);
370 let z = r.dot(*axis);
371 let rho = r.sub(axis.scale(z)).length();
372 outline_signed_distance(meridian, [rho, z])
373 }
374 }
375 }
376
377 fn grazing(&self) -> Result<Vec<(usize, bool)>, KernelRefusal> {
381 let mut uses: HashMap<usize, Vec<(bool, bool)>> = HashMap::new();
382 for cap in &self.caps {
383 for &(curve, forward, _) in &cap.coedges {
384 uses.entry(curve).or_default().push((forward, cap.front));
385 }
386 }
387 let mut grazing = Vec::new();
388 for (curve, list) in uses {
389 if self.curves[curve].degenerate {
390 continue;
391 }
392 match list.as_slice() {
393 [(fa, front_a), (fb, front_b)] => {
394 if fa == fb {
395 return Err(refuse("split_topology", "two caps run one boundary curve the same way".into()));
396 }
397 if front_a != front_b {
398 grazing.push((curve, if *front_a { *fb } else { *fa }));
399 }
400 }
401 _ => return Err(refuse("split_topology", format!("a boundary curve is used {} times", list.len()))),
402 }
403 }
404 grazing.sort_unstable();
405 Ok(grazing)
406 }
407}
408
409fn uv_line(a: [f64; 2], b: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
410 frag(make_line(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0)), "pcurve")
411}
412
413fn plane_patch(origin: Vec3, u: Vec3, v: Vec3, points: &[Vec3]) -> Result<NurbsSurface, KernelRefusal> {
416 let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
417 for p in points {
418 let q = [p.sub(origin).dot(u), p.sub(origin).dot(v)];
419 for k in 0..2 {
420 lo[k] = lo[k].min(q[k]);
421 hi[k] = hi[k].max(q[k]);
422 }
423 }
424 let at = |s: f64, t: f64| Vec4::from_point(origin.add(u.scale(s)).add(v.scale(t)), 1.0);
425 frag(
426 NurbsSurface::new(
427 1,
428 1,
429 vec![lo[0], lo[0], hi[0], hi[0]],
430 vec![lo[1], lo[1], hi[1], hi[1]],
431 vec![vec![at(lo[0], lo[1]), at(lo[0], hi[1])], vec![at(hi[0], lo[1]), at(hi[0], hi[1])]],
432 ),
433 "plane_surface",
434 )
435}
436
437fn plane_pcurve(origin: Vec3, u: Vec3, v: Vec3, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
440 let mut points = Vec::with_capacity(curve.control_points.len());
441 for p in &curve.control_points {
442 let x = frag(p.point(), "control_point")?.sub(origin);
443 points.push(Vec4::from_point(Vec3::new(x.dot(u), x.dot(v), 0.0), p.w));
444 }
445 let pcurve = frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "plane_pcurve")?;
446 if forward { Ok(pcurve) } else { frag(pcurve.reversed(), "plane_pcurve") }
447}
448
449fn split_polyhedron(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<Split, KernelRefusal> {
450 if vertices.len() < 4 || faces.len() < 4 {
451 return Err(input("polyhedron", "a polyhedron needs at least four corners and four faces".into()));
452 }
453 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);
454 let scale = vertices.iter().map(|p| p.sub(centroid).length()).fold(0.0f64, f64::max).max(1e-300);
455 let tolerance = 1e-12 * scale.max(screw.a.sub(centroid).length());
456 let mut loops: Vec<Vec<usize>> = Vec::with_capacity(faces.len());
457 let mut planes = Vec::with_capacity(faces.len());
458 for face in faces {
459 if face.len() < 3 || face.iter().any(|&i| i >= vertices.len()) {
460 return Err(input("face", "a face needs three or more existing corners".into()));
461 }
462 let mut normal = Vec3::new(0.0, 0.0, 0.0);
463 for k in 0..face.len() {
464 let (p, q) = (vertices[face[k]], vertices[face[(k + 1) % face.len()]]);
465 normal = normal.add(p.sub(centroid).cross(q.sub(centroid)));
466 }
467 let mut normal = normal.normalized().map_err(|_| input("face", "a face has no area".into()))?;
468 let mut face = face.clone();
469 if normal.dot(vertices[face[0]].sub(centroid)) < 0.0 {
470 normal = normal.scale(-1.0);
471 face.reverse();
472 }
473 let offset = normal.dot(vertices[face[0]]);
474 for p in vertices {
475 if normal.dot(*p) - offset > 1e-9 * scale {
476 return Err(refuse("not_convex", "the polyhedron is not convex".into()));
477 }
478 }
479 for &i in &face {
480 if (normal.dot(vertices[i]) - offset).abs() > 1e-9 * scale {
481 return Err(input("face", "a face's corners are not in one plane".into()));
482 }
483 }
484 loops.push(face);
485 planes.push((normal, offset));
486 }
487 let g = |face: usize, x: Vec3| -> f64 {
489 let value = screw.velocity(x).dot(planes[face].0);
490 if value.abs() <= tolerance * screw.velocity(x).length().max(1.0) { 0.0 } else { value }
491 };
492 let mut owner: HashMap<(usize, usize), usize> = HashMap::new();
493 for (index, face) in loops.iter().enumerate() {
494 for k in 0..face.len() {
495 owner.insert((face[k], face[(k + 1) % face.len()]), index);
496 }
497 }
498 for &(i, j) in owner.keys() {
499 if !owner.contains_key(&(j, i)) {
500 return Err(input("closed", "the polyhedron's faces do not close".into()));
501 }
502 }
503 let mut points: Vec<Vec3> = vertices.to_vec();
505 let mut edge_splits: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
506 let mut keys: Vec<(usize, usize)> = owner.keys().copied().filter(|&(i, j)| i < j).collect();
507 keys.sort_unstable();
508 for (i, j) in keys {
509 let mut params: Vec<f64> = Vec::new();
510 for f in [owner[&(i, j)], owner[&(j, i)]] {
511 let (gi, gj) = (g(f, vertices[i]), g(f, vertices[j]));
512 if gi * gj < 0.0 {
513 params.push(gi / (gi - gj));
514 }
515 }
516 params.sort_by(|a, b| a.total_cmp(b));
517 params.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
518 let mut list = Vec::new();
519 for s in params {
520 points.push(vertices[i].add(vertices[j].sub(vertices[i]).scale(s)));
521 list.push(points.len() - 1);
522 }
523 edge_splits.insert((i, j), list);
524 }
525 let splits_along = |i: usize, j: usize| -> Vec<usize> {
526 if i < j {
527 edge_splits[&(i, j)].clone()
528 } else {
529 edge_splits[&(j, i)].iter().rev().copied().collect()
530 }
531 };
532 let mut rings: Vec<(Vec<usize>, usize, bool)> = Vec::new();
534 for (index, face) in loops.iter().enumerate() {
535 let mut ring: Vec<usize> = Vec::new();
536 for k in 0..face.len() {
537 ring.push(face[k]);
538 ring.extend(splits_along(face[k], face[(k + 1) % face.len()]));
539 }
540 let values: Vec<f64> = ring.iter().map(|&p| g(index, points[p])).collect();
541 let positive = values.iter().any(|&v| v > 0.0);
542 let negative = values.iter().any(|&v| v < 0.0);
543 if !positive && !negative {
544 return Err(refuse("face_in_own_plane", format!("face {index} moves in its own plane (its sweep is flat)")));
545 }
546 if !(positive && negative) {
547 rings.push((ring, index, positive));
548 continue;
549 }
550 let zeros: Vec<usize> = (0..ring.len()).filter(|&k| values[k] == 0.0).collect();
551 if zeros.len() != 2 {
552 return Err(refuse("grazing_split", format!("face {index} meets its grazing line {} times", zeros.len())));
553 }
554 let (za, zb) = (zeros[0], zeros[1]);
555 if za + 1 == zb || (zb + 1) % ring.len() == za {
556 return Err(refuse("grazing_split", format!("face {index} is split along one of its own edges")));
557 }
558 let first: Vec<usize> = (za..=zb).map(|k| ring[k]).collect();
559 let second: Vec<usize> = (zb..ring.len()).chain(0..=za).map(|k| ring[k]).collect();
560 let first_front = values[za + 1] > 0.0;
561 rings.push((first, index, first_front));
562 rings.push((second, index, !first_front));
563 }
564 if screw.c2 != 0.0 {
567 for (ring, face, front) in &rings {
568 for &p in ring {
569 for t in [screw.t0, screw.t1] {
570 let value = screw.velocity_at(t, points[p]).dot(planes[*face].0);
571 let slack = tolerance * screw.velocity_at(t, points[p]).length().max(1.0);
572 if (*front && value < -slack) || (!*front && value > slack) {
573 return Err(refuse("rising_sides", format!("face {face} changes side as the rise grows")));
574 }
575 }
576 }
577 }
578 }
579 let mut curves: Vec<ToolCurve> = Vec::new();
581 let mut by_pair: HashMap<(usize, usize), usize> = HashMap::new();
582 let mut caps = Vec::with_capacity(rings.len());
583 for (ring, face, front) in rings {
584 let normal = planes[face].0;
585 let origin = points[ring[0]];
586 let u = frag(points[ring[1]].sub(origin).normalized(), "plane_axis")?;
587 let v = normal.cross(u);
588 let corners: Vec<Vec3> = ring.iter().map(|&p| points[p]).collect();
589 let surface = plane_patch(origin, u, v, &corners)?;
590 let mut coedges = Vec::with_capacity(ring.len());
591 for k in 0..ring.len() {
592 let (p, q) = (ring[k], ring[(k + 1) % ring.len()]);
593 let key = (p.min(q), p.max(q));
594 let index = match by_pair.get(&key) {
595 Some(&index) => index,
596 None => {
597 curves.push(ToolCurve {
598 curve: frag(make_line(points[key.0], points[key.1]), "line")?,
599 start: key.0,
600 end: key.1,
601 degenerate: false,
602 samples: None,
603 });
604 by_pair.insert(key, curves.len() - 1);
605 curves.len() - 1
606 }
607 };
608 let forward = p == key.0;
609 coedges.push((index, forward, plane_pcurve(origin, u, v, &curves[index].curve, forward)?));
610 }
611 caps.push(Cap { surface, front, coedges });
612 }
613 Ok(Split { points, curves, caps, scale, shape: Shape::Planes(planes) })
614}
615
616fn harmonic(g: impl Fn(f64) -> f64) -> (f64, f64, f64) {
619 let (g0, g90, g180) = (g(0.0), g(0.5 * PI), g(PI));
620 let c = 0.5 * (g0 + g180);
621 (0.5 * (g0 - g180), g90 - c, c)
622}
623
624fn piece_point(piece: &OutlinePiece, s: f64) -> [f64; 2] {
625 match *piece {
626 OutlinePiece::Line { from, to } => [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s],
627 OutlinePiece::Arc { centre, radius, start, end } => {
628 let angle = start + (end - start) * s;
629 [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
630 }
631 }
632}
633
634fn split_revolved(
635 screw: &Screw,
636 origin: Vec3,
637 axis: Vec3,
638 radial: Vec3,
639 meridian: &[OutlinePiece],
640) -> Result<Split, KernelRefusal> {
641 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
642 let u1 = radial.sub(w.scale(radial.dot(w))).normalized().map_err(|_| input("radial", "the radial direction lies along the axis".into()))?;
643 let u2 = w.cross(u1);
644 if let [OutlinePiece::Arc { centre, radius, start, end }] = meridian {
645 if ((end - start).abs() - TAU).abs() < 1e-12 && centre[0] > *radius {
646 return split_torus(screw, origin, w, u1, u2, *centre, *radius, meridian);
647 }
648 }
649 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()));
650 let scale = meridian
651 .iter()
652 .flat_map(|p| [piece_point(p, 0.0), piece_point(p, 1.0)])
653 .map(|q| q[0].abs().max(q[1].abs()))
654 .fold(0.0f64, f64::max)
655 .max(1e-300);
656 let on_axis = |q: [f64; 2]| q[0].abs() <= 1e-12 * scale;
657 let mut area = 0.0;
659 for piece in meridian {
660 for k in 0..16 {
661 let (p, q) = (piece_point(piece, k as f64 / 16.0), piece_point(piece, (k + 1) as f64 / 16.0));
662 area += p[0] * q[1] - q[0] * p[1];
663 }
664 }
665 let reversed_meridian: Vec<OutlinePiece>;
667 let meridian: &[OutlinePiece] = if area > 0.0 {
668 meridian
669 } else {
670 reversed_meridian = meridian
671 .iter()
672 .rev()
673 .map(|piece| match *piece {
674 OutlinePiece::Line { from, to } => OutlinePiece::Line { from: to, to: from },
675 OutlinePiece::Arc { centre, radius, start, end } => OutlinePiece::Arc { centre, radius, start: end, end: start },
676 })
677 .collect();
678 &reversed_meridian
679 };
680 let ccw = true;
681 let normal = |piece: &OutlinePiece, s: f64| -> [f64; 2] {
683 let (p, q) = (piece_point(piece, (s - 1e-6).max(0.0)), piece_point(piece, (s + 1e-6).min(1.0)));
684 let (dx, dz) = (q[0] - p[0], q[1] - p[1]);
685 let length = dx.hypot(dz);
686 if ccw { [dz / length, -dx / length] } else { [-dz / length, dx / length] }
687 };
688 let g = |piece: &OutlinePiece, s: f64, psi: f64| -> f64 {
689 let m = piece_point(piece, s);
690 let n = normal(piece, s);
691 let x = at(m[0], m[1], psi);
692 let normal = w.scale(n[1]).add(u1.scale(n[0] * psi.cos())).add(u2.scale(n[0] * psi.sin()));
693 screw.velocity(x).dot(normal)
694 };
695 let faces: Vec<usize> = (0..meridian.len())
697 .filter(|&i| !(matches!(meridian[i], OutlinePiece::Line { .. }) && on_axis(piece_point(&meridian[i], 0.0)) && on_axis(piece_point(&meridian[i], 1.0))))
698 .collect();
699 enum Kind {
702 Ruled(Vec<f64>),
705 Moving,
708 Sphere { centre: Vec3, pole_at_start: bool },
711 Ball { centre: Vec3, radius: f64 },
714 TorusZone { psi_a: f64, psi_b: f64, at_end: bool },
719 }
720 let branches = |piece: &OutlinePiece, s: f64| -> Option<(f64, f64)> {
723 let reference = {
724 let (a, b, _) = harmonic(|psi| g(piece, 0.0, psi));
725 b.atan2(a)
726 };
727 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
728 let r = a.hypot(b);
729 if !(c.abs() < r * (1.0 - 1e-9)) {
730 return None;
731 }
732 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
733 let spread = (-c / r).acos();
734 Some((base - spread, base + spread))
735 };
736 let mut kinds: Vec<Kind> = Vec::with_capacity(faces.len());
737 for &i in &faces {
738 let piece = &meridian[i];
739 match *piece {
740 OutlinePiece::Line { .. } => {
741 let roots = |s: f64| -> Vec<f64> {
742 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
743 let r = a.hypot(b);
744 if c.abs() >= r * (1.0 - 1e-12) {
745 return Vec::new();
746 }
747 let base = b.atan2(a);
748 let spread = (-c / r).acos();
749 let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
750 list.sort_by(|x, y| x.total_cmp(y));
751 list
752 };
753 let alive = |s: f64| {
757 let (a, b, c) = harmonic(|psi| g(piece, s, psi));
758 a.hypot(b).max(c.abs()) > 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound())
759 };
760 let readings: Vec<Vec<f64>> = [0.0, 0.5, 1.0].into_iter().filter(|&t| alive(t)).map(roots).collect();
761 if readings.is_empty() {
762 return Err(refuse("revolved_grazing", format!("meridian piece {i} grazes all over")));
763 }
764 let r0 = readings[0].clone();
765 let still = readings.iter().all(|r| r.len() == r0.len() && r.iter().zip(&r0).all(|(x, y)| (x - y).abs() < 1e-10));
766 if still {
767 kinds.push(Kind::Ruled(r0));
768 continue;
769 }
770 let mut previous: Option<(f64, f64)> = None;
771 for k in 0..=64 {
772 let Some(pair) = branches(piece, k as f64 / 64.0) else {
773 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} do not run rim to rim")));
774 };
775 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
776 return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} turn too fast")));
777 }
778 previous = Some(pair);
779 }
780 kinds.push(Kind::Moving);
781 }
782 OutlinePiece::Arc { centre, radius, start, end } => {
783 let (p0, p1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
784 let span = (end - start).abs();
785 let centre_on_axis = on_axis(centre);
786 let pole_at_start = on_axis(p0);
787 let equator = |p: [f64; 2]| (p[1] - centre[1]).abs() <= 1e-12 * scale && (p[0] - radius).abs() <= 1e-12 * scale;
788 if centre_on_axis && (span - PI).abs() < 1e-12 && pole_at_start && on_axis(p1) {
789 kinds.push(Kind::Ball { centre: origin.add(w.scale(centre[1])), radius });
790 continue;
791 }
792 if centre[0] > radius * (1.0 + 1e-9) && end > start && span < PI {
793 let at_rim = |v: f64| [centre[0] + radius * v.cos(), centre[1] + radius * v.sin()];
794 let g_tube = |psi: f64, v: f64| -> f64 {
795 let m = at_rim(v);
796 let x = at(m[0], m[1], psi);
797 let normal = w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
798 screw.velocity(x).dot(normal)
799 };
800 let in_zone = |psi: f64| -> Result<Option<f64>, KernelRefusal> {
801 let (a, b, c) = harmonic(|v| g_tube(psi, v));
802 let r = a.hypot(b);
803 if !(c.abs() < r) {
804 return Ok(None);
805 }
806 let base = b.atan2(a);
807 let spread = (-c / r).acos();
808 let inside: Vec<f64> = [base - spread, base + spread]
809 .into_iter()
810 .map(|v| start + (v - start).rem_euclid(TAU))
811 .filter(|&v| v <= end)
812 .collect();
813 match inside.as_slice() {
814 [] => Ok(None),
815 [v] => Ok(Some(*v)),
816 _ => Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner twice at one angle"))),
817 }
818 };
819 let count = 720;
820 let has: Vec<bool> = (0..count).map(|k| in_zone(TAU * k as f64 / count as f64).map(|r| r.is_some())).collect::<Result<_, _>>()?;
821 let rises: Vec<usize> = (0..count).filter(|&k| has[k] && !has[(k + count - 1) % count]).collect();
822 let falls: Vec<usize> = (0..count).filter(|&k| !has[k] && has[(k + count - 1) % count]).collect();
823 let ([rise], [fall]) = (rises.as_slice(), falls.as_slice()) else {
824 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve does not cross the rounded corner once")));
825 };
826 let estimate = |k: usize| TAU * (k as f64 - 0.5) / count as f64;
828 let (ea, eb) = (estimate(*rise), estimate(*fall));
829 let sample = |k: usize| TAU * (k % count) as f64 / count as f64;
830 let probe_v = in_zone(sample(*rise))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
831 let at_end = (probe_v - end).abs() < (probe_v - start).abs();
832 let rim_v = if at_end { end } else { start };
833 let (a, b, c) = harmonic(|psi| g_tube(psi, rim_v));
834 let r = a.hypot(b);
835 let base = b.atan2(a);
836 let spread = (-c / r).acos();
837 let roots = [base - spread, base + spread];
838 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");
839 let (psi_a, psi_b) = (nearest(ea).rem_euclid(TAU), nearest(eb).rem_euclid(TAU));
840 let psi_b = if psi_b <= psi_a { psi_b + TAU } else { psi_b };
841 let leave_v = in_zone(sample(*fall + count - 1))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
842 if ((leave_v - end).abs() < (leave_v - start).abs()) != at_end {
843 return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner from rim to rim")));
844 }
845 kinds.push(Kind::TorusZone { psi_a, psi_b, at_end });
846 continue;
847 }
848 if !(centre_on_axis && (span - 0.5 * PI).abs() < 1e-12 && (pole_at_start && equator(p1) || on_axis(p1) && equator(p0))) {
849 return Err(refuse("revolved_arc", format!("meridian piece {i}: only a quarter circle from a pole to an equator is swept here")));
850 }
851 kinds.push(Kind::Sphere { centre: origin.add(w.scale(centre[1])), pole_at_start });
852 }
853 }
854 }
855 let count = meridian.len();
857 let junction = |k: usize| piece_point(&meridian[k], 1.0); let face_of = |piece: usize| faces.iter().position(|&f| f == piece);
859 let mut rim_angles: HashMap<usize, Vec<f64>> = HashMap::new();
860 for k in 0..count {
861 if on_axis(junction(k)) {
862 continue;
863 }
864 let mut angles: Vec<f64> = Vec::new();
865 for (piece, s_end) in [(k, 1.0), ((k + 1) % count, 0.0)] {
866 let Some(f) = face_of(piece) else { continue };
867 match &kinds[f] {
868 Kind::Ruled(roots) => angles.extend(roots.iter().copied()),
869 Kind::Ball { .. } => {}
870 Kind::TorusZone { psi_a, psi_b, .. } => angles.extend([psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)]),
871 Kind::Moving => {
872 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
873 angles.extend([lower.rem_euclid(TAU), upper.rem_euclid(TAU)]);
874 }
875 Kind::Sphere { centre, .. } => {
876 let d = screw.velocity(*centre).cross(w);
877 let base = d.dot(u2).atan2(d.dot(u1)).rem_euclid(TAU);
878 angles.extend([base, (base + PI).rem_euclid(TAU)]);
879 }
880 }
881 }
882 angles.sort_by(|a, b| a.total_cmp(b));
883 let mut merged: Vec<f64> = Vec::new();
884 for a in angles {
885 if merged.last().is_some_and(|&b| (a - b).abs() < 1e-9) || merged.first().is_some_and(|&b| (b + TAU - a).abs() < 1e-9) {
886 continue;
887 }
888 merged.push(a);
889 }
890 rim_angles.insert(k, merged);
891 }
892 for (f, &piece) in faces.iter().enumerate() {
894 for (k, s_end) in [((piece + count - 1) % count, 0.0), (piece, 1.0)] {
895 let own: Vec<f64> = match &kinds[f] {
896 Kind::Ruled(roots) => roots.clone(),
897 Kind::Moving => {
898 let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
899 let mut list = vec![lower.rem_euclid(TAU), upper.rem_euclid(TAU)];
900 list.sort_by(|a, b| a.total_cmp(b));
901 list
902 }
903 Kind::TorusZone { psi_a, psi_b, .. } => {
904 let mut list = vec![psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)];
905 list.sort_by(|a, b| a.total_cmp(b));
906 list
907 }
908 Kind::Sphere { .. } | Kind::Ball { .. } => continue,
909 };
910 if let Some(angles) = rim_angles.get(&k) {
911 if own.is_empty() {
912 continue;
913 }
914 if angles.len() != own.len() || angles.iter().zip(&own).any(|(a, b)| (a - b).abs() > 1e-9) {
915 return Err(refuse("revolved_rims", format!("meridian piece {piece} splits at other angles than its rim")));
916 }
917 }
918 }
919 }
920 let mut points: Vec<Vec3> = Vec::new();
921 let mut curves: Vec<ToolCurve> = Vec::new();
922 let mut caps: Vec<Cap> = Vec::new();
923 let mut rim_points: HashMap<(usize, usize), usize> = HashMap::new();
925 let mut rim_arcs: HashMap<(usize, usize), usize> = HashMap::new();
926 let mut rim_keys: Vec<usize> = rim_angles.keys().copied().collect();
927 rim_keys.sort_unstable();
928 for &k in &rim_keys {
929 let q = junction(k);
930 let angles = rim_angles[&k].clone();
931 let angles = if angles.is_empty() { vec![0.0] } else { angles };
932 for (j, &a) in angles.iter().enumerate() {
933 points.push(at(q[0], q[1], a));
934 rim_points.insert((k, j), points.len() - 1);
935 }
936 for j in 0..angles.len() {
937 let a = angles[j];
938 let b = if j + 1 < angles.len() { angles[j + 1] } else { angles[0] + TAU };
939 let base = at(q[0], q[1], a);
940 let surface = frag(make_revolution(origin, w, &frag(make_line(base, base.add(w)), "line")?, b - a), "rim")?;
941 curves.push(ToolCurve {
942 curve: frag(surface.iso_curve_v(0.0), "rim_arc")?,
943 start: rim_points[&(k, j)],
944 end: rim_points[&(k, (j + 1) % angles.len())],
945 degenerate: false,
946 samples: Some((0..=LOFT_SAMPLES).map(|i| at(q[0], q[1], a + (b - a) * i as f64 / LOFT_SAMPLES as f64)).collect()),
947 });
948 rim_arcs.insert((k, j), curves.len() - 1);
949 }
950 rim_angles.insert(k, angles);
951 }
952 let arc_index = |k: usize, angle: f64| -> Result<usize, KernelRefusal> {
953 rim_angles[&k]
954 .iter()
955 .position(|&a| ((a - angle).rem_euclid(TAU)).min((angle - a).rem_euclid(TAU)) < 1e-9)
956 .ok_or_else(|| refuse("revolved_rims", "a patch corner is not on its rim's split".into()))
957 };
958 let sign_at = |piece: &OutlinePiece, psi: f64| g(piece, 0.5, psi) > 0.0;
959 for (f, &piece_index) in faces.iter().enumerate() {
960 let piece = &meridian[piece_index];
961 let below = (piece_index + count - 1) % count; let above = piece_index; let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
964 match &kinds[f] {
965 Kind::Ruled(roots) if roots.is_empty() => {
966 let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
968 if !flat || !(on_axis(m0) || on_axis(m1)) {
969 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a whole side or ring is not swept here")));
970 }
971 let rim = if on_axis(m0) { above } else { below };
972 let front = sign_at(piece, 0.0);
973 if (0..64).any(|k| sign_at(piece, TAU * k as f64 / 64.0) != front) {
974 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a flat end meets its grazing line")));
975 }
976 let n = normal(piece, 0.5);
977 let outward = w.scale(n[1].signum());
978 let centre = origin.add(w.scale(m0[1]));
979 let (u, v) = (u1, outward.cross(u1));
980 let arcs = rim_angles[&rim].len();
981 let mut coedges = Vec::with_capacity(arcs);
982 let mut corners = Vec::new();
983 let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
985 for j in order {
986 let c = rim_arcs[&(rim, j)];
987 let forward = n[1] > 0.0;
988 coedges.push((c, forward, plane_pcurve(centre, u, v, &curves[c].curve, forward)?));
989 corners.push(points[curves[c].start]);
990 }
991 let r = junction(rim)[0];
992 corners.extend([centre.add(u.scale(r)).add(v.scale(r)), centre.sub(u.scale(r)).sub(v.scale(r))]);
993 caps.push(Cap { surface: plane_patch(centre, u, v, &corners)?, front, coedges });
994 }
995 Kind::Ruled(roots) => {
996 if on_axis(m0) || on_axis(m1) {
997 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
998 }
999 let mut generators = Vec::with_capacity(roots.len());
1001 for &root in roots {
1002 let (a, b) = (rim_points[&(below, arc_index(below, root)?)], rim_points[&(above, arc_index(above, root)?)]);
1003 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();
1004 curves.push(ToolCurve { curve: frag(make_line(points[a], points[b]), "generator")?, start: a, end: b, degenerate: false, samples: Some(line_samples) });
1005 generators.push(curves.len() - 1);
1006 }
1007 for j in 0..roots.len() {
1008 let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1009 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1010 let surface = frag(make_revolution(origin, w, &generatrix, beta - alpha), "patch")?;
1011 let front = sign_at(piece, 0.5 * (alpha + beta));
1012 let (lower, upper) = (rim_arcs[&(below, arc_index(below, alpha)?)], rim_arcs[&(above, arc_index(above, alpha)?)]);
1013 let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1014 let coedges = vec![
1015 (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1016 (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1017 (upper, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1018 (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1019 ];
1020 caps.push(Cap { surface, front, coedges });
1021 }
1022 }
1023 Kind::Moving => {
1024 if on_axis(m0) || on_axis(m1) {
1025 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
1026 }
1027 let n = 128;
1028 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1029 let pairs: Vec<(f64, f64)> = params.iter().map(|&t| branches(piece, t).expect("checked")).collect();
1030 let pick = |pair: (f64, f64), which: usize| if which == 0 { pair.0 } else { pair.1 };
1031 let mut branch_curves = [0usize; 2];
1033 for which in 0..2 {
1034 let (psi0, psi1) = (pick(pairs[0], which), pick(pairs[n], which));
1035 let start = rim_points[&(below, arc_index(below, psi0)?)];
1036 let end = rim_points[&(above, arc_index(above, psi1)?)];
1037 let mut samples: Vec<Vec3> = params
1038 .iter()
1039 .zip(&pairs)
1040 .map(|(&t, &pair)| {
1041 let m = piece_point(piece, t);
1042 at(m[0], m[1], pick(pair, which))
1043 })
1044 .collect();
1045 samples[0] = points[start];
1046 samples[n] = points[end];
1047 let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES)
1048 .map(|i| {
1049 let t = i as f64 / LOFT_SAMPLES as f64;
1050 let m = piece_point(piece, t);
1051 at(m[0], m[1], pick(branches(piece, t).expect("checked"), which))
1052 })
1053 .collect();
1054 exact[0] = points[start];
1055 exact[LOFT_SAMPLES] = points[end];
1056 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&samples, 5, ¶ms), "branch")?, start, end, degenerate: false, samples: Some(exact) });
1057 branch_curves[which] = curves.len() - 1;
1058 }
1059 for (lo_which, hi_which, hi_turn) in [(0usize, 1usize, 0.0), (1, 0, TAU)] {
1061 let lo = |i: usize| pick(pairs[i], lo_which);
1062 let hi = |i: usize| pick(pairs[i], hi_which) + hi_turn;
1063 let alpha = (0..=n).map(lo).fold(f64::INFINITY, f64::min) - 1e-3;
1064 let theta = (0..=n).map(hi).fold(f64::NEG_INFINITY, f64::max) + 1e-3 - alpha;
1065 if theta >= TAU {
1066 return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a side patch wraps all the way round")));
1067 }
1068 let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1069 let surface = frag(make_revolution(origin, w, &generatrix, theta), "patch")?;
1070 let spans = (surface.knots_u.len() - 6) / 2 + 1;
1071 let u_of = |psi: f64| crate::circle_angle_to_parameter(spans, theta, psi - alpha);
1072 let angle_near = |x: Vec3, guess: f64| -> f64 {
1073 let r = x.sub(origin);
1074 let a = r.dot(u2).atan2(r.dot(u1));
1075 guess + (a - guess + PI).rem_euclid(TAU) - PI
1076 };
1077 let front = sign_at(piece, 0.5 * (lo(n / 2) + hi(n / 2)));
1078 let rim_pcurve = |arc: usize, v: f64| -> Result<NurbsCurve, KernelRefusal> {
1080 let curve = &curves[arc].curve;
1081 let [d0, d1] = frag(curve.domain(), "domain")?;
1082 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1083 let mut uv = Vec::with_capacity(ts.len());
1084 for &t in &ts {
1085 let x = frag(curve.evaluate(t), "rim")?;
1086 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), v, 0.0));
1087 }
1088 frag(crate::interpolate_curve(&uv, 5, &ts), "rim_pcurve")
1089 };
1090 let branch_pcurve = |which: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> {
1094 let curve = &curves[branch_curves[which]].curve;
1095 let [d0, d1] = frag(curve.domain(), "domain")?;
1096 let (span_rho, span_z) = (m1[0] - m0[0], m1[1] - m0[1]);
1097 let length2 = span_rho * span_rho + span_z * span_z;
1098 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1099 let mut uv = Vec::with_capacity(ts.len());
1100 for &t in &ts {
1101 let x = frag(curve.evaluate(t), "branch")?;
1102 let r = x.sub(origin);
1103 let z = r.dot(w);
1104 let rho = r.sub(w.scale(z)).length();
1105 let along = ((rho - m0[0]) * span_rho + (z - m0[1]) * span_z) / length2;
1106 uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), along, 0.0));
1107 }
1108 frag(crate::interpolate_curve(&uv, 5, &ts), "branch_pcurve")
1109 };
1110 let bottom = rim_arcs[&(below, arc_index(below, lo(0))?)];
1111 let top = rim_arcs[&(above, arc_index(above, lo(n))?)];
1112 let coedges = vec![
1113 (bottom, true, rim_pcurve(bottom, 0.0)?),
1114 (branch_curves[hi_which], true, branch_pcurve(hi_which, hi_turn)?),
1115 (top, false, frag(rim_pcurve(top, 1.0)?.reversed(), "reversed")?),
1116 (branch_curves[lo_which], false, frag(branch_pcurve(lo_which, 0.0)?.reversed(), "reversed")?),
1117 ];
1118 caps.push(Cap { surface, front, coedges });
1119 }
1120 }
1121 Kind::TorusZone { psi_a, psi_b, at_end } => {
1122 let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1123 let (psi_a, psi_b, at_end) = (*psi_a, *psi_b, *at_end);
1124 let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1125 let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1126 let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1127 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()));
1128 let root = |psi: f64| -> Option<f64> {
1129 let (a, b, c) = harmonic(|v| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)));
1130 let r = a.hypot(b);
1131 if !(c.abs() < r) {
1132 return None;
1133 }
1134 let base = b.atan2(a);
1135 let spread = (-c / r).acos();
1136 [base - spread, base + spread].into_iter().map(|v| start + (v - start).rem_euclid(TAU)).find(|&v| v <= end + 1e-12)
1137 };
1138 let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1140 let from = tube_point(psi, start);
1141 let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1142 frag(surface.iso_curve_v(0.0), "tube")
1143 };
1144 let (rim_k, other_k) = if at_end { (above, below) } else { (below, above) };
1145 let ka = rim_points[&(rim_k, arc_index(rim_k, psi_a)?)];
1146 let kb = rim_points[&(rim_k, arc_index(rim_k, psi_b)?)];
1147 let n = 256;
1149 let params: Vec<f64> = (0..=n).map(|j| j as f64 / n as f64).collect();
1150 let psi_of = |t: f64| psi_a + (psi_b - psi_a) * 0.5 * (1.0 - (PI * t).cos());
1154 let mut xyz = Vec::with_capacity(n + 1);
1155 for j in 0..=n {
1156 let psi = psi_of(params[j]);
1157 let v = if j == 0 || j == n { if at_end { end } else { start } } else {
1158 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1159 };
1160 xyz.push(tube_point(psi, v));
1161 }
1162 xyz[0] = points[ka];
1163 xyz[n] = points[kb];
1164 let mut exact = Vec::with_capacity(LOFT_SAMPLES + 1);
1165 for i in 0..=LOFT_SAMPLES {
1166 let psi = psi_of(i as f64 / LOFT_SAMPLES as f64);
1167 let v = if i == 0 || i == LOFT_SAMPLES { if at_end { end } else { start } } else {
1168 root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1169 };
1170 exact.push(tube_point(psi, v));
1171 }
1172 exact[0] = points[ka];
1173 exact[LOFT_SAMPLES] = points[kb];
1174 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "torus_k")?, start: ka, end: kb, degenerate: false, samples: Some(exact) });
1175 let k_curve = curves.len() - 1;
1176 let mut cut = [0usize; 2];
1178 for (slot, psi) in [(0, psi_a), (1, psi_b)] {
1179 let (lo, hi) = (rim_points[&(below, arc_index(below, psi)?)], rim_points[&(above, arc_index(above, psi)?)]);
1180 curves.push(ToolCurve { curve: generatrix(psi)?, start: lo, end: hi, degenerate: false, samples: None });
1181 cut[slot] = curves.len() - 1;
1182 }
1183 let label = |psi: f64, v: f64| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)) > 0.0;
1184 let patch = |alpha: f64, beta: f64| -> Result<(NurbsSurface, usize, usize), KernelRefusal> {
1186 let g = generatrix(alpha)?;
1187 let surface = frag(make_revolution(origin, w, &g, beta - alpha), "torus_patch")?;
1188 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1189 let spans_v = (g.knots.len() - 6) / 2 + 1;
1190 Ok((surface, spans_u, spans_v))
1191 };
1192 let inverse_pcurve = |curve: &NurbsCurve, alpha: f64, beta: f64, spans_u: usize, spans_v: usize, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
1193 let [d0, d1] = frag(curve.domain(), "domain")?;
1194 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1195 let mut uv = Vec::with_capacity(ts.len());
1196 for &t in &ts {
1197 let x = frag(curve.evaluate(t), "point")?;
1198 let r = x.sub(origin);
1199 let mid = 0.5 * (alpha + beta);
1200 let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
1201 let q = x.sub(tube_centre(psi));
1202 let radial = u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1203 let vmid = 0.5 * (start + end);
1204 let v = vmid + (q.dot(w).atan2(q.dot(radial)) - vmid + PI).rem_euclid(TAU) - PI;
1205 uv.push(Vec3::new(
1206 crate::circle_angle_to_parameter(spans_u, beta - alpha, psi - alpha),
1207 crate::circle_angle_to_parameter(spans_v, end - start, v - start),
1208 0.0,
1209 ));
1210 }
1211 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "torus_pcurve")?;
1212 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
1213 };
1214 {
1216 let (surface, _, _) = patch(psi_b, psi_a + TAU)?;
1217 let (bottom, top) = (rim_arcs[&(below, arc_index(below, psi_b)?)], rim_arcs[&(above, arc_index(above, psi_b)?)]);
1218 let coedges = vec![
1219 (bottom, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1220 (cut[0], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1221 (top, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1222 (cut[1], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1223 ];
1224 let mid = psi_b + 0.5 * (psi_a + TAU - psi_b);
1225 caps.push(Cap { surface, front: label(mid, 0.5 * (start + end)), coedges });
1226 }
1227 let (surface, spans_u, spans_v) = patch(psi_a, psi_b)?;
1229 let rim_arc = rim_arcs[&(rim_k, arc_index(rim_k, psi_a)?)];
1230 let other_arc = rim_arcs[&(other_k, arc_index(other_k, psi_a)?)];
1231 let mid = 0.5 * (psi_a + psi_b);
1232 let v_k = root(mid).ok_or_else(|| refuse("torus_zone", "no grazing point mid-way".into()))?;
1233 let k_pc = |forward: bool| inverse_pcurve(&curves[k_curve].curve, psi_a, psi_b, spans_u, spans_v, forward);
1234 let lens_v = if at_end { 0.5 * (v_k + end) } else { 0.5 * (v_k + start) };
1235 let rest_v = if at_end { 0.5 * (v_k + start) } else { 0.5 * (v_k + end) };
1236 if at_end {
1237 let lens = vec![(k_curve, true, k_pc(true)?), (rim_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?)];
1239 caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens });
1240 let rest = vec![
1241 (other_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1242 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1243 (k_curve, false, k_pc(false)?),
1244 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1245 ];
1246 caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest });
1247 } else {
1248 let lens = vec![(rim_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?), (k_curve, false, k_pc(false)?)];
1249 caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens });
1250 let rest = vec![
1251 (k_curve, true, k_pc(true)?),
1252 (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1253 (other_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1254 (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1255 ];
1256 caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest });
1257 }
1258 }
1259 Kind::Ball { centre, radius } => {
1260 let m = frag(screw.velocity(*centre).normalized(), "ball_still")?;
1261 let d = frag(w.cross(m).normalized().or_else(|_| u1.cross(m).normalized()), "ball_axis")?;
1262 let (e1, e2) = (centre.add(d.scale(*radius)), centre.sub(d.scale(*radius)));
1263 points.push(e1);
1264 points.push(e2);
1265 let (i1, i2) = (points.len() - 2, points.len() - 1);
1266 let half = frag(make_revolution(*centre, m, &frag(make_line(e1, e1.add(m)), "line")?, PI), "great")?;
1267 let k_a = frag(half.iso_curve_v(0.0), "great")?;
1268 curves.push(ToolCurve { curve: k_a.clone(), start: i1, end: i2, degenerate: false, samples: None });
1269 let k_a_index = curves.len() - 1;
1270 let [a0, a1] = frag(k_a.domain(), "domain")?;
1271 let h = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?.sub(*centre);
1272 let mut k_b_index: Option<usize> = None;
1273 for front in [true, false] {
1274 let toward = if front { m } else { m.scale(-1.0) };
1275 let da = if d.cross(h).dot(toward) > 0.0 { d } else { d.scale(-1.0) };
1276 let mut generatrix = k_a.clone();
1277 let mut surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1278 let outward = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1279 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1280 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1281 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1282 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1283 };
1284 let mut forward = true;
1285 if !outward(&surface)? {
1286 generatrix = frag(generatrix.reversed(), "reversed")?;
1287 surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1288 forward = false;
1289 }
1290 let (start, end) = if forward { (i1, i2) } else { (i2, i1) };
1291 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1292 let k_b = match k_b_index {
1293 Some(index) => index,
1294 None => {
1295 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1296 k_b_index = Some(curves.len() - 1);
1297 curves.len() - 1
1298 }
1299 };
1300 let k_b_forward = curves[k_b].start == start;
1301 let k_b_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1302 let poles = [start, end].map(|p| {
1303 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1304 curves.len() - 1
1305 });
1306 let coedges = vec![
1307 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1308 (k_b, k_b_forward, k_b_pcurve),
1309 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1310 (k_a_index, !forward, uv_line([0.0, g1], [0.0, g0])?),
1311 ];
1312 caps.push(Cap { surface, front, coedges });
1313 }
1314 }
1315 Kind::Sphere { centre, pole_at_start } => {
1316 let rim = if *pole_at_start { above } else { below };
1317 let angles = rim_angles[&rim].clone();
1318 if angles.len() != 2 {
1319 return Err(refuse("revolved_rims", "a hemisphere's rim is not split in two".into()));
1320 }
1321 let pole_side = w.scale(if *pole_at_start { m0[1] - m1[1] } else { m1[1] - m0[1] }.signum());
1322 let m = screw.velocity(*centre);
1323 let e1 = points[rim_points[&(rim, 0)]];
1324 let d = frag(e1.sub(*centre).normalized(), "lune_axis")?;
1325 let mut great: Option<usize> = None;
1326 for j in 0..2 {
1327 let arc = rim_arcs[&(rim, j)];
1328 let [a0, a1] = frag(curves[arc].curve.domain(), "domain")?;
1329 let middle = frag(curves[arc].curve.evaluate(0.5 * (a0 + a1)), "middle")?;
1330 let hv = middle.sub(*centre);
1331 let da = if d.cross(hv).dot(pole_side) > 0.0 { d } else { d.scale(-1.0) };
1332 let theta = (-m.dot(hv)).atan2(m.dot(da.cross(hv))).rem_euclid(TAU);
1334 let theta = if theta > PI { theta - PI } else { theta };
1335 if !(theta > 1e-9 && theta < PI - 1e-9) {
1336 return Err(refuse("revolved_lune", "the great circle meets the rim's plane edge-on".into()));
1337 }
1338 let mut generatrix = curves[arc].curve.clone();
1339 let mut surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1340 let probe = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1342 let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1343 let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1344 let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1345 Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1346 };
1347 let mut forward = true;
1348 if !probe(&surface)? {
1349 generatrix = frag(generatrix.reversed(), "reversed")?;
1350 surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1351 forward = false;
1352 }
1353 let (start, end) = if forward { (curves[arc].start, curves[arc].end) } else { (curves[arc].end, curves[arc].start) };
1354 let [g0, g1] = frag(generatrix.domain(), "domain")?;
1355 let k_curve = match great {
1358 Some(index) => index,
1359 None => {
1360 curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1361 great = Some(curves.len() - 1);
1362 curves.len() - 1
1363 }
1364 };
1365 let k_forward = curves[k_curve].start == start;
1366 let poles = [start, end].map(|p| {
1367 curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1368 curves.len() - 1
1369 });
1370 let k_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1371 let coedges = vec![
1372 (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1373 (k_curve, k_forward, k_pcurve),
1374 (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1375 (arc, !forward, uv_line([0.0, g1], [0.0, g0])?),
1376 ];
1377 let mid_psi = if j == 0 { 0.5 * (angles[0] + angles[1]) } else { 0.5 * (angles[1] + angles[0] + TAU) };
1378 let front = m.dot(at(1.0, 0.0, mid_psi).sub(origin)) > 0.0;
1379 caps.push(Cap { surface, front, coedges });
1380 }
1381 }
1382 }
1383 }
1384 Ok(Split {
1385 points,
1386 curves,
1387 caps,
1388 scale,
1389 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1390 })
1391}
1392
1393#[allow(clippy::too_many_arguments)]
1400fn split_torus(
1401 screw: &Screw,
1402 origin: Vec3,
1403 w: Vec3,
1404 u1: Vec3,
1405 u2: Vec3,
1406 centre: [f64; 2],
1407 radius: f64,
1408 meridian: &[OutlinePiece],
1409) -> Result<Split, KernelRefusal> {
1410 let (rho_c, z_c) = (centre[0], centre[1]);
1411 let radial = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1412 let tube_centre = |psi: f64| origin.add(w.scale(z_c)).add(radial(psi).scale(rho_c));
1413 let point = |psi: f64, v: f64| tube_centre(psi).add(radial(psi).scale(radius * v.cos())).add(w.scale(radius * v.sin()));
1414 let g = |psi: f64, v: f64| screw.velocity(point(psi, v)).dot(radial(psi).scale(v.cos()).add(w.scale(v.sin())));
1415 let reference = {
1416 let (a, b, _) = harmonic(|v| g(0.0, v));
1417 b.atan2(a)
1418 };
1419 let branches = |psi: f64| -> Option<(f64, f64)> {
1421 let (a, b, c) = harmonic(|v| g(psi, v));
1422 let r = a.hypot(b);
1423 if !(c.abs() < r * (1.0 - 1e-9)) {
1424 return None;
1425 }
1426 let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
1427 let spread = (-c / r).acos();
1428 Some((base - spread, base + spread))
1429 };
1430 let n = 128;
1431 let halves = [(0.0, PI), (PI, TAU)];
1432 let mut samples: Vec<Vec<(f64, f64, f64)>> = Vec::new(); for &(a, b) in &halves {
1434 let mut list = Vec::with_capacity(n + 1);
1435 let mut previous: Option<(f64, f64)> = None;
1436 for i in 0..=n {
1437 let psi = a + (b - a) * i as f64 / n as f64;
1438 let Some(pair) = branches(psi) else {
1439 return Err(refuse("ring_grazing", "the ring's grazing curves do not run all the way round it".into()));
1440 };
1441 if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
1442 return Err(refuse("ring_grazing", "the ring's grazing curves turn too fast".into()));
1443 }
1444 previous = Some(pair);
1445 list.push((psi, pair.0, pair.1));
1446 }
1447 samples.push(list);
1448 }
1449 let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1450 let mut points: Vec<Vec3> = Vec::new();
1451 let mut curves: Vec<ToolCurve> = Vec::new();
1452 let mut corner: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1455 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)) };
1456 for (branch, v) in [(0, lower), (1, upper)] {
1457 points.push(point(psi, v));
1458 corner.insert((seam, branch), points.len() - 1);
1459 }
1460 }
1461 let seam_of_end = |half: usize, at_end: bool| -> usize { if at_end { 1 - half } else { half } };
1462 let mut branch_curve: HashMap<(usize, usize), usize> = HashMap::new(); for half in 0..2 {
1465 for branch in 0..2 {
1466 let mut xyz: Vec<Vec3> = samples[half].iter().map(|&(psi, lo, hi)| point(psi, if branch == 0 { lo } else { hi })).collect();
1467 let (start, end) = (corner[&(seam_of_end(half, false), branch)], corner[&(seam_of_end(half, true), branch)]);
1468 xyz[0] = points[start];
1469 xyz[n] = points[end];
1470 curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, ¶ms), "ring_branch")?, start, end, degenerate: false, samples: None });
1471 branch_curve.insert((half, branch), curves.len() - 1);
1472 }
1473 }
1474 let tube_arc = |psi: f64, v0: f64, v1: f64| -> Result<NurbsCurve, KernelRefusal> {
1477 let start = point(psi, v0);
1478 let axis = radial(psi).cross(w);
1479 let surface = frag(make_revolution(tube_centre(psi), axis, &frag(make_line(start, start.add(axis)), "line")?, v1 - v0), "tube_arc")?;
1480 frag(surface.iso_curve_v(0.0), "tube_arc")
1481 };
1482 let mut seam_arc: HashMap<(usize, usize), usize> = HashMap::new(); for seam in 0..2 {
1484 let psi = if seam == 0 { 0.0 } else { PI };
1485 let (lower, upper) = (samples[seam][0].1, samples[seam][0].2);
1486 let (lo_point, hi_point) = (corner[&(seam, 0)], corner[&(seam, 1)]);
1487 curves.push(ToolCurve { curve: tube_arc(psi, lower, upper)?, start: lo_point, end: hi_point, degenerate: false, samples: None });
1488 seam_arc.insert((seam, 0), curves.len() - 1);
1489 curves.push(ToolCurve { curve: tube_arc(psi, upper, lower + TAU)?, start: hi_point, end: lo_point, degenerate: false, samples: None });
1490 seam_arc.insert((seam, 1), curves.len() - 1);
1491 }
1492 let mut caps = Vec::new();
1493 for half in 0..2 {
1494 let (psi_a, psi_b) = halves[half];
1495 for band in 0..2 {
1496 let (lo_branch, hi_branch, hi_turn) = if band == 0 { (0usize, 1usize, 0.0) } else { (1, 0, TAU) };
1498 let pick = |s: &(f64, f64, f64), b: usize| if b == 0 { s.1 } else { s.2 };
1499 let v_low = samples[half].iter().map(|s| pick(s, lo_branch)).fold(f64::INFINITY, f64::min) - 1e-3;
1500 let v_high = samples[half].iter().map(|s| pick(s, hi_branch) + hi_turn).fold(f64::NEG_INFINITY, f64::max) + 1e-3;
1501 let theta_v = v_high - v_low;
1502 if theta_v >= TAU {
1503 return Err(refuse("ring_grazing", "a band of the ring wraps all the way round its tube".into()));
1504 }
1505 let generatrix = tube_arc(psi_a, v_low, v_high)?;
1506 let surface = frag(make_revolution(origin, w, &generatrix, psi_b - psi_a), "ring_band")?;
1507 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1508 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
1509 let uv_of = |psi: f64, v: f64| {
1510 Vec3::new(
1511 crate::circle_angle_to_parameter(spans_u, psi_b - psi_a, psi - psi_a),
1512 crate::circle_angle_to_parameter(spans_v, theta_v, v - v_low),
1513 0.0,
1514 )
1515 };
1516 let angles_of = |x: Vec3| -> (f64, f64) {
1518 let r = x.sub(origin);
1519 let psi = r.dot(u2).atan2(r.dot(u1));
1520 let psi = 0.5 * (psi_a + psi_b) + (psi - 0.5 * (psi_a + psi_b) + PI).rem_euclid(TAU) - PI;
1521 let q = x.sub(tube_centre(psi));
1522 let v = q.dot(w).atan2(q.dot(radial(psi)));
1523 let mid = 0.5 * (v_low + v_high);
1524 (psi, mid + (v - mid + PI).rem_euclid(TAU) - PI)
1525 };
1526 let inverse_pcurve = |curve: &NurbsCurve| -> Result<NurbsCurve, KernelRefusal> {
1527 let [d0, d1] = frag(curve.domain(), "domain")?;
1528 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1529 let mut uv = Vec::with_capacity(ts.len());
1530 for &t in &ts {
1531 let (psi, v) = angles_of(frag(curve.evaluate(t), "point")?);
1532 uv.push(uv_of(psi, v));
1533 }
1534 frag(crate::interpolate_curve(&uv, 5, &ts), "ring_pcurve")
1535 };
1536 let branch_pcurve = |b: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> { inverse_pcurve(&curves[branch_curve[&(half, b)]].curve) };
1537 let lower_edge = branch_curve[&(half, lo_branch)];
1538 let upper_edge = branch_curve[&(half, hi_branch)];
1539 let right = seam_arc[&(seam_of_end(half, true), band)];
1540 let left = seam_arc[&(seam_of_end(half, false), band)];
1541 let coedges = vec![
1542 (lower_edge, true, branch_pcurve(lo_branch, 0.0)?),
1543 (right, true, inverse_pcurve(&curves[right].curve)?),
1544 (upper_edge, false, frag(branch_pcurve(hi_branch, hi_turn)?.reversed(), "reversed")?),
1545 (left, false, frag(inverse_pcurve(&curves[left].curve)?.reversed(), "reversed")?),
1546 ];
1547 let mid = &samples[half][n / 2];
1548 let front = g(mid.0, 0.5 * (pick(mid, lo_branch) + pick(mid, hi_branch) + hi_turn)) > 0.0;
1549 caps.push(Cap { surface, front, coedges });
1550 }
1551 }
1552 Ok(Split {
1553 points,
1554 curves,
1555 caps,
1556 scale: rho_c + radius,
1557 shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1558 })
1559}
1560
1561struct Shell {
1563 vertices: Vec<VertexRecord>,
1564 edges: Vec<EdgeRecord>,
1565 faces: Vec<FaceRecord>,
1566 next: u64,
1567}
1568
1569impl Shell {
1570 fn new() -> Self {
1571 Self { vertices: Vec::new(), edges: Vec::new(), faces: Vec::new(), next: 1 }
1572 }
1573
1574 fn id(&mut self) -> u64 {
1575 self.next += 1;
1576 self.next
1577 }
1578
1579 fn vertex(&mut self, point: Vec3) -> u64 {
1580 let id = self.id();
1581 self.vertices.push(VertexRecord { id, point });
1582 id
1583 }
1584
1585 fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
1586 let domain = frag(curve.domain(), "edge_domain")?;
1587 let id = self.id();
1588 self.edges.push(EdgeRecord { id, curve, t0: domain[0], t1: domain[1], start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
1589 Ok(id)
1590 }
1591
1592 fn face(&mut self, surface: NurbsSurface, coedges: Vec<(u64, bool, NurbsCurve)>) {
1595 let mut records = Vec::with_capacity(coedges.len());
1596 for (edge, forward, pcurve) in coedges {
1597 let id = self.id();
1598 records.push(CoedgeRecord { id, edge_id: edge, forward, pcurve });
1599 }
1600 let loop_id = self.id();
1601 let id = self.id();
1602 self.faces.push(FaceRecord { id, surface, same_sense: true, loops: vec![LoopRecord { id: loop_id, coedges: records }], name: None });
1603 }
1604
1605 fn solid(mut self) -> BrepSolid {
1606 let shell = self.id();
1607 let id = self.id();
1608 let edges = self.edges.iter().filter(|e| !e.degenerate).count() as i64;
1611 let euler = self.vertices.len() as i64 - edges + self.faces.len() as i64;
1612 let genus = (2 - euler) / 2;
1613 BrepSolid {
1614 mass_properties_cache: Default::default(),
1615 id,
1616 vertices: self.vertices,
1617 edges: self.edges,
1618 shells: vec![ShellRecord { id: shell, faces: self.faces }],
1619 genus,
1620 kind: Default::default(),
1621 cells: Default::default(),
1622 }
1623 }
1624}
1625
1626pub fn sweep_solid(tool: &SolidTool, motion: &SolidMotion) -> Result<BrepSolid, KernelRefusal> {
1629 sweep_with(&Screw::from_motion(motion)?, tool)
1630}
1631
1632fn sweep_with(screw: &Screw, tool: &SolidTool) -> Result<BrepSolid, KernelRefusal> {
1633 let screw = *screw;
1634 if let SolidTool::Revolved { axis_origin, axis_direction, meridian, .. } = tool {
1635 if let Some(body) = ball_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
1636 return Ok(body);
1637 }
1638 }
1639 let split = match tool {
1640 SolidTool::Polyhedron { vertices, faces } => split_polyhedron(&screw, vertices, faces)?,
1641 SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } => {
1642 split_revolved(&screw, *axis_origin, *axis_direction, *radial, meridian)?
1643 }
1644 };
1645 let grazing = split.grazing()?;
1646 if grazing.is_empty() {
1647 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
1648 }
1649 let times = row_times(&screw);
1650 let last = times.len() - 1;
1651 let mut shell = Shell::new();
1652 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
1655 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
1656 let mut at_time = |shell: &mut Shell, j: usize, p: usize| -> u64 {
1657 *vertex_at.entry((j, p)).or_insert_with(|| shell.vertex(screw.apply(times[j], split.points[p])))
1658 };
1659 let mut time_edge = |shell: &mut Shell, j: usize, c: usize, vertex: &mut dyn FnMut(&mut Shell, usize, usize) -> u64| -> Result<u64, KernelRefusal> {
1660 if let Some(&edge) = edge_at.get(&(j, c)) {
1661 return Ok(edge);
1662 }
1663 let curve = &split.curves[c];
1664 let (a, b) = (vertex(shell, j, curve.start), vertex(shell, j, curve.end));
1665 let edge = shell.edge(screw.move_curve(times[j], &curve.curve)?, a, b, curve.degenerate)?;
1666 edge_at.insert((j, c), edge);
1667 Ok(edge)
1668 };
1669 for cap in &split.caps {
1671 let j = if cap.front { last } else { 0 };
1672 let surface = screw.move_surface(times[j], &cap.surface)?;
1673 let mut coedges = Vec::with_capacity(cap.coedges.len());
1674 for (c, forward, pcurve) in &cap.coedges {
1675 let edge = time_edge(&mut shell, j, *c, &mut at_time)?;
1676 coedges.push((edge, *forward, pcurve.clone()));
1677 }
1678 shell.face(surface, coedges);
1679 }
1680 for row in 0..last {
1685 let (ta, tb) = (times[row], times[row + 1]);
1686 let segments = carry_segments(&screw, ta, tb);
1687 let knots_v = carry_knots(segments);
1688 let mut orbits: HashMap<usize, u64> = HashMap::new();
1689 for &(c, _) in &grazing {
1690 for r in [split.curves[c].start, split.curves[c].end] {
1691 if orbits.contains_key(&r) {
1692 continue;
1693 }
1694 let (v0, v1) = (at_time(&mut shell, row, r), at_time(&mut shell, row + 1, r));
1695 let edge = shell.edge(carry_curve(&screw, split.points[r], ta, tb, segments)?, v0, v1, false)?;
1696 orbits.insert(r, edge);
1697 }
1698 }
1699 for &(c, back_forward) in &grazing {
1700 let tool_curve = &split.curves[c];
1701 let along = if back_forward { frag(tool_curve.curve.reversed(), "reversed")? } else { tool_curve.curve.clone() };
1702 let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
1703 let mut rows = Vec::with_capacity(along.control_points.len());
1704 for p in &along.control_points {
1705 let x = frag(p.point(), "control_point")?;
1706 rows.push(carry_points(&screw, x, ta, tb, segments).into_iter().map(|q| Vec4::from_point(q, p.w)).collect::<Vec<_>>());
1707 }
1708 let surface = frag(NurbsSurface::new(along.degree, 5, along.knots.clone(), knots_v.clone(), rows), "wall")?;
1709 let [u0, u1] = frag(along.domain(), "domain")?;
1710 let bottom = time_edge(&mut shell, row, c, &mut at_time)?;
1711 let top = time_edge(&mut shell, row + 1, c, &mut at_time)?;
1712 let coedges = vec![
1713 (bottom, !back_forward, uv_line([u0, 0.0], [u1, 0.0])?),
1714 (orbits[&to], true, uv_line([u1, 0.0], [u1, 1.0])?),
1715 (top, back_forward, uv_line([u1, 1.0], [u0, 1.0])?),
1716 (orbits[&from], false, uv_line([u0, 1.0], [u0, 0.0])?),
1717 ];
1718 shell.face(surface, coedges);
1719 }
1720 }
1721 let body = shell.solid();
1722 let issues = body.validate();
1723 if !issues.is_empty() {
1724 return Err(refuse("assembly", format!("the assembled sweep is not valid: {:?}", issues.first())));
1725 }
1726 check_single_pass(&screw, &split, &body)?;
1727 Ok(body)
1728}
1729
1730pub fn sweep_solid_clipped(tool: &SolidTool, motion: &SolidMotion, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
1741 let screw = Screw::from_motion(motion)?;
1742 if let (SolidTool::Polyhedron { vertices, faces }, Some(wedge)) = (tool, clip.angle) {
1743 let axis = clip.axis_direction.normalized().map_err(|_| input("clip_axis", "the clip's axis has no direction".into()))?;
1744 let same_axis = screw.c == 0.0 && screw.c2 == 0.0
1745 && screw.e.cross(axis).length() < 1e-12
1746 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
1747 if same_axis {
1748 return turn_shadow(&screw, vertices, faces, clip, axis, wedge);
1749 }
1750 }
1751 if let SolidTool::Polyhedron { vertices, faces } = tool {
1752 if let Some((vertices, faces)) = trim_to_clip(&screw, vertices, faces, clip)? {
1753 return sweep_solid(&SolidTool::Polyhedron { vertices, faces }, motion);
1754 }
1755 }
1756 sweep_solid(tool, motion)
1757}
1758
1759pub fn sweep_solid_rising(tool: &SolidTool, motion: &RisingTurn, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
1763 let screw = Screw::from_rising(motion)?;
1764 let SolidTool::Polyhedron { vertices, faces } = tool else {
1765 return Err(refuse("rising_tool", "a growing rise sweeps a polyhedron only".into()));
1766 };
1767 let (vertices, faces) = trim_to_clip(&screw, vertices, faces, clip)?.unwrap_or_else(|| (vertices.clone(), faces.clone()));
1768 sweep_with(&screw, &SolidTool::Polyhedron { vertices, faces })
1769}
1770
1771pub fn sweep_solid_parts(tool: &SolidTool, motion: &SolidMotion) -> Result<Vec<BrepSolid>, KernelRefusal> {
1779 let folded = match sweep_solid(tool, motion) {
1780 Ok(body) => return Ok(vec![body]),
1781 Err(error) if error.message.contains("passes over a point twice") => error,
1782 Err(error) => return Err(error),
1783 };
1784 let SolidTool::Polyhedron { vertices, faces } = tool else { return Err(folded) };
1785 let screw = Screw::from_motion(motion)?;
1786 if screw.omega == 0.0 {
1787 return Err(folded);
1788 }
1789 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);
1790 let mut planes: Vec<(Vec3, f64)> = Vec::new();
1791 for face in faces {
1792 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
1793 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
1794 if n.dot(p.sub(centroid)) < 0.0 {
1795 n = n.scale(-1.0);
1796 }
1797 planes.push((n, n.dot(p)));
1798 }
1799 for (n, d) in planes.clone() {
1803 if n.dot(screw.e).abs() > 1e-12 {
1804 continue;
1805 }
1806 let foot = screw.a.add(n.scale(d - n.dot(screw.a)));
1807 let radial = foot.sub(screw.a).sub(screw.e.scale(foot.sub(screw.a).dot(screw.e)));
1808 if radial.dot(n) <= 0.0 {
1809 continue; }
1811 let cut = match screw.e.cross(radial).normalized() {
1812 Ok(m) => m,
1813 Err(_) => continue,
1814 };
1815 let offset = cut.dot(screw.a);
1816 let mut parts = Vec::with_capacity(2);
1817 for sense in [1.0, -1.0] {
1818 let mut half = planes.clone();
1819 half.push((cut.scale(sense), offset * sense));
1820 let (corners, loops) = polyhedron_of_planes(&half)?;
1821 if corners.len() < 4 || loops.len() < 4 {
1822 parts.clear();
1823 break;
1824 }
1825 match sweep_solid(&SolidTool::Polyhedron { vertices: corners, faces: loops }, motion) {
1826 Ok(body) => parts.push(body),
1827 Err(_) => {
1828 parts.clear();
1829 break;
1830 }
1831 }
1832 }
1833 if parts.len() == 2 {
1834 return Ok(parts);
1835 }
1836 }
1837 Err(folded)
1838}
1839
1840fn trim_to_clip(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip) -> Result<Option<(Vec<Vec3>, Vec<Vec<usize>>)>, KernelRefusal> {
1849 let Ok(axis) = clip.axis_direction.normalized() else { return Ok(None) };
1850 let same_axis = screw.omega != 0.0
1851 && screw.e.cross(axis).length() < 1e-12
1852 && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
1853 if !same_axis || !clip.radius[1].is_finite() {
1854 return Ok(None);
1855 }
1856 let ez = axis;
1857 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))?;
1858 let ey = ez.cross(ex);
1859 let flat = |p: Vec3| {
1860 let r = p.sub(clip.axis_origin);
1861 [r.dot(ex), r.dot(ey)]
1862 };
1863 let radius = clip.radius[1] * 1.02;
1864 let shadow: Vec<[f64; 2]> = vertices.iter().map(|&p| flat(p)).collect();
1865 if shadow.iter().all(|q| q[0].hypot(q[1]) <= radius) {
1866 return Ok(None);
1867 }
1868 let mut points = shadow.clone();
1870 points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
1871 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]);
1872 let mut hull: Vec<[f64; 2]> = Vec::new();
1873 for pass in 0..2 {
1874 let start = hull.len();
1875 let ordered: Vec<[f64; 2]> = if pass == 0 { points.clone() } else { points.iter().rev().copied().collect() };
1876 for p in ordered {
1877 while hull.len() >= start + 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
1878 hull.pop();
1879 }
1880 hull.push(p);
1881 }
1882 hull.pop();
1883 }
1884 let m = hull.len();
1886 let inside = |q: [f64; 2]| q[0].hypot(q[1]) <= radius;
1887 let mut nearest = f64::INFINITY;
1889 let mut wraps = true;
1890 for k in 0..m {
1891 let (a, b) = (hull[k], hull[(k + 1) % m]);
1892 let d = [b[0] - a[0], b[1] - a[1]];
1893 let t = (-(a[0] * d[0] + a[1] * d[1]) / (d[0] * d[0] + d[1] * d[1])).clamp(0.0, 1.0);
1894 nearest = nearest.min((a[0] + d[0] * t).hypot(a[1] + d[1] * t));
1895 if cross(a, b, [0.0, 0.0]) < 0.0 {
1896 wraps = false;
1897 }
1898 }
1899 if wraps || nearest >= radius || (0..m).all(|k| inside(hull[k])) {
1900 return Ok(None);
1901 }
1902 let mut leave: Vec<f64> = Vec::new();
1903 let mut enter: Vec<f64> = Vec::new();
1904 for k in 0..m {
1905 let (a, b) = (hull[k], hull[(k + 1) % m]);
1906 let d = [b[0] - a[0], b[1] - a[1]];
1907 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);
1908 let disc = qb * qb - 4.0 * qa * qc;
1909 if disc <= 0.0 {
1910 continue;
1911 }
1912 for t in [(-qb - disc.sqrt()) / (2.0 * qa), (-qb + disc.sqrt()) / (2.0 * qa)] {
1913 if t > 0.0 && t < 1.0 {
1914 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
1915 let outward = 2.0 * (q[0] * d[0] + q[1] * d[1]) > 0.0;
1916 (if outward { &mut leave } else { &mut enter }).push(q[1].atan2(q[0]));
1917 }
1918 }
1919 }
1920 let ([a], [b]) = (leave.as_slice(), enter.as_slice()) else {
1921 return Err(refuse("clip_trim", "the tool's shadow leaves the clip's disc more than once".into()));
1922 };
1923 let width = (b - a).rem_euclid(TAU);
1924 if width >= 0.9 * PI {
1925 return Err(refuse("clip_trim", "the tool's shadow spans too much of the clip's circle to be trimmed".into()));
1926 }
1927 let mut planes: Vec<(Vec3, f64)> = Vec::new();
1929 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);
1930 for face in faces {
1931 let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
1932 let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
1933 if n.dot(p.sub(centroid)) < 0.0 {
1934 n = n.scale(-1.0);
1935 }
1936 planes.push((n, n.dot(p)));
1937 }
1938 for angle in [*a, *b] {
1939 let n = ex.scale(angle.cos()).add(ey.scale(angle.sin()));
1940 planes.push((n, n.dot(clip.axis_origin) + radius));
1941 }
1942 Ok(Some(polyhedron_of_planes(&planes)?))
1943}
1944
1945fn polyhedron_of_planes(planes: &[(Vec3, f64)]) -> Result<(Vec<Vec3>, Vec<Vec<usize>>), KernelRefusal> {
1948 let k = planes.len();
1949 let mut corners: Vec<Vec3> = Vec::new();
1950 let scale = planes.iter().map(|p| p.1.abs()).fold(1.0f64, f64::max);
1951 for i in 0..k {
1952 for j in i + 1..k {
1953 for l in j + 1..k {
1954 let (a, b, c) = (planes[i].0, planes[j].0, planes[l].0);
1955 let det = a.dot(b.cross(c));
1956 if det.abs() < 1e-12 {
1957 continue;
1958 }
1959 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);
1960 if planes.iter().all(|(n, d)| n.dot(p) - d <= 1e-9 * scale) && !corners.iter().any(|q| q.sub(p).length() <= 1e-9 * scale) {
1961 corners.push(p);
1962 }
1963 }
1964 }
1965 }
1966 let mut faces = Vec::new();
1967 for (n, d) in planes {
1968 let on: Vec<usize> = (0..corners.len()).filter(|&c| (n.dot(corners[c]) - d).abs() <= 1e-9 * scale).collect();
1969 if on.len() < 3 {
1970 continue;
1971 }
1972 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);
1973 let u = frag(corners[on[0]].sub(middle).normalized(), "face_axis")?;
1974 let v = n.cross(u);
1975 let mut ordered = on.clone();
1976 ordered.sort_by(|&p, &q| {
1977 let (rp, rq) = (corners[p].sub(middle), corners[q].sub(middle));
1978 rp.dot(v).atan2(rp.dot(u)).total_cmp(&rq.dot(v).atan2(rq.dot(u)))
1979 });
1980 faces.push(ordered);
1981 }
1982 Ok((corners, faces))
1983}
1984
1985fn turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip, axis: Vec3, wedge: [f64; 2]) -> Result<BrepSolid, KernelRefusal> {
1986 let ez = axis;
1987 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()))?;
1988 let ey = ez.cross(ex);
1989 let rate = screw.omega * screw.e.dot(ez).signum();
1991 let local = |p: Vec3| {
1992 let r = p.sub(clip.axis_origin);
1993 [r.dot(ex), r.dot(ey), r.dot(ez)]
1994 };
1995 let corners: Vec<[f64; 3]> = vertices.iter().map(|&p| local(p)).collect();
1996 let mid = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
1999 let mid_angle = mid[1].atan2(mid[0]);
2000 let angles: Vec<f64> = corners.iter().map(|c| mid_angle + (c[1].atan2(c[0]) - mid_angle + PI).rem_euclid(TAU) - PI).collect();
2001 let (low, high) = (angles.iter().copied().fold(f64::INFINITY, f64::min), angles.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2002 if high - low >= PI {
2003 return Err(refuse("turn_shadow", "the tool reaches round the turn's axis".into()));
2004 }
2005 let (start, end) = (rate * screw.t0, rate * screw.t1);
2006 let (first, last) = if rate > 0.0 { (start, end) } else { (end, start) };
2007 let reference = 0.5 * (wedge[0] + wedge[1]);
2008 let near = |a: f64| reference + (a - reference + PI).rem_euclid(TAU) - PI;
2009 let clear = near(high + first) < wedge[0] - 1e-9 && near(low + last) > wedge[1] + 1e-9 && (last - first) < TAU;
2011 if !clear {
2012 return Err(refuse("turn_shadow", "the turned tool's ends reach the clip's wedge; its sweep there is not its turned shadow".into()));
2013 }
2014 let profile = polyhedron_shadow(&corners, faces)?;
2015 let (c0, s0) = (wedge[0].cos(), wedge[0].sin());
2017 let world = |rho: f64, z: f64| clip.axis_origin.add(ez.scale(z)).add(ex.scale(rho * c0)).add(ey.scale(rho * s0));
2018 let mut curves = Vec::with_capacity(profile.len());
2019 for piece in &profile {
2020 let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2021 let curve = if points.len() == 2 {
2022 frag(make_line(frag(points[0].point(), "point")?, frag(points[1].point(), "point")?), "line")?
2023 } else {
2024 frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")?
2025 };
2026 curves.push(curve);
2027 }
2028 frag(crate::revolve_profile_brep(&curves, clip.axis_origin, ez, wedge[1] - wedge[0]), "turn_shadow")
2029}
2030
2031type ShadowPiece = Vec<(f64, f64, f64)>;
2034
2035fn polyhedron_shadow(corners: &[[f64; 3]], faces: &[Vec<usize>]) -> Result<Vec<ShadowPiece>, KernelRefusal> {
2041 let zs: Vec<f64> = corners.iter().map(|c| c[2]).collect();
2042 let (z_low, z_high) = (zs.iter().copied().fold(f64::INFINITY, f64::min), zs.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2043 let scale = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(1.0f64, f64::max);
2044 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
2046 for (f, face) in faces.iter().enumerate() {
2047 for k in 0..face.len() {
2048 let (i, j) = (face[k], face[(k + 1) % face.len()]);
2049 edge_faces.entry((i.min(j), i.max(j))).or_default().push(f);
2050 }
2051 }
2052 let mut edges: Vec<(usize, usize)> = edge_faces.keys().copied().collect();
2053 edges.sort_unstable();
2054 let planes: Vec<([f64; 3], f64)> = faces
2056 .iter()
2057 .map(|face| {
2058 let (a, b, c) = (corners[face[0]], corners[face[1]], corners[face[2]]);
2059 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]]);
2060 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]];
2061 (n, n[0] * a[0] + n[1] * a[1] + n[2] * a[2])
2062 })
2063 .collect();
2064 #[derive(Clone, Copy, PartialEq, Debug)]
2065 enum Source {
2066 Edge(usize),
2068 Face(usize),
2070 }
2071 let edge_at = |k: usize, z: f64| -> [f64; 2] {
2072 let (a, b) = (corners[edges[k].0], corners[edges[k].1]);
2073 let t = (z - a[2]) / (b[2] - a[2]);
2074 [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
2075 };
2076 let crossing = |z: f64| -> Vec<usize> {
2077 (0..edges.len()).filter(|&k| (corners[edges[k].0][2] - z) * (corners[edges[k].1][2] - z) < 0.0).collect()
2078 };
2079 let farthest = |z: f64| -> (Source, f64) {
2080 let mut best = (Source::Edge(0), f64::NEG_INFINITY);
2081 for k in crossing(z) {
2082 let p = edge_at(k, z);
2083 let r = p[0].hypot(p[1]);
2084 if r > best.1 {
2085 best = (Source::Edge(k), r);
2086 }
2087 }
2088 best
2089 };
2090 let nearest = |z: f64| -> Result<(Source, f64), KernelRefusal> {
2092 let list = crossing(z);
2093 let points: Vec<[f64; 2]> = list.iter().map(|&k| edge_at(k, z)).collect();
2094 let m = points.len();
2095 if m < 3 {
2096 return Err(refuse("turn_shadow", "a section of the tool is not a polygon".into()));
2097 }
2098 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]);
2099 let mut order: Vec<usize> = (0..m).collect();
2100 order.sort_by(|&a, &b| {
2101 let (pa, pb) = (points[a], points[b]);
2102 (pa[1] - centre[1]).atan2(pa[0] - centre[0]).total_cmp(&(pb[1] - centre[1]).atan2(pb[0] - centre[0]))
2103 });
2104 let mut inside = true;
2105 let mut best = (Source::Edge(0), f64::INFINITY);
2106 for k in 0..m {
2107 let (ia, ib) = (order[k], order[(k + 1) % m]);
2108 let (a, b) = (points[ia], points[ib]);
2109 let d = [b[0] - a[0], b[1] - a[1]];
2110 if d[0] * (0.0 - a[1]) - d[1] * (0.0 - a[0]) < 0.0 {
2111 inside = false;
2112 }
2113 let length2 = d[0] * d[0] + d[1] * d[1];
2114 let t = -(a[0] * d[0] + a[1] * d[1]) / length2;
2115 let (source, r) = if t > 0.0 && t < 1.0 {
2116 let (ea, eb) = (edges[list[ia]], edges[list[ib]]);
2117 let shared = edge_faces[&ea].iter().find(|f| edge_faces[&eb].contains(f)).copied();
2118 let Some(face) = shared else {
2119 return Err(refuse("turn_shadow", "a section side does not lie on one face".into()));
2120 };
2121 let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2122 (Source::Face(face), q[0].hypot(q[1]))
2123 } else {
2124 let q = if t <= 0.0 { a } else { b };
2125 (Source::Edge(list[if t <= 0.0 { ia } else { ib }]), q[0].hypot(q[1]))
2126 };
2127 if r < best.1 {
2128 best = (source, r);
2129 }
2130 }
2131 if inside {
2132 return Err(refuse("turn_shadow", "the turn's axis passes through the tool".into()));
2133 }
2134 Ok(best)
2135 };
2136 let value = |source: Source, z: f64| -> f64 {
2138 match source {
2139 Source::Edge(k) => {
2140 let p = edge_at(k, z);
2141 p[0].hypot(p[1])
2142 }
2143 Source::Face(f) => {
2144 let (n, d) = planes[f];
2145 (d - n[2] * z).abs() / n[0].hypot(n[1])
2146 }
2147 }
2148 };
2149 let mut heights: Vec<f64> = zs.clone();
2151 heights.sort_by(|a, b| a.total_cmp(b));
2152 heights.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * scale);
2153 let mut pieces_far: Vec<(f64, f64, Source)> = Vec::new();
2154 let mut pieces_near: Vec<(f64, f64, Source)> = Vec::new();
2155 for window in heights.windows(2) {
2156 let (za, zb) = (window[0], window[1]);
2157 let samples = 400;
2158 let inner = |k: usize| za + (zb - za) * (k as f64 + 0.5) / samples as f64;
2159 for near in [false, true] {
2160 let source_at = |z: f64| -> Result<Source, KernelRefusal> { Ok(if near { nearest(z)?.0 } else { farthest(z).0 }) };
2161 let mut runs: Vec<(f64, Source)> = vec![(za, source_at(inner(0))?)];
2162 for k in 1..samples {
2163 let (z0, z1) = (inner(k - 1), inner(k));
2164 let (s0, s1) = (source_at(z0)?, source_at(z1)?);
2165 if s0 == s1 {
2166 continue;
2167 }
2168 let (mut lo, mut hi) = (z0, z1);
2171 for _ in 0..200 {
2172 let midz = 0.5 * (lo + hi);
2173 if source_at(midz)? == s0 { lo = midz } else { hi = midz }
2174 }
2175 runs.push((0.5 * (lo + hi), s1));
2176 }
2177 let list = if near { &mut pieces_near } else { &mut pieces_far };
2178 for k in 0..runs.len() {
2179 let z_end = if k + 1 < runs.len() { runs[k + 1].0 } else { zb };
2180 list.push((runs[k].0, z_end, runs[k].1));
2181 }
2182 }
2183 }
2184 let piece = |source: Source, za: f64, zb: f64| -> Vec<ShadowPiece> {
2186 let (ra, rb) = (value(source, za), value(source, zb));
2187 if let Source::Face(..) = source {
2188 return vec![vec![(ra, za, 1.0), (rb, zb, 1.0)]];
2189 }
2190 let rho = |z: f64| value(source, z);
2192 let slope = |z: f64| {
2193 let h = 1e-6 * (zb - za).abs().max(1e-300);
2194 (rho(z + h) - rho(z - h)) / (2.0 * h)
2195 };
2196 let parts = 4;
2197 let mut out = Vec::new();
2198 for k in 0..parts {
2199 let (z0, z1) = (za + (zb - za) * k as f64 / parts as f64, za + (zb - za) * (k + 1) as f64 / parts as f64);
2200 let (a, b) = ((rho(z0), z0), (rho(z1), z1));
2201 let (ta, tb) = ((slope(z0), 1.0), (slope(z1), 1.0));
2202 let det = ta.0 * (-tb.1) - ta.1 * (-tb.0);
2204 if det.abs() < 1e-12 {
2205 out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
2206 continue;
2207 }
2208 let rhs = (b.0 - a.0, b.1 - a.1);
2209 let s = (rhs.0 * (-tb.1) - rhs.1 * (-tb.0)) / det;
2210 let c = (a.0 + ta.0 * s, a.1 + ta.1 * s);
2211 let m = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
2213 let on = |u: f64| {
2214 let p = (m.0 + (c.0 - m.0) * u, m.1 + (c.1 - m.1) * u);
2215 p.0 - rho(p.1)
2216 };
2217 let (mut lo, mut hi) = (0.0, 1.0);
2218 let sign_lo = on(lo);
2219 for _ in 0..200 {
2220 let mid = 0.5 * (lo + hi);
2221 if (on(mid) > 0.0) == (sign_lo > 0.0) { lo = mid } else { hi = mid }
2222 }
2223 let u = 0.5 * (lo + hi);
2224 let weight = u / (1.0 - u);
2225 out.push(vec![(a.0, a.1, 1.0), (c.0, c.1, weight), (b.0, b.1, 1.0)]);
2226 }
2227 out
2228 };
2229 let mut outline: Vec<ShadowPiece> = Vec::new();
2231 for &(za, zb, source) in &pieces_near {
2232 outline.extend(piece(source, za, zb));
2233 }
2234 let (near_top, far_top) = (value(pieces_near.last().expect("near").2, z_high), value(pieces_far.last().expect("far").2, z_high));
2235 if far_top - near_top > 1e-12 * scale {
2236 outline.push(vec![(near_top, z_high, 1.0), (far_top, z_high, 1.0)]);
2237 }
2238 for &(za, zb, source) in pieces_far.iter().rev() {
2239 for mut arc in piece(source, za, zb).into_iter().rev() {
2240 arc.reverse();
2241 outline.push(arc);
2242 }
2243 }
2244 let (near_bottom, far_bottom) = (value(pieces_near[0].2, z_low), value(pieces_far[0].2, z_low));
2245 if far_bottom - near_bottom > 1e-12 * scale {
2246 outline.push(vec![(far_bottom, z_low, 1.0), (near_bottom, z_low, 1.0)]);
2247 }
2248 for k in 0..outline.len() {
2250 let next = (k + 1) % outline.len();
2251 let end = *outline[k].last().expect("piece");
2252 outline[next][0] = (end.0, end.1, 1.0);
2253 }
2254 Ok(outline)
2255}
2256
2257fn ball_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2267 let [OutlinePiece::Arc { centre, radius, start, end }, OutlinePiece::Line { .. }] = meridian else { return Ok(None) };
2268 let (centre, r) = (*centre, *radius);
2269 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 {
2270 return Ok(None);
2271 }
2272 let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
2273 let centre_tool = origin.add(w.scale(centre[1]));
2274 let turned = screw.omega * (screw.t1 - screw.t0);
2275 if turned.abs() >= TAU - 1e-9 {
2276 return Ok(None);
2277 }
2278 let e = screw.e.scale(screw.omega.signum()); let c0 = screw.apply(screw.t0, centre_tool);
2280 let c1 = screw.apply(screw.t1, centre_tool);
2281 let foot = |c: Vec3| screw.a.add(e.scale(c.sub(screw.a).dot(e)));
2282 let rho = c0.sub(foot(c0)).length();
2283 if rho >= r * (1.0 - 1e-9) || rho <= 1e-9 * r {
2284 return Ok(None);
2285 }
2286 let h0 = frag(c0.sub(foot(c0)).normalized(), "radial")?;
2287 let height = (r * r - rho * rho).sqrt();
2288 let (north, south) = (foot(c0).add(e.scale(height)), foot(c0).sub(e.scale(height)));
2289 let arc = |about: Vec3, k: Vec3, from: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
2291 let surface = frag(make_revolution(about, k, &frag(make_line(from, from.add(k)), "line")?, angle), "arc")?;
2292 frag(surface.iso_curve_v(0.0), "arc")
2293 };
2294 let alpha = height.atan2(-rho);
2297 let outer = arc(c0, h0.cross(e), south, 2.0 * alpha)?;
2298 let wall = frag(make_revolution(screw.a, e, &outer, turned.abs()), "wall")?;
2299 let outer_end = frag(wall.iso_curve_u(1.0), "wall_end")?;
2300 let mid = c0.add(c1).scale(0.5);
2302 let towards = frag(mid.sub(foot(mid)).normalized(), "bisector")?;
2303 let rho_mid = mid.sub(foot(mid)).length();
2304 let beta = height.atan2(rho_mid); let meet = arc(mid, e.cross(towards), south, 2.0 * beta)?;
2308 {
2309 let [d0, d1] = frag(meet.domain(), "domain")?;
2311 let middle = frag(meet.evaluate(0.5 * (d0 + d1)), "middle")?;
2312 if middle.sub(foot(middle)).dot(towards) > 0.0 {
2313 return Err(refuse("ball_across_axis", "the spheres' common circle runs the wrong way round".into()));
2314 }
2315 }
2316 let cap = |c: Vec3, inward: Vec3, side: Vec3, other: Vec3| -> Result<(NurbsSurface, Box<dyn Fn(Vec3) -> Vec3>), KernelRefusal> {
2318 let toward_south = frag(inward.add(side.scale(-0.3)).normalized(), "pole")?;
2322 let polar = toward_south.scale(-1.0);
2323 let seam = frag(side.sub(polar.scale(side.dot(polar))).normalized(), "seam")?;
2324 for k in 0..=64 {
2325 let a = PI * k as f64 / 64.0;
2328 let q = c.add(polar.scale(-r * a.cos())).add(seam.scale(r * a.sin()));
2329 if q.sub(c).dot(side) < 0.0 && q.sub(other).length() >= r * (1.0 - 1e-9) {
2330 return Err(refuse("ball_across_axis", "no seam place for a cap".into()));
2331 }
2332 }
2333 let south_pole = c.sub(polar.scale(r));
2334 if south_pole.sub(other).length() >= r * (1.0 - 1e-6) {
2335 return Err(refuse("ball_across_axis", "no pole place for a cap".into()));
2336 }
2337 let generatrix = arc(c, polar.scale(-1.0).cross(seam), south_pole, PI)?;
2338 let surface = frag(make_revolution(c, polar, &generatrix, TAU), "cap")?;
2339 let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
2340 let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
2341 let around = polar.cross(seam);
2342 let inverse = move |x: Vec3| -> Vec3 {
2343 let d = x.sub(c).scale(1.0 / r);
2344 let latitude = d.dot(polar).clamp(-1.0, 1.0).asin();
2345 let longitude = d.dot(around).atan2(d.dot(seam)).rem_euclid(TAU);
2346 Vec3::new(
2347 crate::circle_angle_to_parameter(spans_u, TAU, longitude),
2348 crate::circle_angle_to_parameter(spans_v, PI, latitude + 0.5 * PI),
2349 0.0,
2350 )
2351 };
2352 Ok((surface, Box::new(inverse)))
2353 };
2354 let moving0 = e.cross(h0);
2355 let h1 = frag(c1.sub(foot(c1)).normalized(), "radial")?;
2356 let moving1 = e.cross(h1);
2357 let (back, back_inverse) = cap(c0, h0.scale(-1.0), moving0.scale(1.0), c1)?;
2360 let (front, front_inverse) = cap(c1, h1.scale(-1.0), moving1.scale(-1.0), c0)?;
2361 let pcurve = |inverse: &dyn Fn(Vec3) -> Vec3, curve: &NurbsCurve, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
2362 let [d0, d1] = frag(curve.domain(), "domain")?;
2363 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
2364 let uv: Vec<Vec3> = ts.iter().map(|&t| curve.evaluate(t).map(|x| inverse(x))).collect::<Result<_, _>>().map_err(|e| refuse("pcurve", e))?;
2365 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "cap_pcurve")?;
2366 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
2367 };
2368 let mut shell = Shell::new();
2369 let (vn, vs) = (shell.vertex(north), shell.vertex(south));
2370 let k0 = shell.edge(outer.clone(), vs, vn, false)?;
2371 let k1 = shell.edge(outer_end.clone(), vs, vn, false)?;
2372 let c = shell.edge(meet.clone(), vs, vn, false)?;
2373 let ds = shell.edge(frag(make_line(south, south), "point")?, vs, vs, true)?;
2374 let dn = shell.edge(frag(make_line(north, north), "point")?, vn, vn, true)?;
2375 let wall_outward = {
2378 let p = frag(wall.evaluate(0.5, 0.5), "probe")?;
2379 let su = frag(wall.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(wall.evaluate(0.5 - 1e-6, 0.5), "probe")?);
2380 let sv = frag(wall.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(wall.evaluate(0.5, 0.5 - 1e-6), "probe")?);
2381 su.cross(sv).dot(p.sub(foot(p))) > 0.0
2382 };
2383 let [g0, g1] = frag(outer.domain(), "domain")?;
2384 let wall_loop = vec![
2385 (ds, true, uv_line([0.0, g0], [1.0, g0])?),
2386 (k1, true, uv_line([1.0, g0], [1.0, g1])?),
2387 (dn, true, uv_line([1.0, g1], [0.0, g1])?),
2388 (k0, false, uv_line([0.0, g1], [0.0, g0])?),
2389 ];
2390 let reversed = |list: Vec<(u64, bool, NurbsCurve)>| -> Result<Vec<(u64, bool, NurbsCurve)>, KernelRefusal> {
2391 list.into_iter().rev().map(|(e, f, p)| Ok((e, !f, frag(p.reversed(), "reversed")?))).collect()
2392 };
2393 shell.face(wall.clone(), if wall_outward { wall_loop } else { reversed(wall_loop)? });
2394 for (surface, inverse, k_edge, k_curve) in [(back, &back_inverse, k0, &outer), (front, &front_inverse, k1, &outer_end)] {
2397 let mut lp = vec![(k_edge, true, pcurve(inverse.as_ref(), k_curve, true)?), (c, false, pcurve(inverse.as_ref(), &meet, false)?)];
2398 let mut area = 0.0;
2399 for (_, _, pc) in &lp {
2400 let [d0, d1] = frag(pc.domain(), "domain")?;
2401 for j in 0..64 {
2402 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")?);
2403 area += a.x * b.y - b.x * a.y;
2404 }
2405 }
2406 if area < 0.0 {
2407 lp = reversed(lp)?;
2408 }
2409 shell.face(surface, lp);
2410 }
2411 let body = shell.solid();
2412 let issues = body.validate();
2413 if !issues.is_empty() {
2414 return Err(refuse("assembly", format!("the turned ball's sweep is not valid: {:?}", issues.first())));
2415 }
2416 let probe = Split { points: Vec::new(), curves: Vec::new(), caps: Vec::new(), scale: r + rho, shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() } };
2417 check_single_pass(screw, &probe, &body)?;
2418 Ok(Some(body))
2419}
2420
2421#[derive(Clone, Copy, Debug)]
2426pub struct FrameCurve {
2427 pub control: [Vec3; 4],
2428 pub up: Vec3,
2429}
2430
2431pub fn sweep_solid_along(tool: &SolidTool, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
2440 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
2441 let base = path.control[0];
2442 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
2443 return Err(input("plane", "the curve is not in a plane square to up".into()));
2444 }
2445 let tangent = path.control[1].sub(base);
2446 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
2447 let start_tangent = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
2448 let frame = Frame { control: path.control, up, start_tangent };
2449 let (mut speed, mut rate, mut extent) = (0.0f64, 0.0f64, 0.0f64);
2450 let mut previous = 0.0;
2451 for k in 0..=512 {
2452 let t = k as f64 / 512.0;
2453 let first = frame.first(t);
2454 if first.length() <= 1e-12 {
2455 return Err(input("tangent", "the curve stops (zero tangent)".into()));
2456 }
2457 speed = speed.max(first.length());
2458 rate = rate.max(frame.angle_rate(t).abs());
2459 extent = extent.max(frame.point(t).sub(base).length());
2460 let angle = frame.angle(t);
2461 if (angle - previous).abs() > 0.5 {
2462 return Err(refuse("frame_turns", "the frame turns half way round".into()));
2463 }
2464 previous = angle;
2465 }
2466 let screw = Screw { a: base, e: up, omega: rate, c: speed, c2: 0.0, t0: 0.0, t1: 1.0, frame: Some(frame), frozen: None, extent };
2467 let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
2468 return Err(refuse("along_tool", "a frame along a curve carries a solid of revolution only".into()));
2469 };
2470 let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
2471 let mut last = None;
2475 for k in 0..8 {
2476 let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
2477 match lofted(&screw, *axis_origin, axis, reference, meridian) {
2478 Err(error) if error.message.contains("structure") => last = Some(error),
2479 other => return other,
2480 }
2481 }
2482 Err(last.expect("tried"))
2483}
2484
2485pub fn sweep_prism_along(outline: &[[f64; 2]], from: f64, to: f64, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
2496 let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
2497 let base = path.control[0];
2498 if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
2499 return Err(input("plane", "the curve is not in a plane square to up".into()));
2500 }
2501 let tangent = path.control[1].sub(base);
2502 let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
2503 let e1 = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
2504 let e2 = up.cross(e1);
2505 let frame = Frame { control: path.control, up, start_tangent: e1 };
2506 if outline.len() < 3 || !(to > from) {
2507 return Err(input("prism", "a prism needs three corners and a positive height".into()));
2508 }
2509 let n = outline.len();
2511 let mut corners = outline.to_vec();
2512 let area: f64 = (0..n).map(|k| corners[k][0] * corners[(k + 1) % n][1] - corners[(k + 1) % n][0] * corners[k][1]).sum();
2513 if area < 0.0 {
2514 corners.reverse();
2515 }
2516 for k in 0..n {
2517 let (a, b, c) = (corners[k], corners[(k + 1) % n], corners[(k + 2) % n]);
2518 if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) <= 0.0 {
2519 return Err(input("prism", "the outline is not convex".into()));
2520 }
2521 }
2522 let scale = corners.iter().map(|c| c[0].hypot(c[1])).fold(1.0f64, f64::max);
2523 let flat = |p: Vec3| [p.sub(base).dot(e1), p.sub(base).dot(e2)];
2525 let place = |s: f64, q: [f64; 2]| -> [f64; 2] {
2526 let o = flat(frame.point(s));
2527 let (sn, cs) = frame.angle(s).sin_cos();
2528 [o[0] + cs * q[0] - sn * q[1], o[1] + sn * q[0] + cs * q[1]]
2529 };
2530 let local = |s: f64, p: [f64; 2]| -> [f64; 2] {
2531 let o = flat(frame.point(s));
2532 let (sn, cs) = frame.angle(s).sin_cos();
2533 let d = [p[0] - o[0], p[1] - o[1]];
2534 [cs * d[0] + sn * d[1], -sn * d[0] + cs * d[1]]
2535 };
2536 let edges: Vec<([f64; 2], f64)> = (0..n)
2538 .map(|k| {
2539 let (a, b) = (corners[k], corners[(k + 1) % n]);
2540 let d = [b[0] - a[0], b[1] - a[1]];
2541 let length = d[0].hypot(d[1]);
2542 let normal = [d[1] / length, -d[0] / length];
2543 (normal, normal[0] * a[0] + normal[1] * a[1])
2544 })
2545 .collect();
2546 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);
2547 let depth = |p: [f64; 2]| -> f64 {
2549 let samples = 2000;
2550 let f = |s: f64| inside(local(s, p));
2551 let mut best = (f64::INFINITY, 0usize);
2552 let values: Vec<f64> = (0..=samples).map(|k| f(k as f64 / samples as f64)).collect();
2553 for (k, &v) in values.iter().enumerate() {
2554 if v < best.0 {
2555 best = (v, k);
2556 }
2557 }
2558 let mut least = best.0;
2560 for k in 0..=samples {
2561 let left = if k == 0 { f64::INFINITY } else { values[k - 1] };
2562 let right = if k == samples { f64::INFINITY } else { values[k + 1] };
2563 if values[k] <= left && values[k] <= right && values[k] < best.0 + 1e-3 * scale {
2564 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);
2565 for _ in 0..80 {
2566 let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
2567 if f(m1) < f(m2) { hi = m2 } else { lo = m1 }
2568 }
2569 least = least.min(f(0.5 * (lo + hi)));
2570 }
2571 }
2572 least
2573 };
2574 #[derive(Clone, Copy)]
2576 enum Path {
2577 Corner(usize),
2578 Graze(usize, f64, f64),
2580 End(usize, bool),
2582 }
2583 let speed_ratio = |s: f64| frame.angle_rate(s); let graze_point = |k: usize, s: f64| -> Option<f64> {
2585 let (a, b) = (corners[k], corners[(k + 1) % n]);
2587 let (nn, _) = edges[k];
2588 let kappa = speed_ratio(s) / frame.first(s).length();
2589 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)) };
2593 if ga == gb {
2594 return None;
2595 }
2596 let u = ga / (ga - gb);
2597 (u > 0.0 && u < 1.0).then_some(u)
2598 };
2599 let mut paths: Vec<Path> = (0..n).map(Path::Corner).collect();
2600 for k in 0..n {
2601 let steps = 2000;
2603 let mut run: Option<f64> = None;
2604 for j in 0..=steps {
2605 let s = j as f64 / steps as f64;
2606 let on = graze_point(k, s).is_some();
2607 match (on, run) {
2608 (true, None) => run = Some(s),
2609 (false, Some(s0)) => {
2610 paths.push(Path::Graze(k, s0, s - 1.0 / steps as f64));
2611 run = None;
2612 }
2613 _ => {}
2614 }
2615 }
2616 if let Some(s0) = run {
2617 paths.push(Path::Graze(k, s0, 1.0));
2618 }
2619 paths.push(Path::End(k, false));
2620 paths.push(Path::End(k, true));
2621 }
2622 let eval = |path: Path, t: f64| -> [f64; 2] {
2623 match path {
2624 Path::Corner(k) => place(t, corners[k]),
2625 Path::Graze(k, s0, s1) => {
2626 let s = s0 + (s1 - s0) * t;
2627 let u = graze_point(k, s).unwrap_or(if t < 0.5 { 0.0 } else { 1.0 });
2628 let (a, b) = (corners[k], corners[(k + 1) % n]);
2629 place(s, [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u])
2630 }
2631 Path::End(k, at_end) => {
2632 let (a, b) = (corners[k], corners[(k + 1) % n]);
2633 place(if at_end { 1.0 } else { 0.0 }, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])
2634 }
2635 }
2636 };
2637 let m = 400;
2640 let polylines: Vec<Vec<[f64; 2]>> = paths.iter().map(|&p| (0..=m).map(|i| eval(p, i as f64 / m as f64)).collect()).collect();
2641 let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; paths.len()];
2642 let tol = 1e-10 * scale;
2643 for a in 0..paths.len() {
2644 for b in a + 1..paths.len() {
2645 for i in 0..m {
2646 for j in 0..m {
2647 let (p0, p1, q0, q1) = (polylines[a][i], polylines[a][i + 1], polylines[b][j], polylines[b][j + 1]);
2648 let d1 = [p1[0] - p0[0], p1[1] - p0[1]];
2649 let d2 = [q1[0] - q0[0], q1[1] - q0[1]];
2650 let det = d1[0] * d2[1] - d1[1] * d2[0];
2651 if det.abs() < 1e-300 {
2652 continue;
2653 }
2654 let r = [q0[0] - p0[0], q0[1] - p0[1]];
2655 let (x, y) = ((r[0] * d2[1] - r[1] * d2[0]) / det, (r[0] * d1[1] - r[1] * d1[0]) / det);
2656 if !(-1e-9..=1.0 + 1e-9).contains(&x) || !(-1e-9..=1.0 + 1e-9).contains(&y) {
2657 continue;
2658 }
2659 let (mut ta, mut tb) = ((i as f64 + x) / m as f64, (j as f64 + y) / m as f64);
2661 for _ in 0..50 {
2662 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
2663 let h = 1e-7;
2664 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] };
2665 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] };
2666 let f = [pa[0] - pb[0], pa[1] - pb[1]];
2667 let det = -da[0] * db[1] + da[1] * db[0];
2668 if det.abs() < 1e-300 {
2669 break;
2670 }
2671 let dta = (-f[0] * db[1] + f[1] * db[0]) / det;
2672 let dtb = (da[0] * f[1] - da[1] * f[0]) / det;
2673 ta = (ta - dta).clamp(0.0, 1.0);
2674 tb = (tb - dtb).clamp(0.0, 1.0);
2675 if dta.abs() + dtb.abs() < 1e-15 {
2676 break;
2677 }
2678 }
2679 let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
2680 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < tol {
2681 cuts[a].push(ta);
2682 cuts[b].push(tb);
2683 }
2684 }
2685 }
2686 }
2687 }
2688 for c in &mut cuts {
2689 c.sort_by(|x, y| x.total_cmp(y));
2690 c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
2691 }
2692 struct Piece {
2694 path: usize,
2695 t0: f64,
2696 t1: f64,
2697 }
2698 let eps = 1e-7 * scale;
2699 let mut pieces: Vec<Piece> = Vec::new();
2700 for (index, c) in cuts.iter().enumerate() {
2701 for w in c.windows(2) {
2702 let (t0, t1) = (w[0], w[1]);
2703 if t1 - t0 < 1e-9 {
2704 continue;
2705 }
2706 let (pa, pb) = (eval(paths[index], t0), eval(paths[index], t1));
2707 if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < 1e-9 * scale {
2708 continue;
2709 }
2710 let tm = 0.5 * (t0 + t1);
2711 let (p, q) = (eval(paths[index], tm), eval(paths[index], (tm + 1e-6).min(1.0)));
2712 let d = [q[0] - p[0], q[1] - p[1]];
2713 let length = d[0].hypot(d[1]);
2714 if length == 0.0 {
2715 continue;
2716 }
2717 let left = [p[0] - d[1] / length * eps, p[1] + d[0] / length * eps];
2718 let right = [p[0] + d[1] / length * eps, p[1] - d[0] / length * eps];
2719 let (l, r) = (depth(left) < -1e-12 * scale, depth(right) < -1e-12 * scale);
2720 match (l, r) {
2721 (true, false) => pieces.push(Piece { path: index, t0, t1 }),
2722 (false, true) => pieces.push(Piece { path: index, t0: t1, t1: t0 }),
2723 _ => {}
2724 }
2725 }
2726 }
2727 if pieces.is_empty() {
2729 return Err(refuse("prism_along", "no boundary found".into()));
2730 }
2731 let mut order = vec![0usize];
2732 let mut used = vec![false; pieces.len()];
2733 used[0] = true;
2734 loop {
2735 let last = &pieces[*order.last().expect("order")];
2736 let end = eval(paths[last.path], last.t1);
2737 let start = eval(paths[pieces[order[0]].path], pieces[order[0]].t0);
2738 if order.len() > 1 && (end[0] - start[0]).hypot(end[1] - start[1]) < 1e-8 * scale {
2739 break;
2740 }
2741 let next = (0..pieces.len()).filter(|&k| !used[k]).min_by(|&x, &y| {
2742 let (px, py) = (eval(paths[pieces[x].path], pieces[x].t0), eval(paths[pieces[y].path], pieces[y].t0));
2743 (px[0] - end[0]).hypot(px[1] - end[1]).total_cmp(&(py[0] - end[0]).hypot(py[1] - end[1]))
2744 });
2745 let Some(next) = next else {
2746 return Err(refuse("prism_along", "the boundary does not close".into()));
2747 };
2748 let p = eval(paths[pieces[next].path], pieces[next].t0);
2749 if (p[0] - end[0]).hypot(p[1] - end[1]) > 1e-8 * scale {
2750 return Err(refuse("prism_along", format!("the boundary does not close (gap {:e})", (p[0] - end[0]).hypot(p[1] - end[1]))));
2751 }
2752 used[next] = true;
2753 order.push(next);
2754 }
2755 if used.iter().any(|u| !u) {
2756 return Err(refuse("prism_along", "the swept region is not one simple loop".into()));
2757 }
2758 let world = |q: [f64; 2], h: f64| base.add(e1.scale(q[0])).add(e2.scale(q[1])).add(up.scale(h));
2760 let mut loop_points: Vec<[f64; 2]> = order.iter().map(|&k| eval(paths[pieces[k].path], pieces[k].t0)).collect();
2761 loop_points.push(loop_points[0]);
2762 let count = order.len();
2763 let mut shell = Shell::new();
2764 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();
2765 let mut bottom_e = Vec::with_capacity(count);
2766 let mut top_e = Vec::with_capacity(count);
2767 let mut curves2d = Vec::with_capacity(count);
2768 for (slot, &k) in order.iter().enumerate() {
2769 let piece = &pieces[k];
2770 let straight = matches!(paths[piece.path], Path::End(..));
2771 let next = (slot + 1) % count;
2772 let curve_at = |h: f64| -> Result<NurbsCurve, KernelRefusal> {
2773 if straight {
2774 return frag(make_line(world(loop_points[slot], h), world(loop_points[next], h)), "line");
2775 }
2776 let samples = 192;
2777 let ts: Vec<f64> = (0..=samples).map(|i| i as f64 / samples as f64).collect();
2778 let mut points: Vec<Vec3> = ts.iter().map(|&t| world(eval(paths[piece.path], piece.t0 + (piece.t1 - piece.t0) * t), h)).collect();
2779 points[0] = world(loop_points[slot], h);
2780 points[samples] = world(loop_points[next], h);
2781 frag(crate::interpolate_curve(&points, 5, &ts), "plane_path")
2782 };
2783 let (cb, ct) = (curve_at(from)?, curve_at(to)?);
2784 bottom_e.push(shell.edge(cb.clone(), bottom_v[slot], bottom_v[next], false)?);
2785 top_e.push(shell.edge(ct.clone(), top_v[slot], top_v[next], false)?);
2786 curves2d.push((cb, ct));
2787 }
2788 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<_, _>>()?;
2789 let top_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, to)).collect();
2791 let top_surface = plane_patch(world([0.0, 0.0], to), e1, e2, &top_points)?;
2792 let mut coedges = Vec::with_capacity(count);
2793 for k in 0..count {
2794 coedges.push((top_e[k], true, plane_pcurve(world([0.0, 0.0], to), e1, e2, &curves2d[k].1, true)?));
2795 }
2796 shell.face(top_surface, coedges);
2797 let bottom_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, from)).collect();
2798 let bottom_surface = plane_patch(world([0.0, 0.0], from), e2, e1, &bottom_points)?;
2799 let mut coedges = Vec::with_capacity(count);
2800 for k in (0..count).rev() {
2801 coedges.push((bottom_e[k], false, plane_pcurve(world([0.0, 0.0], from), e2, e1, &curves2d[k].0, false)?));
2802 }
2803 shell.face(bottom_surface, coedges);
2804 for k in 0..count {
2806 let next = (k + 1) % count;
2807 let (cb, _) = &curves2d[k];
2808 let rows: Vec<Vec<Vec4>> = cb
2809 .control_points
2810 .iter()
2811 .map(|p| {
2812 let x = p.point().expect("point");
2813 vec![Vec4::from_point(x, p.w), Vec4::from_point(x.add(up.scale(to - from)), p.w)]
2814 })
2815 .collect();
2816 let surface = frag(NurbsSurface::new(cb.degree, 1, cb.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], rows), "side")?;
2817 let [u0, u1] = frag(cb.domain(), "domain")?;
2818 let coedges = vec![
2819 (bottom_e[k], true, uv_line([u0, 0.0], [u1, 0.0])?),
2820 (verticals[next], true, uv_line([u1, 0.0], [u1, 1.0])?),
2821 (top_e[k], false, uv_line([u1, 1.0], [u0, 1.0])?),
2822 (verticals[k], false, uv_line([u0, 1.0], [u0, 0.0])?),
2823 ];
2824 shell.face(surface, coedges);
2825 }
2826 let body = shell.solid();
2827 let issues = body.validate();
2828 if !issues.is_empty() {
2829 return Err(refuse("assembly", format!("the swept prism is not valid: {:?}", issues.first())));
2830 }
2831 Ok(body)
2832}
2833
2834fn projected_pcurve(surface: &NurbsSurface, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
2836 let [d0, d1] = frag(curve.domain(), "domain")?;
2837 let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
2838 let mut uv = Vec::with_capacity(ts.len());
2839 for &t in &ts {
2840 let x = frag(curve.evaluate(t), "point")?;
2841 let foot = frag(crate::project_point_to_surface(surface, x), "project")?;
2842 if foot.distance > 1e-6 * (1.0 + x.length()) {
2843 return Err(refuse("loft_cap", format!("a lofted edge leaves its cap by {} at {x:?}", foot.distance)));
2844 }
2845 uv.push(Vec3::new(foot.u, foot.v, 0.0));
2846 }
2847 let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "projected_pcurve")?;
2848 if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
2849}
2850
2851type Signature = (Vec<(usize, usize, bool)>, Vec<(bool, Vec<(usize, bool)>)>, Vec<(usize, bool)>);
2852
2853fn signature(split: &Split) -> Result<Signature, KernelRefusal> {
2854 Ok((
2855 split.curves.iter().map(|c| (c.start, c.end, c.degenerate)).collect(),
2856 split.caps.iter().map(|cap| (cap.front, cap.coedges.iter().map(|&(c, f, _)| (c, f)).collect())).collect(),
2857 split.grazing()?,
2858 ))
2859}
2860
2861fn lofted(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<BrepSolid, KernelRefusal> {
2862 let split_at = |t: f64| -> Result<Split, KernelRefusal> {
2863 let mut frozen = *screw;
2864 frozen.frozen = Some(t);
2865 split_revolved(&frozen, origin, axis, radial, meridian)
2866 };
2867 let lambdas: Vec<f64> = (0..=LOFT_SAMPLES).map(|i| i as f64 / LOFT_SAMPLES as f64).collect();
2868 let samples_of = |split: &Split, c: usize, back_forward: bool, t: f64| -> Result<Vec<Vec3>, KernelRefusal> {
2870 let Some(list) = &split.curves[c].samples else {
2871 return Err(refuse("loft_samples", "a grazing curve has no exact samples to loft".into()));
2872 };
2873 let mut world: Vec<Vec3> = list.iter().map(|&x| screw.apply(t, x)).collect();
2874 if back_forward {
2875 world.reverse();
2876 }
2877 Ok(world)
2878 };
2879 let mut count = 63usize;
2880 loop {
2881 let times: Vec<f64> = (0..=count).map(|j| j as f64 / count as f64).collect();
2882 let splits: Vec<Split> = times.iter().map(|&t| split_at(t)).collect::<Result<_, _>>()?;
2883 let shape = signature(&splits[0])?;
2884 for split in &splits[1..] {
2885 if signature(split)? != shape {
2886 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
2887 }
2888 }
2889 let grazing = shape.2.clone();
2890 if grazing.is_empty() {
2891 return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
2892 }
2893 let scale = splits[0].scale + screw.extent;
2894 let mut walls = Vec::with_capacity(grazing.len());
2896 for &(c, back_forward) in &grazing {
2897 let mut rows: Vec<NurbsCurve> = Vec::with_capacity(times.len());
2898 for (j, split) in splits.iter().enumerate() {
2899 let points = samples_of(split, c, back_forward, times[j])?;
2900 rows.push(frag(crate::interpolate_curve(&points, 5, &lambdas), "loft_row")?);
2901 }
2902 let knots_u = rows[0].knots.clone();
2903 let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows[0].control_points.len());
2904 let mut knots_v = Vec::new();
2905 for k in 0..rows[0].control_points.len() {
2906 let column: Vec<Vec3> = rows.iter().map(|row| row.control_points[k].point()).collect::<Result<_, _>>().map_err(|e| refuse("loft", e))?;
2907 let across = frag(crate::interpolate_curve(&column, 5, ×), "loft_column")?;
2908 knots_v = across.knots.clone();
2909 net.push(across.control_points.clone());
2910 }
2911 let surface = frag(NurbsSurface::new(5, 5, knots_u, knots_v, net), "loft_wall")?;
2912 walls.push((c, back_forward, surface, rows.first().cloned().expect("rows"), rows.last().cloned().expect("rows")));
2913 }
2914 let mut worst = 0.0f64;
2916 for j in 0..count {
2917 let t = 0.5 * (times[j] + times[j + 1]);
2918 let split = split_at(t)?;
2919 if signature(&split)? != shape {
2920 return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
2921 }
2922 for (c, back_forward, surface, _, _) in &walls {
2923 let exact = samples_of(&split, *c, *back_forward, t)?;
2924 for (i, x) in exact.iter().enumerate() {
2925 worst = worst.max(frag(surface.evaluate(lambdas[i], t), "loft_check")?.sub(*x).length());
2926 }
2927 }
2928 }
2929 if worst > 1e-10 * scale {
2930 if count > 1000 {
2931 return Err(refuse("loft_accuracy", format!("the lofted wall stays {worst:e} off the grazing loop")));
2932 }
2933 count = 2 * count + 1;
2934 continue;
2935 }
2936 let mut paths: HashMap<usize, Vec<Vec3>> = HashMap::new();
2938 for &(c, _) in &grazing {
2939 for r in [splits[0].curves[c].start, splits[0].curves[c].end] {
2940 paths.entry(r).or_insert_with(|| splits.iter().zip(×).map(|(split, &t)| screw.apply(t, split.points[r])).collect());
2941 }
2942 }
2943 return assemble_lofted(screw, &splits[0], &splits[count], &walls, ×, &paths);
2944 }
2945}
2946
2947#[allow(clippy::too_many_arguments)]
2948fn assemble_lofted(
2949 screw: &Screw,
2950 first: &Split,
2951 last: &Split,
2952 walls: &[(usize, bool, NurbsSurface, NurbsCurve, NurbsCurve)],
2953 times: &[f64],
2954 paths: &HashMap<usize, Vec<Vec3>>,
2955) -> Result<BrepSolid, KernelRefusal> {
2956 let mut shell = Shell::new();
2957 let ends = [(first, screw.t0), (last, screw.t1)];
2958 let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
2959 let mut vertex = |shell: &mut Shell, end: usize, p: usize| -> u64 {
2960 *vertex_at.entry((end, p)).or_insert_with(|| shell.vertex(screw.apply(ends[end].1, ends[end].0.points[p])))
2961 };
2962 let grazing: HashMap<usize, usize> = walls.iter().enumerate().map(|(index, w)| (w.0, index)).collect();
2963 let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
2966 let mut edge_curve: HashMap<(usize, usize), NurbsCurve> = HashMap::new();
2967 for end in 0..2 {
2968 let (split, t) = ends[end];
2969 for (c, tool_curve) in split.curves.iter().enumerate() {
2970 let used = split.caps.iter().any(|cap| cap.front == (end == 1) && cap.coedges.iter().any(|e| e.0 == c));
2971 if !used {
2972 continue;
2973 }
2974 let curve = match grazing.get(&c) {
2975 Some(&index) => {
2976 let (_, back_forward, _, row0, row1) = &walls[index];
2977 let row = if end == 0 { row0 } else { row1 };
2978 if *back_forward { frag(row.reversed(), "reversed")? } else { row.clone() }
2979 }
2980 None => screw.move_curve(t, &tool_curve.curve)?,
2981 };
2982 let (a, b) = (vertex(&mut shell, end, tool_curve.start), vertex(&mut shell, end, tool_curve.end));
2983 let edge = shell.edge(curve.clone(), a, b, tool_curve.degenerate)?;
2984 edge_at.insert((end, c), edge);
2985 edge_curve.insert((end, c), curve);
2986 }
2987 }
2988 for end in 0..2 {
2989 let (split, t) = ends[end];
2990 for cap in split.caps.iter().filter(|cap| cap.front == (end == 1)) {
2991 let surface = screw.move_surface(t, &cap.surface)?;
2992 let mut coedges = Vec::with_capacity(cap.coedges.len());
2993 for (c, forward, pcurve) in &cap.coedges {
2994 let pcurve = if grazing.contains_key(c) {
2995 match projected_pcurve(&surface, &edge_curve[&(end, *c)], *forward) {
2996 Ok(p) => p,
2997 Err(error) => {
2998 let samples = split.curves[*c].samples.as_ref().map(|list| {
3000 list.iter()
3001 .map(|&x| crate::project_point_to_surface(&surface, screw.apply(t, x)).map(|f| f.distance).unwrap_or(f64::NAN))
3002 .fold(0.0f64, f64::max)
3003 });
3004 let line = split.curves[*c].curve.degree;
3005 return Err(refuse("loft_cap", 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)));
3006 }
3007 }
3008 } else {
3009 pcurve.clone()
3010 };
3011 coedges.push((edge_at[&(end, *c)], *forward, pcurve));
3012 }
3013 shell.face(surface, coedges);
3014 }
3015 }
3016 let mut orbits: HashMap<usize, u64> = HashMap::new();
3019 let mut keys: Vec<usize> = paths.keys().copied().collect();
3020 keys.sort_unstable();
3021 for r in keys {
3022 let curve = frag(crate::interpolate_curve(&paths[&r], 5, times), "orbit")?;
3023 let (a, b) = (vertex(&mut shell, 0, r), vertex(&mut shell, 1, r));
3024 orbits.insert(r, shell.edge(curve, a, b, false)?);
3025 }
3026 for (c, back_forward, surface, _, _) in walls {
3027 let tool_curve = &first.curves[*c];
3028 let (from, to) = if *back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
3029 let coedges = vec![
3030 (edge_at[&(0, *c)], !*back_forward, uv_line([0.0, 0.0], [1.0, 0.0])?),
3031 (orbits[&to], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
3032 (edge_at[&(1, *c)], *back_forward, uv_line([1.0, 1.0], [0.0, 1.0])?),
3033 (orbits[&from], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
3034 ];
3035 shell.face(surface.clone(), coedges);
3036 }
3037 let body = shell.solid();
3038 let issues = body.validate();
3039 if !issues.is_empty() {
3040 return Err(refuse("assembly", format!("the lofted sweep is not valid: {:?}", issues.first())));
3041 }
3042 check_single_pass(screw, first, &body)?;
3043 Ok(body)
3044}
3045
3046fn covered(screw: &Screw, split: &Split, y: Vec3, margin: f64) -> Result<bool, KernelRefusal> {
3049 let reach = y.sub(screw.a).length() + split.scale + screw.extent;
3050 let speed = (screw.omega.abs() * reach + screw.rate_bound()).max(1e-300);
3051 let floor = (screw.t1 - screw.t0) * 1e-7;
3052 let mut t = screw.t0;
3053 let mut steps = 0usize;
3054 loop {
3055 let d = split.distance(screw.invert(t, y));
3056 if d < -margin {
3057 return Ok(true);
3058 }
3059 if t >= screw.t1 {
3060 return Ok(false);
3061 }
3062 if steps > 2_000_000 {
3063 return Err(refuse("probe_budget", "the one-pass check could not walk the motion past a point near the sweep".into()));
3064 }
3065 t = (t + ((d.abs() + margin) / speed).max(floor)).min(screw.t1);
3066 steps += 1;
3067 }
3068}
3069
3070fn check_single_pass(screw: &Screw, split: &Split, body: &BrepSolid) -> Result<(), KernelRefusal> {
3072 let step = 1e-6 * split.scale;
3073 let margin = 1e-9 * split.scale;
3074 for face in body.shells.iter().flat_map(|s| &s.faces) {
3075 let surface = &face.surface;
3076 let mut corners: Vec<[f64; 2]> = Vec::new();
3080 for coedge in &face.loops[0].coedges {
3081 let domain = frag(coedge.pcurve.domain(), "domain")?;
3082 for k in 0..4 {
3083 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 4.0), "pcurve")?;
3084 corners.push([p.x, p.y]);
3085 }
3086 }
3087 let middle = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
3088 let middle = [middle[0] / corners.len() as f64, middle[1] / corners.len() as f64];
3089 let mut probes: Vec<[f64; 2]> = vec![middle];
3090 for c in &corners {
3091 for w in [0.25, 0.5, 0.75, 0.9, 0.99] {
3092 probes.push([middle[0] + (c[0] - middle[0]) * w, middle[1] + (c[1] - middle[1]) * w]);
3093 }
3094 }
3095 if surface.degree_v == 5 {
3096 let [u0, u1] = frag(surface.domain_u(), "domain")?;
3097 for i in 1..8 {
3098 for j in 1..64 {
3099 probes.push([u0 + (u1 - u0) * i as f64 / 8.0, j as f64 / 64.0]);
3100 }
3101 }
3102 }
3103 let ([u0, u1], [v0, v1]) = (frag(surface.domain_u(), "domain")?, frag(surface.domain_v(), "domain")?);
3104 let mut ring: Vec<[f64; 2]> = Vec::new();
3107 for coedge in &face.loops[0].coedges {
3108 let domain = frag(coedge.pcurve.domain(), "domain")?;
3109 for k in 0..32 {
3110 let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 32.0), "pcurve")?;
3111 ring.push([p.x, p.y]);
3112 }
3113 }
3114 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])]));
3115 for i in 1..12 {
3116 for j in 1..12 {
3117 probes.push([lo[0] + (hi[0] - lo[0]) * i as f64 / 12.0, lo[1] + (hi[1] - lo[1]) * j as f64 / 12.0]);
3118 }
3119 }
3120 let inside = |q: [f64; 2]| -> bool {
3121 let mut crossings = 0;
3122 for k in 0..ring.len() {
3123 let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
3124 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]) {
3125 crossings += 1;
3126 }
3127 }
3128 crossings % 2 == 1
3129 };
3130 for [u, v] in probes.into_iter().filter(|&q| inside(q)) {
3131 let p = frag(surface.evaluate(u, v), "probe")?;
3132 let (hu, hv) = (1e-6 * (u1 - u0), 1e-6 * (v1 - v0));
3133 let du = frag(surface.evaluate((u + hu).min(u1), v), "probe")?.sub(frag(surface.evaluate((u - hu).max(u0), v), "probe")?);
3134 let dv = frag(surface.evaluate(u, (v + hv).min(v1)), "probe")?.sub(frag(surface.evaluate(u, (v - hv).max(v0)), "probe")?);
3135 let Ok(n) = du.cross(dv).normalized() else { continue };
3136 let n = if face.same_sense { n } else { n.scale(-1.0) };
3137 if covered(screw, split, p.add(n.scale(step)), margin)? {
3138 return Err(refuse("folds", format!("the sweep passes over a point twice (just outside its own face at {p:?}); not one pass")));
3139 }
3140 }
3141 }
3142 Ok(())
3143}
3144