1use super::*;
2use crate::topology::CoedgeRecord;
3
4#[derive(Clone, Copy, Debug)]
9pub(super) struct ChordArc {
10 pub loop_index: usize,
11 pub coedge_index: usize,
12 pub t0: f64,
13 pub t1: f64,
14 pub offset: [f64; 2],
15}
16
17#[derive(Clone, Debug)]
26pub(super) struct TrimPolygon {
27 pub points: Vec<[f64; 2]>,
28 pub arcs: Vec<Option<ChordArc>>,
29 pending_end: Option<[f64; 2]>,
33 jump_tolerance: f64,
36}
37
38pub(super) fn is_straight_in_uv(curve: &NurbsCurve) -> bool {
48 let mut points = Vec::with_capacity(curve.control_points.len());
49 for control in &curve.control_points {
50 let Ok(point) = control.point() else {
51 return false;
52 };
53 points.push([point.x, point.y]);
54 }
55 let (Some(first), Some(last)) = (points.first().copied(), points.last().copied()) else {
56 return false;
57 };
58 let extent = points
59 .iter()
60 .map(|point| (point[0] - first[0]).hypot(point[1] - first[1]))
61 .fold(0.0f64, f64::max);
62 if extent <= 0.0 {
63 return true;
64 }
65 let direction = [last[0] - first[0], last[1] - first[1]];
66 let length = direction[0].hypot(direction[1]);
67 if length <= 1e-12 * extent {
68 return false;
69 }
70 points.iter().all(|point| {
71 ((point[0] - first[0]) * direction[1] - (point[1] - first[1]) * direction[0]).abs()
72 <= 1e-12 * extent * length
73 })
74}
75
76fn is_straight_polyline(curve: &NurbsCurve) -> bool {
77 let rational = curve
78 .control_points
79 .iter()
80 .any(|point| (point.w - curve.control_points[0].w).abs() > 1e-12);
81 curve.degree == 1 && !rational
82}
83
84impl TrimPolygon {
85 pub(super) fn new(domain: [f64; 4]) -> Self {
86 let [u0, u1, v0, v1] = domain;
87 let jump_tolerance = 1e-3 * (u1 - u0).abs().min((v1 - v0).abs()).max(1e-300);
88 Self {
89 points: Vec::new(),
90 arcs: Vec::new(),
91 pending_end: None,
92 jump_tolerance,
93 }
94 }
95
96 pub(super) fn for_face(face: &FaceRecord) -> Result<Self, String> {
97 let [u0, u1] = face.surface.domain_u()?;
98 let [v0, v1] = face.surface.domain_v()?;
99 Ok(Self::new([u0, u1, v0, v1]))
100 }
101
102 fn settle_pending(&mut self, next: [f64; 2]) {
103 if let Some(end) = self.pending_end.take() {
104 let gap = ((next[0] - end[0]).powi(2) + (next[1] - end[1]).powi(2)).sqrt();
105 if gap > self.jump_tolerance {
106 if let Some(last) = self.arcs.last_mut() {
107 *last = None;
108 }
109 }
110 }
111 }
112
113 pub(super) fn push_point(&mut self, point: [f64; 2]) {
115 self.settle_pending(point);
116 self.points.push(point);
117 self.arcs.push(None);
118 }
119
120 pub(super) fn push_coedge(
126 &mut self,
127 loop_index: usize,
128 coedge_index: usize,
129 coedge: &CoedgeRecord,
130 offset: [f64; 2],
131 ) -> Result<(), String> {
132 let curve = &coedge.pcurve;
133 let interior = interior_knots(&curve.knots, curve.degree);
134 if is_straight_polyline(curve) {
135 let [start, _] = curve.domain()?;
136 for parameter in std::iter::once(start).chain(interior.iter().copied()) {
137 let point = curve.evaluate(parameter)?;
138 self.push_point([point.x + offset[0], point.y + offset[1]]);
139 }
140 return Ok(());
141 }
142 let sample_count = (24usize).max((interior.len() + 1) * 16).min(128);
143 self.push_coedge_spans(loop_index, coedge_index, coedge, offset, sample_count, false)
144 }
145
146 pub(super) fn push_coedge_spans(
151 &mut self,
152 loop_index: usize,
153 coedge_index: usize,
154 coedge: &CoedgeRecord,
155 offset: [f64; 2],
156 target: usize,
157 include_end: bool,
158 ) -> Result<(), String> {
159 let curve = &coedge.pcurve;
160 let spans = curve_breaks(curve)?;
161 let [_, end] = curve.domain()?;
162 if is_straight_polyline(curve) {
163 let last = if include_end { spans.len() } else { spans.len() - 1 };
164 for ¶meter in &spans[..last] {
165 let point = curve.evaluate(parameter)?;
166 self.push_point([point.x + offset[0], point.y + offset[1]]);
167 }
168 return Ok(());
169 }
170 let span_count = spans.len().saturating_sub(1).max(1);
171 let per_span = target.div_ceil(span_count).max(1);
172 let mut previous: Option<(f64, [f64; 2])> = None;
173 let mut emit = |this: &mut Self, t: f64| -> Result<(), String> {
174 let point = curve.evaluate(t)?;
175 let point = [point.x + offset[0], point.y + offset[1]];
176 if let Some((t_prev, _)) = previous {
177 if let Some(last) = this.arcs.last_mut() {
178 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: t, offset });
179 }
180 }
181 this.settle_pending(point);
182 this.points.push(point);
183 this.arcs.push(None);
184 previous = Some((t, point));
185 Ok(())
186 };
187 for pair in spans.windows(2) {
188 for index in 0..per_span {
189 emit(self, pair[0] + (pair[1] - pair[0]) * index as f64 / per_span as f64)?;
190 }
191 }
192 if include_end {
193 emit(self, end)?;
194 } else if let Some((t_prev, _)) = previous {
195 if let Some(last) = self.arcs.last_mut() {
198 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: end, offset });
199 }
200 let point = curve.evaluate(end)?;
201 self.pending_end = Some([point.x + offset[0], point.y + offset[1]]);
202 }
203 Ok(())
204 }
205
206 pub(super) fn finish(&mut self) {
209 if let Some(first) = self.points.first().copied() {
210 self.settle_pending(first);
211 }
212 self.pending_end = None;
213 }
214
215 pub(super) fn reverse(&mut self) {
216 self.finish();
217 let n = self.points.len();
218 if n == 0 {
219 return;
220 }
221 let old = std::mem::take(&mut self.arcs);
222 let swap = |arc: Option<ChordArc>| arc.map(|a| ChordArc { t0: a.t1, t1: a.t0, ..a });
223 let mut arcs = Vec::with_capacity(n);
224 for k in 0..n - 1 {
225 arcs.push(swap(old[n - 2 - k]));
226 }
227 arcs.push(swap(old[n - 1]));
228 self.arcs = arcs;
229 self.points.reverse();
230 }
231
232 pub(super) fn shift_u(&mut self, du: f64) {
233 for point in &mut self.points {
234 point[0] += du;
235 }
236 for arc in self.arcs.iter_mut().flatten() {
237 arc.offset[0] += du;
238 }
239 if let Some(end) = &mut self.pending_end {
240 end[0] += du;
241 }
242 }
243
244 pub(super) fn append(&mut self, mut other: TrimPolygon) {
246 self.finish();
247 other.finish();
248 if let Some(last) = self.arcs.last_mut() {
249 *last = None;
250 }
251 if let Some(last) = other.arcs.last_mut() {
252 *last = None;
253 }
254 self.points.append(&mut other.points);
255 self.arcs.append(&mut other.arcs);
256 }
257}
258
259pub fn trim_polygons(face: &FaceRecord) -> Result<Vec<Vec<[f64; 2]>>, String> {
261 Ok(trim_polygons_tagged(face)?
262 .into_iter()
263 .map(|polygon| polygon.points)
264 .collect())
265}
266
267pub(super) fn trim_polygons_tagged(face: &FaceRecord) -> Result<Vec<TrimPolygon>, String> {
268 let mut polygons = Vec::new();
269 for (loop_index, loop_record) in face.loops.iter().enumerate() {
270 let mut polygon = TrimPolygon::for_face(face)?;
271 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
272 polygon.push_coedge(loop_index, coedge_index, coedge, [0.0, 0.0])?;
273 }
274 polygon.finish();
275 polygons.push(polygon);
276 }
277 Ok(polygons)
278}
279
280pub(super) fn singly_periodic_sphere_cap_polygons(
285 face: &FaceRecord,
286 closed_u: bool,
287 closed_v: bool,
288 domain: [f64; 4],
289) -> Result<Option<Vec<TrimPolygon>>, String> {
290 if !matches!(
291 face.surface.analytic(),
292 Some(crate::AnalyticSurface::Sphere { .. })
293 ) || (closed_u, closed_v) != (true, false)
294 || face.loops.len() != 2
295 {
296 return Ok(None);
297 }
298 let [u0, u1, v0, v1] = domain;
299 let period = u1 - u0;
300 let v_span = v1 - v0;
301 struct Rim {
302 polygon: TrimPolygon,
303 winding: f64,
304 vmin: f64,
305 vmax: f64,
306 }
307 let mut rims = Vec::with_capacity(2);
308 for (loop_index, loop_record) in face.loops.iter().enumerate() {
309 let mut polygon = TrimPolygon::new(domain);
310 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
311 polygon.push_coedge_spans(loop_index, coedge_index, coedge, [0.0, 0.0], 64, true)?;
312 }
313 polygon.finish();
314 let points = &polygon.points;
315 if points.len() < 2 {
316 return Ok(None);
317 }
318 let winding = points.last().unwrap()[0] - points[0][0];
319 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
320 for point in points {
321 vmin = vmin.min(point[1]);
322 vmax = vmax.max(point[1]);
323 }
324 rims.push(Rim {
325 polygon,
326 winding,
327 vmin,
328 vmax,
329 });
330 }
331 if rims
332 .iter()
333 .any(|rim| (rim.winding.abs() - period).abs() > 0.05 * period)
334 || rims[0].winding * rims[1].winding >= 0.0
335 {
336 return Ok(None);
337 }
338 let flat = |rim: &Rim| rim.vmax - rim.vmin <= 1e-6 * v_span;
339 let pole = match (flat(&rims[0]), flat(&rims[1])) {
340 (true, false) => &rims[0],
341 (false, true) => &rims[1],
342 _ => return Ok(None),
343 };
344 let pole_v = 0.5 * (pole.vmin + pole.vmax);
345 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
346 return Ok(None);
347 }
348
349 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
350 (&rims[0], &rims[1])
351 } else if rims[1].vmax <= rims[0].vmin {
352 (&rims[1], &rims[0])
353 } else {
354 return Ok(None);
355 };
356 let mut lower_points = lower.polygon.clone();
357 if lower.winding < 0.0 {
358 lower_points.reverse();
359 }
360 let mut upper_points = upper.polygon.clone();
361 if upper.winding > 0.0 {
362 upper_points.reverse();
363 }
364 let shift = ((lower_points.points[0][0] - upper_points.points.last().unwrap()[0]) / period)
365 .round()
366 * period;
367 upper_points.shift_u(shift);
368 lower_points.append(upper_points);
369 Ok(Some(vec![lower_points]))
370}
371
372pub(super) fn trim_polygons_unwrapped(
378 face: &FaceRecord,
379 closed_u: bool,
380 closed_v: bool,
381 u_period: f64,
382 v_period: f64,
383) -> Result<(Vec<TrimPolygon>, bool), String> {
384 let mut polygons = Vec::new();
385 let mut unwrapped = false;
386 for (loop_index, loop_record) in face.loops.iter().enumerate() {
387 let offsets = crate::topology::loop_seam_offsets(
388 &loop_record.coedges,
389 closed_u,
390 closed_v,
391 u_period,
392 v_period,
393 )?;
394 let mut polygon = TrimPolygon::for_face(face)?;
395 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
396 let offset = offsets[coedge_index];
397 if offset[0] != 0.0 || offset[1] != 0.0 {
398 unwrapped = true;
399 }
400 polygon.push_coedge(loop_index, coedge_index, coedge, offset)?;
401 }
402 polygon.finish();
403 polygons.push(polygon);
404 }
405 Ok((polygons, unwrapped))
406}
407
408pub(super) fn singly_periodic_wall_polygons(
426 face: &FaceRecord,
427 closed_u: bool,
428 closed_v: bool,
429 domain: [f64; 4],
430) -> Result<Option<Vec<TrimPolygon>>, String> {
431 if closed_u == closed_v {
434 return Ok(None);
435 }
436 let [u0, u1, v0, v1] = domain;
437 let p_is_u = closed_u;
438 let period = if p_is_u { u1 - u0 } else { v1 - v0 };
439 if !(period > 0.0) {
440 return Ok(None);
441 }
442 let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
443 let q_extent = (q_dom_hi - q_dom_lo).abs();
444 if !(q_extent > 0.0) {
445 return Ok(None);
446 }
447 let coord = |p: [f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
449
450 let mut rim_indices: Vec<usize> = Vec::new();
451 let mut rim_levels: Vec<f64> = Vec::new();
452 for (index, loop_record) in face.loops.iter().enumerate() {
453 let mut points: Vec<[f64; 2]> = Vec::new();
455 for coedge in &loop_record.coedges {
456 let [d0, d1] = coedge.pcurve.domain()?;
457 let samples = 16;
458 for k in 0..=samples {
459 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
460 let p = coedge.pcurve.evaluate(t)?;
461 points.push([p.x, p.y]);
462 }
463 }
464 if points.len() < 2 {
465 return Ok(None);
466 }
467 let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
468 let mut net = 0.0;
469 for pair in points.windows(2) {
470 let (p0, q0) = coord(pair[0]);
471 let (p1, _q1) = coord(pair[1]);
472 qmin = qmin.min(q0);
473 qmax = qmax.max(q0);
474 let mut delta = p1 - p0;
475 if delta > 0.5 * period {
476 delta -= period;
477 } else if delta < -0.5 * period {
478 delta += period;
479 }
480 net += delta;
481 }
482 if let Some(last) = points.last() {
483 let (_p, q) = coord(*last);
484 qmin = qmin.min(q);
485 qmax = qmax.max(q);
486 }
487 let is_rim = net.abs() > 0.75 * period && (qmax - qmin) <= 0.02 * q_extent;
490 if is_rim {
491 rim_indices.push(index);
492 rim_levels.push(0.5 * (qmin + qmax));
493 }
494 }
495 if rim_levels.len() != 2 {
496 return Ok(None);
497 }
498 let (q_lo, q_hi) = if rim_levels[0] <= rim_levels[1] {
499 (rim_levels[0], rim_levels[1])
500 } else {
501 (rim_levels[1], rim_levels[0])
502 };
503 let tol = 0.02 * q_extent;
507 if (q_lo - q_dom_lo).abs() > tol || (q_hi - q_dom_hi).abs() > tol {
508 return Ok(None);
509 }
510 if (q_hi - q_lo) <= 0.5 * q_extent {
511 return Ok(None);
512 }
513
514 let (r_ulo, r_uhi, r_vlo, r_vhi) = if p_is_u {
516 (u0, u1, q_lo, q_hi)
517 } else {
518 (q_lo, q_hi, v0, v1)
519 };
520 let mut rectangle = TrimPolygon::new(domain);
521 for corner in [
522 [r_ulo, r_vlo],
523 [r_uhi, r_vlo],
524 [r_uhi, r_vhi],
525 [r_ulo, r_vhi],
526 ] {
527 rectangle.push_point(corner);
528 }
529 let mut polygons: Vec<TrimPolygon> = vec![rectangle];
530 for (index, loop_record) in face.loops.iter().enumerate() {
533 if rim_indices.contains(&index) {
534 continue;
535 }
536 let offsets = crate::topology::loop_seam_offsets(
537 &loop_record.coedges,
538 closed_u,
539 closed_v,
540 u1 - u0,
541 v1 - v0,
542 )?;
543 let mut polygon = TrimPolygon::new(domain);
544 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
545 polygon.push_coedge(index, coedge_index, coedge, offsets[coedge_index])?;
546 }
547 polygon.finish();
548 polygons.push(polygon);
549 }
550 Ok(Some(polygons))
551}
552
553pub(super) fn doubly_periodic_seam_band_polygons(
567 face: &FaceRecord,
568 domain: [f64; 4],
569) -> Result<Option<Vec<TrimPolygon>>, String> {
570 let (closed_u, closed_v) = face.surface.closed_directions()?;
571 if !(closed_u && closed_v) {
572 return Ok(None);
573 }
574 type Tagged = ([f64; 2], Option<(usize, usize, f64)>);
578 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
579 let mut loops_tagged: Vec<Vec<Tagged>> = Vec::with_capacity(face.loops.len());
580 for (loop_index, loop_record) in face.loops.iter().enumerate() {
581 let mut points: Vec<[f64; 2]> = Vec::new();
582 let mut tagged: Vec<Tagged> = Vec::new();
583 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
584 let [d0, d1] = coedge.pcurve.domain()?;
585 let samples = 24;
586 let curved = !is_straight_polyline(&coedge.pcurve);
587 for k in 0..=samples {
588 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
589 let p = coedge.pcurve.evaluate(t)?;
590 points.push([p.x, p.y]);
591 tagged.push(([p.x, p.y], curved.then_some((loop_index, coedge_index, t))));
592 }
593 }
594 loops_uv.push(points);
595 loops_tagged.push(tagged);
596 }
597 let Some(band) =
598 crate::topology::analyze_doubly_periodic_seam_band(&loops_uv, domain, face.same_sense)
599 else {
600 return Ok(None);
601 };
602 let vertices = crate::topology::seam_band_uv_polygon_with(
603 &loops_tagged,
604 |vertex: &Tagged| vertex.0,
605 |point| (point, None),
606 domain,
607 &band,
608 );
609 let mut polygon = TrimPolygon::new(domain);
610 let n = vertices.len();
611 for (k, (point, tag)) in vertices.iter().enumerate() {
612 polygon.push_point(*point);
613 let next = &vertices[(k + 1) % n];
617 if let (Some((l0, c0, t0)), Some((l1, c1, t1))) = (tag, next.1) {
618 if *l0 == l1 && *c0 == c1 {
619 polygon.arcs[k] = Some(ChordArc {
620 loop_index: *l0,
621 coedge_index: *c0,
622 t0: *t0,
623 t1,
624 offset: [0.0, 0.0],
625 });
626 }
627 }
628 }
629 Ok(Some(vec![polygon]))
630}
631
632pub(super) fn is_untrimmed(face: &FaceRecord) -> Result<bool, String> {
633 if face.loops.len() != 1 {
634 return Ok(false);
635 }
636 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
637 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
638 let [u0, u1] = ku.domain();
639 let [v0, v1] = kv.domain();
640 let domain_area = (u1 - u0) * (v1 - v0);
641 Ok((closed_parameter_space_area_on_carrier(face)?.abs() - domain_area).abs() <= 1e-6 * domain_area)
646}
647
648pub(super) const TRIANGLE_CUBATURE: [([f64; 3], f64); 7] = [
651 ([1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.225),
652 (
653 [0.101286507323456, 0.101286507323456, 0.797426985353087],
654 0.125939180544827,
655 ),
656 (
657 [0.101286507323456, 0.797426985353087, 0.101286507323456],
658 0.125939180544827,
659 ),
660 (
661 [0.797426985353087, 0.101286507323456, 0.101286507323456],
662 0.125939180544827,
663 ),
664 (
665 [0.470142064105115, 0.470142064105115, 0.059715871789770],
666 0.132394152788506,
667 ),
668 (
669 [0.470142064105115, 0.059715871789770, 0.470142064105115],
670 0.132394152788506,
671 ),
672 (
673 [0.059715871789770, 0.470142064105115, 0.470142064105115],
674 0.132394152788506,
675 ),
676];
677
678pub(super) fn polygon_signed_area(polygon: &[[f64; 2]]) -> f64 {
679 let mut area = 0.0;
680 for index in 0..polygon.len() {
681 let a = polygon[index];
682 let b = polygon[(index + 1) % polygon.len()];
683 area += a[0] * b[1] - b[0] * a[1];
684 }
685 area * 0.5
686}
687
688pub(super) fn clip_polygon_to_cell_into(
700 polygon: &[[f64; 2]],
701 cell: [f64; 4],
702 scratch: &mut Vec<[f64; 2]>,
703 out: &mut Vec<[f64; 2]>,
704) {
705 let [u_low, u_high, v_low, v_high] = cell;
706 out.clear();
707 out.extend_from_slice(polygon);
708 for (axis, bound, keep_below) in [
710 (0, u_low, false),
711 (0, u_high, true),
712 (1, v_low, false),
713 (1, v_high, true),
714 ] {
715 if out.len() < 3 {
716 out.clear();
717 return;
718 }
719 let inside = |point: &[f64; 2]| {
720 if keep_below {
721 point[axis] <= bound
722 } else {
723 point[axis] >= bound
724 }
725 };
726 scratch.clear();
727 scratch.reserve(out.len() + 4);
728 for index in 0..out.len() {
729 let a = out[index];
730 let b = out[(index + 1) % out.len()];
731 let a_in = inside(&a);
732 let b_in = inside(&b);
733 if a_in {
734 scratch.push(a);
735 }
736 if a_in != b_in {
737 let t = (bound - a[axis]) / (b[axis] - a[axis]);
738 let mut crossing = [0.0; 2];
739 crossing[axis] = bound;
740 crossing[1 - axis] = a[1 - axis] + t * (b[1 - axis] - a[1 - axis]);
741 scratch.push(crossing);
742 }
743 }
744 std::mem::swap(scratch, out);
745 }
746}
747
748pub(super) fn cell_breaks(knots: &[f64], degree: usize, low: f64, high: f64) -> Vec<f64> {
749 let mut breaks = vec![low, high];
750 breaks.extend(interior_knots(knots, degree));
751 breaks.sort_by(f64::total_cmp);
752 breaks.dedup_by(|a, b| (*a - *b).abs() <= (high - low) * 1e-12);
753 const MINIMUM_CELLS: usize = 8;
756 let target = (high - low) / MINIMUM_CELLS as f64;
757 let mut refined = Vec::with_capacity(breaks.len() * 2);
758 for pair in breaks.windows(2) {
759 refined.push(pair[0]);
760 let span = pair[1] - pair[0];
761 let pieces = (span / target).ceil().max(1.0) as usize;
762 for piece in 1..pieces {
763 refined.push(pair[0] + span * piece as f64 / pieces as f64);
764 }
765 }
766 refined.push(high);
767 refined
768}
769
770pub(super) fn tiled_breaks(base: &[f64], lo: f64, hi: f64, period: f64) -> Vec<f64> {
775 if period <= 0.0 || base.len() < 2 {
776 return base.to_vec();
777 }
778 let origin = base[0];
779 let first = ((lo - origin) / period).floor() as i64;
780 let last = ((hi - origin) / period).ceil() as i64;
781 let mut breaks = Vec::new();
782 for tile in first..=last {
783 let shift = tile as f64 * period;
784 for &value in base {
785 let shifted = value + shift;
786 if breaks.last().map_or(true, |&previous: &f64| {
787 (shifted - previous).abs() > 1e-12 * period
788 }) {
789 breaks.push(shifted);
790 }
791 }
792 }
793 breaks
794}