1use std::collections::HashMap;
66
67use brep_kernel::{
68 containment_lane, intersect_analytic_pair, intersect_curve_surface, intersect_surfaces,
69 make_line, make_plane, parameter_point_in_face, project_point_to_curve,
70 project_point_to_surface, segment_intersection, trim_sample_count, trim_station,
71 AnalyticSurface, BrepSolid, ContainmentLane, FaceRecord, KernelTolerances, NurbsCurve,
72 NurbsSurface, PolygonClass, SurfaceIntersectionOptions, Vec2, Vec3,
73};
74
75use super::project::{ViewFrame, EDGE_ANGLE_DEG};
76use super::PlacedView;
77
78pub const CHORD_MM: f64 = 0.005;
83
84const MAX_SUBDIVISION: u32 = 14;
86
87const GRAZING_COS: f64 = 1e-3;
91
92const TRIM_STATIONS: usize = 64;
96
97const BISECTION_STEPS: u32 = 40;
101
102
103fn v3(p: [f64; 3]) -> Vec3 {
104 Vec3::new(p[0], p[1], p[2])
105}
106
107fn a3(p: Vec3) -> [f64; 3] {
108 [p.x, p.y, p.z]
109}
110
111pub enum Clip<'a> {
117 Whole,
119 Beyond { cap: &'a [Vec<[f64; 2]>], whole: &'a [bool], half: Option<HalfLine> },
129 Circle { radius: f64 },
132}
133
134impl Clip<'_> {
135 fn whole(&self, index: usize) -> bool {
137 matches!(self, Clip::Beyond { whole, .. } if whole.get(index) == Some(&true))
138 }
139
140 fn margin(&self, view: [f64; 3]) -> f64 {
144 match self {
145 Clip::Beyond { half: Some(half), .. } => view[2].max(half.at(view)),
146 _ => view[2],
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
156pub struct HalfLine {
157 pub normal: [f64; 2],
158 pub offset: f64,
159}
160
161impl HalfLine {
162 pub fn at(&self, view: [f64; 3]) -> f64 {
165 view[0] * self.normal[0] + view[1] * self.normal[1] - self.offset
166 }
167}
168
169#[derive(Debug, Clone, Copy)]
171pub struct Floors {
172 pub bar: f64,
174 pub depth_mm: f64,
176 pub crossing_mm: f64,
178}
179
180impl Floors {
181 pub fn of(scale: f64) -> Floors {
182 let bar = KernelTolerances::default().pcurve_consistency;
183 Floors { bar, depth_mm: bar * scale, crossing_mm: bar * scale }
184 }
185}
186
187#[derive(Debug, Clone, Default)]
190pub struct HlrResult {
191 pub runs: Vec<Vec<[f64; 2]>>,
192 pub inked: usize,
194 pub silhouettes: usize,
196 pub pieces: usize,
198 pub trim_queries: usize,
202 pub ray_queries: usize,
205}
206
207thread_local! {
208 static TRIM_QUERIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
211 static RAY_QUERIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
212}
213
214
215
216#[derive(Clone)]
222enum Carrier<'a> {
223 Edge(&'a NurbsCurve),
225 Curve(NurbsCurve),
227 Line { a: Vec3, b: Vec3 },
229 Circle { centre: Vec3, e1: Vec3, e2: Vec3 },
231 Traced { surface: &'a NurbsSurface, uv: Vec<[f64; 2]>, dir: Vec3 },
234 Polyline(Vec<Vec3>),
237}
238
239impl Carrier<'_> {
240 fn at(&self, s: f64) -> Option<Vec3> {
241 match self {
242 Carrier::Edge(curve) => curve.evaluate(s).ok(),
243 Carrier::Curve(curve) => curve.evaluate(s).ok(),
244 Carrier::Line { a, b } => Some(a.add(b.sub(*a).scale(s))),
245 Carrier::Circle { centre, e1, e2 } => Some(centre.add(e1.scale(s.cos())).add(e2.scale(s.sin()))),
246 Carrier::Traced { surface, uv, dir } => {
247 let (point, _) = traced_at(surface, uv, *dir, s)?;
248 Some(point)
249 }
250 Carrier::Polyline(points) => {
251 let last = points.len().checked_sub(1)?;
252 let i = (s.floor().max(0.0) as usize).min(last.saturating_sub(1));
253 let f = (s - i as f64).clamp(0.0, 1.0);
254 let (a, b) = (points[i], points[(i + 1).min(last)]);
255 Some(a.add(b.sub(a).scale(f)))
256 }
257 }
258 }
259
260 fn straight(&self) -> bool {
263 match self {
264 Carrier::Line { .. } => true,
265 Carrier::Edge(curve) => curve.degree == 1 && curve.control_points.len() == 2,
266 Carrier::Curve(curve) => curve.degree == 1 && curve.control_points.len() == 2,
267 _ => false,
268 }
269 }
270}
271
272fn grazing(surface: &NurbsSurface, dir: Vec3, u: f64, v: f64) -> Option<f64> {
275 surface.normal(u, v).ok().map(|n| n.dot(dir))
276}
277
278fn traced_at(surface: &NurbsSurface, uv: &[[f64; 2]], dir: Vec3, s: f64) -> Option<(Vec3, [f64; 2])> {
282 let last = uv.len().checked_sub(1)?;
283 if last == 0 {
284 let p = surface.evaluate(uv[0][0], uv[0][1]).ok()?;
285 return Some((p, uv[0]));
286 }
287 let i = (s.floor().max(0.0) as usize).min(last - 1);
288 let f = (s - i as f64).clamp(0.0, 1.0);
289 let (a, b) = (uv[i], uv[i + 1]);
290 let base = [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
291 let step = [b[0] - a[0], b[1] - a[1]];
292 let length = (step[0] * step[0] + step[1] * step[1]).sqrt();
293 let solved = if f <= 0.0 || f >= 1.0 || length <= 0.0 {
294 base
295 } else {
296 let across = [-step[1], step[0]];
297 root_along(surface, dir, base, across).unwrap_or(base)
298 };
299 let p = surface.evaluate(solved[0], solved[1]).ok()?;
300 Some((p, solved))
301}
302
303fn root_along(surface: &NurbsSurface, dir: Vec3, base: [f64; 2], across: [f64; 2]) -> Option<[f64; 2]> {
306 let at = |t: f64| [base[0] + across[0] * t, base[1] + across[1] * t];
307 let g0 = {
308 let p = at(0.0);
309 grazing(surface, dir, p[0], p[1])?
310 };
311 if g0 == 0.0 {
312 return Some(base);
313 }
314 for (lo, hi) in [(0.0, 0.5), (0.0, -0.5), (0.5, 1.0), (-0.5, -1.0)] {
315 let (p_lo, p_hi) = (at(lo), at(hi));
316 let g_lo = grazing(surface, dir, p_lo[0], p_lo[1])?;
317 let g_hi = grazing(surface, dir, p_hi[0], p_hi[1])?;
318 if g_lo.signum() == g_hi.signum() {
319 continue;
320 }
321 let (mut lo, mut hi, mut g_lo) = (lo, hi, g_lo);
322 for _ in 0..BISECTION_STEPS {
323 let mid = 0.5 * (lo + hi);
324 let p = at(mid);
325 let g = grazing(surface, dir, p[0], p[1])?;
326 if g.signum() == g_lo.signum() {
327 lo = mid;
328 g_lo = g;
329 } else {
330 hi = mid;
331 }
332 }
333 return Some(at(0.5 * (lo + hi)));
334 }
335 None
336}
337
338fn project(frame: &ViewFrame, placed: &PlacedView, p: Vec3) -> ([f64; 2], [f64; 3]) {
340 let view = frame.to_view(a3(p));
341 ([placed.position[0] + view[0] * placed.scale, placed.position[1] - view[1] * placed.scale], view)
342}
343
344fn paper_distance(a: [f64; 2], b: [f64; 2]) -> f64 {
345 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
346}
347
348fn segment_foot(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> (f64, f64) {
351 let d = [b[0] - a[0], b[1] - a[1]];
352 let length2 = d[0] * d[0] + d[1] * d[1];
353 let t = if length2 > 0.0 {
354 (((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / length2).clamp(0.0, 1.0)
355 } else {
356 0.0
357 };
358 let foot = [a[0] + d[0] * t, a[1] + d[1] * t];
359 (paper_distance(p, foot), t)
360}
361
362#[derive(Debug, Clone, Default)]
364struct Samples {
365 params: Vec<f64>,
366 points: Vec<Vec3>,
367 paper: Vec<[f64; 2]>,
368 view: Vec<[f64; 3]>,
369}
370
371impl Samples {
372 fn push(&mut self, s: f64, p: Vec3, frame: &ViewFrame, placed: &PlacedView) {
373 let (paper, view) = project(frame, placed, p);
374 self.params.push(s);
375 self.points.push(p);
376 self.paper.push(paper);
377 self.view.push(view);
378 }
379
380 fn len(&self) -> usize {
381 self.params.len()
382 }
383
384 fn paper_length(&self) -> f64 {
385 self.paper.windows(2).map(|w| paper_distance(w[0], w[1])).sum()
386 }
387}
388
389fn sample(
394 carrier: &Carrier,
395 s0: f64,
396 s1: f64,
397 initial: usize,
398 frame: &ViewFrame,
399 placed: &PlacedView,
400) -> Samples {
401 let mut out = Samples::default();
402 let initial = if carrier.straight() { 1 } else { initial.max(2) };
403 let Some(first) = carrier.at(s0) else { return out };
404 out.push(s0, first, frame, placed);
405 for k in 0..initial {
406 let a = s0 + (s1 - s0) * k as f64 / initial as f64;
407 let b = s0 + (s1 - s0) * (k + 1) as f64 / initial as f64;
408 let (Some(pa), Some(pb)) = (carrier.at(a), carrier.at(b)) else { continue };
409 subdivide(carrier, (a, pa), (b, pb), 0, frame, placed, &mut out);
410 }
411 out
412}
413
414fn subdivide(
415 carrier: &Carrier,
416 (a, pa): (f64, Vec3),
417 (b, pb): (f64, Vec3),
418 depth: u32,
419 frame: &ViewFrame,
420 placed: &PlacedView,
421 out: &mut Samples,
422) {
423 if depth < MAX_SUBDIVISION && !carrier.straight() {
424 let mid = 0.5 * (a + b);
425 if let Some(pm) = carrier.at(mid) {
426 let (paper_a, _) = project(frame, placed, pa);
427 let (paper_b, _) = project(frame, placed, pb);
428 let (paper_m, _) = project(frame, placed, pm);
429 if segment_foot(paper_m, paper_a, paper_b).0 > CHORD_MM {
430 subdivide(carrier, (a, pa), (mid, pm), depth + 1, frame, placed, out);
431 subdivide(carrier, (mid, pm), (b, pb), depth + 1, frame, placed, out);
432 return;
433 }
434 }
435 }
436 out.push(b, pb, frame, placed);
437}
438
439type VertexKey = (usize, u64);
445
446struct Candidate<'a> {
447 carrier: Carrier<'a>,
448 samples: Samples,
449 inked: bool,
451 silhouette: bool,
452 edge: Option<(usize, u64)>,
454 ends: Option<(VertexKey, VertexKey)>,
455 bounds: [f64; 4],
457 whole: bool,
459}
460
461fn bounds_of(paper: &[[f64; 2]]) -> [f64; 4] {
462 let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
463 for p in paper {
464 b[0] = b[0].min(p[0]);
465 b[1] = b[1].min(p[1]);
466 b[2] = b[2].max(p[0]);
467 b[3] = b[3].max(p[1]);
468 }
469 b
470}
471
472fn curve_initial(curve: &NurbsCurve) -> usize {
474 let spans = curve.control_points.len().saturating_sub(curve.degree).max(1);
475 (spans * (curve.degree + 1)).clamp(2, 256)
476}
477
478fn outward_normal(face: &FaceRecord, p: Vec3) -> Option<Vec3> {
481 let foot = project_point_to_surface(&face.surface, p).ok()?;
482 let n = face.surface.normal(foot.u, foot.v).ok()?;
483 Some(if face.same_sense { n } else { n.scale(-1.0) })
484}
485
486fn is_crease(curve: &NurbsCurve, t0: f64, t1: f64, a: &FaceRecord, b: &FaceRecord) -> bool {
491 let bar = EDGE_ANGLE_DEG.to_radians().cos();
492 for k in 1..=5 {
493 let t = t0 + (t1 - t0) * k as f64 / 6.0;
494 let Ok(p) = curve.evaluate(t) else { return true };
495 let (Some(na), Some(nb)) = (outward_normal(a, p), outward_normal(b, p)) else { return true };
496 if na.dot(nb) < bar {
497 return true;
498 }
499 }
500 false
501}
502
503fn edge_faces(solid: &BrepSolid) -> HashMap<u64, Vec<(usize, usize)>> {
505 let mut map: HashMap<u64, Vec<(usize, usize)>> = HashMap::new();
506 for (shell_index, shell) in solid.shells.iter().enumerate() {
507 for (face_index, face) in shell.faces.iter().enumerate() {
508 for lp in &face.loops {
509 for coedge in &lp.coedges {
510 map.entry(coedge.edge_id).or_default().push((shell_index, face_index));
511 }
512 }
513 }
514 }
515 map
516}
517
518fn pair_extent(solid: &BrepSolid, a: &FaceRecord, b: &FaceRecord, edge_index: &HashMap<u64, usize>) -> f64 {
523 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
524 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
525 for coedge in [a, b].into_iter().flat_map(|face| &face.loops).flat_map(|lp| &lp.coedges) {
526 let Some(edge) = edge_index.get(&coedge.edge_id).map(|&i| &solid.edges[i]) else { continue };
527 for t in [edge.t0, 0.5 * (edge.t0 + edge.t1), edge.t1] {
528 if let Ok(q) = edge.curve.evaluate(t) {
529 lo = Vec3::new(lo.x.min(q.x), lo.y.min(q.y), lo.z.min(q.z));
530 hi = Vec3::new(hi.x.max(q.x), hi.y.max(q.y), hi.z.max(q.z));
531 }
532 }
533 }
534 if lo.x <= hi.x {
535 hi.sub(lo).length()
536 } else {
537 0.0
538 }
539}
540
541fn curvature_across(face: &FaceRecord, p: Vec3, tangent: Vec3) -> Option<f64> {
545 let foot = project_point_to_surface(&face.surface, p).ok()?;
546 let d = face.surface.derivatives(foot.u, foot.v, 2).ok()?;
547 let (su, sv) = (d[1][0], d[0][1]);
548 let normal = su.cross(sv).normalized().ok()?;
549 let normal = if face.same_sense { normal } else { normal.scale(-1.0) };
550 let across = normal.cross(tangent).normalized().ok()?;
551 let (e, f, g) = (su.dot(su), su.dot(sv), sv.dot(sv));
553 let det = e * g - f * f;
554 if det.abs() <= f64::EPSILON * e * g {
555 return None;
556 }
557 let (cu, cv) = (across.dot(su), across.dot(sv));
558 let (a, b) = ((g * cu - f * cv) / det, (e * cv - f * cu) / det);
559 let second = d[2][0].dot(normal) * a * a + 2.0 * d[1][1].dot(normal) * a * b + d[0][2].dot(normal) * b * b;
560 let first = e * a * a + 2.0 * f * a * b + g * b * b;
561 (first > 0.0).then(|| second / first)
562}
563
564fn one_surface(curve: &NurbsCurve, t0: f64, t1: f64, a: &FaceRecord, b: &FaceRecord, extent: f64, bar: f64) -> bool {
579 for k in 1..=5 {
580 let t = t0 + (t1 - t0) * k as f64 / 6.0;
581 let (Ok(p), Ok(d)) = (curve.evaluate(t), curve.derivatives(t, 1)) else { return false };
582 let (Some(ka), Some(kb)) = (curvature_across(a, p, d[1]), curvature_across(b, p, d[1])) else { return false };
583 if (ka - kb).abs() * extent * extent * 0.5 > bar {
584 return false;
585 }
586 }
587 true
588}
589
590fn edge_candidates<'a>(
591 solid_index: usize,
592 solid: &'a BrepSolid,
593 frame: &ViewFrame,
594 placed: &PlacedView,
595 floors: &Floors,
596) -> Vec<Candidate<'a>> {
597 let faces = edge_faces(solid);
598 let edge_index: HashMap<u64, usize> = solid.edges.iter().enumerate().map(|(i, edge)| (edge.id, i)).collect();
599 let mut extents: HashMap<[(usize, usize); 2], f64> = HashMap::new();
601 let mut out = Vec::new();
602 for edge in &solid.edges {
603 if edge.degenerate {
604 continue;
605 }
606 let owners = faces.get(&edge.id).cloned().unwrap_or_default();
607 let mut distinct = owners.clone();
608 distinct.sort_unstable();
609 distinct.dedup();
610 let inked = match distinct.len() {
611 1 if owners.len() >= 2 => continue,
615 2 => {
618 let face = |(s, f): (usize, usize)| &solid.shells[s].faces[f];
619 let (a, b) = (face(distinct[0]), face(distinct[1]));
620 is_crease(&edge.curve, edge.t0, edge.t1, a, b) || {
621 let extent = *extents
622 .entry([distinct[0], distinct[1]])
623 .or_insert_with(|| pair_extent(solid, a, b, &edge_index));
624 !one_surface(&edge.curve, edge.t0, edge.t1, a, b, extent, floors.bar)
625 }
626 }
627 _ => true,
628 };
629 let carrier = Carrier::Edge(&edge.curve);
630 let samples = sample(&carrier, edge.t0, edge.t1, curve_initial(&edge.curve), frame, placed);
631 if samples.len() < 2 {
632 continue;
633 }
634 let bounds = bounds_of(&samples.paper);
635 out.push(Candidate {
636 carrier,
637 samples,
638 inked,
639 silhouette: false,
640 edge: Some((solid_index, edge.id)),
641 ends: Some(((solid_index, edge.start_vertex_id), (solid_index, edge.end_vertex_id))),
642 bounds,
643 whole: false,
644 });
645 }
646 out
647}
648
649fn planar(surface: &NurbsSurface) -> bool {
651 matches!(surface.analytic(), Some(AnalyticSurface::Plane { .. })) || surface.is_affine().unwrap_or(false)
652}
653
654fn trim_box(face: &FaceRecord) -> Option<[f64; 4]> {
657 let du = face.surface.domain_u().ok()?;
658 let dv = face.surface.domain_v().ok()?;
659 let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
660 for lp in &face.loops {
661 for coedge in &lp.coedges {
662 for cp in &coedge.pcurve.control_points {
663 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
664 b[0] = b[0].min(cp.x / w);
665 b[1] = b[1].min(cp.y / w);
666 b[2] = b[2].max(cp.x / w);
667 b[3] = b[3].max(cp.y / w);
668 }
669 }
670 }
671 if !(b[0] <= b[2]) {
672 return Some([du[0], dv[0], du[1], dv[1]]);
673 }
674 Some([b[0].max(du[0]), b[1].max(dv[0]), b[2].min(du[1]), b[3].min(dv[1])])
675}
676
677pub struct SolidTrims {
683 faces: Vec<Vec<TrimTests>>,
684}
685
686impl SolidTrims {
687 pub fn of(solid: &BrepSolid) -> SolidTrims {
688 SolidTrims { faces: solid.shells.iter().map(|shell| shell.faces.iter().map(|_| TrimTests::new()).collect()).collect() }
689 }
690}
691
692struct TrimTests {
699 lane: std::cell::OnceCell<Option<ContainmentLane>>,
700 reach: std::cell::OnceCell<Option<UvReach>>,
701 uv: std::cell::OnceCell<Option<UvTrim>>,
702}
703
704impl TrimTests {
705 fn new() -> TrimTests {
706 TrimTests { lane: std::cell::OnceCell::new(), reach: std::cell::OnceCell::new(), uv: std::cell::OnceCell::new() }
707 }
708
709 fn lane(&self, face: &FaceRecord) -> Option<&ContainmentLane> {
712 self.lane.get_or_init(|| containment_lane(face).ok()).as_ref()
713 }
714
715 fn reach(&self, face: &FaceRecord) -> Option<&UvReach> {
717 self.reach.get_or_init(|| self.lane(face).and_then(|lane| UvReach::of(face, lane))).as_ref()
718 }
719
720 fn uv_trim(&self, face: &FaceRecord) -> Option<&UvTrim> {
722 self.uv
723 .get_or_init(|| {
724 let reach = self.reach(face)?;
725 UvTrim::of(face, reach, self.lane(face)?)
726 })
727 .as_ref()
728 }
729}
730
731struct UvTrim {
769 segments: Vec<([f64; 2], [f64; 2], usize)>,
771 loops: usize,
772 grid: Grid,
773 margin: Margin,
775 images: Vec<f64>,
777}
778
779#[derive(Debug, Clone, Copy)]
780enum Margin {
781 Plus(f64),
783 Twice,
785}
786
787impl UvTrim {
788 fn of(face: &FaceRecord, reach: &UvReach, lane: &ContainmentLane) -> Option<UvTrim> {
789 match lane {
790 ContainmentLane::WrappedHorizon { loops, u_period, cross_frame: true } => {
791 let loops = loops.iter().map(|lp| lp.iter().map(|p| [p.x, p.y]).collect()).collect();
792 return Some(UvTrim::grid_of(loops, Margin::Twice, vec![-u_period, 0.0, *u_period]));
793 }
794 ContainmentLane::Generic { sphere_chart: false } => {}
795 _ => return None,
796 }
797 let surface = &face.surface;
798 let centre = [0.5 * (reach.lo[0] + reach.hi[0]), 0.5 * (reach.lo[1] + reach.hi[1])];
801 let fine = trim_band(surface, centre, KernelTolerances::default().pcurve_consistency);
802 let mut rings: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
803 let mut sag: f64 = 0.0;
804 for lp in &face.loops {
805 let mut ring = Vec::new();
806 for coedge in &lp.coedges {
807 let curve = &coedge.pcurve;
808 let [start, end] = curve.domain().ok()?;
809 let count = trim_sample_count(curve);
810 let station = |i: usize| if i < count { trim_station(start, end, i, count) } else { end };
811 let at = |i: usize| -> Option<[f64; 2]> { curve.evaluate(station(i)).ok().map(|p| [p.x, p.y]) };
812 let mut stride = 1;
815 while count % (stride * 2) == 0 && count / (stride * 2) >= 2 {
816 stride *= 2;
817 }
818 let mut a = at(0)?;
819 if let Some(previous) = ring.last() {
823 sag = sag.max(paper_distance(*previous, a));
824 }
825 for from in (0..count).step_by(stride) {
826 let to = from + stride;
827 let b = at(to)?;
828 let mut pending = vec![(from, a, to, b)];
832 while let Some((i, pi, j, pj)) = pending.pop() {
833 if j - i >= 2 {
834 let m = (i + j) / 2;
835 let pm = at(m)?;
836 let off = segment_foot(pm, pi, pj).0;
837 if off > fine {
838 pending.push((m, pm, j, pj));
839 pending.push((i, pi, m, pm));
840 continue;
841 }
842 sag = sag.max(off);
843 } else {
844 let pm = curve.evaluate(0.5 * (station(i) + station(j))).ok()?;
845 sag = sag.max(segment_foot([pm.x, pm.y], pi, pj).0);
846 }
847 ring.push(pi);
848 }
849 a = b;
850 }
851 ring.push(a);
852 }
853 if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
854 sag = sag.max(paper_distance(*first, *last));
855 }
856 if ring.len() >= 2 {
857 rings.push(ring);
858 }
859 }
860 let area = |ring: &[[f64; 2]]| -> f64 {
861 (0..ring.len()).map(|i| {
862 let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
863 a[0] * b[1] - b[0] * a[1]
864 }).sum()
865 };
866 let areas: Vec<f64> = rings.iter().map(|ring| area(ring)).collect();
867 let outer = (0..rings.len()).max_by(|&i, &j| areas[i].abs().partial_cmp(&areas[j].abs()).unwrap_or(std::cmp::Ordering::Equal))?;
868 let left = if face.same_sense { 1.0 } else { -1.0 };
869 for (index, a) in areas.iter().enumerate() {
870 let expected = if index == outer { left } else { -left };
871 if !(a * expected > 0.0) {
872 return None;
873 }
874 }
875 Some(UvTrim::grid_of(rings, Margin::Plus(sag), vec![0.0]))
876 }
877
878 fn grid_of(rings: Vec<Vec<[f64; 2]>>, margin: Margin, images: Vec<f64>) -> UvTrim {
879 let mut segments = Vec::new();
880 for (index, ring) in rings.iter().enumerate() {
881 for i in 0..ring.len() {
882 segments.push((ring[i], ring[(i + 1) % ring.len()], index));
883 }
884 }
885 let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
886 let boxed = |(a, b, _): &([f64; 2], [f64; 2], usize)| [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])];
887 for segment in &segments {
888 let g = boxed(segment);
889 extent = [extent[0].min(g[0]), extent[1].min(g[1]), extent[2].max(g[2]), extent[3].max(g[3])];
890 }
891 let mut grid = Grid::new(extent, segments.len().max(1));
892 for (index, segment) in segments.iter().enumerate() {
893 grid.insert(boxed(segment), index);
894 }
895 UvTrim { segments, loops: rings.len(), grid, margin, images }
896 }
897
898 fn covers(&self, uv: [f64; 2], band: f64) -> Option<bool> {
901 let near = match self.margin {
902 Margin::Plus(sag) => band + sag,
903 Margin::Twice => 2.0 * band,
904 };
905 let mut odd = vec![0u32; self.loops];
906 for shift in &self.images {
907 let image = [uv[0] + shift, uv[1]];
908 let (x0, y0, x1, y1) = self.grid.range([image[0] - near, image[1] - near, image[0] + near, image[1] + near]);
909 for y in y0..=y1 {
910 for x in x0..=x1 {
911 for &index in &self.grid.buckets[y * self.grid.width + x] {
912 let (a, b, _) = self.segments[index];
913 if segment_foot(image, a, b).0 <= near {
914 return None;
915 }
916 }
917 }
918 }
919 let (column, row, _, _) = self.grid.range([image[0], image[1], image[0], image[1]]);
921 let mut inside = vec![false; self.loops];
922 for x in column..self.grid.width {
923 for &index in &self.grid.buckets[row * self.grid.width + x] {
924 let (a, b, ring) = self.segments[index];
925 if (a[1] > image[1]) == (b[1] > image[1]) {
926 continue;
927 }
928 let crossing = a[0] + (image[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]);
929 if crossing > image[0] && self.grid.range([crossing, image[1], crossing, image[1]]).0 == x {
930 inside[ring] = !inside[ring];
931 }
932 }
933 }
934 for (ring, inside) in inside.into_iter().enumerate() {
935 odd[ring] += inside as u32;
936 }
937 }
938 Some(odd.iter().map(|hits| hits % 2).sum::<u32>() % 2 == 1)
939 }
940}
941
942#[derive(Debug, Clone)]
973struct PaperTrim {
974 segments: Vec<([f64; 2], [f64; 2], i32)>,
977 grid: Grid,
979 band: f64,
980}
981
982impl PaperTrim {
983 fn of(solid: &BrepSolid, face: &FaceRecord, paper_of_edge: &HashMap<u64, &[[f64; 2]]>, floors: &Floors) -> Option<PaperTrim> {
987 let band = CHORD_MM + 2.0 * floors.crossing_mm;
988 let gap = 2.0 * floors.crossing_mm + CHORD_MM;
989 let mut rings: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
990 for lp in &face.loops {
991 let mut pieces: Vec<&[[f64; 2]]> = Vec::with_capacity(lp.coedges.len());
992 for coedge in &lp.coedges {
993 match paper_of_edge.get(&coedge.edge_id) {
994 Some(paper) if paper.len() >= 2 => pieces.push(paper),
995 _ if solid.edges.iter().any(|edge| edge.id == coedge.edge_id && edge.degenerate) => {}
996 _ => return None,
997 }
998 }
999 let Some(first) = pieces.first() else { continue };
1000 let ends = |piece: &[[f64; 2]]| (piece[0], piece[piece.len() - 1]);
1004 let first_forward = pieces.get(1).is_none_or(|next| {
1005 let (n0, n1) = ends(next);
1006 let near = |p: [f64; 2]| paper_distance(p, n0).min(paper_distance(p, n1));
1007 near(ends(first).1) <= near(ends(first).0)
1008 });
1009 let mut ring: Vec<[f64; 2]> = if first_forward { first.to_vec() } else { first.iter().rev().copied().collect() };
1010 for piece in &pieces[1..] {
1011 let last = ring[ring.len() - 1];
1012 let (p0, p1) = ends(piece);
1013 let forward = paper_distance(last, p0) <= paper_distance(last, p1);
1014 if paper_distance(last, if forward { p0 } else { p1 }) > gap {
1015 return None;
1016 }
1017 if forward {
1018 ring.extend_from_slice(piece);
1019 } else {
1020 ring.extend(piece.iter().rev());
1021 }
1022 }
1023 if paper_distance(ring[0], ring[ring.len() - 1]) > gap {
1024 return None;
1025 }
1026 rings.push(ring);
1027 }
1028 let areas: Vec<f64> = rings
1030 .iter()
1031 .map(|ring| (0..ring.len()).map(|i| {
1032 let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
1033 a[0] * b[1] - b[0] * a[1]
1034 }).sum())
1035 .collect();
1036 let outer = (0..rings.len()).max_by(|&i, &j| areas[i].abs().partial_cmp(&areas[j].abs()).unwrap_or(std::cmp::Ordering::Equal))?;
1037 let mut segments: Vec<([f64; 2], [f64; 2], i32)> = Vec::new();
1038 for (index, ring) in rings.iter().enumerate() {
1039 let turned = if areas[index] < 0.0 { -1 } else { 1 };
1040 let sign = if index == outer { turned } else { -turned };
1041 segments.extend((0..ring.len()).map(|i| (ring[i], ring[(i + 1) % ring.len()], sign)));
1042 }
1043 let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1044 let grown = |(a, b, _): &([f64; 2], [f64; 2], i32)| {
1045 [a[0].min(b[0]) - band, a[1].min(b[1]) - band, a[0].max(b[0]) + band, a[1].max(b[1]) + band]
1046 };
1047 for segment in &segments {
1048 let g = grown(segment);
1049 extent = [extent[0].min(g[0]), extent[1].min(g[1]), extent[2].max(g[2]), extent[3].max(g[3])];
1050 }
1051 let mut grid = Grid::new(extent, segments.len());
1052 for (index, segment) in segments.iter().enumerate() {
1053 grid.insert(grown(segment), index);
1054 }
1055 Some(PaperTrim { segments, grid, band })
1056 }
1057
1058 fn covers(&self, p: [f64; 2]) -> Option<bool> {
1062 if self.grid.at(p).iter().any(|&index| {
1065 let (a, b, _) = self.segments[index];
1066 segment_foot(p, a, b).0 <= self.band
1067 }) {
1068 return None;
1069 }
1070 let (column, row, _, _) = self.grid.range([p[0], p[1], p[0], p[1]]);
1073 let mut winding = 0;
1074 for x in column..self.grid.width {
1075 for &index in &self.grid.buckets[row * self.grid.width + x] {
1076 let (a, b, sign) = self.segments[index];
1077 if (a[1] > p[1]) == (b[1] > p[1]) {
1078 continue;
1079 }
1080 let crossing = a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]);
1081 if crossing > p[0] && self.grid.range([crossing, p[1], crossing, p[1]]).0 == x {
1082 winding += if b[1] > p[1] { sign } else { -sign };
1083 }
1084 }
1085 }
1086 Some(winding != 0)
1087 }
1088}
1089
1090fn trim_band(surface: &NurbsSurface, uv: [f64; 2], bar: f64) -> f64 {
1093 let speed = surface
1094 .derivatives(uv[0], uv[1], 1)
1095 .ok()
1096 .map(|d| d[1][0].length().max(d[0][1].length()))
1097 .unwrap_or(0.0);
1098 if speed > 1e-12 {
1099 bar / speed
1100 } else {
1101 1e-9
1102 }
1103}
1104
1105fn in_face(face: &FaceRecord, tests: Option<&TrimTests>, uv: [f64; 2], bar: f64) -> bool {
1109 let band = trim_band(&face.surface, uv, bar);
1110 if let Some(tests) = tests {
1111 if tests.reach(face).is_some_and(|reach| reach.outside(uv, band)) {
1112 return false;
1113 }
1114 if let Some(inside) = tests.uv_trim(face).and_then(|trim| trim.covers(uv, band)) {
1115 return inside;
1116 }
1117 }
1118 TRIM_QUERIES.with(|count| count.set(count.get() + 1));
1119 !matches!(
1120 parameter_point_in_face(face, Vec2 { x: uv[0], y: uv[1] }, band),
1121 Ok(PolygonClass::Outside) | Err(_)
1122 )
1123}
1124
1125#[derive(Debug, Clone)]
1139struct UvReach {
1140 lo: [f64; 2],
1141 hi: [f64; 2],
1142 period: [Option<f64>; 2],
1143}
1144
1145impl UvReach {
1146 fn of(face: &FaceRecord, lane: &ContainmentLane) -> Option<UvReach> {
1153 if !matches!(lane, ContainmentLane::Generic { sphere_chart: false } | ContainmentLane::WrappedHorizon { .. }) {
1154 return None;
1155 }
1156 let surface = &face.surface;
1157 let closed = surface.closed_directions().ok()?;
1158 let (du, dv) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1159 let mut lo = [f64::INFINITY; 2];
1160 let mut hi = [f64::NEG_INFINITY; 2];
1161 for cp in face.loops.iter().flat_map(|lp| &lp.coedges).flat_map(|coedge| &coedge.pcurve.control_points) {
1162 if !(cp.w > 0.0) {
1163 return None;
1164 }
1165 let (u, v) = (cp.x / cp.w, cp.y / cp.w);
1166 lo = [lo[0].min(u), lo[1].min(v)];
1167 hi = [hi[0].max(u), hi[1].max(v)];
1168 }
1169 if !(lo[0] <= hi[0] && lo[1] <= hi[1]) {
1170 return None;
1171 }
1172 let period = [closed.0.then_some(du[1] - du[0]), closed.1.then_some(dv[1] - dv[0])];
1173 Some(UvReach { lo, hi, period })
1174 }
1175
1176 fn outside(&self, uv: [f64; 2], band: f64) -> bool {
1177 (0..2).any(|k| {
1178 let (lo, hi) = (self.lo[k] - band, self.hi[k] + band);
1179 match self.period[k] {
1180 None => uv[k] < lo || uv[k] > hi,
1181 Some(period) if period > 0.0 && hi - lo < period => {
1182 uv[k] - ((uv[k] - lo) / period).floor() * period > hi
1183 }
1184 Some(_) => false,
1185 }
1186 })
1187 }
1188}
1189
1190fn uv_on(face: &FaceRecord, p: Vec3, bar: f64) -> Option<[f64; 2]> {
1192 let foot = project_point_to_surface(&face.surface, p).ok()?;
1193 (foot.distance <= bar).then_some([foot.u, foot.v])
1194}
1195
1196fn silhouette_candidates<'a>(
1199 face: &'a FaceRecord,
1200 tests: &TrimTests,
1201 frame: &ViewFrame,
1202 placed: &PlacedView,
1203 floors: &Floors,
1204) -> Vec<Candidate<'a>> {
1205 let surface = &face.surface;
1206 if planar(surface) {
1207 return Vec::new();
1208 }
1209 let dir = v3(frame.forward);
1210 let mut carriers: Vec<(Carrier<'a>, f64, f64, usize)> = Vec::new();
1211 match surface.analytic() {
1212 Some(AnalyticSurface::RuledRevolution { frame: rev, rho0, rho1, height }) => {
1213 let a = height * dir.dot(rev.x_axis);
1215 let b = height * dir.dot(rev.y_axis);
1216 let c = (rho1 - rho0) * dir.dot(rev.axis);
1217 let r = (a * a + b * b).sqrt();
1218 if r > 1e-12 * (height.abs() + 1.0) && c.abs() <= r {
1219 let base = b.atan2(a);
1220 let spread = (c / r).clamp(-1.0, 1.0).acos();
1221 let mut angles = vec![base + spread];
1222 if spread > 1e-12 {
1223 angles.push(base - spread);
1224 }
1225 for theta in angles {
1226 let radial = rev.x_axis.scale(theta.cos()).add(rev.y_axis.scale(theta.sin()));
1227 let start = rev.origin.add(radial.scale(*rho0));
1228 let end = rev.origin.add(rev.axis.scale(*height)).add(radial.scale(*rho1));
1229 carriers.push((Carrier::Line { a: start, b: end }, 0.0, 1.0, 1));
1230 }
1231 }
1232 }
1233 Some(AnalyticSurface::Sphere { frame: rev, radius }) => {
1234 let Ok(e1) = dir.perpendicular().and_then(|e| e.normalized()) else { return Vec::new() };
1235 let e2 = dir.cross(e1);
1236 carriers.push((
1237 Carrier::Circle { centre: rev.origin, e1: e1.scale(*radius), e2: e2.scale(*radius) },
1238 0.0,
1239 std::f64::consts::TAU,
1240 16,
1241 ));
1242 }
1243 _ => {
1244 let traced = trace_silhouette(face, dir);
1245 for uv in traced {
1246 let n = uv.len();
1247 if n < 2 {
1248 continue;
1249 }
1250 carriers.push((Carrier::Traced { surface, uv, dir }, 0.0, (n - 1) as f64, n - 1));
1251 }
1252 }
1253 }
1254 let mut out = Vec::new();
1255 for (carrier, s0, s1, initial) in carriers {
1256 let samples = sample(&carrier, s0, s1, initial, frame, placed);
1257 let spans_found = trimmed_spans(&carrier, &samples, face, tests, floors.bar);
1258 for (lo, hi) in spans_found {
1259 let piece = sample(&carrier, lo, hi, initial_between(&samples, lo, hi), frame, placed);
1260 if piece.len() < 2 || piece.paper_length() < floors.crossing_mm {
1261 continue;
1262 }
1263 let bounds = bounds_of(&piece.paper);
1264 out.push(Candidate {
1265 carrier: carrier.clone(),
1266 samples: piece,
1267 inked: true,
1268 silhouette: true,
1269 edge: None,
1270 ends: None,
1271 bounds,
1272 whole: false,
1273 });
1274 }
1275 }
1276 out
1277}
1278
1279fn initial_between(samples: &Samples, lo: f64, hi: f64) -> usize {
1282 samples.params.iter().filter(|s| **s > lo && **s < hi).count() + 1
1283}
1284
1285fn trimmed_spans(carrier: &Carrier, samples: &Samples, face: &FaceRecord, tests: &TrimTests, bar: f64) -> Vec<(f64, f64)> {
1288 let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
1292 let mut stations: Vec<f64> = (0..=TRIM_STATIONS).map(|k| s0 + (s1 - s0) * k as f64 / TRIM_STATIONS as f64).collect();
1293 stations.extend(samples.params.iter().copied());
1294 stations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1295 stations.dedup();
1296 let inside = |s: f64| -> bool {
1297 let uv = match carrier {
1298 Carrier::Traced { surface, uv, dir } => traced_at(surface, uv, *dir, s).map(|(_, uv)| uv),
1299 _ => carrier.at(s).and_then(|p| uv_on(face, p, bar)),
1300 };
1301 uv.is_some_and(|uv| in_face(face, Some(tests), uv, bar))
1302 };
1303 let flags: Vec<bool> = stations.iter().map(|s| inside(*s)).collect();
1304 let mut spans = Vec::new();
1305 let mut start: Option<f64> = None;
1306 for i in 0..flags.len() {
1307 if i > 0 && flags[i] != flags[i - 1] {
1308 let (mut lo, mut hi) = (stations[i - 1], stations[i]);
1309 let lo_flag = flags[i - 1];
1310 for _ in 0..BISECTION_STEPS {
1311 let mid = 0.5 * (lo + hi);
1312 if inside(mid) == lo_flag {
1313 lo = mid;
1314 } else {
1315 hi = mid;
1316 }
1317 }
1318 let cut = 0.5 * (lo + hi);
1319 if flags[i] {
1320 start = Some(cut);
1321 } else if let Some(s) = start.take() {
1322 spans.push((s, cut));
1323 }
1324 } else if i == 0 && flags[0] {
1325 start = Some(stations[0]);
1326 }
1327 }
1328 if let (Some(s), Some(last)) = (start, stations.last()) {
1329 spans.push((s, *last));
1330 }
1331 spans
1332}
1333
1334fn trace_silhouette(face: &FaceRecord, dir: Vec3) -> Vec<Vec<[f64; 2]>> {
1338 let surface = &face.surface;
1339 let Some(uv_box) = trim_box(face) else { return Vec::new() };
1340 let spans = |knots: &[f64], degree: usize| knots.len().saturating_sub(2 * degree + 1).max(1);
1341 let nu = (spans(&surface.knots_u, surface.degree_u) * (surface.degree_u + 1) * 2).clamp(12, 96);
1342 let nv = (spans(&surface.knots_v, surface.degree_v) * (surface.degree_v + 1) * 2).clamp(12, 96);
1343 let at = |i: usize, j: usize| {
1344 [
1345 uv_box[0] + (uv_box[2] - uv_box[0]) * i as f64 / nu as f64,
1346 uv_box[1] + (uv_box[3] - uv_box[1]) * j as f64 / nv as f64,
1347 ]
1348 };
1349 let mut values = vec![None; (nu + 1) * (nv + 1)];
1350 for j in 0..=nv {
1351 for i in 0..=nu {
1352 let p = at(i, j);
1353 values[j * (nu + 1) + i] = grazing(surface, dir, p[0], p[1]);
1354 }
1355 }
1356 let value = |i: usize, j: usize| values[j * (nu + 1) + i];
1357 let mut nodes: HashMap<(usize, usize, bool), [f64; 2]> = HashMap::new();
1359 let mut crossing = |i: usize, j: usize, horizontal: bool| -> Option<(usize, usize, bool)> {
1360 let (i2, j2) = if horizontal { (i + 1, j) } else { (i, j + 1) };
1361 let (g0, g1) = (value(i, j)?, value(i2, j2)?);
1362 if (g0 < 0.0) == (g1 < 0.0) {
1363 return None;
1364 }
1365 let key = (i, j, horizontal);
1366 if !nodes.contains_key(&key) {
1367 let (a, b) = (at(i, j), at(i2, j2));
1368 let (mut lo, mut hi, mut g_lo) = (0.0, 1.0, g0);
1369 for _ in 0..BISECTION_STEPS {
1370 let mid = 0.5 * (lo + hi);
1371 let p = [a[0] + (b[0] - a[0]) * mid, a[1] + (b[1] - a[1]) * mid];
1372 let Some(g) = grazing(surface, dir, p[0], p[1]) else { break };
1373 if (g < 0.0) == (g_lo < 0.0) {
1374 lo = mid;
1375 g_lo = g;
1376 } else {
1377 hi = mid;
1378 }
1379 }
1380 let t = 0.5 * (lo + hi);
1381 nodes.insert(key, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
1382 }
1383 Some(key)
1384 };
1385 let mut links: Vec<[(usize, usize, bool); 2]> = Vec::new();
1386 for j in 0..nv {
1387 for i in 0..nu {
1388 let edges = [
1389 crossing(i, j, true),
1390 crossing(i + 1, j, false),
1391 crossing(i, j + 1, true),
1392 crossing(i, j, false),
1393 ];
1394 let hits: Vec<_> = edges.iter().flatten().copied().collect();
1395 match hits.len() {
1396 2 => links.push([hits[0], hits[1]]),
1397 4 => {
1398 let centre = at(i, j);
1400 let half = [
1401 centre[0] + (uv_box[2] - uv_box[0]) * 0.5 / nu as f64,
1402 centre[1] + (uv_box[3] - uv_box[1]) * 0.5 / nv as f64,
1403 ];
1404 let g = grazing(surface, dir, half[0], half[1]).unwrap_or(0.0);
1405 let corner = value(i, j).unwrap_or(0.0);
1406 if (g < 0.0) == (corner < 0.0) {
1407 links.push([hits[0], hits[1]]);
1408 links.push([hits[2], hits[3]]);
1409 } else {
1410 links.push([hits[0], hits[3]]);
1411 links.push([hits[1], hits[2]]);
1412 }
1413 }
1414 _ => {}
1415 }
1416 }
1417 }
1418 let mut incident: HashMap<(usize, usize, bool), Vec<usize>> = HashMap::new();
1420 for (index, link) in links.iter().enumerate() {
1421 incident.entry(link[0]).or_default().push(index);
1422 incident.entry(link[1]).or_default().push(index);
1423 }
1424 let mut used = vec![false; links.len()];
1425 let mut order: Vec<usize> = (0..links.len()).collect();
1426 order.sort_by_key(|&i| {
1429 let open = incident[&links[i][0]].len() == 1 || incident[&links[i][1]].len() == 1;
1430 (!open, i)
1431 });
1432 let mut out = Vec::new();
1433 for start in order {
1434 if used[start] {
1435 continue;
1436 }
1437 used[start] = true;
1438 let [a, b] = links[start];
1439 let (head, mut tail) = if incident[&a].len() == 1 { (a, b) } else { (b, a) };
1440 let mut path = vec![head, tail];
1441 while let Some(next) = incident[&tail].iter().copied().find(|i| !used[*i]) {
1442 used[next] = true;
1443 tail = if links[next][0] == tail { links[next][1] } else { links[next][0] };
1444 path.push(tail);
1445 if tail == head {
1446 break;
1447 }
1448 }
1449 out.push(path.into_iter().map(|key| nodes[&key]).collect());
1450 }
1451 out
1452}
1453
1454struct Occluder<'a> {
1460 cover: Cover<'a>,
1461 bounds: [f64; 4],
1462 whole: bool,
1464}
1465
1466enum Cover<'a> {
1468 Face {
1470 face: &'a FaceRecord,
1471 plane: Option<(Vec3, Vec3)>,
1473 spans: Vec<SpanHull>,
1482 tests: &'a TrimTests,
1484 trim: Option<PaperTrim>,
1486 },
1487 Triangle { corners: [Vec3; 3], normal: Vec3 },
1490}
1491
1492fn face_bounds(
1496 face: &FaceRecord,
1497 boundary: &[[f64; 4]],
1498 silhouettes: &[[f64; 4]],
1499 frame: &ViewFrame,
1500 placed: &PlacedView,
1501) -> [f64; 4] {
1502 let exact_outline = planar(&face.surface)
1503 || matches!(face.surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. } | AnalyticSurface::Sphere { .. }));
1504 let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1505 let mut grow = |c: [f64; 4]| {
1506 b[0] = b[0].min(c[0]);
1507 b[1] = b[1].min(c[1]);
1508 b[2] = b[2].max(c[2]);
1509 b[3] = b[3].max(c[3]);
1510 };
1511 if exact_outline && !boundary.is_empty() {
1512 for c in boundary.iter().chain(silhouettes) {
1513 grow(*c);
1514 }
1515 } else {
1516 for row in &face.surface.control_points {
1517 for cp in row {
1518 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1519 let (paper, _) = project(frame, placed, Vec3::new(cp.x / w, cp.y / w, cp.z / w));
1520 grow([paper[0], paper[1], paper[0], paper[1]]);
1521 }
1522 }
1523 }
1524 b
1525}
1526
1527struct SpanHull {
1529 bounds: [f64; 4],
1530 hull: Vec<[f64; 2]>,
1532 near: f64,
1534}
1535
1536impl SpanHull {
1537 fn holds(&self, p: [f64; 2], pad: f64) -> bool {
1539 let b = self.bounds;
1540 if p[0] < b[0] - pad || p[0] > b[2] + pad || p[1] < b[1] - pad || p[1] > b[3] + pad {
1541 return false;
1542 }
1543 let n = self.hull.len();
1544 if n >= 3 && (0..n).all(|k| {
1545 let (a, c) = (self.hull[k], self.hull[(k + 1) % n]);
1546 (c[0] - a[0]) * (p[1] - a[1]) - (c[1] - a[1]) * (p[0] - a[0]) >= 0.0
1547 }) {
1548 return true;
1549 }
1550 (0..n.max(1)).any(|k| segment_foot(p, self.hull[k], self.hull[(k + 1) % n]).0 <= pad)
1551 }
1552}
1553
1554fn convex_hull(mut points: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
1556 points.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1557 points.dedup();
1558 if points.len() < 3 {
1559 return points;
1560 }
1561 let turn = |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]);
1562 let mut hull: Vec<[f64; 2]> = Vec::with_capacity(points.len() + 1);
1563 for pass in 0..2 {
1564 let start = hull.len();
1565 let ordered: Box<dyn Iterator<Item = &[f64; 2]>> =
1566 if pass == 0 { Box::new(points.iter()) } else { Box::new(points.iter().rev()) };
1567 for &p in ordered {
1568 while hull.len() >= start + 2 && turn(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
1569 hull.pop();
1570 }
1571 hull.push(p);
1572 }
1573 hull.pop();
1574 }
1575 hull
1576}
1577
1578fn span_hulls(face: &FaceRecord, frame: &ViewFrame, placed: &PlacedView) -> Vec<SpanHull> {
1581 let surface = &face.surface;
1582 let uv_box = trim_box(face).unwrap_or([f64::NEG_INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::INFINITY]);
1583 let spans = |knots: &[f64], degree: usize, count: usize, lo: f64, hi: f64| -> Vec<usize> {
1584 (degree..count)
1587 .filter(|&i| knots[i + 1] > knots[i] && knots[i + 1] >= lo && knots[i] <= hi)
1588 .collect()
1589 };
1590 let rows = surface.control_points.len();
1591 let columns = surface.control_points.first().map_or(0, Vec::len);
1592 let su = spans(&surface.knots_u, surface.degree_u, rows, uv_box[0], uv_box[2]);
1593 let sv = spans(&surface.knots_v, surface.degree_v, columns, uv_box[1], uv_box[3]);
1594 let mut out = Vec::with_capacity(su.len() * sv.len());
1595 for &i in &su {
1596 for &j in &sv {
1597 let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1598 let mut near = f64::INFINITY;
1599 let mut points = Vec::with_capacity((surface.degree_u + 1) * (surface.degree_v + 1));
1600 for row in &surface.control_points[i - surface.degree_u..=i] {
1601 for cp in &row[j - surface.degree_v..=j] {
1602 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1603 let (paper, view) = project(frame, placed, Vec3::new(cp.x / w, cp.y / w, cp.z / w));
1604 b = [b[0].min(paper[0]), b[1].min(paper[1]), b[2].max(paper[0]), b[3].max(paper[1])];
1605 near = near.min(view[2]);
1606 points.push(paper);
1607 }
1608 }
1609 out.push(SpanHull { bounds: b, hull: convex_hull(points), near });
1610 }
1611 }
1612 out
1613}
1614
1615#[derive(Debug, Clone)]
1617struct Grid {
1618 origin: [f64; 2],
1619 cell: f64,
1620 width: usize,
1621 height: usize,
1622 buckets: Vec<Vec<usize>>,
1623}
1624
1625impl Grid {
1626 fn new(extent: [f64; 4], count: usize) -> Grid {
1627 let span = (extent[2] - extent[0]).max(extent[3] - extent[1]).max(1e-9);
1628 let per_side = ((count as f64).sqrt().ceil() as usize).clamp(1, 256);
1629 let cell = span / per_side as f64;
1630 let width = (((extent[2] - extent[0]) / cell).ceil() as usize).clamp(1, 512);
1631 let height = (((extent[3] - extent[1]) / cell).ceil() as usize).clamp(1, 512);
1632 Grid { origin: [extent[0], extent[1]], cell, width, height, buckets: vec![Vec::new(); width * height] }
1633 }
1634
1635 fn range(&self, b: [f64; 4]) -> (usize, usize, usize, usize) {
1636 let clamp = |v: f64, n: usize| (v.floor().max(0.0) as usize).min(n - 1);
1637 (
1638 clamp((b[0] - self.origin[0]) / self.cell, self.width),
1639 clamp((b[1] - self.origin[1]) / self.cell, self.height),
1640 clamp((b[2] - self.origin[0]) / self.cell, self.width),
1641 clamp((b[3] - self.origin[1]) / self.cell, self.height),
1642 )
1643 }
1644
1645 fn insert(&mut self, b: [f64; 4], id: usize) {
1646 let (x0, y0, x1, y1) = self.range(b);
1647 for y in y0..=y1 {
1648 for x in x0..=x1 {
1649 self.buckets[y * self.width + x].push(id);
1650 }
1651 }
1652 }
1653
1654 fn at(&self, p: [f64; 2]) -> &[usize] {
1655 let (x, y, _, _) = self.range([p[0], p[1], p[0], p[1]]);
1656 &self.buckets[y * self.width + x]
1657 }
1658}
1659
1660struct Scene<'a> {
1662 frame: ViewFrame,
1663 placed: &'a PlacedView,
1664 floors: Floors,
1665 occluders: Vec<Occluder<'a>>,
1666 grid: Grid,
1667 near: f64,
1670 clip: &'a Clip<'a>,
1671}
1672
1673impl Scene<'_> {
1674 fn hidden(&self, p: Vec3, hint: &mut Option<usize>) -> bool {
1679 let (paper, view) = project(&self.frame, self.placed, p);
1680 let bar = self.floors.bar;
1681 if let Clip::Beyond { cap, .. } = self.clip {
1682 if view[2] > bar && inside_region(cap, paper) {
1683 return true;
1684 }
1685 }
1686 let forward = v3(self.frame.forward);
1687 let in_front = |hit: Vec3, whole: bool| {
1691 let depth = dot3(sub3(a3(hit), self.frame.target), self.frame.forward);
1692 depth <= view[2] - bar
1693 && !(matches!(self.clip, Clip::Beyond { .. })
1694 && !whole
1695 && depth < -bar
1696 && self.clip.margin(self.frame.to_view(a3(hit))) < -bar)
1697 };
1698 let plane_hit = |origin: Vec3, normal: Vec3| -> Option<Vec3> {
1700 let denominator = normal.dot(forward);
1701 if denominator.abs() <= GRAZING_COS {
1702 return None;
1703 }
1704 let lambda = normal.dot(origin.sub(p)) / denominator;
1705 (lambda < -bar).then(|| p.add(forward.scale(lambda)))
1706 };
1707 let reach = view[2] - self.near;
1711 let ray = std::cell::OnceCell::new();
1712 let curved_hides = |face: &FaceRecord, tests: &TrimTests, whole: bool| -> bool {
1713 if reach <= bar {
1714 return false;
1715 }
1716 let Some(ray) = ray.get_or_init(|| make_line(p.sub(forward.scale(reach)), p.sub(forward.scale(bar))).ok()) else {
1717 return false;
1718 };
1719 RAY_QUERIES.with(|count| count.set(count.get() + 1));
1720 let fit = KernelTolerances::default().intersection_fit;
1721 let hits = intersect_curve_surface(ray, &face.surface, fit);
1722 hits.is_ok_and(|hits| {
1723 hits.into_iter()
1724 .filter(|hit| !hit.tangential)
1725 .any(|hit| in_front(hit.point, whole) && in_face(face, Some(tests), [hit.u, hit.v], bar))
1726 })
1727 };
1728 let flat_hides = |face: &FaceRecord, tests: &TrimTests, hit: Vec3| -> bool {
1730 uv_on(face, hit, bar).is_some_and(|uv| in_face(face, Some(tests), uv, bar))
1731 };
1732 let mut curved: Vec<usize> = Vec::new();
1740 let mut flat: Vec<(usize, Vec3)> = Vec::new();
1741 let first = hint.take();
1742 for index in first.into_iter().chain(self.grid.at(paper).iter().copied().filter(|index| Some(*index) != first)) {
1743 let occluder = &self.occluders[index];
1744 let pad = self.floors.crossing_mm + CHORD_MM;
1745 let holds = |b: &[f64; 4]| {
1746 paper[0] >= b[0] - pad && paper[0] <= b[2] + pad && paper[1] >= b[1] - pad && paper[1] <= b[3] + pad
1747 };
1748 if !holds(&occluder.bounds) {
1749 continue;
1750 }
1751 let hides = match &occluder.cover {
1752 Cover::Triangle { corners, normal } => {
1753 plane_hit(corners[0], *normal).is_some_and(|hit| in_front(hit, occluder.whole) && in_triangle(corners, *normal, hit, bar))
1754 }
1755 Cover::Face { face, plane, spans, tests, trim } => {
1756 if !spans.iter().any(|span| span.near <= view[2] - bar && span.holds(paper, pad)) {
1757 continue;
1758 }
1759 let asked_first = Some(index) == first;
1760 match plane {
1761 Some((origin, normal)) => {
1762 let Some(hit) = plane_hit(*origin, *normal).filter(|hit| in_front(*hit, occluder.whole)) else { continue };
1763 if trim.as_ref().and_then(|trim| trim.covers(paper)) == Some(true) {
1764 true
1765 } else if asked_first {
1766 flat_hides(face, tests, hit)
1767 } else {
1768 flat.push((index, hit));
1769 continue;
1770 }
1771 }
1772 None if asked_first => curved_hides(face, tests, occluder.whole),
1773 None => {
1774 curved.push(index);
1775 continue;
1776 }
1777 }
1778 }
1779 };
1780 if hides {
1781 *hint = Some(index);
1782 return true;
1783 }
1784 }
1785 let face_of = |index: usize| match &self.occluders[index].cover {
1786 Cover::Face { face, tests, .. } => Some((*face, tests)),
1787 Cover::Triangle { .. } => None,
1788 };
1789 let hider = curved
1790 .into_iter()
1791 .find(|&index| face_of(index).is_some_and(|(face, tests)| curved_hides(face, tests, self.occluders[index].whole)))
1792 .or_else(|| {
1793 flat.into_iter()
1794 .find(|&(index, hit)| face_of(index).is_some_and(|(face, tests)| flat_hides(face, tests, hit)))
1795 .map(|(index, _)| index)
1796 });
1797 if hider.is_some() {
1798 *hint = hider;
1799 }
1800 hider.is_some()
1801 }
1802}
1803
1804fn in_triangle(corners: &[Vec3; 3], normal: Vec3, hit: Vec3, bar: f64) -> bool {
1809 (0..3).all(|k| {
1810 let (a, b) = (corners[k], corners[(k + 1) % 3]);
1811 let side = b.sub(a);
1812 let length = side.length();
1813 length <= 0.0 || side.cross(hit.sub(a)).dot(normal) / length >= -bar
1814 })
1815}
1816
1817use crate::geometry3d::sub3;
1818
1819use crate::geometry3d::dot3;
1820
1821pub fn inside_region(region: &[Vec<[f64; 2]>], p: [f64; 2]) -> bool {
1824 let mut inside = false;
1825 for line in region {
1826 for w in line.windows(2) {
1827 let (a, b) = (w[0], w[1]);
1828 if (a[1] <= p[1]) == (b[1] <= p[1]) {
1829 continue;
1830 }
1831 let x = a[0] + (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]);
1832 if x > p[0] {
1833 inside = !inside;
1834 }
1835 }
1836 }
1837 inside
1838}
1839
1840pub fn visible_edges(solids: &[&BrepSolid], frame: &ViewFrame, placed: &PlacedView, clip: &Clip) -> HlrResult {
1847 let trims: Vec<SolidTrims> = solids.iter().map(|solid| SolidTrims::of(solid)).collect();
1848 let trims: Vec<&SolidTrims> = trims.iter().collect();
1849 visible_edges_with(solids, &trims, frame, placed, clip)
1850}
1851
1852pub fn visible_edges_with(
1854 solids: &[&BrepSolid],
1855 trims: &[&SolidTrims],
1856 frame: &ViewFrame,
1857 placed: &PlacedView,
1858 clip: &Clip,
1859) -> HlrResult {
1860 TRIM_QUERIES.with(|count| count.set(0));
1861 RAY_QUERIES.with(|count| count.set(0));
1862 let floors = Floors::of(placed.scale);
1863 let mut candidates: Vec<Candidate> = Vec::new();
1864 let mut face_boxes: HashMap<(usize, usize, usize), (Vec<[f64; 4]>, Vec<[f64; 4]>)> = HashMap::new();
1866 let mut paper_trims: HashMap<(usize, usize, usize), Option<PaperTrim>> = HashMap::new();
1868 for (solid_index, solid) in solids.iter().enumerate() {
1869 let owners = edge_faces(solid);
1870 let edges = edge_candidates(solid_index, solid, frame, placed, &floors);
1871 for candidate in &edges {
1872 let Some((_, edge_id)) = candidate.edge else { continue };
1873 for (shell, face) in owners.get(&edge_id).into_iter().flatten() {
1874 face_boxes.entry((solid_index, *shell, *face)).or_default().0.push(candidate.bounds);
1875 }
1876 }
1877 let paper_of_edge: HashMap<u64, &[[f64; 2]]> =
1878 edges.iter().filter_map(|c| c.edge.map(|(_, id)| (id, c.samples.paper.as_slice()))).collect();
1879 let mut silhouettes: Vec<Candidate> = Vec::new();
1880 for (shell_index, shell) in solid.shells.iter().enumerate() {
1881 for (face_index, face) in shell.faces.iter().enumerate() {
1882 let found = silhouette_candidates(face, &trims[solid_index].faces[shell_index][face_index], frame, placed, &floors);
1883 let entry = face_boxes.entry((solid_index, shell_index, face_index)).or_default();
1884 entry.1.extend(found.iter().map(|c| c.bounds));
1885 silhouettes.extend(found);
1886 if planar(&face.surface) {
1887 paper_trims.insert((solid_index, shell_index, face_index), PaperTrim::of(solid, face, &paper_of_edge, &floors));
1888 }
1889 }
1890 }
1891 let whole = clip.whole(solid_index);
1892 candidates.extend(edges.into_iter().chain(silhouettes).map(|c| Candidate { whole, ..c }));
1893 }
1894 let mut occluders: Vec<Occluder> = Vec::new();
1895 let mut near = f64::INFINITY;
1896 let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1897 for (solid_index, solid) in solids.iter().enumerate() {
1898 for (shell_index, shell) in solid.shells.iter().enumerate() {
1899 for (face_index, face) in shell.faces.iter().enumerate() {
1900 let (boundary, silhouettes) =
1901 face_boxes.get(&(solid_index, shell_index, face_index)).cloned().unwrap_or_default();
1902 let bounds = face_bounds(face, &boundary, &silhouettes, frame, placed);
1903 if !(bounds[0] <= bounds[2]) {
1904 continue;
1905 }
1906 let spans = span_hulls(face, frame, placed);
1907 near = spans.iter().fold(near, |near, span| near.min(span.near));
1908 extent = [
1909 extent[0].min(bounds[0]),
1910 extent[1].min(bounds[1]),
1911 extent[2].max(bounds[2]),
1912 extent[3].max(bounds[3]),
1913 ];
1914 let plane = if planar(&face.surface) {
1915 let du = face.surface.domain_u().unwrap_or([0.0, 1.0]);
1916 let dv = face.surface.domain_v().unwrap_or([0.0, 1.0]);
1917 let (u, v) = (0.5 * (du[0] + du[1]), 0.5 * (dv[0] + dv[1]));
1918 match (face.surface.evaluate(u, v), face.surface.normal(u, v)) {
1919 (Ok(origin), Ok(normal)) => Some((origin, normal)),
1920 _ => None,
1921 }
1922 } else {
1923 None
1924 };
1925 let trim = paper_trims.remove(&(solid_index, shell_index, face_index)).flatten();
1926 let tests = &trims[solid_index].faces[shell_index][face_index];
1927 occluders.push(Occluder { cover: Cover::Face { face, plane, spans, tests, trim }, bounds, whole: clip.whole(solid_index) });
1928 }
1929 }
1930 }
1931 decide(candidates, occluders, extent, near, frame, placed, clip)
1932}
1933
1934fn decide(
1938 candidates: Vec<Candidate>,
1939 occluders: Vec<Occluder>,
1940 extent: [f64; 4],
1941 near: f64,
1942 frame: &ViewFrame,
1943 placed: &PlacedView,
1944 clip: &Clip,
1945) -> HlrResult {
1946 let floors = Floors::of(placed.scale);
1947 let mut result = HlrResult::default();
1948 if occluders.is_empty() {
1949 return result;
1950 }
1951 let mut grid = Grid::new(extent, occluders.len());
1952 for (index, occluder) in occluders.iter().enumerate() {
1953 grid.insert(occluder.bounds, index);
1954 }
1955 let scene = Scene {
1956 frame: *frame,
1957 placed,
1958 floors,
1959 occluders,
1960 grid,
1961 near: near - floors.bar * 10.0 - 1.0,
1962 clip,
1963 };
1964
1965 let stations = split_parameters(&candidates, &scene);
1966 result.inked = candidates.iter().filter(|c| c.inked).count();
1967 result.silhouettes = candidates.iter().filter(|c| c.silhouette).count();
1968
1969 let mut valence: HashMap<VertexKey, usize> = HashMap::new();
1973 for candidate in candidates.iter().filter(|c| c.inked) {
1974 if let Some((start, end)) = candidate.ends {
1975 *valence.entry(start).or_default() += 1;
1976 *valence.entry(end).or_default() += 1;
1977 }
1978 }
1979 let mut runs: Vec<Run> = Vec::new();
1980 for (index, candidate) in candidates.iter().enumerate() {
1981 if !candidate.inked {
1982 continue;
1983 }
1984 let pieces = pieces_of(candidate, &stations[index], &scene);
1985 result.pieces += pieces.len();
1986 runs.extend(runs_of(candidate, &pieces, &scene));
1987 }
1988 result.runs = chain_runs(runs, &valence, floors.crossing_mm);
1989 result.trim_queries = TRIM_QUERIES.with(|count| count.get());
1990 result.ray_queries = RAY_QUERIES.with(|count| count.get());
1991 result
1992}
1993
1994#[derive(Debug, Clone, Default)]
2001pub struct MeshSolid {
2002 pub positions: Vec<[f64; 3]>,
2003 pub triangles: Vec<[u32; 3]>,
2004 pub faces: Vec<u32>,
2006 pub edges: Vec<MeshEdge>,
2008}
2009
2010#[derive(Debug, Clone, Default)]
2015pub struct MeshEdge {
2016 pub points: Vec<[f64; 3]>,
2017 pub ends: Option<(u64, u64)>,
2018}
2019
2020pub fn visible_mesh_lines(meshes: &[MeshSolid], frame: &ViewFrame, placed: &PlacedView, clip: &Clip) -> HlrResult {
2038 TRIM_QUERIES.with(|count| count.set(0));
2039 RAY_QUERIES.with(|count| count.set(0));
2040 let forward = v3(frame.forward);
2041 let mut candidates: Vec<Candidate> = Vec::new();
2042 let mut occluders: Vec<Occluder> = Vec::new();
2043 let mut near = f64::INFINITY;
2044 let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2045 for (solid_index, mesh) in meshes.iter().enumerate() {
2046 let points: Vec<Vec3> = mesh.positions.iter().map(|p| v3(*p)).collect();
2047 let welded = WeldedMesh::of(mesh, &points, forward);
2048 for edge in &mesh.edges {
2049 let chain: Vec<Vec3> = edge.points.iter().map(|p| v3(*p)).collect();
2050 if welded.seam(&chain) {
2051 continue;
2052 }
2053 if let Some(candidate) = polyline_candidate(chain, false, frame, placed) {
2056 candidates.push(Candidate {
2057 ends: edge.ends.map(|(start, end)| ((solid_index, start), (solid_index, end))),
2058 whole: clip.whole(solid_index),
2059 ..candidate
2060 });
2061 }
2062 }
2063 for chain in welded.silhouettes() {
2064 candidates.extend(polyline_candidate(chain, true, frame, placed).map(|c| Candidate { whole: clip.whole(solid_index), ..c }));
2065 }
2066 for triangle in &mesh.triangles {
2067 let Some(corners) = triangle.iter().map(|&i| points.get(i as usize).copied()).collect::<Option<Vec<Vec3>>>() else {
2068 continue;
2069 };
2070 let corners = [corners[0], corners[1], corners[2]];
2071 let Ok(normal) = corners[1].sub(corners[0]).cross(corners[2].sub(corners[0])).normalized() else {
2072 continue;
2073 };
2074 let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2075 for corner in corners {
2076 let (paper, view) = project(frame, placed, corner);
2077 bounds = [bounds[0].min(paper[0]), bounds[1].min(paper[1]), bounds[2].max(paper[0]), bounds[3].max(paper[1])];
2078 near = near.min(view[2]);
2079 }
2080 extent = [extent[0].min(bounds[0]), extent[1].min(bounds[1]), extent[2].max(bounds[2]), extent[3].max(bounds[3])];
2081 occluders.push(Occluder { cover: Cover::Triangle { corners, normal }, bounds, whole: clip.whole(solid_index) });
2082 }
2083 }
2084 decide(candidates, occluders, extent, near, frame, placed, clip)
2085}
2086
2087fn polyline_candidate<'a>(chain: Vec<Vec3>, silhouette: bool, frame: &ViewFrame, placed: &PlacedView) -> Option<Candidate<'a>> {
2090 if chain.len() < 2 {
2091 return None;
2092 }
2093 let last = (chain.len() - 1) as f64;
2094 let intervals = chain.len() - 1;
2095 let carrier = Carrier::Polyline(chain);
2096 let samples = sample(&carrier, 0.0, last, intervals, frame, placed);
2097 if samples.len() < 2 || samples.paper_length() <= 0.0 {
2098 return None;
2099 }
2100 let bounds = bounds_of(&samples.paper);
2101 Some(Candidate { carrier, samples, inked: true, silhouette, edge: None, ends: None, bounds, whole: false })
2102}
2103
2104struct WeldedMesh {
2108 index: HashMap<[u64; 3], u32>,
2109 position: Vec<Vec3>,
2110 owners: HashMap<[u32; 2], Vec<(u32, bool)>>,
2112}
2113
2114impl WeldedMesh {
2115 fn of(mesh: &MeshSolid, points: &[Vec3], forward: Vec3) -> WeldedMesh {
2116 let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
2117 let mut index: HashMap<[u64; 3], u32> = HashMap::new();
2118 let mut position = Vec::new();
2119 let welded: Vec<u32> = points
2120 .iter()
2121 .map(|p| {
2122 *index.entry(key(*p)).or_insert_with(|| {
2123 position.push(*p);
2124 (position.len() - 1) as u32
2125 })
2126 })
2127 .collect();
2128 let mut owners: HashMap<[u32; 2], Vec<(u32, bool)>> = HashMap::new();
2129 for (t, triangle) in mesh.triangles.iter().enumerate() {
2130 if triangle.iter().any(|&i| i as usize >= points.len()) {
2131 continue;
2132 }
2133 let [a, b, c] = [points[triangle[0] as usize], points[triangle[1] as usize], points[triangle[2] as usize]];
2134 let facing = b.sub(a).cross(c.sub(a)).dot(forward);
2135 if facing == 0.0 {
2136 continue;
2137 }
2138 let face = mesh.faces.get(t).copied().unwrap_or(u32::MAX);
2139 let w = [welded[triangle[0] as usize], welded[triangle[1] as usize], welded[triangle[2] as usize]];
2140 for k in 0..3 {
2141 let (i, j) = (w[k], w[(k + 1) % 3]);
2142 if i != j {
2143 owners.entry([i.min(j), i.max(j)]).or_default().push((face, facing < 0.0));
2144 }
2145 }
2146 }
2147 WeldedMesh { index, position, owners }
2148 }
2149
2150 fn faces_on(&self, a: Vec3, b: Vec3) -> Option<&[(u32, bool)]> {
2153 let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
2154 let (i, j) = (*self.index.get(&key(a))?, *self.index.get(&key(b))?);
2155 self.owners.get(&[i.min(j), i.max(j)]).map(Vec::as_slice)
2156 }
2157
2158 fn seam(&self, chain: &[Vec3]) -> bool {
2162 chain.windows(2).all(|w| {
2163 self.faces_on(w[0], w[1]).is_some_and(|faces| faces.len() == 2 && faces[0].0 == faces[1].0)
2164 })
2165 }
2166
2167 fn silhouettes(&self) -> Vec<Vec<Vec3>> {
2172 let mut segments: Vec<[u32; 2]> = self
2173 .owners
2174 .iter()
2175 .filter(|(_, list)| list.len() == 2 && list[0].0 == list[1].0 && list[0].1 != list[1].1)
2176 .map(|(edge, _)| *edge)
2177 .collect();
2178 segments.sort_unstable();
2180 let mut incident: HashMap<u32, Vec<usize>> = HashMap::new();
2181 for (i, segment) in segments.iter().enumerate() {
2182 incident.entry(segment[0]).or_default().push(i);
2183 incident.entry(segment[1]).or_default().push(i);
2184 }
2185 let valence = |v: u32| incident.get(&v).map_or(0, Vec::len);
2186 let mut used = vec![false; segments.len()];
2187 let mut starts: Vec<usize> = (0..segments.len()).collect();
2188 starts.sort_by_key(|&i| (valence(segments[i][0]) == 2 && valence(segments[i][1]) == 2, i));
2190 let mut out = Vec::new();
2191 for start in starts {
2192 if used[start] {
2193 continue;
2194 }
2195 used[start] = true;
2196 let segment = segments[start];
2197 let (head, mut tail) = if valence(segment[0]) != 2 { (segment[0], segment[1]) } else { (segment[1], segment[0]) };
2198 let mut path = vec![head, tail];
2199 while valence(tail) == 2 && tail != head {
2200 let Some(next) = incident[&tail].iter().copied().find(|i| !used[*i]) else { break };
2201 used[next] = true;
2202 tail = if segments[next][0] == tail { segments[next][1] } else { segments[next][0] };
2203 path.push(tail);
2204 }
2205 out.push(path.into_iter().map(|v| self.position[v as usize]).collect());
2206 }
2207 out
2208 }
2209}
2210
2211fn split_parameters(candidates: &[Candidate], scene: &Scene) -> Vec<Vec<f64>> {
2214 let floors = scene.floors;
2215 let mut splits: Vec<Vec<f64>> = vec![Vec::new(); candidates.len()];
2216 let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2218 let mut count = 0;
2219 for c in candidates {
2220 extent = [extent[0].min(c.bounds[0]), extent[1].min(c.bounds[1]), extent[2].max(c.bounds[2]), extent[3].max(c.bounds[3])];
2221 count += c.samples.len();
2222 }
2223 if !(extent[0] <= extent[2]) {
2224 return splits;
2225 }
2226 let mut grid = Grid::new(extent, count.max(1));
2227 let mut segments: Vec<(usize, usize)> = Vec::new();
2228 for (ci, c) in candidates.iter().enumerate() {
2229 for si in 0..c.samples.len().saturating_sub(1) {
2230 let (a, b) = (c.samples.paper[si], c.samples.paper[si + 1]);
2231 let pad = floors.crossing_mm + CHORD_MM;
2232 grid.insert(
2233 [a[0].min(b[0]) - pad, a[1].min(b[1]) - pad, a[0].max(b[0]) + pad, a[1].max(b[1]) + pad],
2234 segments.len(),
2235 );
2236 segments.push((ci, si));
2237 }
2238 }
2239 let param_at = |ci: usize, si: usize, t: f64| {
2240 let params = &candidates[ci].samples.params;
2241 params[si] + (params[si + 1] - params[si]) * t
2242 };
2243 let tolerance = floors.crossing_mm + CHORD_MM;
2244 let mut seen: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
2245 for bucket in &grid.buckets {
2246 for (k, &first) in bucket.iter().enumerate() {
2247 for &second in &bucket[k + 1..] {
2248 let (lo, hi) = (first.min(second), first.max(second));
2249 let ((ca, sa), (cb, sb)) = (segments[lo], segments[hi]);
2250 if ca == cb && sa.abs_diff(sb) <= 1 {
2251 continue;
2252 }
2253 if !candidates[ca].inked && !candidates[cb].inked {
2254 continue;
2255 }
2256 if !seen.insert((lo, hi)) {
2257 continue;
2258 }
2259 let pa = &candidates[ca].samples.paper;
2260 let pb = &candidates[cb].samples.paper;
2261 let vec2 = |p: [f64; 2]| Vec2 { x: p[0], y: p[1] };
2262 if let Some([ta, tb]) = segment_intersection(vec2(pa[sa]), vec2(pa[sa + 1]), vec2(pb[sb]), vec2(pb[sb + 1]), tolerance) {
2263 splits[ca].push(param_at(ca, sa, ta));
2264 splits[cb].push(param_at(cb, sb, tb));
2265 }
2266 for (from, from_seg, onto, onto_seg) in [(ca, sa, cb, sb), (cb, sb, ca, sa)] {
2269 let fp = &candidates[from].samples.paper;
2270 let op = &candidates[onto].samples.paper;
2271 let last = fp.len() - 1;
2272 for (end_seg, end) in [(0usize, fp[0]), (last - 1, fp[last])] {
2273 if from_seg != end_seg {
2274 continue;
2275 }
2276 let (distance, t) = segment_foot(end, op[onto_seg], op[onto_seg + 1]);
2277 if distance <= tolerance {
2278 splits[onto].push(param_at(onto, onto_seg, t));
2279 }
2280 }
2281 }
2282 }
2283 }
2284 }
2285 for (ci, c) in candidates.iter().enumerate() {
2287 if !c.inked || c.whole {
2288 continue;
2289 }
2290 let side = |view: [f64; 3]| -> f64 {
2291 match scene.clip {
2292 Clip::Whole => 1.0,
2293 Clip::Beyond { .. } => scene.clip.margin(view),
2294 Clip::Circle { radius } => radius - (view[0] * view[0] + view[1] * view[1]).sqrt(),
2295 }
2296 };
2297 if matches!(scene.clip, Clip::Whole) {
2298 continue;
2299 }
2300 for i in 1..c.samples.len() {
2301 let (g0, g1) = (side(c.samples.view[i - 1]), side(c.samples.view[i]));
2302 if (g0 < 0.0) == (g1 < 0.0) {
2303 continue;
2304 }
2305 let (mut lo, mut hi) = (c.samples.params[i - 1], c.samples.params[i]);
2306 for _ in 0..BISECTION_STEPS {
2307 let mid = 0.5 * (lo + hi);
2308 let Some(p) = c.carrier.at(mid) else { break };
2309 let g = side(scene.frame.to_view(a3(p)));
2310 if (g < 0.0) == (g0 < 0.0) {
2311 lo = mid;
2312 } else {
2313 hi = mid;
2314 }
2315 }
2316 splits[ci].push(0.5 * (lo + hi));
2317 }
2318 }
2319 for list in &mut splits {
2320 list.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2321 }
2322 splits
2323}
2324
2325struct Piece {
2327 lo: f64,
2328 hi: f64,
2329 visible: bool,
2330}
2331
2332fn pieces_of(candidate: &Candidate, splits: &[f64], scene: &Scene) -> Vec<Piece> {
2335 let samples = &candidate.samples;
2336 let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
2337 let mut cuts = vec![s0];
2338 let paper_at = |s: f64| candidate.carrier.at(s).map(|p| project(&scene.frame, scene.placed, p).0);
2339 let mut last_paper = samples.paper[0];
2340 for &s in splits {
2341 if s <= s0 || s >= s1 {
2342 continue;
2343 }
2344 let Some(p) = paper_at(s) else { continue };
2345 if paper_distance(p, last_paper) < scene.floors.crossing_mm {
2346 continue;
2347 }
2348 cuts.push(s);
2349 last_paper = p;
2350 }
2351 let end_paper = *samples.paper.last().unwrap();
2352 if cuts.len() > 1 && paper_distance(last_paper, end_paper) < scene.floors.crossing_mm {
2353 cuts.pop();
2354 }
2355 cuts.push(s1);
2356 let mut pieces = Vec::with_capacity(cuts.len() - 1);
2357 let mut hint = None;
2358 for w in cuts.windows(2) {
2359 let (lo, hi) = (w[0], w[1]);
2360 let Some(mid) = candidate.carrier.at(0.5 * (lo + hi)) else { continue };
2361 let view = scene.frame.to_view(a3(mid));
2362 let kept = match scene.clip {
2363 Clip::Whole => true,
2364 Clip::Beyond { .. } => candidate.whole || scene.clip.margin(view) >= -scene.floors.bar,
2365 Clip::Circle { radius } => (view[0] * view[0] + view[1] * view[1]).sqrt() <= radius + scene.floors.bar,
2366 };
2367 let visible = kept && !scene.hidden(mid, &mut hint);
2368 pieces.push(Piece { lo, hi, visible });
2369 }
2370 pieces
2371}
2372
2373struct Run {
2376 points: Vec<[f64; 2]>,
2377 start: Option<VertexKey>,
2378 end: Option<VertexKey>,
2379}
2380
2381fn runs_of(candidate: &Candidate, pieces: &[Piece], scene: &Scene) -> Vec<Run> {
2385 let samples = &candidate.samples;
2386 let closed = samples.len() > 2
2387 && paper_distance(samples.paper[0], *samples.paper.last().unwrap()) < scene.floors.crossing_mm
2388 && samples.points[0].sub(*samples.points.last().unwrap()).length() <= scene.floors.bar;
2389 let mut spans: Vec<(f64, f64, bool, bool)> = Vec::new(); let mut i = 0;
2391 while i < pieces.len() {
2392 if !pieces[i].visible {
2393 i += 1;
2394 continue;
2395 }
2396 let first = i;
2397 while i < pieces.len() && pieces[i].visible {
2398 i += 1;
2399 }
2400 spans.push((pieces[first].lo, pieces[i - 1].hi, first == 0, i == pieces.len()));
2401 }
2402 let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
2403 let mut wrapped: Option<(f64, f64)> = None;
2405 if closed && spans.len() >= 2 && spans[0].2 && spans.last().unwrap().3 {
2406 let head = spans.remove(0);
2407 let tail = spans.pop().unwrap();
2408 wrapped = Some((tail.0, head.1));
2409 }
2410 let point_at = |s: f64| candidate.carrier.at(s).map(|p| project(&scene.frame, scene.placed, p).0);
2411 let mut out = Vec::new();
2412 let mut emit = |pieces: &[(f64, f64)], start: Option<VertexKey>, end: Option<VertexKey>| {
2413 let mut points = Vec::new();
2414 for (k, &(lo, hi)) in pieces.iter().enumerate() {
2415 if k == 0 {
2416 if let Some(p) = point_at(lo) {
2417 points.push(p);
2418 }
2419 }
2420 for (index, s) in samples.params.iter().enumerate() {
2421 if *s > lo && *s < hi {
2422 points.push(samples.paper[index]);
2423 }
2424 }
2425 if let Some(p) = point_at(hi) {
2426 points.push(p);
2427 }
2428 }
2429 if points.len() >= 2 {
2430 out.push(Run { points, start, end });
2431 }
2432 };
2433 if let Some((lo, hi)) = wrapped {
2434 emit(&[(lo, s1), (s0, hi)], None, None);
2435 }
2436 for (lo, hi, at_start, at_end) in spans {
2437 let ends = candidate.ends;
2438 let whole_closed = closed && at_start && at_end;
2439 emit(
2440 &[(lo, hi)],
2441 if at_start && !whole_closed { ends.map(|e| e.0) } else { None },
2442 if at_end && !whole_closed { ends.map(|e| e.1) } else { None },
2443 );
2444 }
2445 out
2446}
2447
2448fn chain_runs(runs: Vec<Run>, valence: &HashMap<VertexKey, usize>, floor: f64) -> Vec<Vec<[f64; 2]>> {
2451 let mut at_vertex: HashMap<VertexKey, Vec<(usize, bool)>> = HashMap::new();
2452 for (index, run) in runs.iter().enumerate() {
2453 if let Some(v) = run.start {
2454 at_vertex.entry(v).or_default().push((index, true));
2455 }
2456 if let Some(v) = run.end {
2457 at_vertex.entry(v).or_default().push((index, false));
2458 }
2459 }
2460 let mut next: Vec<[Option<(usize, bool)>; 2]> = vec![[None, None]; runs.len()];
2462 for (vertex, list) in &at_vertex {
2463 if valence.get(vertex) != Some(&2) || list.len() != 2 || list[0].0 == list[1].0 {
2464 continue;
2465 }
2466 let ((a, a_start), (b, b_start)) = (list[0], list[1]);
2467 let (pa, pb) = (
2468 if a_start { runs[a].points[0] } else { *runs[a].points.last().unwrap() },
2469 if b_start { runs[b].points[0] } else { *runs[b].points.last().unwrap() },
2470 );
2471 if paper_distance(pa, pb) > floor + CHORD_MM {
2472 continue;
2473 }
2474 next[a][if a_start { 0 } else { 1 }] = Some((b, b_start));
2475 next[b][if b_start { 0 } else { 1 }] = Some((a, a_start));
2476 }
2477 let mut used = vec![false; runs.len()];
2478 let mut out = Vec::new();
2479 let mut order: Vec<usize> = (0..runs.len()).collect();
2481 order.sort_by_key(|&i| (next[i][0].is_some() && next[i][1].is_some(), i));
2482 for start in order {
2483 if used[start] {
2484 continue;
2485 }
2486 used[start] = true;
2487 let mut points = runs[start].points.clone();
2490 if next[start][0].is_some() && next[start][1].is_none() {
2491 points.reverse();
2492 let mut cursor = next[start][0];
2493 extend_chain(&runs, &next, &mut used, &mut points, &mut cursor);
2494 } else {
2495 let mut cursor = next[start][1];
2496 extend_chain(&runs, &next, &mut used, &mut points, &mut cursor);
2497 }
2498 out.push(points);
2499 }
2500 out
2501}
2502
2503fn extend_chain(
2504 runs: &[Run],
2505 next: &[[Option<(usize, bool)>; 2]],
2506 used: &mut [bool],
2507 points: &mut Vec<[f64; 2]>,
2508 cursor: &mut Option<(usize, bool)>,
2509) {
2510 while let Some((run, enter_at_start)) = *cursor {
2511 if used[run] {
2512 break;
2513 }
2514 used[run] = true;
2515 let mut piece = runs[run].points.clone();
2516 if !enter_at_start {
2517 piece.reverse();
2518 }
2519 points.extend(piece.into_iter().skip(1));
2520 *cursor = next[run][if enter_at_start { 1 } else { 0 }];
2521 }
2522}
2523
2524pub type CutCurve = Vec<[f64; 2]>;
2532
2533pub fn section_curves(solids: &[&BrepSolid], frame: &ViewFrame, placed: &PlacedView) -> Vec<CutCurve> {
2541 let trims: Vec<SolidTrims> = solids.iter().map(|solid| SolidTrims::of(solid)).collect();
2542 let trims: Vec<&SolidTrims> = trims.iter().collect();
2543 section_curves_with(solids, &trims, frame, placed)
2544}
2545
2546pub fn section_curves_with(solids: &[&BrepSolid], trims: &[&SolidTrims], frame: &ViewFrame, placed: &PlacedView) -> Vec<CutCurve> {
2548 let floors = Floors::of(placed.scale);
2549 let tolerances = KernelTolerances::default();
2550 let normal = v3(frame.forward);
2551 let target = v3(frame.target);
2552 let mut out = Vec::new();
2553 for (solid, trims) in solids.iter().zip(trims) {
2554 let mut reach: f64 = 1.0;
2555 for shell in &solid.shells {
2556 for face in &shell.faces {
2557 for row in &face.surface.control_points {
2558 for cp in row {
2559 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
2560 reach = reach.max(Vec3::new(cp.x / w, cp.y / w, cp.z / w).sub(target).length());
2561 }
2562 }
2563 }
2564 }
2565 let reach = reach * 1.5;
2566 let (right, up) = (v3(frame.right), v3(frame.up));
2567 let Ok(cutting) = make_plane(target.sub(right.scale(reach)).sub(up.scale(reach)), right, up, 2.0 * reach, 2.0 * reach)
2568 else {
2569 continue;
2570 };
2571 for (shell, shell_trims) in solid.shells.iter().zip(&trims.faces) {
2572 for (face, tests) in shell.faces.iter().zip(shell_trims) {
2573 for carrier in section_carriers(&cutting, face, normal, target, reach, &tolerances) {
2574 for curve in trim_section(solid, face, Some(tests), carrier, &cutting, frame, placed, &floors) {
2575 out.push(curve);
2576 }
2577 }
2578 }
2579 }
2580 }
2581 out
2582}
2583
2584fn section_carriers<'a>(
2586 cutting: &NurbsSurface,
2587 face: &'a FaceRecord,
2588 normal: Vec3,
2589 target: Vec3,
2590 reach: f64,
2591 tolerances: &KernelTolerances,
2592) -> Vec<(Carrier<'a>, f64, f64)> {
2593 let surface = &face.surface;
2594 if planar(surface) {
2595 let du = surface.domain_u().unwrap_or([0.0, 1.0]);
2596 let dv = surface.domain_v().unwrap_or([0.0, 1.0]);
2597 let (u, v) = (0.5 * (du[0] + du[1]), 0.5 * (dv[0] + dv[1]));
2598 let (Ok(origin), Ok(face_normal)) = (surface.evaluate(u, v), surface.normal(u, v)) else { return Vec::new() };
2599 let direction = normal.cross(face_normal);
2600 let Ok(direction) = direction.normalized() else { return Vec::new() }; let (h1, h2) = (normal.dot(target), face_normal.dot(origin));
2604 let c = normal.dot(face_normal);
2605 let det = 1.0 - c * c;
2606 if det.abs() < 1e-18 {
2607 return Vec::new();
2608 }
2609 let a = (h1 - h2 * c) / det;
2610 let b = (h2 - h1 * c) / det;
2611 let point = normal.scale(a).add(face_normal.scale(b));
2612 let point = point.add(direction.scale(target.sub(point).dot(direction)));
2613 return vec![(
2614 Carrier::Line { a: point.sub(direction.scale(reach)), b: point.add(direction.scale(reach)) },
2615 0.0,
2616 1.0,
2617 )];
2618 }
2619 if let Some(curves) = intersect_analytic_pair(cutting, surface, tolerances.intersection_fit) {
2620 return curves
2621 .into_iter()
2622 .filter_map(|curve| {
2623 let [t0, t1] = curve.domain().ok()?;
2624 Some((Carrier::Curve(curve), t0, t1))
2625 })
2626 .collect();
2627 }
2628 let options = SurfaceIntersectionOptions { tolerance: tolerances.intersection_fit, ..Default::default() };
2629 match intersect_surfaces(cutting, surface, &options) {
2630 Ok(curves) => curves
2631 .into_iter()
2632 .filter(|curve| curve.points.len() >= 2)
2633 .map(|curve| {
2634 let mut points = curve.points;
2635 if curve.closed && points.first().map(|p| p.sub(*points.last().unwrap()).length()) > Some(0.0) {
2636 points.push(points[0]);
2637 }
2638 let last = (points.len() - 1) as f64;
2639 (Carrier::Polyline(points), 0.0, last)
2640 })
2641 .collect(),
2642 Err(_) => Vec::new(),
2643 }
2644}
2645
2646fn trim_section(
2651 solid: &BrepSolid,
2652 face: &FaceRecord,
2653 tests: Option<&TrimTests>,
2654 carrier: (Carrier, f64, f64),
2655 cutting: &NurbsSurface,
2656 frame: &ViewFrame,
2657 placed: &PlacedView,
2658 floors: &Floors,
2659) -> Vec<CutCurve> {
2660 let (carrier, s0, s1) = carrier;
2661 let tolerances = KernelTolerances::default();
2662 let initial = match &carrier {
2663 Carrier::Polyline(points) => points.len().saturating_sub(1).max(1),
2664 Carrier::Curve(curve) => curve_initial(curve),
2665 _ => 1,
2666 };
2667 let base = sample(&carrier, s0, s1, initial, frame, placed);
2668 if base.len() < 2 {
2669 return Vec::new();
2670 }
2671 let mut cuts: Vec<f64> = vec![s0, s1];
2673 for lp in &face.loops {
2674 for coedge in &lp.coedges {
2675 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else { continue };
2676 let Ok(hits) = intersect_curve_surface(&edge.curve, cutting, tolerances.intersection_fit) else { continue };
2677 for hit in hits {
2678 if hit.t < edge.t0.min(edge.t1) - 1e-12 || hit.t > edge.t0.max(edge.t1) + 1e-12 {
2679 continue;
2680 }
2681 if let Some(s) = nearest_parameter(&carrier, &base, hit.point) {
2682 cuts.push(s);
2683 }
2684 }
2685 }
2686 }
2687 cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2688 let mut out = Vec::new();
2689 for w in cuts.windows(2) {
2690 let (lo, hi) = (w[0], w[1]);
2691 if hi - lo <= 0.0 {
2692 continue;
2693 }
2694 let Some(mid) = carrier.at(0.5 * (lo + hi)) else { continue };
2695 let Some(uv) = uv_on(face, mid, floors.bar) else { continue };
2696 if !in_face(face, tests, uv, floors.bar) {
2697 continue;
2698 }
2699 let piece = sample(&carrier, lo, hi, initial_between(&base, lo, hi), frame, placed);
2700 if piece.len() < 2 || piece.paper_length() < floors.crossing_mm {
2701 continue;
2702 }
2703 let mut paper = piece.paper;
2704 let step = (hi - lo) * 1e-4;
2708 let tangent = match (carrier.at(0.5 * (lo + hi) + step), carrier.at(0.5 * (lo + hi) - step)) {
2709 (Some(a), Some(b)) => a.sub(b),
2710 _ => piece.points[piece.points.len() - 1].sub(piece.points[0]),
2711 };
2712 if let Some(n_face) = outward_normal(face, mid) {
2713 let eye = v3(frame.forward).scale(-1.0);
2714 if tangent.dot(eye.cross(n_face)) < 0.0 {
2715 paper.reverse();
2716 }
2717 }
2718 out.push(paper);
2719 }
2720 out
2721}
2722
2723fn nearest_parameter(carrier: &Carrier, samples: &Samples, p: Vec3) -> Option<f64> {
2725 if let Carrier::Edge(curve) = carrier {
2726 return project_point_to_curve(curve, p).ok().map(|foot| foot.u);
2727 }
2728 let mut best: Option<(f64, f64)> = None;
2730 for i in 0..samples.len().saturating_sub(1) {
2731 let (a, b) = (samples.points[i], samples.points[i + 1]);
2732 let d = b.sub(a);
2733 let length2 = d.dot(d);
2734 let t = if length2 > 0.0 { (p.sub(a).dot(d) / length2).clamp(0.0, 1.0) } else { 0.0 };
2735 let distance = p.sub(a.add(d.scale(t))).length();
2736 let s = samples.params[i] + (samples.params[i + 1] - samples.params[i]) * t;
2737 if best.is_none_or(|(bd, _)| distance < bd) {
2738 best = Some((distance, s));
2739 }
2740 }
2741 best.map(|(_, s)| s)
2742}
2743