# Workflows: Builder API and Edit API
This document provides small, practical recipes for working with triangulations.
- **Builder API**: construct and maintain Delaunay triangulations via `DelaunayTriangulation`.
- **Edit API**: explicitly edit triangulation topology via bistellar flips.
For the full design discussion (and more extensive examples), see [`api_design.md`](api_design.md).
For validation semantics and configuration details, see [`validation.md`](validation.md).
For the theoretical background and rationale behind the invariants, see [`invariants.md`](invariants.md).
## Builder API: the happy path
For most use cases, construction is a single call:
```rust
use delaunay::prelude::triangulation::*;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
// Optional verification (see docs/validation.md for when to use each):
dt.is_valid().unwrap(); // Level 4 only (Delaunay property)
```
## Builder API: topology guarantees and automatic validation
Two knobs are commonly used for insertion-time safety vs performance:
- `TopologyGuarantee`: what Level 3 topology invariants are enforced.
- `ValidationPolicy`: when Level 3 topology validation runs automatically during incremental insertion.
See [`validation.md`](validation.md) for details.
```rust
use delaunay::prelude::triangulation::*;
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty();
// Enforce stricter topology checks.
dt.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict);
// In tests/debugging, validate Level 3 after every insertion.
dt.set_validation_policy(ValidationPolicy::Always);
```
### What the topology guarantees mean (quick summary)
- `TopologyGuarantee::Pseudomanifold`:
validates facet degree (each facet is incident to 1 or 2 cells) and a closed boundary
("no boundary of boundary").
- `TopologyGuarantee::PLManifold` *(default)*:
adds **ridge-link validation during insertion** and requires a **vertex-link validation pass at
construction completion** to certify full PL-manifoldness.
- `TopologyGuarantee::PLManifoldStrict`:
runs **vertex-link validation after every insertion** (slowest, maximum safety).
See [`validation.md`](validation.md) for the precise invariants and which methods validate which
levels.
## Builder API: flip-based Delaunay repair (details)
The Builder API is designed to construct Delaunay triangulations, and (by default) schedules local
flip-based repair passes after insertions.
Automatic repair scheduling is controlled by `DelaunayRepairPolicy` (default: `EveryInsertion`).
The explicit repair methods (`repair_delaunay_with_flips`, `repair_delaunay_with_flips_advanced`,
`rebuild_with_heuristic`) require `K: ExactPredicates` at compile time. `AdaptiveKernel` and
`RobustKernel` implement this trait; `FastKernel` does not. See
[`numerical_robustness_guide.md`](numerical_robustness_guide.md) for kernel selection guidance.
```rust
use delaunay::prelude::triangulation::*;
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty();
// Default:
assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::EveryInsertion);
// Disable automatic repairs (manual repair is still available):
dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never);
```
You can also run a global repair pass manually:
```rust
use delaunay::prelude::triangulation::*;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
let _stats = dt.repair_delaunay_with_flips().unwrap();
```
### Topology and kernel requirements
Flip-based repair requires a PL-manifold topology guarantee. If your triangulation is configured as
`TopologyGuarantee::Pseudomanifold`, `repair_delaunay_with_flips()` returns an error.
Additionally, all explicit repair methods require `K: ExactPredicates` (compile-time bound).
The default `AdaptiveKernel` satisfies this. `FastKernel` does not — its automatic
insertion-time repair uses a `RobustKernel` fallback internally, but the public repair
methods are not available.
### Repair attempts and diagnostics
Internally, standard flip-based repair uses two bounded attempts:
1. Attempt 1: FIFO queue order seeded from the requested local frontier, or from
all cells when the caller explicitly requests a global repair.
2. Attempt 2: LIFO queue order with a full re-seed of the repair queue. This
runs only after attempt 1 fails to converge or fails its postcondition.
After an attempt completes, repair verifies the Delaunay postcondition with the
same flip predicates used by the repair loop. A postcondition failure is treated
similarly to non-convergence and triggers the second attempt or a caller-level
fallback.
The public advanced repair path can then try a robust-kernel pass and, if that
still fails, a deterministic heuristic rebuild.
If repair fails to converge within the flip budget, you get
`DelaunayRepairError::NonConvergent { .. }`, which contains a `DelaunayRepairDiagnostics` payload
(facets checked, flips performed, max queue length, ambiguous predicate counts + samples, cycle
detections, etc.).
```rust
use delaunay::core::algorithms::flips::DelaunayRepairError;
use delaunay::prelude::triangulation::*;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
match dt.repair_delaunay_with_flips() {
Ok(_stats) => {}
Err(DelaunayRepairError::NonConvergent { diagnostics, .. }) => {
eprintln!("repair non-convergent: {diagnostics}");
}
Err(err) => {
eprintln!("repair failed: {err}");
}
}
```
### Advanced repair with heuristic rebuild
If you want a stronger "try harder" path, call
`repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig)`, which returns a
`DelaunayRepairOutcome`.
This method:
1. Runs the standard flip-repair.
2. If it fails (non-convergent or postcondition failure), tries a robust-kernel repair pass.
3. If it still fails, rebuilds the triangulation from the **current vertex set** using a shuffled
insertion order and a perturbation seed, then runs a final flip-repair pass.
If a heuristic rebuild is used, the outcome records the seeds in `outcome.heuristic`.
You can provide explicit seeds for reproducibility; otherwise deterministic defaults are derived
from the current vertex set.
```rust
use delaunay::prelude::triangulation::*;
use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicConfig;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
let outcome = dt
.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default())
.unwrap();
if let Some(seeds) = outcome.heuristic {
eprintln!("heuristic rebuild used: {seeds:?}");
}
```
## Builder API: toroidal (periodic) triangulations
Toroidal triangulations handle periodic boundary conditions. Use
`DelaunayTriangulationBuilder` to construct them:
```rust
use delaunay::prelude::triangulation::*;
// 2D periodic triangulation with unit square domain
let vertices = vec![
vertex!([0.1, 0.1]),
vertex!([0.9, 0.9]),
vertex!([0.5, 0.5]),
];
let mut dt = DelaunayTriangulationBuilder::new(&vertices)
.toroidal([1.0, 1.0]) // Phase 1: canonicalized toroidal construction
.build::<()>()
.unwrap();
// Insert more points - they'll be wrapped to [0,1)×[0,1)
dt.insert(vertex!([1.2, 0.3])).unwrap(); // wraps to [0.2, 0.3]
dt.insert(vertex!([-0.1, 0.7])).unwrap(); // wraps to [0.9, 0.7]
```
**Key points:**
- **Domain wrapping**: Vertex coordinates are automatically canonicalized (wrapped) to the
fundamental domain `[0, period)` for each dimension
- **Distance computation**: Distances are computed accounting for periodic boundaries (toroidal
metric)
- **Construction modes**:
- `.toroidal([..])`: Phase 1 canonicalized construction (wrap into fundamental domain)
- `.toroidal_periodic([..])`: Phase 2 periodic image-point construction (true toroidal quotient)
For more details, see `docs/topology.md` and the toroidal section in the main `README.md`.
## Builder API: auxiliary vertex and cell data
Vertices and cells can carry user-defined auxiliary data (`U` for vertices, `V` for cells).
Data is attached at construction time via `VertexBuilder::data()`, read via the `data()` accessor,
and modified post-construction via `set_vertex_data` / `set_cell_data`.
```rust
use delaunay::prelude::triangulation::*;
// Attach integer labels at construction time
let vertices: [Vertex<f64, i32, 2>; 3] = [
vertex!([0.0, 0.0], 10i32),
vertex!([1.0, 0.0], 20),
vertex!([0.0, 1.0], 30),
];
let mut dt = DelaunayTriangulationBuilder::new(&vertices)
.build::<()>()
.unwrap();
// Read vertex data
for (_key, vertex) in dt.vertices() {
println!("data = {:?}", vertex.data()); // Some(10), Some(20), or Some(30)
}
// Modify vertex data (O(1), does not affect geometry or topology)
let key = dt.vertices().next().unwrap().0;
let prev = dt.set_vertex_data(key, Some(99));
assert!(prev.is_some()); // returns the old Option<U>
// Cell data works the same way
let cell_key = dt.cells().next().unwrap().0;
dt.set_cell_data(cell_key, Some(42));
assert_eq!(dt.tds().get_cell(cell_key).unwrap().data(), Some(&42));
```
`set_vertex_data` and `set_cell_data` are safe O(1) operations — they modify only the
user-data field and do not invalidate geometry, topology, or Delaunay invariants.
## Builder API: insertion statistics
If you need observability (or you want to handle skipped vertices explicitly), use
`insert_with_statistics()`.
```rust
use delaunay::prelude::triangulation::*;
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty();
let (outcome, stats) = dt.insert_with_statistics(vertex!([0.5, 0.5, 0.5])).unwrap();
if stats.used_perturbation() {
println!("used perturbation (attempts={})", stats.attempts);
}
match outcome {
InsertionOutcome::Inserted { vertex_key, hint: _ } => {
println!("inserted: {vertex_key:?}");
}
InsertionOutcome::Skipped { error } => {
println!("skipped: {error}");
}
}
```
For guidance on retry/skip behavior and choosing `RobustKernel`, see
[`numerical_robustness_guide.md`](numerical_robustness_guide.md).
## Builder API: removing a vertex
Vertex removal is supported and preserves Levels 1–3, but it may not preserve the Delaunay
property in all cases. If you need the Delaunay property after removals, run a repair pass and/or
validate explicitly.
```rust
use delaunay::prelude::triangulation::*;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
vertex!([0.2, 0.2, 0.2]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
let vertex_key = dt.vertices().next().unwrap().0;
let _cells_removed = dt.remove_vertex(vertex_key).unwrap();
// Topology should still be valid:
assert!(dt.as_triangulation().validate().is_ok());
// If you need Delaunay after edits (requires K: ExactPredicates):
// dt.repair_delaunay_with_flips().unwrap();
// dt.is_valid().unwrap();
```
## Edit API: minimal flip example
The Edit API exposes explicit bistellar flips. These operations do **not** automatically restore
(or preserve) the Delaunay property.
After using flips, you typically:
1. validate topology (Level 3), and
2. optionally repair / verify the Delaunay property.
See [`api_design.md`](api_design.md) for the full Builder vs Edit API design.
```rust
use delaunay::prelude::triangulation::*;
use delaunay::prelude::triangulation::flips::*;
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap();
// k=1: split a cell by inserting a vertex.
let cell_key = dt.cells().next().unwrap().0;
let info = dt.flip_k1_insert(cell_key, vertex!([0.1, 0.1, 0.1])).unwrap();
let inserted_vertex = info.inserted_face_vertices[0];
// k=1 inverse: remove the inserted vertex (collapse its star).
let _ = dt.flip_k1_remove(inserted_vertex).unwrap();
// Validate the stack (Levels 1–3) after topological edits.
assert!(dt.as_triangulation().validate().is_ok());
// If you need Delaunay after edits (requires K: ExactPredicates):
// dt.repair_delaunay_with_flips().unwrap();
// dt.is_valid().unwrap();
```