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 if switch!("BREP_MASS_TRIM_JOINS", false) {
105 if end != next && (self.points.last() != Some(&end)
109 || self.arcs.last().is_some_and(Option::is_some))
110 {
111 self.points.push(end);
112 self.arcs.push(None);
113 }
114 return;
115 }
116 let gap = ((next[0] - end[0]).powi(2) + (next[1] - end[1]).powi(2)).sqrt();
117 if gap > self.jump_tolerance {
118 if let Some(last) = self.arcs.last_mut() {
119 *last = None;
120 }
121 }
122 }
123 }
124
125 pub(super) fn push_point(&mut self, point: [f64; 2]) {
127 self.settle_pending(point);
128 self.points.push(point);
129 self.arcs.push(None);
130 }
131
132 pub(super) fn push_coedge(
138 &mut self,
139 loop_index: usize,
140 coedge_index: usize,
141 coedge: &CoedgeRecord,
142 offset: [f64; 2],
143 ) -> Result<(), String> {
144 let curve = &coedge.pcurve;
145 let interior = interior_knots(&curve.knots, curve.degree);
146 if is_straight_polyline(curve) {
147 let [start, end] = curve.domain()?;
148 for parameter in std::iter::once(start).chain(interior.iter().copied()) {
149 let point = curve.evaluate(parameter)?;
150 self.push_point([point.x + offset[0], point.y + offset[1]]);
151 }
152 if switch!("BREP_MASS_TRIM_JOINS", false) {
153 let point = curve.evaluate(end)?;
154 self.pending_end = Some([point.x + offset[0], point.y + offset[1]]);
155 }
156 return Ok(());
157 }
158 let sample_count = (24usize).max((interior.len() + 1) * 16).min(128);
159 self.push_coedge_spans(loop_index, coedge_index, coedge, offset, sample_count, false)
160 }
161
162 pub(super) fn push_coedge_spans(
167 &mut self,
168 loop_index: usize,
169 coedge_index: usize,
170 coedge: &CoedgeRecord,
171 offset: [f64; 2],
172 target: usize,
173 include_end: bool,
174 ) -> Result<(), String> {
175 let curve = &coedge.pcurve;
176 let spans = curve_breaks(curve)?;
177 let [_, end] = curve.domain()?;
178 if is_straight_polyline(curve) {
179 let last = if include_end { spans.len() } else { spans.len() - 1 };
180 for ¶meter in &spans[..last] {
181 let point = curve.evaluate(parameter)?;
182 self.push_point([point.x + offset[0], point.y + offset[1]]);
183 }
184 if !include_end && switch!("BREP_MASS_TRIM_JOINS", false) {
185 let point = curve.evaluate(end)?;
186 self.pending_end = Some([point.x + offset[0], point.y + offset[1]]);
187 }
188 return Ok(());
189 }
190 let span_count = spans.len().saturating_sub(1).max(1);
191 let per_span = target.div_ceil(span_count).max(1);
192 let mut previous: Option<(f64, [f64; 2])> = None;
193 let mut emit = |this: &mut Self, t: f64| -> Result<(), String> {
194 let point = curve.evaluate(t)?;
195 let point = [point.x + offset[0], point.y + offset[1]];
196 if let Some((t_prev, _)) = previous {
197 if let Some(last) = this.arcs.last_mut() {
198 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: t, offset });
199 }
200 }
201 this.settle_pending(point);
202 this.points.push(point);
203 this.arcs.push(None);
204 previous = Some((t, point));
205 Ok(())
206 };
207 for pair in spans.windows(2) {
208 for index in 0..per_span {
209 emit(self, pair[0] + (pair[1] - pair[0]) * index as f64 / per_span as f64)?;
210 }
211 }
212 if include_end {
213 emit(self, end)?;
214 } else if let Some((t_prev, _)) = previous {
215 if let Some(last) = self.arcs.last_mut() {
218 *last = Some(ChordArc { loop_index, coedge_index, t0: t_prev, t1: end, offset });
219 }
220 let point = curve.evaluate(end)?;
221 self.pending_end = Some([point.x + offset[0], point.y + offset[1]]);
222 }
223 Ok(())
224 }
225
226 pub(super) fn finish(&mut self) {
229 if let Some(first) = self.points.first().copied() {
230 self.settle_pending(first);
231 }
232 self.pending_end = None;
233 }
234
235 pub(super) fn reverse(&mut self) {
236 self.finish();
237 let n = self.points.len();
238 if n == 0 {
239 return;
240 }
241 let old = std::mem::take(&mut self.arcs);
242 let swap = |arc: Option<ChordArc>| arc.map(|a| ChordArc { t0: a.t1, t1: a.t0, ..a });
243 let mut arcs = Vec::with_capacity(n);
244 for k in 0..n - 1 {
245 arcs.push(swap(old[n - 2 - k]));
246 }
247 arcs.push(swap(old[n - 1]));
248 self.arcs = arcs;
249 self.points.reverse();
250 }
251
252 pub(super) fn shift_u(&mut self, du: f64) {
253 for point in &mut self.points {
254 point[0] += du;
255 }
256 for arc in self.arcs.iter_mut().flatten() {
257 arc.offset[0] += du;
258 }
259 if let Some(end) = &mut self.pending_end {
260 end[0] += du;
261 }
262 }
263
264 pub(super) fn append(&mut self, mut other: TrimPolygon) {
266 self.finish();
267 other.finish();
268 if let Some(last) = self.arcs.last_mut() {
269 *last = None;
270 }
271 if let Some(last) = other.arcs.last_mut() {
272 *last = None;
273 }
274 self.points.append(&mut other.points);
275 self.arcs.append(&mut other.arcs);
276 }
277}
278
279pub fn trim_polygons(face: &FaceRecord) -> Result<Vec<Vec<[f64; 2]>>, String> {
281 Ok(trim_polygons_tagged(face)?
282 .into_iter()
283 .map(|polygon| polygon.points)
284 .collect())
285}
286
287pub(super) fn trim_polygons_tagged(face: &FaceRecord) -> Result<Vec<TrimPolygon>, String> {
288 let mut polygons = Vec::new();
289 for (loop_index, loop_record) in face.loops.iter().enumerate() {
290 let mut polygon = TrimPolygon::for_face(face)?;
291 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
292 polygon.push_coedge(loop_index, coedge_index, coedge, [0.0, 0.0])?;
293 }
294 polygon.finish();
295 polygons.push(polygon);
296 }
297 Ok(polygons)
298}
299
300pub(super) fn singly_periodic_sphere_cap_polygons(
305 face: &FaceRecord,
306 closed_u: bool,
307 closed_v: bool,
308 domain: [f64; 4],
309) -> Result<Option<Vec<TrimPolygon>>, String> {
310 if !matches!(
311 face.surface.analytic(),
312 Some(crate::AnalyticSurface::Sphere { .. })
313 ) || (closed_u, closed_v) != (true, false)
314 || face.loops.len() != 2
315 {
316 return Ok(None);
317 }
318 let [u0, u1, v0, v1] = domain;
319 let period = u1 - u0;
320 let v_span = v1 - v0;
321 struct Rim {
322 polygon: TrimPolygon,
323 winding: f64,
324 vmin: f64,
325 vmax: f64,
326 }
327 let mut rims = Vec::with_capacity(2);
328 for (loop_index, loop_record) in face.loops.iter().enumerate() {
329 let mut polygon = TrimPolygon::new(domain);
330 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
331 polygon.push_coedge_spans(loop_index, coedge_index, coedge, [0.0, 0.0], 64, true)?;
332 }
333 polygon.finish();
334 let points = &polygon.points;
335 if points.len() < 2 {
336 return Ok(None);
337 }
338 let winding = points.last().unwrap()[0] - points[0][0];
339 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
340 for point in points {
341 vmin = vmin.min(point[1]);
342 vmax = vmax.max(point[1]);
343 }
344 rims.push(Rim {
345 polygon,
346 winding,
347 vmin,
348 vmax,
349 });
350 }
351 if rims
352 .iter()
353 .any(|rim| (rim.winding.abs() - period).abs() > 0.05 * period)
354 || rims[0].winding * rims[1].winding >= 0.0
355 {
356 return Ok(None);
357 }
358 let flat = |rim: &Rim| rim.vmax - rim.vmin <= 1e-6 * v_span;
359 let pole = match (flat(&rims[0]), flat(&rims[1])) {
360 (true, false) => &rims[0],
361 (false, true) => &rims[1],
362 _ => return Ok(None),
363 };
364 let pole_v = 0.5 * (pole.vmin + pole.vmax);
365 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
366 return Ok(None);
367 }
368
369 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
370 (&rims[0], &rims[1])
371 } else if rims[1].vmax <= rims[0].vmin {
372 (&rims[1], &rims[0])
373 } else {
374 return Ok(None);
375 };
376 let mut lower_points = lower.polygon.clone();
377 if lower.winding < 0.0 {
378 lower_points.reverse();
379 }
380 let mut upper_points = upper.polygon.clone();
381 if upper.winding > 0.0 {
382 upper_points.reverse();
383 }
384 let shift = ((lower_points.points[0][0] - upper_points.points.last().unwrap()[0]) / period)
385 .round()
386 * period;
387 upper_points.shift_u(shift);
388 lower_points.append(upper_points);
389 Ok(Some(vec![lower_points]))
390}
391
392pub(super) fn trim_polygons_unwrapped(
398 face: &FaceRecord,
399 closed_u: bool,
400 closed_v: bool,
401 u_period: f64,
402 v_period: f64,
403) -> Result<(Vec<TrimPolygon>, bool), String> {
404 let mut polygons = Vec::new();
405 let mut unwrapped = false;
406 for (loop_index, loop_record) in face.loops.iter().enumerate() {
407 let offsets = crate::topology::loop_seam_offsets(
408 &loop_record.coedges,
409 closed_u,
410 closed_v,
411 u_period,
412 v_period,
413 )?;
414 let mut polygon = TrimPolygon::for_face(face)?;
415 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
416 let offset = offsets[coedge_index];
417 if offset[0] != 0.0 || offset[1] != 0.0 {
418 unwrapped = true;
419 }
420 polygon.push_coedge(loop_index, coedge_index, coedge, offset)?;
421 }
422 polygon.finish();
423 polygons.push(polygon);
424 }
425 Ok((polygons, unwrapped))
426}
427
428pub(super) fn singly_periodic_wall_polygons(
446 face: &FaceRecord,
447 closed_u: bool,
448 closed_v: bool,
449 domain: [f64; 4],
450) -> Result<Option<Vec<TrimPolygon>>, String> {
451 if closed_u == closed_v {
454 return Ok(None);
455 }
456 let [u0, u1, v0, v1] = domain;
457 let p_is_u = closed_u;
458 let period = if p_is_u { u1 - u0 } else { v1 - v0 };
459 if !(period > 0.0) {
460 return Ok(None);
461 }
462 let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
463 let q_extent = (q_dom_hi - q_dom_lo).abs();
464 if !(q_extent > 0.0) {
465 return Ok(None);
466 }
467 let coord = |p: [f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
469
470 let mut rim_indices: Vec<usize> = Vec::new();
471 let mut rim_levels: Vec<f64> = Vec::new();
472 for (index, loop_record) in face.loops.iter().enumerate() {
473 let mut points: Vec<[f64; 2]> = Vec::new();
475 for coedge in &loop_record.coedges {
476 let [d0, d1] = coedge.pcurve.domain()?;
477 let samples = 16;
478 for k in 0..=samples {
479 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
480 let p = coedge.pcurve.evaluate(t)?;
481 points.push([p.x, p.y]);
482 }
483 }
484 if points.len() < 2 {
485 return Ok(None);
486 }
487 let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
488 let mut net = 0.0;
489 for pair in points.windows(2) {
490 let (p0, q0) = coord(pair[0]);
491 let (p1, _q1) = coord(pair[1]);
492 qmin = qmin.min(q0);
493 qmax = qmax.max(q0);
494 let mut delta = p1 - p0;
495 if delta > 0.5 * period {
496 delta -= period;
497 } else if delta < -0.5 * period {
498 delta += period;
499 }
500 net += delta;
501 }
502 if let Some(last) = points.last() {
503 let (_p, q) = coord(*last);
504 qmin = qmin.min(q);
505 qmax = qmax.max(q);
506 }
507 let is_rim = net.abs() > 0.75 * period && (qmax - qmin) <= 0.02 * q_extent;
510 if is_rim {
511 rim_indices.push(index);
512 rim_levels.push(0.5 * (qmin + qmax));
513 }
514 }
515 if rim_levels.len() != 2 {
516 return Ok(None);
517 }
518 let (q_lo, q_hi) = if rim_levels[0] <= rim_levels[1] {
519 (rim_levels[0], rim_levels[1])
520 } else {
521 (rim_levels[1], rim_levels[0])
522 };
523 let tol = 0.02 * q_extent;
527 if (q_lo - q_dom_lo).abs() > tol || (q_hi - q_dom_hi).abs() > tol {
528 return Ok(None);
529 }
530 if (q_hi - q_lo) <= 0.5 * q_extent {
531 return Ok(None);
532 }
533
534 let (r_ulo, r_uhi, r_vlo, r_vhi) = if p_is_u {
536 (u0, u1, q_lo, q_hi)
537 } else {
538 (q_lo, q_hi, v0, v1)
539 };
540 let mut rectangle = TrimPolygon::new(domain);
541 for corner in [
542 [r_ulo, r_vlo],
543 [r_uhi, r_vlo],
544 [r_uhi, r_vhi],
545 [r_ulo, r_vhi],
546 ] {
547 rectangle.push_point(corner);
548 }
549 let mut polygons: Vec<TrimPolygon> = vec![rectangle];
550 for (index, loop_record) in face.loops.iter().enumerate() {
553 if rim_indices.contains(&index) {
554 continue;
555 }
556 let offsets = crate::topology::loop_seam_offsets(
557 &loop_record.coedges,
558 closed_u,
559 closed_v,
560 u1 - u0,
561 v1 - v0,
562 )?;
563 let mut polygon = TrimPolygon::new(domain);
564 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
565 polygon.push_coedge(index, coedge_index, coedge, offsets[coedge_index])?;
566 }
567 polygon.finish();
568 polygons.push(polygon);
569 }
570 Ok(Some(polygons))
571}
572
573pub(super) fn doubly_periodic_seam_band_polygons(
587 face: &FaceRecord,
588 domain: [f64; 4],
589) -> Result<Option<Vec<TrimPolygon>>, String> {
590 let (closed_u, closed_v) = face.surface.closed_directions()?;
591 if !(closed_u && closed_v) {
592 return Ok(None);
593 }
594 type Tagged = ([f64; 2], Option<(usize, usize, f64)>);
598 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
599 let mut loops_tagged: Vec<Vec<Tagged>> = Vec::with_capacity(face.loops.len());
600 for (loop_index, loop_record) in face.loops.iter().enumerate() {
601 let mut points: Vec<[f64; 2]> = Vec::new();
602 let mut tagged: Vec<Tagged> = Vec::new();
603 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
604 let [d0, d1] = coedge.pcurve.domain()?;
605 let samples = 24;
606 let curved = !is_straight_polyline(&coedge.pcurve);
607 for k in 0..=samples {
608 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
609 let p = coedge.pcurve.evaluate(t)?;
610 points.push([p.x, p.y]);
611 tagged.push(([p.x, p.y], curved.then_some((loop_index, coedge_index, t))));
612 }
613 }
614 loops_uv.push(points);
615 loops_tagged.push(tagged);
616 }
617 let Some(band) =
618 crate::topology::analyze_doubly_periodic_seam_band(&loops_uv, domain, face.same_sense)
619 else {
620 return Ok(None);
621 };
622 let vertices = crate::topology::seam_band_uv_polygon_with(
623 &loops_tagged,
624 |vertex: &Tagged| vertex.0,
625 |point| (point, None),
626 domain,
627 &band,
628 );
629 let mut polygon = TrimPolygon::new(domain);
630 let n = vertices.len();
631 for (k, (point, tag)) in vertices.iter().enumerate() {
632 polygon.push_point(*point);
633 let next = &vertices[(k + 1) % n];
637 if let (Some((l0, c0, t0)), Some((l1, c1, t1))) = (tag, next.1) {
638 if *l0 == l1 && *c0 == c1 {
639 polygon.arcs[k] = Some(ChordArc {
640 loop_index: *l0,
641 coedge_index: *c0,
642 t0: *t0,
643 t1,
644 offset: [0.0, 0.0],
645 });
646 }
647 }
648 }
649 Ok(Some(vec![polygon]))
650}
651
652pub(super) fn trim_on_domain_boundary(face: &FaceRecord, domain: [f64; 4]) -> Result<bool, String> {
657 let [u0, u1, v0, v1] = domain;
658 let sides = |p: Vec3| -> u8 {
659 if !p.x.is_finite() || !p.y.is_finite()
660 || p.x < u0 || p.x > u1 || p.y < v0 || p.y > v1 {
661 return 0;
662 }
663 u8::from(p.x == u0) | (u8::from(p.x == u1) << 1)
664 | (u8::from(p.y == v0) << 2) | (u8::from(p.y == v1) << 3)
665 };
666 for rim in &face.loops {
667 if rim.coedges.is_empty() { return Ok(false); }
668 let mut ends = Vec::with_capacity(rim.coedges.len());
669 for coedge in &rim.coedges {
670 let curve = &coedge.pcurve;
671 let mut common = 15;
672 for control in &curve.control_points {
673 if !(control.w > 0.0) { return Ok(false); }
674 common &= sides(control.point()?);
675 }
676 if common == 0 || curve.control_points.is_empty() { return Ok(false); }
677 let [a, b] = curve.domain()?;
678 ends.push((sides(curve.evaluate(a)?), sides(curve.evaluate(b)?)));
679 }
680 for i in 0..ends.len() {
681 if ends[i].1 & ends[(i + 1) % ends.len()].0 == 0 { return Ok(false); }
682 }
683 }
684 Ok(true)
685}
686
687pub(super) fn is_untrimmed(face: &FaceRecord) -> Result<bool, String> {
688 if face.loops.len() != 1 {
689 return Ok(false);
690 }
691 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
692 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
693 let [u0, u1] = ku.domain();
694 let [v0, v1] = kv.domain();
695 let domain_area = (u1 - u0) * (v1 - v0);
696 if switch!("BREP_MASS_EXACT_DOMAIN_COVER", false)
697 && !trim_on_domain_boundary(face, [u0, u1, v0, v1])? {
698 return Ok(false);
699 }
700 Ok((closed_parameter_space_area_on_carrier(face)?.abs() - domain_area).abs() <= 1e-6 * domain_area)
704}
705
706pub(super) fn iso_rectangle_trim(face: &FaceRecord) -> Result<Option<[f64; 4]>, String> {
717 let found = iso_rectangle_trim_impl(face)?;
718 if let Err(reason) = &found {
719 if super::profile::face_lines_enabled() {
720 eprintln!("mass.iso_rect face={} declined: {reason}", face.id);
721 }
722 }
723 Ok(found.ok())
724}
725
726pub(super) fn pole_side_clip_on() -> bool {
732 switch!("BREP_MASS_ISO_RECT_TRIMMED", true)
733}
734
735fn iso_rectangle_trim_impl(face: &FaceRecord) -> Result<Result<[f64; 4], &'static str>, String> {
736 if face.loops.len() != 1 {
737 return Ok(Err("not one loop"));
738 }
739 if face.loops[0].coedges.len() != 4 {
740 return Ok(Err("not four coedges"));
741 }
742 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
743 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
744 let [u0, u1] = ku.domain();
745 let [v0, v1] = kv.domain();
746 let (span_u, span_v) = (u1 - u0, v1 - v0);
747 if !(span_u > 0.0 && span_v > 0.0) {
748 return Ok(Err("empty domain"));
749 }
750 let exact = switch!("BREP_MASS_EXACT_DOMAIN_COVER", false);
758 let (eps_u, eps_v) = if exact { (0.0, 0.0) } else { (1e-7 * span_u, 1e-7 * span_v) };
759 let mut segments: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(4);
760 for coedge in &face.loops[0].coedges {
761 let pcurve = &coedge.pcurve;
768 if pcurve.control_points.len() < 2 {
769 return Ok(Err("a pcurve with one control point"));
770 }
771 let mut controls = Vec::with_capacity(pcurve.control_points.len());
772 for control in &pcurve.control_points {
773 controls.push(control.point()?);
774 }
775 let (first, last) = (controls[0], controls[controls.len() - 1]);
776 let (du, dv) = ((last.x - first.x).abs(), (last.y - first.y).abs());
777 let along_u = dv <= eps_v && du > eps_u;
778 let along_v = du <= eps_u && dv > eps_v;
779 if !(along_u || along_v) {
780 return Ok(Err("a pcurve not parallel to a parameter axis"));
781 }
782 let (constant, varying, eps_constant): (fn(&Vec3) -> f64, fn(&Vec3) -> f64, f64) =
783 if along_u { (|p| p.y, |p| p.x, eps_v) } else { (|p| p.x, |p| p.y, eps_u) };
784 let level = constant(&first);
785 if controls.iter().any(|control| (constant(control) - level).abs() > eps_constant) {
786 return Ok(Err("a pcurve whose controls leave its line"));
787 }
788 let direction = (varying(&last) - varying(&first)).signum();
789 if controls.windows(2).any(|pair| (varying(&pair[1]) - varying(&pair[0])) * direction < 0.0) {
790 return Ok(Err("a pcurve whose controls turn back"));
791 }
792 segments.push(([first.x, first.y], [last.x, last.y]));
795 }
796 let along_u = |segment: &([f64; 2], [f64; 2])| (segment.1[1] - segment.0[1]).abs() <= eps_v;
797 if pole_side_clip_on() {
807 for index in 0..4 {
808 let segment = segments[index];
809 if !along_u(&segment) {
810 continue;
811 }
812 let level = segment.0[1];
813 let probe = |u: f64| face.surface.evaluate(u, level);
814 let (a, b, c) = (probe(u0)?, probe(0.5 * (u0 + u1))?, probe(u1)?);
815 let scale = a.length().max(1.0) * 1e-12;
816 if a.sub(b).length() > scale || a.sub(c).length() > scale {
817 continue;
818 }
819 let previous = segments[(index + 3) % 4].1;
820 let next = segments[(index + 1) % 4].0;
821 if (previous[1] - level).abs() <= eps_v && (next[1] - level).abs() <= eps_v {
822 segments[index] = ([previous[0], level], [next[0], level]);
823 }
824 }
825 }
826 for index in 0..4 {
827 let (_, to) = segments[index];
828 let (from, _) = segments[(index + 1) % 4];
829 if (to[0] - from[0]).abs() > eps_u || (to[1] - from[1]).abs() > eps_v {
830 if super::profile::face_lines_enabled() {
831 eprintln!(
832 "mass.iso_rect face={} gap between pcurves {} and {}: ({:.3e}, {:.3e}) of spans ({:.3e}, {:.3e})",
833 face.id, index, (index + 1) % 4, to[0] - from[0], to[1] - from[1], span_u, span_v
834 );
835 }
836 return Ok(Err("consecutive pcurves do not meet"));
837 }
838 if along_u(&segments[index]) == along_u(&segments[(index + 1) % 4]) {
839 return Ok(Err("consecutive pcurves run the same way"));
840 }
841 }
842 let corners: Vec<[f64; 2]> = segments.iter().map(|segment| segment.0).collect();
843 if !(polygon_signed_area(&corners) > 0.0) {
844 return Ok(Err("not wound counter-clockwise"));
845 }
846 let (mut u_lo, mut u_hi, mut v_lo, mut v_hi) = (f64::INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::NEG_INFINITY);
847 for corner in &corners {
848 u_lo = u_lo.min(corner[0]);
849 u_hi = u_hi.max(corner[0]);
850 v_lo = v_lo.min(corner[1]);
851 v_hi = v_hi.max(corner[1]);
852 }
853 let (slack_u, slack_v) = if exact { (0.0, 0.0) } else { (1e-6 * span_u, 1e-6 * span_v) };
856 if u_lo < u0 - slack_u || u_hi > u1 + slack_u || v_lo < v0 - slack_v || v_hi > v1 + slack_v {
857 return Ok(Err("a corner outside the domain"));
858 }
859 let rectangle = [u_lo.max(u0), u_hi.min(u1), v_lo.max(v0), v_hi.min(v1)];
860 if rectangle[1] - rectangle[0] <= eps_u || rectangle[3] - rectangle[2] <= eps_v {
861 return Ok(Err("a collapsed rectangle"));
862 }
863 Ok(Ok(rectangle))
864}
865
866pub(super) const TRIANGLE_CUBATURE: [([f64; 3], f64); 7] = [
869 ([1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.225),
870 (
871 [0.101286507323456, 0.101286507323456, 0.797426985353087],
872 0.125939180544827,
873 ),
874 (
875 [0.101286507323456, 0.797426985353087, 0.101286507323456],
876 0.125939180544827,
877 ),
878 (
879 [0.797426985353087, 0.101286507323456, 0.101286507323456],
880 0.125939180544827,
881 ),
882 (
883 [0.470142064105115, 0.470142064105115, 0.059715871789770],
884 0.132394152788506,
885 ),
886 (
887 [0.470142064105115, 0.059715871789770, 0.470142064105115],
888 0.132394152788506,
889 ),
890 (
891 [0.059715871789770, 0.470142064105115, 0.470142064105115],
892 0.132394152788506,
893 ),
894];
895
896pub(super) fn polygon_signed_area(polygon: &[[f64; 2]]) -> f64 {
897 crate::polygon::signed_area_2d(polygon.len(), |i| polygon[i])
898}
899
900pub(super) fn clip_polygon_to_cell_into(
912 polygon: &[[f64; 2]],
913 cell: [f64; 4],
914 scratch: &mut Vec<[f64; 2]>,
915 out: &mut Vec<[f64; 2]>,
916) {
917 let [u_low, u_high, v_low, v_high] = cell;
918 out.clear();
919 out.extend_from_slice(polygon);
920 for (axis, bound, keep_below) in [
922 (0, u_low, false),
923 (0, u_high, true),
924 (1, v_low, false),
925 (1, v_high, true),
926 ] {
927 if out.len() < 3 {
928 out.clear();
929 return;
930 }
931 let inside = |point: &[f64; 2]| {
932 if keep_below {
933 point[axis] <= bound
934 } else {
935 point[axis] >= bound
936 }
937 };
938 scratch.clear();
939 scratch.reserve(out.len() + 4);
940 for index in 0..out.len() {
941 let a = out[index];
942 let b = out[(index + 1) % out.len()];
943 let a_in = inside(&a);
944 let b_in = inside(&b);
945 if a_in {
946 scratch.push(a);
947 }
948 if a_in != b_in {
949 let t = (bound - a[axis]) / (b[axis] - a[axis]);
950 let mut crossing = [0.0; 2];
951 crossing[axis] = bound;
952 crossing[1 - axis] = a[1 - axis] + t * (b[1 - axis] - a[1 - axis]);
953 scratch.push(crossing);
954 }
955 }
956 std::mem::swap(scratch, out);
957 }
958}
959
960pub(super) fn clip_polygon_to_column_into(
967 polygon: &[[f64; 2]],
968 u_low: f64,
969 u_high: f64,
970 scratch: &mut Vec<[f64; 2]>,
971 out: &mut Vec<[f64; 2]>,
972) {
973 out.clear();
974 out.extend_from_slice(polygon);
975 clip_passes(&[(0, u_low, false), (0, u_high, true)], scratch, out);
976}
977
978pub(super) fn clip_column_to_cell_into(
981 column: &[[f64; 2]],
982 v_low: f64,
983 v_high: f64,
984 scratch: &mut Vec<[f64; 2]>,
985 out: &mut Vec<[f64; 2]>,
986) {
987 out.clear();
988 out.extend_from_slice(column);
989 clip_passes(&[(1, v_low, false), (1, v_high, true)], scratch, out);
990}
991
992fn clip_passes(passes: &[(usize, f64, bool)], scratch: &mut Vec<[f64; 2]>, out: &mut Vec<[f64; 2]>) {
995 for &(axis, bound, keep_below) in passes {
996 if out.len() < 3 {
997 out.clear();
998 return;
999 }
1000 let inside = |point: &[f64; 2]| {
1001 if keep_below {
1002 point[axis] <= bound
1003 } else {
1004 point[axis] >= bound
1005 }
1006 };
1007 scratch.clear();
1008 scratch.reserve(out.len() + 4);
1009 for index in 0..out.len() {
1010 let a = out[index];
1011 let b = out[(index + 1) % out.len()];
1012 let a_in = inside(&a);
1013 let b_in = inside(&b);
1014 if a_in {
1015 scratch.push(a);
1016 }
1017 if a_in != b_in {
1018 let t = (bound - a[axis]) / (b[axis] - a[axis]);
1019 let mut crossing = [0.0; 2];
1020 crossing[axis] = bound;
1021 crossing[1 - axis] = a[1 - axis] + t * (b[1 - axis] - a[1 - axis]);
1022 scratch.push(crossing);
1023 }
1024 }
1025 std::mem::swap(scratch, out);
1026 }
1027}
1028
1029pub(super) fn cell_breaks(knots: &[f64], degree: usize, low: f64, high: f64) -> Vec<f64> {
1030 let mut breaks = vec![low, high];
1031 breaks.extend(interior_knots(knots, degree));
1032 breaks.sort_by(f64::total_cmp);
1033 breaks.dedup_by(|a, b| (*a - *b).abs() <= (high - low) * 1e-12);
1034 const MINIMUM_CELLS: usize = 8;
1037 let target = (high - low) / MINIMUM_CELLS as f64;
1038 let mut refined = Vec::with_capacity(breaks.len() * 2);
1039 for pair in breaks.windows(2) {
1040 refined.push(pair[0]);
1041 let span = pair[1] - pair[0];
1042 let pieces = (span / target).ceil().max(1.0) as usize;
1043 for piece in 1..pieces {
1044 refined.push(pair[0] + span * piece as f64 / pieces as f64);
1045 }
1046 }
1047 refined.push(high);
1048 refined
1049}
1050
1051pub(super) fn tiled_breaks(base: &[f64], lo: f64, hi: f64, period: f64) -> Vec<f64> {
1056 if period <= 0.0 || base.len() < 2 {
1057 return base.to_vec();
1058 }
1059 let origin = base[0];
1060 let first = ((lo - origin) / period).floor() as i64;
1061 let last = ((hi - origin) / period).ceil() as i64;
1062 let mut breaks = Vec::new();
1063 for tile in first..=last {
1064 let shift = tile as f64 * period;
1065 for &value in base {
1066 let shifted = value + shift;
1067 if breaks.last().map_or(true, |&previous: &f64| {
1068 (shifted - previous).abs() > 1e-12 * period
1069 }) {
1070 breaks.push(shifted);
1071 }
1072 }
1073 }
1074 breaks
1075}