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
38fn is_straight_polyline(curve: &NurbsCurve) -> bool {
39 let rational = curve
40 .control_points
41 .iter()
42 .any(|point| (point.w - curve.control_points[0].w).abs() > 1e-12);
43 curve.degree == 1 && !rational
44}
45
46impl TrimPolygon {
47 pub(super) fn new(domain: [f64; 4]) -> Self {
48 let [u0, u1, v0, v1] = domain;
49 let jump_tolerance = 1e-3 * (u1 - u0).abs().min((v1 - v0).abs()).max(1e-300);
50 Self {
51 points: Vec::new(),
52 arcs: Vec::new(),
53 pending_end: None,
54 jump_tolerance,
55 }
56 }
57
58 pub(super) fn for_face(face: &FaceRecord) -> Result<Self, String> {
59 let [u0, u1] = face.surface.domain_u()?;
60 let [v0, v1] = face.surface.domain_v()?;
61 Ok(Self::new([u0, u1, v0, v1]))
62 }
63
64 fn settle_pending(&mut self, next: [f64; 2]) {
65 if let Some(end) = self.pending_end.take() {
66 let gap = ((next[0] - end[0]).powi(2) + (next[1] - end[1]).powi(2)).sqrt();
67 if gap > self.jump_tolerance {
68 if let Some(last) = self.arcs.last_mut() {
69 *last = None;
70 }
71 }
72 }
73 }
74
75 pub(super) fn push_point(&mut self, point: [f64; 2]) {
77 self.settle_pending(point);
78 self.points.push(point);
79 self.arcs.push(None);
80 }
81
82 pub(super) fn push_coedge(
88 &mut self,
89 loop_index: usize,
90 coedge_index: usize,
91 coedge: &CoedgeRecord,
92 offset: [f64; 2],
93 ) -> Result<(), String> {
94 let curve = &coedge.pcurve;
95 let interior = interior_knots(&curve.knots, curve.degree);
96 if is_straight_polyline(curve) {
97 let [start, _] = curve.domain()?;
98 for parameter in std::iter::once(start).chain(interior.iter().copied()) {
99 let point = curve.evaluate(parameter)?;
100 self.push_point([point.x + offset[0], point.y + offset[1]]);
101 }
102 return Ok(());
103 }
104 let sample_count = (24usize).max((interior.len() + 1) * 16).min(128);
105 self.push_coedge_spans(loop_index, coedge_index, coedge, offset, sample_count, false)
106 }
107
108 pub(super) fn push_coedge_spans(
113 &mut self,
114 loop_index: usize,
115 coedge_index: usize,
116 coedge: &CoedgeRecord,
117 offset: [f64; 2],
118 target: usize,
119 include_end: bool,
120 ) -> Result<(), String> {
121 let curve = &coedge.pcurve;
122 let spans = curve_breaks(curve)?;
123 let [_, end] = curve.domain()?;
124 if is_straight_polyline(curve) {
125 let last = if include_end { spans.len() } else { spans.len() - 1 };
126 for ¶meter in &spans[..last] {
127 let point = curve.evaluate(parameter)?;
128 self.push_point([point.x + offset[0], point.y + offset[1]]);
129 }
130 return Ok(());
131 }
132 let span_count = spans.len().saturating_sub(1).max(1);
133 let per_span = target.div_ceil(span_count).max(1);
134 let mut previous: Option<(f64, [f64; 2])> = None;
135 let mut emit = |this: &mut Self, t: f64| -> Result<(), String> {
136 let point = curve.evaluate(t)?;
137 let point = [point.x + offset[0], point.y + offset[1]];
138 if let Some((t_prev, _)) = previous {
139 if let Some(last) = this.arcs.last_mut() {
140 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: t, offset });
141 }
142 }
143 this.settle_pending(point);
144 this.points.push(point);
145 this.arcs.push(None);
146 previous = Some((t, point));
147 Ok(())
148 };
149 for pair in spans.windows(2) {
150 for index in 0..per_span {
151 emit(self, pair[0] + (pair[1] - pair[0]) * index as f64 / per_span as f64)?;
152 }
153 }
154 if include_end {
155 emit(self, end)?;
156 } else if let Some((t_prev, _)) = previous {
157 if let Some(last) = self.arcs.last_mut() {
160 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: end, offset });
161 }
162 let point = curve.evaluate(end)?;
163 self.pending_end = Some([point.x + offset[0], point.y + offset[1]]);
164 }
165 Ok(())
166 }
167
168 pub(super) fn finish(&mut self) {
171 if let Some(first) = self.points.first().copied() {
172 self.settle_pending(first);
173 }
174 self.pending_end = None;
175 }
176
177 pub(super) fn reverse(&mut self) {
178 self.finish();
179 let n = self.points.len();
180 if n == 0 {
181 return;
182 }
183 let old = std::mem::take(&mut self.arcs);
184 let swap = |arc: Option<ChordArc>| arc.map(|a| ChordArc { t0: a.t1, t1: a.t0, ..a });
185 let mut arcs = Vec::with_capacity(n);
186 for k in 0..n - 1 {
187 arcs.push(swap(old[n - 2 - k]));
188 }
189 arcs.push(swap(old[n - 1]));
190 self.arcs = arcs;
191 self.points.reverse();
192 }
193
194 pub(super) fn shift_u(&mut self, du: f64) {
195 for point in &mut self.points {
196 point[0] += du;
197 }
198 for arc in self.arcs.iter_mut().flatten() {
199 arc.offset[0] += du;
200 }
201 if let Some(end) = &mut self.pending_end {
202 end[0] += du;
203 }
204 }
205
206 pub(super) fn append(&mut self, mut other: TrimPolygon) {
208 self.finish();
209 other.finish();
210 if let Some(last) = self.arcs.last_mut() {
211 *last = None;
212 }
213 if let Some(last) = other.arcs.last_mut() {
214 *last = None;
215 }
216 self.points.append(&mut other.points);
217 self.arcs.append(&mut other.arcs);
218 }
219}
220
221pub fn trim_polygons(face: &FaceRecord) -> Result<Vec<Vec<[f64; 2]>>, String> {
223 Ok(trim_polygons_tagged(face)?
224 .into_iter()
225 .map(|polygon| polygon.points)
226 .collect())
227}
228
229pub(super) fn trim_polygons_tagged(face: &FaceRecord) -> Result<Vec<TrimPolygon>, String> {
230 let mut polygons = Vec::new();
231 for (loop_index, loop_record) in face.loops.iter().enumerate() {
232 let mut polygon = TrimPolygon::for_face(face)?;
233 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
234 polygon.push_coedge(loop_index, coedge_index, coedge, [0.0, 0.0])?;
235 }
236 polygon.finish();
237 polygons.push(polygon);
238 }
239 Ok(polygons)
240}
241
242pub(super) fn singly_periodic_sphere_cap_polygons(
247 face: &FaceRecord,
248 closed_u: bool,
249 closed_v: bool,
250 domain: [f64; 4],
251) -> Result<Option<Vec<TrimPolygon>>, String> {
252 if !matches!(
253 face.surface.analytic(),
254 Some(crate::AnalyticSurface::Sphere { .. })
255 ) || (closed_u, closed_v) != (true, false)
256 || face.loops.len() != 2
257 {
258 return Ok(None);
259 }
260 let [u0, u1, v0, v1] = domain;
261 let period = u1 - u0;
262 let v_span = v1 - v0;
263 struct Rim {
264 polygon: TrimPolygon,
265 winding: f64,
266 vmin: f64,
267 vmax: f64,
268 }
269 let mut rims = Vec::with_capacity(2);
270 for (loop_index, loop_record) in face.loops.iter().enumerate() {
271 let mut polygon = TrimPolygon::new(domain);
272 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
273 polygon.push_coedge_spans(loop_index, coedge_index, coedge, [0.0, 0.0], 64, true)?;
274 }
275 polygon.finish();
276 let points = &polygon.points;
277 if points.len() < 2 {
278 return Ok(None);
279 }
280 let winding = points.last().unwrap()[0] - points[0][0];
281 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
282 for point in points {
283 vmin = vmin.min(point[1]);
284 vmax = vmax.max(point[1]);
285 }
286 rims.push(Rim {
287 polygon,
288 winding,
289 vmin,
290 vmax,
291 });
292 }
293 if rims
294 .iter()
295 .any(|rim| (rim.winding.abs() - period).abs() > 0.05 * period)
296 || rims[0].winding * rims[1].winding >= 0.0
297 {
298 return Ok(None);
299 }
300 let flat = |rim: &Rim| rim.vmax - rim.vmin <= 1e-6 * v_span;
301 let pole = match (flat(&rims[0]), flat(&rims[1])) {
302 (true, false) => &rims[0],
303 (false, true) => &rims[1],
304 _ => return Ok(None),
305 };
306 let pole_v = 0.5 * (pole.vmin + pole.vmax);
307 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
308 return Ok(None);
309 }
310
311 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
312 (&rims[0], &rims[1])
313 } else if rims[1].vmax <= rims[0].vmin {
314 (&rims[1], &rims[0])
315 } else {
316 return Ok(None);
317 };
318 let mut lower_points = lower.polygon.clone();
319 if lower.winding < 0.0 {
320 lower_points.reverse();
321 }
322 let mut upper_points = upper.polygon.clone();
323 if upper.winding > 0.0 {
324 upper_points.reverse();
325 }
326 let shift = ((lower_points.points[0][0] - upper_points.points.last().unwrap()[0]) / period)
327 .round()
328 * period;
329 upper_points.shift_u(shift);
330 lower_points.append(upper_points);
331 Ok(Some(vec![lower_points]))
332}
333
334pub(super) fn trim_polygons_unwrapped(
340 face: &FaceRecord,
341 closed_u: bool,
342 closed_v: bool,
343 u_period: f64,
344 v_period: f64,
345) -> Result<(Vec<TrimPolygon>, bool), String> {
346 let mut polygons = Vec::new();
347 let mut unwrapped = false;
348 for (loop_index, loop_record) in face.loops.iter().enumerate() {
349 let offsets = crate::topology::loop_seam_offsets(
350 &loop_record.coedges,
351 closed_u,
352 closed_v,
353 u_period,
354 v_period,
355 )?;
356 let mut polygon = TrimPolygon::for_face(face)?;
357 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
358 let offset = offsets[coedge_index];
359 if offset[0] != 0.0 || offset[1] != 0.0 {
360 unwrapped = true;
361 }
362 polygon.push_coedge(loop_index, coedge_index, coedge, offset)?;
363 }
364 polygon.finish();
365 polygons.push(polygon);
366 }
367 Ok((polygons, unwrapped))
368}
369
370pub(super) fn singly_periodic_wall_polygons(
388 face: &FaceRecord,
389 closed_u: bool,
390 closed_v: bool,
391 domain: [f64; 4],
392) -> Result<Option<Vec<TrimPolygon>>, String> {
393 if closed_u == closed_v {
396 return Ok(None);
397 }
398 let [u0, u1, v0, v1] = domain;
399 let p_is_u = closed_u;
400 let period = if p_is_u { u1 - u0 } else { v1 - v0 };
401 if !(period > 0.0) {
402 return Ok(None);
403 }
404 let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
405 let q_extent = (q_dom_hi - q_dom_lo).abs();
406 if !(q_extent > 0.0) {
407 return Ok(None);
408 }
409 let coord = |p: [f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
411
412 let mut rim_indices: Vec<usize> = Vec::new();
413 let mut rim_levels: Vec<f64> = Vec::new();
414 for (index, loop_record) in face.loops.iter().enumerate() {
415 let mut points: Vec<[f64; 2]> = Vec::new();
417 for coedge in &loop_record.coedges {
418 let [d0, d1] = coedge.pcurve.domain()?;
419 let samples = 16;
420 for k in 0..=samples {
421 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
422 let p = coedge.pcurve.evaluate(t)?;
423 points.push([p.x, p.y]);
424 }
425 }
426 if points.len() < 2 {
427 return Ok(None);
428 }
429 let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
430 let mut net = 0.0;
431 for pair in points.windows(2) {
432 let (p0, q0) = coord(pair[0]);
433 let (p1, _q1) = coord(pair[1]);
434 qmin = qmin.min(q0);
435 qmax = qmax.max(q0);
436 let mut delta = p1 - p0;
437 if delta > 0.5 * period {
438 delta -= period;
439 } else if delta < -0.5 * period {
440 delta += period;
441 }
442 net += delta;
443 }
444 if let Some(last) = points.last() {
445 let (_p, q) = coord(*last);
446 qmin = qmin.min(q);
447 qmax = qmax.max(q);
448 }
449 let is_rim = net.abs() > 0.75 * period && (qmax - qmin) <= 0.02 * q_extent;
452 if is_rim {
453 rim_indices.push(index);
454 rim_levels.push(0.5 * (qmin + qmax));
455 }
456 }
457 if rim_levels.len() != 2 {
458 return Ok(None);
459 }
460 let (q_lo, q_hi) = if rim_levels[0] <= rim_levels[1] {
461 (rim_levels[0], rim_levels[1])
462 } else {
463 (rim_levels[1], rim_levels[0])
464 };
465 let tol = 0.02 * q_extent;
469 if (q_lo - q_dom_lo).abs() > tol || (q_hi - q_dom_hi).abs() > tol {
470 return Ok(None);
471 }
472 if (q_hi - q_lo) <= 0.5 * q_extent {
473 return Ok(None);
474 }
475
476 let (r_ulo, r_uhi, r_vlo, r_vhi) = if p_is_u {
478 (u0, u1, q_lo, q_hi)
479 } else {
480 (q_lo, q_hi, v0, v1)
481 };
482 let mut rectangle = TrimPolygon::new(domain);
483 for corner in [
484 [r_ulo, r_vlo],
485 [r_uhi, r_vlo],
486 [r_uhi, r_vhi],
487 [r_ulo, r_vhi],
488 ] {
489 rectangle.push_point(corner);
490 }
491 let mut polygons: Vec<TrimPolygon> = vec![rectangle];
492 for (index, loop_record) in face.loops.iter().enumerate() {
495 if rim_indices.contains(&index) {
496 continue;
497 }
498 let offsets = crate::topology::loop_seam_offsets(
499 &loop_record.coedges,
500 closed_u,
501 closed_v,
502 u1 - u0,
503 v1 - v0,
504 )?;
505 let mut polygon = TrimPolygon::new(domain);
506 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
507 polygon.push_coedge(index, coedge_index, coedge, offsets[coedge_index])?;
508 }
509 polygon.finish();
510 polygons.push(polygon);
511 }
512 Ok(Some(polygons))
513}
514
515pub(super) fn doubly_periodic_seam_band_polygons(
529 face: &FaceRecord,
530 domain: [f64; 4],
531) -> Result<Option<Vec<TrimPolygon>>, String> {
532 let (closed_u, closed_v) = face.surface.closed_directions()?;
533 if !(closed_u && closed_v) {
534 return Ok(None);
535 }
536 type Tagged = ([f64; 2], Option<(usize, usize, f64)>);
540 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
541 let mut loops_tagged: Vec<Vec<Tagged>> = Vec::with_capacity(face.loops.len());
542 for (loop_index, loop_record) in face.loops.iter().enumerate() {
543 let mut points: Vec<[f64; 2]> = Vec::new();
544 let mut tagged: Vec<Tagged> = Vec::new();
545 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
546 let [d0, d1] = coedge.pcurve.domain()?;
547 let samples = 24;
548 let curved = !is_straight_polyline(&coedge.pcurve);
549 for k in 0..=samples {
550 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
551 let p = coedge.pcurve.evaluate(t)?;
552 points.push([p.x, p.y]);
553 tagged.push(([p.x, p.y], curved.then_some((loop_index, coedge_index, t))));
554 }
555 }
556 loops_uv.push(points);
557 loops_tagged.push(tagged);
558 }
559 let Some(band) =
560 crate::topology::analyze_doubly_periodic_seam_band(&loops_uv, domain, face.same_sense)
561 else {
562 return Ok(None);
563 };
564 let vertices = crate::topology::seam_band_uv_polygon_with(
565 &loops_tagged,
566 |vertex: &Tagged| vertex.0,
567 |point| (point, None),
568 domain,
569 &band,
570 );
571 let mut polygon = TrimPolygon::new(domain);
572 let n = vertices.len();
573 for (k, (point, tag)) in vertices.iter().enumerate() {
574 polygon.push_point(*point);
575 let next = &vertices[(k + 1) % n];
579 if let (Some((l0, c0, t0)), Some((l1, c1, t1))) = (tag, next.1) {
580 if *l0 == l1 && *c0 == c1 {
581 polygon.arcs[k] = Some(ChordArc {
582 loop_index: *l0,
583 coedge_index: *c0,
584 t0: *t0,
585 t1,
586 offset: [0.0, 0.0],
587 });
588 }
589 }
590 }
591 Ok(Some(vec![polygon]))
592}
593
594pub(super) fn is_untrimmed(face: &FaceRecord) -> Result<bool, String> {
595 if face.loops.len() != 1 {
596 return Ok(false);
597 }
598 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
599 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
600 let [u0, u1] = ku.domain();
601 let [v0, v1] = kv.domain();
602 let domain_area = (u1 - u0) * (v1 - v0);
603 Ok((parameter_space_area(face)?.abs() - domain_area).abs() <= 1e-6 * domain_area)
604}
605
606pub(super) const TRIANGLE_CUBATURE: [([f64; 3], f64); 7] = [
609 ([1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.225),
610 (
611 [0.101286507323456, 0.101286507323456, 0.797426985353087],
612 0.125939180544827,
613 ),
614 (
615 [0.101286507323456, 0.797426985353087, 0.101286507323456],
616 0.125939180544827,
617 ),
618 (
619 [0.797426985353087, 0.101286507323456, 0.101286507323456],
620 0.125939180544827,
621 ),
622 (
623 [0.470142064105115, 0.470142064105115, 0.059715871789770],
624 0.132394152788506,
625 ),
626 (
627 [0.470142064105115, 0.059715871789770, 0.470142064105115],
628 0.132394152788506,
629 ),
630 (
631 [0.059715871789770, 0.470142064105115, 0.470142064105115],
632 0.132394152788506,
633 ),
634];
635
636pub(super) fn polygon_signed_area(polygon: &[[f64; 2]]) -> f64 {
637 let mut area = 0.0;
638 for index in 0..polygon.len() {
639 let a = polygon[index];
640 let b = polygon[(index + 1) % polygon.len()];
641 area += a[0] * b[1] - b[0] * a[1];
642 }
643 area * 0.5
644}
645
646pub(super) fn clip_polygon_to_cell(polygon: &[[f64; 2]], cell: [f64; 4]) -> Vec<[f64; 2]> {
652 let [u_low, u_high, v_low, v_high] = cell;
653 let mut current = polygon.to_vec();
654 for (axis, bound, keep_below) in [
656 (0, u_low, false),
657 (0, u_high, true),
658 (1, v_low, false),
659 (1, v_high, true),
660 ] {
661 if current.len() < 3 {
662 return Vec::new();
663 }
664 let inside = |point: &[f64; 2]| {
665 if keep_below {
666 point[axis] <= bound
667 } else {
668 point[axis] >= bound
669 }
670 };
671 let mut next = Vec::with_capacity(current.len() + 4);
672 for index in 0..current.len() {
673 let a = current[index];
674 let b = current[(index + 1) % current.len()];
675 let a_in = inside(&a);
676 let b_in = inside(&b);
677 if a_in {
678 next.push(a);
679 }
680 if a_in != b_in {
681 let t = (bound - a[axis]) / (b[axis] - a[axis]);
682 let mut crossing = [0.0; 2];
683 crossing[axis] = bound;
684 crossing[1 - axis] = a[1 - axis] + t * (b[1 - axis] - a[1 - axis]);
685 next.push(crossing);
686 }
687 }
688 current = next;
689 }
690 current
691}
692
693pub(super) fn cell_breaks(knots: &[f64], degree: usize, low: f64, high: f64) -> Vec<f64> {
694 let mut breaks = vec![low, high];
695 breaks.extend(interior_knots(knots, degree));
696 breaks.sort_by(f64::total_cmp);
697 breaks.dedup_by(|a, b| (*a - *b).abs() <= (high - low) * 1e-12);
698 const MINIMUM_CELLS: usize = 8;
701 let target = (high - low) / MINIMUM_CELLS as f64;
702 let mut refined = Vec::with_capacity(breaks.len() * 2);
703 for pair in breaks.windows(2) {
704 refined.push(pair[0]);
705 let span = pair[1] - pair[0];
706 let pieces = (span / target).ceil().max(1.0) as usize;
707 for piece in 1..pieces {
708 refined.push(pair[0] + span * piece as f64 / pieces as f64);
709 }
710 }
711 refined.push(high);
712 refined
713}
714
715pub(super) fn tiled_breaks(base: &[f64], lo: f64, hi: f64, period: f64) -> Vec<f64> {
720 if period <= 0.0 || base.len() < 2 {
721 return base.to_vec();
722 }
723 let origin = base[0];
724 let first = ((lo - origin) / period).floor() as i64;
725 let last = ((hi - origin) / period).ceil() as i64;
726 let mut breaks = Vec::new();
727 for tile in first..=last {
728 let shift = tile as f64 * period;
729 for &value in base {
730 let shifted = value + shift;
731 if breaks.last().map_or(true, |&previous: &f64| {
732 (shifted - previous).abs() > 1e-12 * period
733 }) {
734 breaks.push(shifted);
735 }
736 }
737 }
738 breaks
739}