1use crate::area::{areas_conserved, twice_area_fp2};
2use crate::bayazit::{bayazit_decompose, find_steiner_points};
3use crate::ear_clip::ear_clip_triangulate;
4use crate::exact_partition::exact_vertex_partition;
5use crate::hertel_mehlhorn::optimize_partition;
6use crate::ring::{is_simple, normalize_ring, rotate_ring};
7use crate::topology::validate_multipart_topology;
8use crate::types::{
9 Attempt, DecompError, DecomposeOptions, DecomposeResult, Outcome, ProtocolConfig, Strategy,
10};
11use crate::validation::validate_part;
12
13type Parts = Vec<Vec<[i64; 2]>>;
14
15#[derive(Debug)]
16enum AttemptError {
17 TooManyParts { count: usize },
18 ValidationFailed(Vec<String>),
19 Failed(String),
20}
21
22struct StrategySuccess {
23 parts: Parts,
24 strategy: Strategy,
25}
26
27pub fn decompose(
28 ring: &[[i64; 2]],
29 options: &DecomposeOptions,
30 config: &ProtocolConfig,
31) -> Result<DecomposeResult, DecompError> {
32 decompose_with_strategies(
33 ring,
34 options,
35 config,
36 exact_vertex_partition,
37 bayazit_decompose,
38 ear_clip_triangulate,
39 find_steiner_points,
40 )
41}
42
43fn decompose_with_strategies<Exact, Bayazit, EarClip, Steiner>(
44 ring: &[[i64; 2]],
45 options: &DecomposeOptions,
46 config: &ProtocolConfig,
47 exact: Exact,
48 bayazit: Bayazit,
49 ear_clip: EarClip,
50 steiner_points: Steiner,
51) -> Result<DecomposeResult, DecompError>
52where
53 Exact: Fn(&[[i64; 2]]) -> Result<Parts, String>,
54 Bayazit: Fn(&[[i64; 2]], bool, &ProtocolConfig) -> Result<Parts, String>,
55 EarClip: Fn(&[[i64; 2]]) -> Result<Parts, String>,
56 Steiner: Fn(&[[i64; 2]], &[Vec<[i64; 2]>]) -> Vec<[i64; 2]>,
57{
58 if ring.len() < 3 {
59 return Err(DecompError::TooFewVertices);
60 }
61
62 let normalized = normalize_ring(ring).ok_or(DecompError::TooFewVertices)?;
63 if !is_simple(&normalized) {
64 return Err(DecompError::NotSimple);
65 }
66
67 let original_area = twice_area_fp2(&normalized);
68 if original_area == 0 {
69 return Err(DecompError::TooFewVertices);
70 }
71
72 let attempt_count = normalized.len().min(options.max_rotation_attempts.max(1));
73 let mut trace = options.collect_trace.then(Vec::new);
74
75 if options.minimize_parts {
76 return run_minimize_parts(
77 ring,
78 &normalized,
79 options,
80 config,
81 original_area,
82 attempt_count,
83 &exact,
84 &bayazit,
85 &ear_clip,
86 &steiner_points,
87 &mut trace,
88 );
89 }
90
91 let mut saw_too_many_parts = false;
92 let mut last_error: Option<String> = None;
93
94 for rotation in 0..attempt_count {
95 let rotated = rotate_ring(&normalized, rotation);
96 match try_decompose(
97 &rotated,
98 options,
99 config,
100 original_area,
101 &exact,
102 &bayazit,
103 &ear_clip,
104 rotation,
105 &mut trace,
106 ) {
107 Ok(success) => {
108 let steiner = steiner_points(ring, &success.parts);
109 let strategy = wrap_rotation(success.strategy, rotation);
110 return Ok(DecomposeResult {
111 parts: success.parts,
112 steiner_points: steiner,
113 strategy,
114 trace,
115 });
116 }
117 Err(AttemptError::TooManyParts { count }) => {
118 saw_too_many_parts = true;
119 last_error = Some(format!("decomposition produced {count} parts"));
120 }
121 Err(AttemptError::ValidationFailed(errors)) => {
122 last_error = Some(errors.join("; "));
123 }
124 Err(AttemptError::Failed(err)) => last_error = Some(err),
125 }
126 }
127
128 if saw_too_many_parts {
129 Err(DecompError::TooManyParts)
130 } else {
131 Err(DecompError::Failed(last_error.unwrap_or_else(|| {
132 "all decomposition strategies exhausted".into()
133 })))
134 }
135}
136
137struct Candidate {
139 parts: Parts,
140 strategy: Strategy,
141 rotation: usize,
142 steiner_points: Vec<[i64; 2]>,
143}
144
145fn candidate_key(c: &Candidate) -> (usize, usize, usize, u8) {
148 let strategy_rank = match c.strategy {
149 Strategy::AlreadyConvex => 0,
150 Strategy::ExactPartition => 1,
151 Strategy::Bayazit => 2,
152 Strategy::EarClipMerge => 3,
153 Strategy::Rotation { .. } => 4, };
155 (
156 c.parts.len(),
157 c.steiner_points.len(),
158 c.rotation,
159 strategy_rank,
160 )
161}
162
163#[allow(clippy::too_many_arguments)]
164fn run_minimize_parts<Exact, Bayazit, EarClip, Steiner>(
165 original_ring: &[[i64; 2]],
166 normalized: &[[i64; 2]],
167 options: &DecomposeOptions,
168 config: &ProtocolConfig,
169 original_area: u128,
170 attempt_count: usize,
171 exact: &Exact,
172 bayazit: &Bayazit,
173 ear_clip: &EarClip,
174 steiner_points: &Steiner,
175 trace: &mut Option<Vec<Attempt>>,
176) -> Result<DecomposeResult, DecompError>
177where
178 Exact: Fn(&[[i64; 2]]) -> Result<Parts, String>,
179 Bayazit: Fn(&[[i64; 2]], bool, &ProtocolConfig) -> Result<Parts, String>,
180 EarClip: Fn(&[[i64; 2]]) -> Result<Parts, String>,
181 Steiner: Fn(&[[i64; 2]], &[Vec<[i64; 2]>]) -> Vec<[i64; 2]>,
182{
183 let mut candidates: Vec<Candidate> = Vec::new();
184 let mut saw_too_many_parts = false;
185 let mut last_error: Option<String> = None;
186
187 for rotation in 0..attempt_count {
188 let rotated = rotate_ring(normalized, rotation);
189 collect_candidates_for_rotation(
190 original_ring,
191 &rotated,
192 options,
193 config,
194 original_area,
195 rotation,
196 exact,
197 bayazit,
198 ear_clip,
199 steiner_points,
200 trace,
201 &mut candidates,
202 &mut saw_too_many_parts,
203 &mut last_error,
204 );
205 }
206
207 match candidates.into_iter().min_by_key(candidate_key) {
208 Some(best) => {
209 let wrapped = wrap_rotation(best.strategy, best.rotation);
210 Ok(DecomposeResult {
211 parts: best.parts,
212 steiner_points: best.steiner_points,
213 strategy: wrapped,
214 trace: trace.take(),
215 })
216 }
217 None => {
218 if saw_too_many_parts {
219 Err(DecompError::TooManyParts)
220 } else {
221 Err(DecompError::Failed(last_error.unwrap_or_else(|| {
222 "no strategy produced a valid decomposition".into()
223 })))
224 }
225 }
226 }
227}
228
229#[allow(clippy::too_many_arguments)]
230fn collect_candidates_for_rotation<Exact, Bayazit, EarClip, Steiner>(
231 original_ring: &[[i64; 2]],
232 rotated: &[[i64; 2]],
233 options: &DecomposeOptions,
234 config: &ProtocolConfig,
235 original_area: u128,
236 rotation: usize,
237 exact: &Exact,
238 bayazit: &Bayazit,
239 ear_clip: &EarClip,
240 steiner_points: &Steiner,
241 trace: &mut Option<Vec<Attempt>>,
242 candidates: &mut Vec<Candidate>,
243 saw_too_many_parts: &mut bool,
244 last_error: &mut Option<String>,
245) where
246 Exact: Fn(&[[i64; 2]]) -> Result<Parts, String>,
247 Bayazit: Fn(&[[i64; 2]], bool, &ProtocolConfig) -> Result<Parts, String>,
248 EarClip: Fn(&[[i64; 2]]) -> Result<Parts, String>,
249 Steiner: Fn(&[[i64; 2]], &[Vec<[i64; 2]>]) -> Vec<[i64; 2]>,
250{
251 record_strategy_candidate(
253 Strategy::ExactPartition,
254 rotation,
255 exact(rotated),
256 config,
257 original_area,
258 original_ring,
259 steiner_points,
260 trace,
261 candidates,
262 saw_too_many_parts,
263 last_error,
264 );
265
266 if options.allow_steiner {
268 record_strategy_candidate(
269 Strategy::Bayazit,
270 rotation,
271 bayazit(rotated, true, config),
272 config,
273 original_area,
274 original_ring,
275 steiner_points,
276 trace,
277 candidates,
278 saw_too_many_parts,
279 last_error,
280 );
281 }
282
283 let ear_result = ear_clip(rotated).map(|triangles| optimize_partition(&triangles));
287 record_strategy_candidate(
288 Strategy::EarClipMerge,
289 rotation,
290 ear_result,
291 config,
292 original_area,
293 original_ring,
294 steiner_points,
295 trace,
296 candidates,
297 saw_too_many_parts,
298 last_error,
299 );
300}
301
302#[allow(clippy::too_many_arguments)]
303fn record_strategy_candidate<Steiner>(
304 strategy: Strategy,
305 rotation: usize,
306 raw: Result<Parts, String>,
307 config: &ProtocolConfig,
308 original_area: u128,
309 original_ring: &[[i64; 2]],
310 steiner_points: &Steiner,
311 trace: &mut Option<Vec<Attempt>>,
312 candidates: &mut Vec<Candidate>,
313 saw_too_many_parts: &mut bool,
314 last_error: &mut Option<String>,
315) where
316 Steiner: Fn(&[[i64; 2]], &[Vec<[i64; 2]>]) -> Vec<[i64; 2]>,
317{
318 match raw {
319 Ok(parts) => match finalize_parts(parts, config, original_area) {
320 Ok(parts) => {
321 push_attempt(
322 trace,
323 strategy.clone(),
324 rotation,
325 Outcome::Success {
326 part_count: parts.len(),
327 },
328 );
329 let steiner = steiner_points(original_ring, &parts);
330 candidates.push(Candidate {
331 parts,
332 strategy,
333 rotation,
334 steiner_points: steiner,
335 });
336 }
337 Err(err) => {
338 push_attempt(trace, strategy, rotation, outcome_from_error(&err));
339 match err {
340 AttemptError::TooManyParts { .. } => *saw_too_many_parts = true,
341 AttemptError::ValidationFailed(errors) => {
342 *last_error = Some(errors.join("; "));
343 }
344 AttemptError::Failed(message) => *last_error = Some(message),
345 }
346 }
347 },
348 Err(err) => {
349 let formatted = format!("{strategy:?} failed: {err}");
350 push_attempt(
351 trace,
352 strategy,
353 rotation,
354 Outcome::AlgorithmFailed {
355 error: formatted.clone(),
356 },
357 );
358 *last_error = Some(formatted);
359 }
360 }
361}
362
363fn try_decompose<Exact, Bayazit, EarClip>(
364 ring: &[[i64; 2]],
365 options: &DecomposeOptions,
366 config: &ProtocolConfig,
367 original_area: u128,
368 exact: &Exact,
369 bayazit: &Bayazit,
370 ear_clip: &EarClip,
371 rotation: usize,
372 trace: &mut Option<Vec<Attempt>>,
373) -> Result<StrategySuccess, AttemptError>
374where
375 Exact: Fn(&[[i64; 2]]) -> Result<Parts, String>,
376 Bayazit: Fn(&[[i64; 2]], bool, &ProtocolConfig) -> Result<Parts, String>,
377 EarClip: Fn(&[[i64; 2]]) -> Result<Parts, String>,
378{
379 let mut saw_too_many_parts = false;
380 let mut last_error: Option<String> = None;
381
382 match exact(ring) {
383 Ok(parts) => match finalize_parts(parts, config, original_area) {
384 Ok(parts) => {
385 push_attempt(
386 trace,
387 Strategy::ExactPartition,
388 rotation,
389 Outcome::Success {
390 part_count: parts.len(),
391 },
392 );
393 return Ok(StrategySuccess {
394 parts,
395 strategy: Strategy::ExactPartition,
396 });
397 }
398 Err(err @ AttemptError::TooManyParts { .. }) => {
399 push_attempt(
400 trace,
401 Strategy::ExactPartition,
402 rotation,
403 outcome_from_error(&err),
404 );
405 saw_too_many_parts = true;
406 }
407 Err(err @ AttemptError::ValidationFailed(_)) => {
408 push_attempt(
409 trace,
410 Strategy::ExactPartition,
411 rotation,
412 outcome_from_error(&err),
413 );
414 if let AttemptError::ValidationFailed(errors) = err {
415 last_error = Some(errors.join("; "));
416 }
417 }
418 Err(err @ AttemptError::Failed(_)) => {
419 push_attempt(
420 trace,
421 Strategy::ExactPartition,
422 rotation,
423 outcome_from_error(&err),
424 );
425 if let AttemptError::Failed(message) = err {
426 last_error = Some(message);
427 }
428 }
429 },
430 Err(err) => {
431 let error = format!("exact vertex partition failed: {err}");
432 push_attempt(
433 trace,
434 Strategy::ExactPartition,
435 rotation,
436 Outcome::AlgorithmFailed {
437 error: error.clone(),
438 },
439 );
440 last_error = Some(error);
441 }
442 }
443
444 if options.allow_steiner {
445 match bayazit(ring, true, config) {
446 Ok(parts) => match finalize_parts(parts, config, original_area) {
447 Ok(parts) => {
448 push_attempt(
449 trace,
450 Strategy::Bayazit,
451 rotation,
452 Outcome::Success {
453 part_count: parts.len(),
454 },
455 );
456 return Ok(StrategySuccess {
457 parts,
458 strategy: Strategy::Bayazit,
459 });
460 }
461 Err(err @ AttemptError::TooManyParts { .. }) => {
462 push_attempt(trace, Strategy::Bayazit, rotation, outcome_from_error(&err));
463 saw_too_many_parts = true;
464 }
465 Err(err @ AttemptError::ValidationFailed(_)) => {
466 push_attempt(trace, Strategy::Bayazit, rotation, outcome_from_error(&err));
467 if let AttemptError::ValidationFailed(errors) = err {
468 last_error = Some(errors.join("; "));
469 }
470 }
471 Err(err @ AttemptError::Failed(_)) => {
472 push_attempt(trace, Strategy::Bayazit, rotation, outcome_from_error(&err));
473 if let AttemptError::Failed(message) = err {
474 last_error = Some(message);
475 }
476 }
477 },
478 Err(err) => {
479 let error = format!("bayazit failed: {err}");
480 push_attempt(
481 trace,
482 Strategy::Bayazit,
483 rotation,
484 Outcome::AlgorithmFailed {
485 error: error.clone(),
486 },
487 );
488 last_error = Some(error);
489 }
490 }
491 }
492
493 match ear_clip(ring) {
494 Ok(parts) => match finalize_parts(optimize_partition(&parts), config, original_area) {
495 Ok(parts) => {
496 push_attempt(
497 trace,
498 Strategy::EarClipMerge,
499 rotation,
500 Outcome::Success {
501 part_count: parts.len(),
502 },
503 );
504 Ok(StrategySuccess {
505 parts,
506 strategy: Strategy::EarClipMerge,
507 })
508 }
509 Err(err @ AttemptError::TooManyParts { .. }) => {
510 push_attempt(
511 trace,
512 Strategy::EarClipMerge,
513 rotation,
514 outcome_from_error(&err),
515 );
516 Err(err)
517 }
518 Err(err @ AttemptError::ValidationFailed(_)) => {
519 push_attempt(
520 trace,
521 Strategy::EarClipMerge,
522 rotation,
523 outcome_from_error(&err),
524 );
525 if saw_too_many_parts {
526 Err(AttemptError::TooManyParts {
527 count: config.max_parts.saturating_add(1),
528 })
529 } else {
530 Err(err)
531 }
532 }
533 Err(err @ AttemptError::Failed(_)) => {
534 push_attempt(
535 trace,
536 Strategy::EarClipMerge,
537 rotation,
538 outcome_from_error(&err),
539 );
540 if saw_too_many_parts {
541 Err(AttemptError::TooManyParts {
542 count: config.max_parts.saturating_add(1),
543 })
544 } else {
545 Err(err)
546 }
547 }
548 },
549 Err(err) => {
550 let error = format!("ear clipping failed: {err}");
551 push_attempt(
552 trace,
553 Strategy::EarClipMerge,
554 rotation,
555 Outcome::AlgorithmFailed {
556 error: error.clone(),
557 },
558 );
559 if saw_too_many_parts {
560 Err(AttemptError::TooManyParts {
561 count: config.max_parts.saturating_add(1),
562 })
563 } else {
564 Err(AttemptError::Failed(last_error.unwrap_or(error)))
565 }
566 }
567 }
568}
569
570fn finalize_parts(
571 parts: Parts,
572 config: &ProtocolConfig,
573 original_area: u128,
574) -> Result<Parts, AttemptError> {
575 if parts.len() > config.max_parts {
576 return Err(AttemptError::TooManyParts { count: parts.len() });
577 }
578
579 let validation_errors = validate_all_parts(&parts, config);
580 if !validation_errors.is_empty() {
581 return Err(AttemptError::ValidationFailed(validation_errors));
582 }
583
584 let part_areas: Vec<u128> = parts.iter().map(|part| twice_area_fp2(part)).collect();
585 if !areas_conserved(original_area, &part_areas) {
586 return Err(AttemptError::ValidationFailed(vec![
587 "area not conserved".into()
588 ]));
589 }
590
591 if let Err(topology_err) = validate_multipart_topology(&parts, false, config) {
598 return Err(AttemptError::ValidationFailed(vec![format!(
599 "topology: {topology_err}"
600 )]));
601 }
602
603 Ok(parts)
604}
605
606fn validate_all_parts(parts: &[Vec<[i64; 2]>], config: &ProtocolConfig) -> Vec<String> {
607 parts
608 .iter()
609 .enumerate()
610 .filter_map(|(idx, part)| {
611 validate_part(part, config).map(|err| format!("part {idx}: {err}"))
612 })
613 .collect()
614}
615
616fn push_attempt(
617 trace: &mut Option<Vec<Attempt>>,
618 strategy: Strategy,
619 rotation: usize,
620 outcome: Outcome,
621) {
622 if let Some(trace) = trace.as_mut() {
623 trace.push(Attempt {
624 strategy,
625 rotation,
626 outcome,
627 });
628 }
629}
630
631fn outcome_from_error(error: &AttemptError) -> Outcome {
632 match error {
633 AttemptError::TooManyParts { count } => Outcome::TooManyParts { count: *count },
634 AttemptError::ValidationFailed(errors) => Outcome::ValidationFailed {
635 errors: errors.clone(),
636 },
637 AttemptError::Failed(error) => Outcome::AlgorithmFailed {
638 error: error.clone(),
639 },
640 }
641}
642
643fn wrap_rotation(strategy: Strategy, rotation: usize) -> Strategy {
644 if rotation == 0 {
645 strategy
646 } else {
647 Strategy::Rotation {
648 offset: rotation,
649 inner: Box::new(strategy),
650 }
651 }
652}
653
654pub fn collect_steiner_points(original: &[[i64; 2]], parts: &[Vec<[i64; 2]>]) -> Vec<[i64; 2]> {
655 find_steiner_points(original, parts)
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use std::cell::Cell;
662
663 const M: i64 = 1_000_000;
664
665 fn square() -> Vec<[i64; 2]> {
666 vec![[0, 0], [12 * M, 0], [12 * M, 12 * M], [0, 12 * M]]
667 }
668
669 fn triangle() -> Vec<[i64; 2]> {
670 vec![[0, 0], [12 * M, 0], [6 * M, 10 * M]]
671 }
672
673 fn l_shape() -> Vec<[i64; 2]> {
674 vec![
675 [0, 0],
676 [24 * M, 0],
677 [24 * M, 8 * M],
678 [8 * M, 8 * M],
679 [8 * M, 24 * M],
680 [0, 24 * M],
681 ]
682 }
683
684 fn t_shape() -> Vec<[i64; 2]> {
685 vec![
686 [0, 0],
687 [30 * M, 0],
688 [30 * M, 10 * M],
689 [20 * M, 10 * M],
690 [20 * M, 24 * M],
691 [10 * M, 24 * M],
692 [10 * M, 10 * M],
693 [0, 10 * M],
694 ]
695 }
696
697 fn step_shape() -> Vec<[i64; 2]> {
698 vec![
699 [0, 0],
700 [30 * M, 0],
701 [30 * M, 10 * M],
702 [20 * M, 10 * M],
703 [20 * M, 20 * M],
704 [10 * M, 20 * M],
705 [10 * M, 30 * M],
706 [0, 30 * M],
707 ]
708 }
709
710 fn comb_twelve_vertices() -> Vec<[i64; 2]> {
717 vec![
718 [0, 0],
719 [10 * M, 0],
720 [10 * M, 10 * M],
721 [8 * M, 10 * M],
722 [8 * M, 4 * M],
723 [6 * M, 4 * M],
724 [6 * M, 10 * M],
725 [4 * M, 10 * M],
726 [4 * M, 4 * M],
727 [2 * M, 4 * M],
728 [2 * M, 10 * M],
729 [0, 10 * M],
730 ]
731 }
732
733 fn valid_samples() -> Vec<Vec<[i64; 2]>> {
734 vec![
735 square(),
736 triangle(),
737 l_shape(),
738 t_shape(),
739 step_shape(),
740 comb_twelve_vertices(),
741 ]
742 }
743
744 fn default_opts() -> DecomposeOptions {
745 DecomposeOptions::default()
746 }
747
748 fn merca_config() -> ProtocolConfig {
749 ProtocolConfig::merca()
750 }
751
752 fn single_part_config() -> ProtocolConfig {
753 ProtocolConfig {
754 max_parts: 1,
755 ..ProtocolConfig::merca()
756 }
757 }
758
759 fn assert_valid_decomposition(ring: &[[i64; 2]], options: &DecomposeOptions) {
760 let result = decompose(ring, options, &merca_config()).unwrap();
761 assert!(!result.parts.is_empty());
762
763 let original_area = twice_area_fp2(&normalize_ring(ring).unwrap());
764 let parts_area: u128 = result.parts.iter().map(|part| twice_area_fp2(part)).sum();
765 assert_eq!(parts_area, original_area);
766
767 for part in &result.parts {
768 assert!(
769 validate_part(part, &merca_config()).is_none(),
770 "invalid part: {part:?}"
771 );
772 }
773 }
774
775 #[test]
776 fn convex_family_stays_single_part() {
777 for ring in [square(), triangle()] {
778 let result = decompose(&ring, &default_opts(), &merca_config()).unwrap();
779 assert_eq!(result.parts.len(), 1, "ring={ring:?}");
780 }
781 }
782
783 #[test]
789 fn comb_polygon_decomposes_and_passes_topology() {
790 let ring = comb_twelve_vertices();
791 let result =
792 decompose(&ring, &default_opts(), &merca_config()).expect("comb must decompose");
793
794 assert!(!result.parts.is_empty());
795 assert!(result.parts.len() <= crate::constants::MAX_PARTS);
796
797 assert_eq!(
801 crate::topology::validate_multipart_topology(&result.parts, false, &merca_config()),
802 Ok(())
803 );
804 }
805
806 #[test]
807 fn all_sample_polygons_conserve_area() {
808 for ring in valid_samples() {
809 assert_valid_decomposition(&ring, &default_opts());
810 }
811 }
812
813 #[test]
814 fn all_sample_polygons_respect_max_parts() {
815 for ring in valid_samples() {
816 let result = decompose(&ring, &default_opts(), &merca_config()).unwrap();
817 assert!(result.parts.len() <= crate::constants::MAX_PARTS);
818 }
819 }
820
821 #[test]
822 fn all_output_parts_validate_for_sample_family() {
823 for ring in valid_samples() {
824 let result = decompose(&ring, &default_opts(), &merca_config()).unwrap();
825 assert!(
826 validate_all_parts(&result.parts, &merca_config()).is_empty(),
827 "ring={ring:?}"
828 );
829 }
830 }
831
832 #[test]
833 fn clockwise_inputs_normalize_successfully() {
834 for mut ring in valid_samples() {
835 ring.reverse();
836 assert_valid_decomposition(&ring, &default_opts());
837 }
838 }
839
840 #[test]
841 fn steiner_points_empty_when_disallowed() {
842 let options = DecomposeOptions {
843 allow_steiner: false,
844 ..default_opts()
845 };
846
847 for ring in valid_samples() {
848 let result = decompose(&ring, &options, &merca_config()).unwrap();
849 assert!(result.steiner_points.is_empty(), "ring={ring:?}");
850 }
851 }
852
853 #[test]
854 fn too_few_vertices_returns_error() {
855 let ring = vec![[0, 0], [M, 0]];
856 assert!(matches!(
857 decompose(&ring, &default_opts(), &merca_config()),
858 Err(DecompError::TooFewVertices)
859 ));
860 }
861
862 #[test]
863 fn self_intersecting_polygon_returns_error() {
864 let bow_tie = vec![[0, 0], [4 * M, 4 * M], [4 * M, 0], [0, 4 * M]];
865 assert!(matches!(
866 decompose(&bow_tie, &default_opts(), &merca_config()),
867 Err(DecompError::NotSimple)
868 ));
869 }
870
871 #[test]
872 fn cascade_prefers_exact_before_other_strategies() {
873 let bayazit_calls = Cell::new(0usize);
874 let ear_calls = Cell::new(0usize);
875 let ring = square();
876
877 let result = decompose_with_strategies(
878 &ring,
879 &default_opts(),
880 &merca_config(),
881 |poly| Ok(vec![poly.to_vec()]),
882 |_, _, _| {
883 bayazit_calls.set(bayazit_calls.get() + 1);
884 Err("should not run".into())
885 },
886 |_| {
887 ear_calls.set(ear_calls.get() + 1);
888 Err("should not run".into())
889 },
890 |_, _| Vec::new(),
891 )
892 .unwrap();
893
894 assert_eq!(result.parts.len(), 1);
895 assert_eq!(bayazit_calls.get(), 0);
896 assert_eq!(ear_calls.get(), 0);
897 }
898
899 #[test]
900 fn cascade_falls_back_to_bayazit_when_exact_fails() {
901 let bayazit_calls = Cell::new(0usize);
902 let ear_calls = Cell::new(0usize);
903 let ring = square();
904
905 let result = decompose_with_strategies(
906 &ring,
907 &default_opts(),
908 &merca_config(),
909 |_| Err("exact failed".into()),
910 |poly, allow_steiner, _| {
911 bayazit_calls.set(bayazit_calls.get() + 1);
912 assert!(allow_steiner);
913 Ok(vec![poly.to_vec()])
914 },
915 |_| {
916 ear_calls.set(ear_calls.get() + 1);
917 Err("should not run".into())
918 },
919 |_, _| Vec::new(),
920 )
921 .unwrap();
922
923 assert_eq!(result.parts.len(), 1);
924 assert_eq!(bayazit_calls.get(), 1);
925 assert_eq!(ear_calls.get(), 0);
926 }
927
928 #[test]
929 fn cascade_falls_back_to_ear_clip_when_others_fail() {
930 let bayazit_calls = Cell::new(0usize);
931 let ear_calls = Cell::new(0usize);
932 let ring = square();
933
934 let result = decompose_with_strategies(
935 &ring,
936 &default_opts(),
937 &merca_config(),
938 |_| Err("exact failed".into()),
939 |_, _, _| {
940 bayazit_calls.set(bayazit_calls.get() + 1);
941 Err("bayazit failed".into())
942 },
943 |poly| {
944 ear_calls.set(ear_calls.get() + 1);
945 Ok(vec![
946 vec![poly[0], poly[1], poly[2]],
947 vec![poly[0], poly[2], poly[3]],
948 ])
949 },
950 |_, _| Vec::new(),
951 )
952 .unwrap();
953
954 assert_eq!(bayazit_calls.get(), 1);
955 assert_eq!(ear_calls.get(), 1);
956 assert!(!result.parts.is_empty());
957 assert!(validate_all_parts(&result.parts, &merca_config()).is_empty());
958 let original_area = twice_area_fp2(&normalize_ring(&ring).unwrap());
959 let parts_area: u128 = result.parts.iter().map(|part| twice_area_fp2(part)).sum();
960 assert_eq!(parts_area, original_area);
961 }
962
963 #[test]
964 fn bayazit_is_skipped_when_steiner_is_disallowed() {
965 let bayazit_calls = Cell::new(0usize);
966 let ring = square();
967 let options = DecomposeOptions {
968 allow_steiner: false,
969 ..default_opts()
970 };
971
972 let result = decompose_with_strategies(
973 &ring,
974 &options,
975 &merca_config(),
976 |_| Err("exact failed".into()),
977 |_, _, _| {
978 bayazit_calls.set(bayazit_calls.get() + 1);
979 Err("should not run".into())
980 },
981 |poly| {
982 Ok(vec![
983 vec![poly[0], poly[1], poly[2]],
984 vec![poly[0], poly[2], poly[3]],
985 ])
986 },
987 |_, _| Vec::new(),
988 )
989 .unwrap();
990
991 assert_eq!(bayazit_calls.get(), 0);
992 assert!(!result.parts.is_empty());
993 assert!(validate_all_parts(&result.parts, &merca_config()).is_empty());
994 assert!(matches!(result.strategy, Strategy::EarClipMerge));
995 }
996
997 #[test]
998 fn rotation_retry_recovers_on_later_start_vertex() {
999 let attempts = Cell::new(0usize);
1000 let ring = square();
1001 let expected_start = ring[1];
1002
1003 let result = decompose_with_strategies(
1004 &ring,
1005 &default_opts(),
1006 &merca_config(),
1007 |poly| {
1008 attempts.set(attempts.get() + 1);
1009 if poly[0] == expected_start {
1010 Ok(vec![
1011 vec![poly[0], poly[1], poly[2]],
1012 vec![poly[0], poly[2], poly[3]],
1013 ])
1014 } else {
1015 Err("try another rotation".into())
1016 }
1017 },
1018 |_, _, _| Err("should not run".into()),
1019 |_| Err("should not run".into()),
1020 |_, _| Vec::new(),
1021 )
1022 .unwrap();
1023
1024 assert_eq!(attempts.get(), 2);
1025 assert_eq!(result.parts.len(), 2);
1026 }
1027
1028 #[test]
1029 fn max_rotation_attempts_limits_retry_budget() {
1030 let attempts = Cell::new(0usize);
1031 let ring = square();
1032 let options = DecomposeOptions {
1033 max_rotation_attempts: 1,
1034 ..default_opts()
1035 };
1036
1037 let error = decompose_with_strategies(
1038 &ring,
1039 &options,
1040 &merca_config(),
1041 |poly| {
1042 attempts.set(attempts.get() + 1);
1043 if poly[0] == ring[1] {
1044 Ok(vec![poly.to_vec()])
1045 } else {
1046 Err("need more rotations".into())
1047 }
1048 },
1049 |_, _, _| Err("bayazit failed".into()),
1050 |_| Err("ear failed".into()),
1051 |_, _| Vec::new(),
1052 )
1053 .unwrap_err();
1054
1055 assert_eq!(attempts.get(), 1);
1056 assert!(matches!(error, DecompError::Failed(_)));
1057 }
1058
1059 #[test]
1060 fn exact_partition_can_succeed_with_single_part_budget() {
1061 let ring = square();
1062 let result = decompose_with_strategies(
1063 &ring,
1064 &default_opts(),
1065 &single_part_config(),
1066 |poly| Ok(vec![poly.to_vec()]),
1067 |_, _, _| Err("bayazit failed".into()),
1068 |poly| {
1069 Ok(vec![
1070 vec![poly[0], poly[1], poly[2]],
1071 vec![poly[0], poly[2], poly[3]],
1072 ])
1073 },
1074 |_, _| Vec::new(),
1075 )
1076 .unwrap();
1077
1078 assert_eq!(result.parts.len(), 1);
1079 assert!(validate_all_parts(&result.parts, &single_part_config()).is_empty());
1080 assert!(matches!(result.strategy, Strategy::ExactPartition));
1081 }
1082
1083 #[test]
1084 fn collect_steiner_points_reports_new_vertices_only() {
1085 let original = square();
1086 let midpoint = [6 * M, 0];
1087 let parts = vec![
1088 vec![original[0], midpoint, original[3]],
1089 vec![midpoint, original[1], original[2], original[3]],
1090 ];
1091
1092 assert_eq!(collect_steiner_points(&original, &parts), vec![midpoint]);
1093 }
1094
1095 #[test]
1096 fn collect_steiner_points_from_bayazit_parts_stays_within_bounds() {
1097 let ring = comb_twelve_vertices();
1098 let midpoint = [6 * M, 0];
1099 let parts = vec![
1100 vec![ring[0], midpoint, ring[5]],
1101 vec![midpoint, ring[1], ring[2], ring[3], ring[4], ring[5]],
1102 ];
1103
1104 let steiner = collect_steiner_points(&ring, &parts);
1105 assert_eq!(steiner, vec![midpoint]);
1106
1107 let (min_x, max_x) = ring.iter().fold((i64::MAX, i64::MIN), |(min_x, max_x), v| {
1108 (min_x.min(v[0]), max_x.max(v[0]))
1109 });
1110 let (min_y, max_y) = ring.iter().fold((i64::MAX, i64::MIN), |(min_y, max_y), v| {
1111 (min_y.min(v[1]), max_y.max(v[1]))
1112 });
1113
1114 for point in steiner {
1115 assert!(!ring.contains(&point), "{point:?} should be new");
1116 assert!(point[0] >= min_x && point[0] <= max_x);
1117 assert!(point[1] >= min_y && point[1] <= max_y);
1118 }
1119 }
1120
1121 #[test]
1122 fn hertel_mehlhorn_optimizes_ear_clip_fallback() {
1123 let ring = vec![
1124 [0, 0],
1125 [20 * M, 0],
1126 [20 * M, 10 * M],
1127 [10 * M, 10 * M],
1128 [10 * M, 20 * M],
1129 [0, 20 * M],
1130 ];
1131
1132 let result = decompose_with_strategies(
1133 &ring,
1134 &default_opts(),
1135 &merca_config(),
1136 |_| Err("exact failed".into()),
1137 |_, _, _| Err("bayazit failed".into()),
1138 |poly| ear_clip_triangulate(poly),
1139 |_, _| Vec::new(),
1140 )
1141 .unwrap();
1142
1143 assert!(result.parts.len() < 4);
1144 assert!(result.parts.len() >= 2);
1145 }
1146
1147 #[test]
1148 fn trace_disabled_by_default() {
1149 let ring = vec![[0i64, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
1150 let result = decompose(&ring, &DecomposeOptions::default(), &merca_config()).unwrap();
1151 assert!(result.trace.is_none());
1152 }
1153
1154 #[test]
1155 fn trace_enabled_records_attempts() {
1156 let ring = vec![[0i64, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
1157 let opts = DecomposeOptions {
1158 collect_trace: true,
1159 ..Default::default()
1160 };
1161 let result = decompose(&ring, &opts, &merca_config()).unwrap();
1162 let trace = result.trace.unwrap();
1163 assert!(!trace.is_empty());
1164 assert!(trace
1165 .iter()
1166 .any(|a| matches!(a.outcome, Outcome::Success { .. })));
1167 }
1168
1169 #[test]
1170 fn strategy_reports_correct_winner() {
1171 let square = vec![[0i64, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
1172 let result = decompose(&square, &DecomposeOptions::default(), &merca_config()).unwrap();
1173 assert_eq!(result.parts.len(), 1);
1174 assert!(matches!(
1175 result.strategy,
1176 Strategy::AlreadyConvex | Strategy::ExactPartition
1177 ));
1178 }
1179
1180 #[test]
1181 fn minimize_parts_picks_fewest_across_strategies() {
1182 let ring = square();
1187 let opts = DecomposeOptions {
1188 minimize_parts: true,
1189 ..default_opts()
1190 };
1191
1192 let result = decompose_with_strategies(
1193 &ring,
1194 &opts,
1195 &merca_config(),
1196 |poly| {
1197 let y0 = poly[0][1];
1199 let y1 = poly[2][1];
1200 let third = (y1 - y0) / 3;
1201 let a = [poly[0][0], y0 + third];
1202 let b = [poly[1][0], y0 + third];
1203 let c = [poly[0][0], y0 + 2 * third];
1204 let d = [poly[1][0], y0 + 2 * third];
1205 Ok(vec![
1206 vec![poly[0], poly[1], b, a],
1207 vec![a, b, d, c],
1208 vec![c, d, poly[2], poly[3]],
1209 ])
1210 },
1211 |poly, _allow, _| {
1212 let mid_bot = [(poly[0][0] + poly[1][0]) / 2, poly[0][1]];
1214 let mid_top = [(poly[2][0] + poly[3][0]) / 2, poly[2][1]];
1215 Ok(vec![
1216 vec![poly[0], mid_bot, mid_top, poly[3]],
1217 vec![mid_bot, poly[1], poly[2], mid_top],
1218 ])
1219 },
1220 |poly| {
1221 Ok(vec![
1223 vec![poly[0], poly[1], [poly[0][0] + M, poly[0][1] + M]],
1224 vec![poly[1], poly[2], [poly[0][0] + M, poly[0][1] + M]],
1225 vec![poly[2], poly[3], [poly[0][0] + M, poly[0][1] + M]],
1226 vec![poly[3], poly[0], [poly[0][0] + M, poly[0][1] + M]],
1227 ])
1228 },
1229 |_, _| Vec::new(),
1230 )
1231 .unwrap();
1232
1233 assert_eq!(
1234 result.parts.len(),
1235 2,
1236 "minimize_parts should pick the 2-part split"
1237 );
1238 assert!(matches!(
1239 result.strategy,
1240 Strategy::Bayazit | Strategy::Rotation { inner: _, .. }
1241 ));
1242 }
1243
1244 #[test]
1245 fn minimize_parts_breaks_ties_with_exact_over_bayazit() {
1246 let ring = square();
1254 let opts = DecomposeOptions {
1255 minimize_parts: true,
1256 ..default_opts()
1257 };
1258
1259 let result = decompose_with_strategies(
1260 &ring,
1261 &opts,
1262 &merca_config(),
1263 |poly| {
1264 Ok(vec![
1265 vec![poly[0], poly[1], poly[2]],
1266 vec![poly[0], poly[2], poly[3]],
1267 ])
1268 },
1269 |poly, _, _| {
1270 Ok(vec![
1271 vec![poly[0], poly[1], poly[2]],
1272 vec![poly[0], poly[2], poly[3]],
1273 ])
1274 },
1275 |_| Err("ear clip disabled for this tiebreak test".into()),
1276 |_, _| Vec::new(),
1277 )
1278 .unwrap();
1279
1280 assert_eq!(result.parts.len(), 2);
1281 assert!(matches!(result.strategy, Strategy::ExactPartition));
1282 }
1283
1284 #[test]
1285 fn minimize_parts_prefers_fewer_steiner_points_on_tie() {
1286 let ring = square();
1290 let opts = DecomposeOptions {
1291 minimize_parts: true,
1292 ..default_opts()
1293 };
1294
1295 let result = decompose_with_strategies(
1296 &ring,
1297 &opts,
1298 &merca_config(),
1299 |poly| {
1300 Ok(vec![
1301 vec![poly[0], poly[1], poly[2]],
1302 vec![poly[0], poly[2], poly[3]],
1303 ])
1304 },
1305 |poly, _, _| {
1306 let steiner = [(poly[0][0] + poly[1][0]) / 2, poly[0][1]];
1308 Ok(vec![
1309 vec![poly[0], steiner, poly[2], poly[3]],
1310 vec![steiner, poly[1], poly[2]],
1311 ])
1312 },
1313 |_| Err("ear clip skipped".into()),
1314 |original, parts| {
1315 let mut originals: std::collections::HashSet<[i64; 2]> =
1317 std::collections::HashSet::new();
1318 for v in original {
1319 originals.insert(*v);
1320 }
1321 let mut result: Vec<[i64; 2]> = Vec::new();
1322 for part in parts {
1323 for v in part {
1324 if !originals.contains(v) && !result.contains(v) {
1325 result.push(*v);
1326 }
1327 }
1328 }
1329 result
1330 },
1331 )
1332 .unwrap();
1333
1334 assert_eq!(result.parts.len(), 2);
1335 assert!(matches!(result.strategy, Strategy::ExactPartition));
1336 assert!(result.steiner_points.is_empty());
1337 }
1338
1339 #[test]
1340 fn minimize_parts_falls_through_when_exact_invalid() {
1341 let ring = square();
1345 let opts = DecomposeOptions {
1346 minimize_parts: true,
1347 ..default_opts()
1348 };
1349 let config = ProtocolConfig {
1350 max_parts: 3,
1351 ..merca_config()
1352 };
1353
1354 let result = decompose_with_strategies(
1355 &ring,
1356 &opts,
1357 &config,
1358 |poly| {
1359 Ok(vec![
1361 vec![poly[0], poly[1], poly[2]],
1362 vec![poly[0], poly[2], poly[3]],
1363 vec![poly[0], poly[1], poly[2]],
1364 vec![poly[0], poly[2], poly[3]],
1365 ])
1366 },
1367 |poly, _, _| {
1368 Ok(vec![
1369 vec![poly[0], poly[1], poly[2]],
1370 vec![poly[0], poly[2], poly[3]],
1371 ])
1372 },
1373 |_| Err("ear clip skipped".into()),
1374 |_, _| Vec::new(),
1375 )
1376 .unwrap();
1377
1378 assert_eq!(result.parts.len(), 2);
1379 assert!(matches!(result.strategy, Strategy::Bayazit));
1380 }
1381
1382 #[test]
1383 fn l_shape_trace_shows_multiple_attempts() {
1384 let l_shape = vec![
1385 [0, 0],
1386 [20 * M, 0],
1387 [20 * M, 10 * M],
1388 [10 * M, 10 * M],
1389 [10 * M, 20 * M],
1390 [0, 20 * M],
1391 ];
1392 let opts = DecomposeOptions {
1393 collect_trace: true,
1394 ..Default::default()
1395 };
1396 let result = decompose(&l_shape, &opts, &merca_config()).unwrap();
1397 let trace = result.trace.unwrap();
1398 assert!(!trace.is_empty());
1399 assert!(trace.len() >= 1);
1400 }
1401}