A lightweight, zero-dependency, and 2D polygon triangulation solution fully in Rust, based on horizontal slab (trapezoidal) decomposition for fast and robust post-boolean op rendering.
- Zero dependencies
- Robustness
- Naturally handles nested holes and islands without requiring complex polygon boundary nesting trees or hole-connection bridges.
- Configurable grid and epsilon values to adapt perfectly to your graphics model or application constraints.
- Simple and readable (~140 lines total and actual code is under 100 lines) for quick review and implementation into your custom editors or game engines.
- Post boolean operations rendering where:
- Triangle count/quality does not matter
- Winding agnostic
- Input cannot be controlled
- Where robustness, latency, and speed are vital
- Art and Editing studios with boolean operations
- Game Engine: real-time procedural terrain generation
- Font rasterization
- Map Renderer: Filled Polygons
Add this to your Cargo.toml:
[dependencies]
slab_tessellator = { path = "dev_docs/OSS" } # Or standard crates.io import when publisheduse slab_tessellator::{Point, triangulate_polygon};
fn main() {
// Define a square contour (vertices must represent a closed loop)
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
// Triangulate the shape (returns a flat list of Points in triangle-triple groupings)
let triangles = triangulate_polygon(&[outer]);
println!("Generated {} triangles!", triangles.len() / 3);
for chunk in triangles.chunks_exact(3) {
println!("Triangle: {:?}, {:?}, {:?}", chunk[0], chunk[1], chunk[2]);
}
}If you need a specific precision threshold or snap grid for complex geometry, use triangulate_polygon_with_options:
use slab_tessellator::{Point, TessellationOptions, triangulate_polygon_with_options};
let options = TessellationOptions {
grid_unit: 0.001, // Distance resolution to safely inset coordinate sampling
epsilon: 0.000001, // Float comparison epsilon
};
let triangles = triangulate_polygon_with_options(&[/* contours */], options);Traditional triangulation libraries (like Lyon or iTriangle) rely on sweep-line algorithms — a technique where an imaginary horizontal line scans from top to bottom, maintaining a dynamically updated data structure of active segments.
While elegant in theory, this approach demands thousands of lines of intricate, stateful code that is difficult to audit, debug, or maintain.
slab_tessellator makes a deliberate trade-off: it abandons
- Slice: All unique Y-coordinates of all vertices are collected and sorted to establish horizontal "slabs".
- Slab Invariance: Since there are no vertices strictly between the Y-levels of a slab, all boundary segments crossing the slab are guaranteed to be straight, non-intersecting line segments.
- Sample and Match: Coordinate intersections are sampled at the slab's top, bottom, and midpoints. The shape interior in each slab corresponds exactly to the alternating intervals between sorted X-coordinates.
- Triangulate: Every active interval forms a simple trapezoid (or a triangle when a boundary width is zero). Then, each trapezoid is split into two triangles and pushed to the mesh.
For vector graphics, UI designs, and game paths (which typically have under 10,000 vertices), this approach runs in microseconds while being under 100 lines of logic!
This package was created because existing solutions in Rust couldn't efficiently handle the requirements of boolean operations. Well-known packages like Lyon or iTriangle assume controlled input and prioritize triangle quality, relying on complex sweep-line algorithms.
Lyon or iTriangle requires preprocessing:
- Detect and fix winding — iterate all contours, compute signed area, flip if wrong
- Remove duplicate/near-coincident vertices — O(n²) or requires spatial indexing
- Classify rings as outer vs. hole — requires point-in-polygon tests across all ring pairs
- Build the library's input format — Lyon's PathBuilder or iTriangle's integer coordinate system
The Cost: More latency between user input and output. In an art editor:
User action
→ Boolean op (Clipper2) ~0.1–1ms
→ Preprocessing (if needed) ~0.5–2ms ← this is the problem
→ Triangulation ~0.1–0.5ms
→ Upload to GPU ~0.1ms
→ Render ~1ms
At 60fps you have 16ms total per frame. If preprocessing costs 2ms per operation and you're doing multiple operations, you've lost the frame before triangulation even starts. Zero preprocessing is critical for interactive applications.
Boolean ops are rarely done once. A user might:
- Select 10 shapes and union them all → 9 sequential boolean ops
- Use a "stamp" tool that subtracts a shape 30 times along a path
- Have a live boolean preview updating every pointer-move event at 60fps
When Lyon encounters problematic geometry such as:
- Nearly-coincident vertices
- Zero-length edges
- Inconsistent winding
It either panics or produces incorrect geometry silently.
The Solution: A triple-sample check that simply skips bad bands. The result is a mesh with a microscopic gap nobody can see. This is far better than crashing or wasting resources on complex repairs.
What Winding Is:
Every polygon ring has a direction: vertices go either clockwise or counter-clockwise. Most triangulators use winding to determine inside vs. outside:
- Counter-clockwise = outer ring (filled)
- Clockwise = hole ring (empty)
Why Boolean Ops Break This:
Consider subtracting shape B from shape A. The boolean function computes the correct geometric result, but the output might have:
Ring 0: [p1, p2, p3, p4...] ← outer boundary (CCW) ✅
Ring 1: [p5, p6, p7, p8...] ← should be a hole, but came out CCW too ❌
Both rings are counter-clockwise. To Lyon, both look like outer fills — it would fill the hole solid instead of leaving it empty. The geometry is correct but the winding label is wrong.
This happens because boolean operations compute intersections and region membership, not winding consistency.
The Solution: Use the even-odd rule. Instead of asking "Is this ring a hole?", ask "How many edges of the shape did I cross to reach this X position?" This is winding-agnostic and works with any polygon configuration.
Licensed under the MIT License. Feel free to use, modify, and distribute this in personal or commercial projects.
Note on AI use: I used AI to spruce up the code with in-line comments as explainers and for formatting the read.me. The actual code however, was written by me.