# exact-poly
[](https://github.com/mercaearth/exact-poly/actions/workflows/build.yml)
**Live demo:** [exact-poly.merca.earth](https://exact-poly.merca.earth)
Integer polygon geometry for deterministic on-chain validation. Rust library compiled to WebAssembly.
## Used by
- [mercaearth/mercator](https://github.com/mercaearth/mercator) — reverse side, Move VM polygon validation on-chain
- [merca.earth](https://merca.earth) — production land registry frontend
All arithmetic uses `i64` coordinates in fixed-point representation (1 unit = 1 micrometer at SCALE = 1,000,000). No floats anywhere — results are bit-exact and reproducible across every platform.
## Why
Floating-point polygon math is nondeterministic across architectures. On-chain land registry needs geometry operations that produce identical results on every validator node, in every browser, and in every Move VM execution. `exact-poly` solves this by using integer-only arithmetic with 128-bit intermediates where needed.
## Features
**Convex decomposition** — split any simple polygon into convex parts:
- Cascade strategy: tries ExactPartition → Bayazit → EarClip+Hertel-Mehlhorn, picks the first valid result
- Ring rotation for difficult geometries
- Minimize-parts mode: exhaustive search across all strategies and rotations
- Steiner point tracking
**Area & perimeter** — exact computation without division:
- `twice_area` returns 2× the area (avoids the ÷2 that would lose precision)
- Signed area for winding detection
- L1 (Manhattan) perimeter
- Area conservation verification across decomposed parts
**Ring operations**:
- CCW/CW winding detection and normalization
- Simplicity check (self-intersection detection)
- Convexity check
- Collinear vertex removal
- Canonical normalization (rotation to smallest vertex)
**Spatial queries**:
- Point-in-convex-polygon (strict interior)
- Point-on-boundary
- Point-inside-or-on-boundary (inclusive)
**Overlap detection**:
- SAT (Separating Axis Theorem) for convex pairs
- AABB pre-filter for early rejection
- Multi-part overlap search
**Topology validation**:
- Shared edge detection between adjacent parts
- BFS connectivity check
- Hole detection via boundary graph analysis
- Vertex-only contact detection
- T-junction / partial overlap classification
- Compactness (isoperimetric ratio) validation
**Geometric primitives**:
- Cross product, orientation (CCW / CW / Collinear)
- Point-on-segment, segment intersection
- Reflex vertex detection
## Install
### npm (WASM)
```
npm install exact-poly
```
Package ships three wasm-bindgen targets — picked automatically via conditional `exports`:
| Node.js (`import`/`require`) | `pkg/node/` | Sync init, works out of the box |
| Bundlers (Vite, webpack, Rollup) | `pkg/bundler/` | Vite needs [`vite-plugin-wasm`](https://www.npmjs.com/package/vite-plugin-wasm) + [`vite-plugin-top-level-await`](https://www.npmjs.com/package/vite-plugin-top-level-await); webpack 5 supports WASM natively |
| Browser direct (no bundler) | `import "exact-poly/web"` | Returns an `init()` you must `await` before calling exports |
All functions accept `BigInt64Array` for polygon rings encoded as flat `[x0, y0, x1, y1, ...]`.
**Node / bundler:**
```js
import { decompose_polygon, twice_area, is_convex } from "exact-poly";
const ring = BigInt64Array.from([0n, 0n, 60n, 0n, 60n, 40n, 30n, 40n, 30n, 80n, 0n, 80n]);
const result = decompose_polygon(ring, true);
console.log(result.parts.length); // convex parts
const area = twice_area(ring);
console.log(area); // "7200" (2× the actual area)
console.log(is_convex(ring)); // false (L-shape)
```
**Browser direct:**
```js
import init, { twice_area } from "exact-poly/web";
await init();
const ring = BigInt64Array.from([0n, 0n, 60n, 0n, 60n, 40n, 30n, 40n, 30n, 80n, 0n, 80n]);
console.log(twice_area(ring)); // "7200"
```
### Rust (crate)
```toml
[dependencies]
exact-poly = { git = "https://github.com/mercaearth/exact-poly" }
```
```rust
use exact_poly::decompose::decompose;
use exact_poly::types::{DecomposeOptions, ProtocolConfig};
let ring = vec![[0, 0], [60, 0], [60, 40], [30, 40], [30, 80], [0, 80]];
let result = decompose(&ring, &DecomposeOptions::default(), &ProtocolConfig::default()).unwrap();
println!("{} parts", result.parts.len());
```
## Build
Requires [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/):
```
bash build.sh
```
Builds three wasm-bindgen targets in parallel — `pkg/bundler/`, `pkg/node/`, `pkg/web/`. Run tests with:
```
cargo test
```
222 tests covering decomposition strategies, area conservation, topology validation, edge cases, and geometric primitives.
## Demo
Interactive demo app with 7 tabs visualizing every feature:
```
cd demo && npm install && npm run dev
```
Draw polygons or pick presets. See decomposition results, area metrics, reflex vertices, point-in-polygon testing, SAT overlap detection, topology validation, and primitive operations — all running the WASM module in the browser.
## Coordinate convention
| Type | `i64` (signed 64-bit integer) |
| Scale | 1,000,000 units = 1 meter |
| Winding | Counter-clockwise (CCW) |
| Encoding | Flat array: `[x0, y0, x1, y1, ...]` |
| JS type | `BigInt64Array` |
Areas are returned as `2 × area` (string-encoded `u128` / `i128`) to avoid precision loss from division.
## Protocol config
Validation functions accept an optional `ProtocolConfig` controlling on-chain limits:
| `max_parts` | 10 | Maximum convex parts per polygon |
| `max_vertices_per_part` | 64 | Maximum vertices per convex part |
| `min_edge_length_squared` | 10^12 | Minimum edge length² (1 meter²) |
| `min_compactness_ppm` | 150,000 | Isoperimetric ratio floor (ppm) |
| `area_divisor` | 2 × 10^12 | Converts 2×area to display m² |
Use `ProtocolConfig::permissive()` (Rust) or omit the config parameter (JS) for demo/testing with no validation limits.
## API reference
### Decomposition
| `decompose_polygon(ring, allow_steiner, trace?, minimize?, config?)` | Cascade decomposition into convex parts |
| `bayazit_decompose_polygon(ring, allow_steiner)` | Bayazit algorithm only |
| `exact_vertex_partition_polygon(ring)` | Diagonal-only partition (no Steiner points) |
| `ear_clip_triangulate_polygon(ring)` | Ear clipping triangulation |
| `optimize_partition(parts)` | Hertel-Mehlhorn merge of triangulated parts |
### Area & metrics
| `twice_area_ring(ring)` | Unsigned 2×area as string |
| `signed_area_2x_ring(ring)` | Signed 2×area (positive = CCW) |
| `areas_conserved_values(original, part_areas)` | `true` if sum of parts equals original |
| `perimeter_l1_ring(ring)` | Manhattan perimeter as string |
### Ring operations
| `is_ccw_ring(ring)` | Winding direction check |
| `ensure_ccw_ring(ring)` | Reverse if CW → return CCW |
| `is_simple_ring(ring)` | No self-intersections |
| `is_convex_ring(ring)` | All vertices convex |
| `remove_collinear_ring(ring)` | Strip collinear vertices |
| `normalize_polygon_ring(ring)` | Canonical form (rotate to smallest vertex) |
### Spatial queries
| `point_strictly_inside_convex_ring(px, py, ring)` | Strict interior (convex only) |
| `point_on_polygon_boundary_ring(px, py, ring)` | On any edge |
| `point_inside_or_on_boundary_ring(px, py, ring)` | Interior or boundary |
| `point_inside_any_part(parts, x, y)` | Point in any convex part |
| `contains_polygon(outer_parts, inner_parts)` | Full polygon containment |
### Overlap
| `sat_overlap(a, b)` | SAT overlap for two convex polygons |
| `sat_overlap_with_aabb(a, b)` | SAT with AABB early-out |
| `convex_parts_overlap(a, b)` | Interior overlap (touching OK) |
| `parts_overlap(a_parts, b_parts)` | Any part pair overlaps |
### Topology
| `validate_multipart_topology(parts, allow_vertex_contact?, config?)` | Full topology validation |
| `has_exact_shared_edge(a, b)` | Shared edge between two parts |
| `classify_contact(a, b)` | `"shared_edge"` / `"partial_contact"` / `"none"` |
| `validate_decomposition(ring, parts, config?)` | End-to-end on-chain validation |
### Primitives
| `orientation(ax, ay, bx, by, cx, cy)` | `"CounterClockwise"` / `"Clockwise"` / `"Collinear"` |
| `cross2d(ax, ay, bx, by, cx, cy)` | Cross product as string |
| `is_left(ax, ay, bx, by, px, py)` | Point left of directed line |
| `is_reflex(prev_x, prev_y, curr_x, curr_y, next_x, next_y)` | Reflex vertex test |
| `segments_intersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)` | Segment intersection (including endpoints) |
| `segments_properly_intersect(...)` | Strict crossing (excluding endpoints) |
| `point_on_segment(px, py, ax, ay, bx, by)` | Point lies on segment |
## Architecture
```
src/
lib.rs WASM bindings (#[wasm_bindgen] exports)
types.rs Core types: Point, Part, DecomposeResult, ProtocolConfig
constants.rs On-chain protocol constants
decompose.rs Cascade decomposition engine
exact_partition.rs Diagonal-only convex partition
bayazit.rs Bayazit convex decomposition
ear_clip.rs Ear clipping triangulation
hertel_mehlhorn.rs Triangle merge optimization
area.rs Exact area computation (i128)
ring.rs Ring operations (CCW, simple, normalize)
spatial.rs Point-in-polygon queries
sat.rs Separating Axis Theorem
aabb.rs Axis-aligned bounding boxes
overlap.rs Multi-part overlap detection
containment.rs Polygon containment
shared_edge.rs Edge sharing and contact classification
topology.rs Multipart topology validation
validation.rs Per-part structural validation
validate_onchain.rs End-to-end on-chain validation
primitives.rs Cross product, orientation, segment ops
signed.rs Signed integer helpers (i128)
```
## License
MIT — see [LICENSE](LICENSE).