1pub mod classify;
4pub mod combine;
5pub mod intersect;
6
7use crate::types::BooleanOp;
8use neco_nurbs::{dedup_piecewise_sample, NurbsCurve2D, NurbsRegion};
9
10#[derive(Clone, Debug, Default)]
11pub struct RegionSet {
12 pub regions: Vec<NurbsRegion>,
13}
14
15impl RegionSet {
16 pub fn new(regions: Vec<NurbsRegion>) -> Self {
17 Self { regions }
18 }
19
20 pub fn is_empty(&self) -> bool {
21 self.regions.is_empty()
22 }
23
24 pub fn len(&self) -> usize {
25 self.regions.len()
26 }
27
28 pub fn as_slice(&self) -> &[NurbsRegion] {
29 &self.regions
30 }
31
32 pub fn into_regions(self) -> Vec<NurbsRegion> {
33 self.regions
34 }
35}
36
37#[inline]
39fn dist2(a: [f64; 2], b: [f64; 2]) -> f64 {
40 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
41}
42
43#[derive(Clone, Debug)]
45pub enum Intersection {
46 Point {
47 point: [f64; 2],
48 t_a: f64,
49 t_b: f64,
50 },
51 Overlap {
53 t_a: (f64, f64),
54 t_b: (f64, f64),
55 },
56}
57
58pub fn boolean_2d(a: &NurbsRegion, b: &NurbsRegion, op: BooleanOp) -> Result<NurbsRegion, String> {
63 let result = boolean_2d_all(a, b, op)?;
64 match result.len() {
65 0 => Err("boolean result is empty; use boolean_2d_all".into()),
66 1 => match result.into_regions().into_iter().next() {
67 Some(region) => Ok(region),
68 None => Err("boolean result lost its single region unexpectedly".into()),
69 },
70 n => Err(format!(
71 "boolean result has {} regions; use boolean_2d_all",
72 n
73 )),
74 }
75}
76
77pub fn boolean_2d_all(
79 a: &NurbsRegion,
80 b: &NurbsRegion,
81 op: BooleanOp,
82) -> Result<RegionSet, String> {
83 boolean_2d_inner(a, b, op)
84}
85
86#[derive(Clone, Debug)]
88pub enum SegmentKind {
89 Normal,
90 Overlap {
92 other_curve_t_mid: f64,
93 other_curve_index: usize,
94 },
95}
96
97struct PiecewiseOverlap {
99 my: (usize, f64, f64),
100 other: (usize, f64, f64),
101}
102
103fn classify_segment_kinds(
105 segs: &[NurbsCurve2D],
106 overlaps: &[PiecewiseOverlap],
107 seg_offsets: &[usize],
108 is_curve_a: bool,
109) -> Vec<SegmentKind> {
110 segs.iter()
111 .enumerate()
112 .map(|(i, seg)| {
113 let n = seg.control_points.len();
114 let t_min = seg.knots[seg.degree];
115 let t_max = seg.knots[n];
116 let t_mid = 0.5 * (t_min + t_max);
117
118 for ov in overlaps {
119 let (my, other) = if is_curve_a {
120 (&ov.my, &ov.other)
121 } else {
122 (&ov.other, &ov.my)
123 };
124 let (my_seg_idx, my_t0, my_t1) = *my;
125 let (other_seg_idx, other_t0, other_t1) = *other;
126
127 let split_start = seg_offsets[my_seg_idx];
129 let split_end = if my_seg_idx + 1 < seg_offsets.len() {
130 seg_offsets[my_seg_idx + 1]
131 } else {
132 segs.len()
133 };
134 if i < split_start || i >= split_end {
135 continue;
136 }
137
138 let (my_lo, my_hi) = if my_t0 <= my_t1 {
140 (my_t0, my_t1)
141 } else {
142 (my_t1, my_t0)
143 };
144 if t_mid >= my_lo - 1e-8 && t_mid <= my_hi + 1e-8 {
146 let other_mid = 0.5 * (other_t0 + other_t1);
147 return SegmentKind::Overlap {
148 other_curve_t_mid: other_mid,
149 other_curve_index: other_seg_idx,
150 };
151 }
152 }
153 SegmentKind::Normal
154 })
155 .collect()
156}
157
158fn boolean_2d_inner(a: &NurbsRegion, b: &NurbsRegion, op: BooleanOp) -> Result<RegionSet, String> {
159 let outer_a = &a.outer;
160 let outer_b = &b.outer;
161
162 let mut params_a: Vec<Vec<f64>> = vec![Vec::new(); outer_a.len()];
164 let mut params_b: Vec<Vec<f64>> = vec![Vec::new(); outer_b.len()];
165 let mut pw_overlaps: Vec<PiecewiseOverlap> = Vec::new();
166 let mut has_any_intersection = false;
167
168 for (ia, seg_a) in outer_a.iter().enumerate() {
169 for (ib, seg_b) in outer_b.iter().enumerate() {
170 let ixns = intersect::find_intersections(seg_a, seg_b);
171 for ix in &ixns {
172 has_any_intersection = true;
173 match ix {
174 Intersection::Point { t_a, t_b, .. } => {
175 params_a[ia].push(*t_a);
176 params_b[ib].push(*t_b);
177 }
178 Intersection::Overlap { t_a, t_b } => {
179 params_a[ia].push(t_a.0);
180 params_a[ia].push(t_a.1);
181 params_b[ib].push(t_b.0);
182 params_b[ib].push(t_b.1);
183 pw_overlaps.push(PiecewiseOverlap {
184 my: (ia, t_a.0, t_a.1),
185 other: (ib, t_b.0, t_b.1),
186 });
187 }
188 }
189 }
190 }
191 }
192
193 if !has_any_intersection {
194 return handle_no_intersection(a, b, op);
195 }
196
197 let eps = 1e-10;
198
199 fn prepare_and_split(
201 outer: &[NurbsCurve2D],
202 params: &mut [Vec<f64>],
203 eps: f64,
204 ) -> (Vec<NurbsCurve2D>, Vec<usize>) {
205 let mut all_segs: Vec<NurbsCurve2D> = Vec::new();
206 let mut seg_offsets: Vec<usize> = Vec::new();
207
208 for (i, curve) in outer.iter().enumerate() {
209 seg_offsets.push(all_segs.len());
210
211 let n = curve.control_points.len();
212 let t_lo = curve.knots[curve.degree];
213 let t_hi = curve.knots[n];
214
215 let ts = &mut params[i];
216 ts.sort_by(|x, y| x.total_cmp(y));
217 ts.dedup_by(|a, b| (*a - *b).abs() < eps);
218 ts.retain(|&t| t > t_lo + eps && t < t_hi - eps);
219
220 let split = curve.split_at_params(ts);
221 all_segs.extend(split);
222 }
223 (all_segs, seg_offsets)
224 }
225
226 let (segs_a, seg_offsets_a) = prepare_and_split(outer_a, &mut params_a, eps);
227 let (segs_b, seg_offsets_b) = prepare_and_split(outer_b, &mut params_b, eps);
228
229 let all_trivial_a = segs_a.len() == outer_a.len();
231 let all_trivial_b = segs_b.len() == outer_b.len();
232 if all_trivial_a && all_trivial_b && pw_overlaps.is_empty() {
233 return handle_no_intersection(a, b, op);
234 }
235
236 let kinds_a = classify_segment_kinds(&segs_a, &pw_overlaps, &seg_offsets_a, true);
237 let kinds_b = classify_segment_kinds(&segs_b, &pw_overlaps, &seg_offsets_b, false);
238
239 let locs_a = classify::classify_segments_with_kinds(&segs_a, &kinds_a, outer_b, outer_a);
240 let locs_b = classify::classify_segments_with_kinds(&segs_b, &kinds_b, outer_a, outer_b);
241
242 let selected = combine::select_segments(op, &segs_a, &locs_a, &segs_b, &locs_b);
243 let loops = if selected.is_empty() {
244 Vec::new()
245 } else {
246 combine::build_loops(selected, 1e-6)?
247 };
248 Ok(region_set_from_loops(loops))
249}
250
251fn sample_start_point(outer: &[NurbsCurve2D]) -> [f64; 2] {
253 let seg = &outer[0];
254 let t_start = seg.knots[seg.degree];
255 seg.evaluate(t_start)
256}
257
258fn handle_no_intersection(
260 a: &NurbsRegion,
261 b: &NurbsRegion,
262 op: BooleanOp,
263) -> Result<RegionSet, String> {
264 let a_sample = sample_start_point(&a.outer);
265 let b_sample = sample_start_point(&b.outer);
266 let a_in_b = classify::point_in_nurbs_region(&a_sample, &b.outer);
267 let b_in_a = classify::point_in_nurbs_region(&b_sample, &a.outer);
268
269 match op {
270 BooleanOp::Union => {
271 if a_in_b {
272 Ok(RegionSet::new(vec![b.clone()]))
273 } else if b_in_a {
274 Ok(RegionSet::new(vec![a.clone()]))
275 } else {
276 Ok(RegionSet::new(vec![a.clone(), b.clone()]))
277 }
278 }
279 BooleanOp::Subtract => {
280 if b_in_a {
281 Ok(RegionSet::new(vec![NurbsRegion {
283 outer: a.outer.clone(),
284 holes: vec![b.outer.clone()],
285 }]))
286 } else if a_in_b {
287 Ok(RegionSet::default())
288 } else {
289 Ok(RegionSet::new(vec![a.clone()]))
291 }
292 }
293 BooleanOp::Intersect => {
294 if a_in_b {
295 Ok(RegionSet::new(vec![a.clone()]))
296 } else if b_in_a {
297 Ok(RegionSet::new(vec![b.clone()]))
298 } else {
299 Ok(RegionSet::default())
300 }
301 }
302 }
303}
304
305fn region_set_from_loops(loops: Vec<Vec<NurbsCurve2D>>) -> RegionSet {
306 if loops.is_empty() {
307 return RegionSet::default();
308 }
309
310 #[derive(Clone)]
311 struct LoopInfo {
312 curves: Vec<NurbsCurve2D>,
313 sample: [f64; 2],
314 area: f64,
315 parent: Option<usize>,
316 depth: usize,
317 }
318
319 let mut infos: Vec<LoopInfo> = loops
320 .into_iter()
321 .map(|curves| {
322 let sample = sample_start_point(&curves);
323 let area = sampled_loop_area(&curves).abs();
324 LoopInfo {
325 curves,
326 sample,
327 area,
328 parent: None,
329 depth: 0,
330 }
331 })
332 .collect();
333
334 for i in 0..infos.len() {
335 let mut parent = None;
336 let mut parent_area = f64::INFINITY;
337 for j in 0..infos.len() {
338 if i == j {
339 continue;
340 }
341 if classify::point_in_nurbs_region(&infos[i].sample, &infos[j].curves)
342 && infos[j].area < parent_area
343 {
344 parent = Some(j);
345 parent_area = infos[j].area;
346 }
347 }
348 infos[i].parent = parent;
349 }
350
351 for i in 0..infos.len() {
352 let mut depth = 0usize;
353 let mut cursor = infos[i].parent;
354 while let Some(parent) = cursor {
355 depth += 1;
356 cursor = infos[parent].parent;
357 }
358 infos[i].depth = depth;
359 }
360
361 let mut regions: Vec<NurbsRegion> = infos
362 .iter()
363 .filter(|info| info.depth % 2 == 0)
364 .map(|info| NurbsRegion {
365 outer: info.curves.clone(),
366 holes: vec![],
367 })
368 .collect();
369
370 for info in infos.iter().filter(|info| info.depth % 2 == 1) {
371 let mut cursor = info.parent;
372 while let Some(parent) = cursor {
373 if infos[parent].depth % 2 == 0 {
374 if let Some(region) = regions
375 .iter_mut()
376 .find(|region| same_loop(region.outer_curves(), &infos[parent].curves))
377 {
378 region.holes.push(info.curves.clone());
379 }
380 break;
381 }
382 cursor = infos[parent].parent;
383 }
384 }
385
386 RegionSet::new(regions)
387}
388
389fn same_loop(a: &[NurbsCurve2D], b: &[NurbsCurve2D]) -> bool {
390 let pa = sample_start_point(a);
391 let pb = sample_start_point(b);
392 dist2(pa, pb) < 1e-8 && (sampled_loop_area(a) - sampled_loop_area(b)).abs() < 1e-6
393}
394
395fn sampled_loop_area(curves: &[NurbsCurve2D]) -> f64 {
396 let polygon = dedup_piecewise_sample(curves.iter(), 0.01);
397 signed_polygon_area(&polygon)
398}
399
400fn signed_polygon_area(points: &[[f64; 2]]) -> f64 {
401 if points.len() < 3 {
402 return 0.0;
403 }
404 let mut area = 0.0;
405 for i in 0..points.len() {
406 let j = (i + 1) % points.len();
407 area += points[i][0] * points[j][1] - points[j][0] * points[i][1];
408 }
409 area * 0.5
410}