Skip to main content

exact_poly/
types.rs

1//! Core types for exact-poly integer polygon geometry.
2//! All coordinates are i64 (fixed-point, SCALE=1_000_000 units = 1 meter).
3//! No float arithmetic anywhere in this library.
4
5/// Protocol-specific configuration for polygon validation.
6/// Passed to validation and decomposition functions.
7/// Use `ProtocolConfig::merca()` for explicit on-chain Merca rules.
8/// Use `ProtocolConfig::permissive()` for demos/testing with no validation limits.
9#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
10pub struct ProtocolConfig {
11    /// Maximum number of convex parts per polygon.
12    pub max_parts: usize,
13    /// Maximum vertices per convex part.
14    pub max_vertices_per_part: usize,
15    /// Minimum edge length squared (avoids sqrt). Compare: dx²+dy² >= this value.
16    pub min_edge_length_squared: u128,
17    /// Minimum compactness ratio in parts-per-million.
18    /// Formula: 8_000_000 * twice_area >= min_compactness_ppm * L1_perimeter²
19    pub min_compactness_ppm: u128,
20    /// Divisor to convert twice_area_fp2 to display units.
21    pub area_divisor: u128,
22}
23
24impl ProtocolConfig {
25    /// Merca on-chain protocol defaults (matching polygon.move).
26    pub fn merca() -> Self {
27        Self {
28            max_parts: crate::constants::MAX_PARTS,
29            max_vertices_per_part: crate::constants::MAX_VERTICES_PER_PART,
30            min_edge_length_squared: crate::constants::MIN_EDGE_LENGTH_SQUARED,
31            min_compactness_ppm: crate::constants::MIN_COMPACTNESS_PPM,
32            area_divisor: crate::constants::AREA_DIVISOR,
33        }
34    }
35
36    /// No validation limits — for demos, testing, visualization.
37    pub fn permissive() -> Self {
38        Self {
39            max_parts: usize::MAX,
40            max_vertices_per_part: usize::MAX,
41            min_edge_length_squared: 0,
42            min_compactness_ppm: 0,
43            area_divisor: 1,
44        }
45    }
46}
47
48impl Default for ProtocolConfig {
49    fn default() -> Self {
50        Self::permissive()
51    }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
55pub enum Strategy {
56    AlreadyConvex,
57    ExactPartition,
58    Bayazit,
59    EarClipMerge,
60    Rotation { offset: usize, inner: Box<Strategy> },
61}
62
63#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
64pub struct Attempt {
65    pub strategy: Strategy,
66    pub rotation: usize,
67    pub outcome: Outcome,
68}
69
70#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
71pub enum Outcome {
72    Success { part_count: usize },
73    TooManyParts { count: usize },
74    ValidationFailed { errors: Vec<String> },
75    AlgorithmFailed { error: String },
76}
77
78/// Options for the decompose() cascade.
79#[derive(Clone, Debug)]
80pub struct DecomposeOptions {
81    /// If true, Bayazit may introduce Steiner points at edge intersections.
82    /// If false, only original vertices are used (exact_vertex_partition preferred).
83    pub allow_steiner: bool,
84    /// Number of ring rotation attempts if initial decomposition fails. Default: vertex_count.
85    pub max_rotation_attempts: usize,
86    pub collect_trace: bool,
87    /// If `false` (default) — cascade mode: try ExactPartition → Bayazit →
88    /// EarClipMerge in order and return the **first** strategy that passes
89    /// validation. This minimizes Steiner-point pollution and keeps the
90    /// on-chain client behaviour deterministic.
91    ///
92    /// If `true` — run **every** strategy across **every** rotation that is
93    /// attempted, collect all validated candidates, and return the one with
94    /// the fewest parts. Ties are broken by: fewer Steiner points, then
95    /// earlier rotation, then strategy preference (Exact > Bayazit > EarClip).
96    /// Strictly more work than cascade mode; intended for demos, tooling,
97    /// and research where "absolute minimum parts" matters more than
98    /// cascade determinism.
99    pub minimize_parts: bool,
100}
101
102impl Default for DecomposeOptions {
103    fn default() -> Self {
104        Self {
105            allow_steiner: true,
106            max_rotation_attempts: usize::MAX,
107            collect_trace: false,
108            minimize_parts: false,
109        }
110    }
111}
112
113/// Result of a successful decomposition.
114#[derive(Clone, Debug)]
115pub struct DecomposeResult {
116    /// The convex parts. Each is a ring of [x, y] points.
117    pub parts: Vec<Vec<[i64; 2]>>,
118    /// Vertices introduced by decomposition (not in original ring).
119    /// Empty when allow_steiner=false or exact partition succeeded.
120    pub steiner_points: Vec<[i64; 2]>,
121    pub strategy: Strategy,
122    pub trace: Option<Vec<Attempt>>,
123}
124
125/// Decomposition error.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum DecompError {
128    /// Input ring has fewer than 3 vertices.
129    TooFewVertices,
130    /// Input ring is not simple (self-intersecting).
131    NotSimple,
132    /// Could not decompose within MAX_PARTS limit.
133    TooManyParts,
134    /// All strategies failed.
135    Failed(String),
136}
137
138impl std::fmt::Display for DecompError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            DecompError::TooFewVertices => write!(f, "ring has fewer than 3 vertices"),
142            DecompError::NotSimple => write!(f, "ring is self-intersecting"),
143            DecompError::TooManyParts => write!(f, "decomposition exceeds MAX_PARTS"),
144            DecompError::Failed(s) => write!(f, "decomposition failed: {s}"),
145        }
146    }
147}
148
149/// Topology validation error with structured diagnostic data.
150/// Serializable to JSON via serde for WASM boundary.
151#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
152pub enum TopologyError {
153    /// Parts are not all connected via shared edges.
154    /// `disconnected_parts`: indices of parts not reachable from part 0 via BFS.
155    NotConnected { disconnected_parts: Vec<usize> },
156
157    /// Boundary graph has multiple connected components (polygon has holes).
158    /// `boundary_components`: number of separate boundary loops found.
159    HasHoles { boundary_components: usize },
160
161    /// Two parts touch only at a vertex, not along a shared edge.
162    VertexOnlyContact { part_a: usize, part_b: usize },
163
164    /// Two parts have unsupported contact (T-junction, partial overlap, etc.)
165    UnsupportedContact {
166        part_a: usize,
167        part_b: usize,
168        reason: String,
169    },
170
171    /// Too many parts for the protocol.
172    TooManyParts { count: usize, max: usize },
173
174    /// Polygon boundary is not compact enough (isoperimetric ratio too low).
175    NotCompact {
176        compactness_ppm: u128,
177        min_ppm: u128,
178    },
179}
180
181impl std::fmt::Display for TopologyError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            TopologyError::NotConnected { disconnected_parts } => {
185                write!(
186                    f,
187                    "parts are not all connected (disconnected: {disconnected_parts:?})"
188                )
189            }
190            TopologyError::HasHoles {
191                boundary_components,
192            } => {
193                write!(
194                    f,
195                    "multipart polygon has {boundary_components} boundary components (holes)"
196                )
197            }
198            TopologyError::VertexOnlyContact { part_a, part_b } => {
199                write!(f, "parts {part_a} and {part_b} have only vertex contact")
200            }
201            TopologyError::UnsupportedContact {
202                part_a,
203                part_b,
204                reason,
205            } => {
206                write!(
207                    f,
208                    "parts {part_a} and {part_b} have unsupported contact: {reason}"
209                )
210            }
211            TopologyError::TooManyParts { count, max } => {
212                write!(f, "too many parts ({count} > max {max})")
213            }
214            TopologyError::NotCompact {
215                compactness_ppm,
216                min_ppm,
217            } => {
218                write!(
219                    f,
220                    "polygon is not compact enough ({compactness_ppm} ppm < min {min_ppm} ppm)"
221                )
222            }
223        }
224    }
225}