trimesh2d: clip ears with smallest triangle area first

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2022-09-18 17:21:19 -07:00 committed by Stephen Gutekanst
parent ae699565bb
commit 43e1dcbb50
2 changed files with 61 additions and 77 deletions

View file

@ -1,12 +1,14 @@
# mach/trimesh2d - simple polygon triangulation in linear time # mach/trimesh2d - simple polygon triangulation
Converts 'simple' polygons (i.e. no holes) into triangle meshes in linear time using a modern earcut algorithm that works in linear time and has proven correctness. Triangulates 'simple' polygons (i.e. no holes) into triangle meshes.
This is a Zig implementation of the paper: This uses inspiration/ideas from the paper:
> "_[Deterministic Linear Time Constrained Triangulation using Simplified Earcut](https://arxiv.org/abs/2009.04294)_" - Marco Livesu, Gianmarco Cherchi, Riccardo Scateni, Marco Attene, 2020. > "_[Deterministic Linear Time Constrained Triangulation using Simplified Earcut](https://arxiv.org/abs/2009.04294)_" - Marco Livesu, Gianmarco Cherchi, Riccardo Scateni, Marco Attene, 2020.
> IEEE Transactions on Visualization and Computer Graphics, 2021. [arXiv:2009.04294](https://arxiv.org/abs/2009.04294) > IEEE Transactions on Visualization and Computer Graphics, 2021. [arXiv:2009.04294](https://arxiv.org/abs/2009.04294)
Notably, this paper does not consider lateral ears ("we never consider lateral ears in our triangulation algorithm"); and so we found many polygons that failed to triangulate with it due to extremas. To correct this, we adjusted the algorithm to clip ears with the smallest produced triangle area first.
(This repository is a separate copy of the same library in the [main Mach repository](https://github.com/hexops/mach), and is automatically kept in sync, so that anyone can use this library in their own project if they like!) (This repository is a separate copy of the same library in the [main Mach repository](https://github.com/hexops/mach), and is automatically kept in sync, so that anyone can use this library in their own project if they like!)
## Getting started ## Getting started

View file

@ -1,4 +1,6 @@
const std = @import("std"); const std = @import("std");
const pow = std.math.pow;
const sqrt = std.math.sqrt;
/// Returns a trimesh2d processor, which can reuse its internal buffers to process multiple polygons /// Returns a trimesh2d processor, which can reuse its internal buffers to process multiple polygons
/// (call reset between process calls.) The type T denotes e.g. f16, f32, or f64 vertices. /// (call reset between process calls.) The type T denotes e.g. f16, f32, or f64 vertices.
@ -63,81 +65,53 @@ pub fn Processor(comptime T: type) type {
// [1, 2, 3, 4, 0] // [1, 2, 3, 4, 0]
for (self.next.items) |_, i| self.next.items[i] = @intCast(u32, if (i == size - 1) 0 else i + 1); for (self.next.items) |_, i| self.next.items[i] = @intCast(u32, if (i == size - 1) 0 else i + 1);
// Detect all safe ears in O(n). // var length: usize = size;
// This amounts to finding all convex vertices but the endpoints of the constrained edge var begin: usize = 0;
var curr: u32 = 1; while (true) {
while (curr < size - 1) : (curr += 1) { // length -= 1;
// NOTE: the polygon may contain dangling edges // if (length < 3) return; // last triangle
if (orient2d(
polygon.items[self.prev.items[curr]],
polygon.items[curr],
polygon.items[self.next.items[curr]],
) > 0) {
try self.ears.append(allocator, curr);
self.is_ear.items[curr] = true;
}
}
// Progressively delete all ears, updating the data structure // Find the convex ear in the polygon that has the shortest distance between two
while (self.ears.items.len > 0) { // vertices we would need to connect in order to clip the ear.
curr = self.ears.pop(); var ear: u32 = undefined;
var min_dist = std.math.floatMax(T);
// make new tri var i: u32 = @intCast(u32, begin);
try out_triangles.append(allocator, self.prev.items[curr]); var found: bool = false;
try out_triangles.append(allocator, curr); while (true) {
try out_triangles.append(allocator, self.next.items[curr]); const prev = self.prev.items[i];
const next = self.next.items[i];
// exclude curr from the polygon, connecting prev and next if (orient2d(polygon.items[prev], polygon.items[i], polygon.items[next]) > 0) {
self.next.items[self.prev.items[curr]] = self.next.items[curr]; // Convex
self.prev.items[self.next.items[curr]] = self.prev.items[curr]; // const d = dist(polygon.items[prev], polygon.items[next]);
const d = triangleArea(polygon.items[prev], polygon.items[i], polygon.items[next]);
// check if prev and next have become new ears if (d < min_dist) {
if (!self.is_ear.items[self.prev.items[curr]] and self.prev.items[curr] != 0) { // Smaller distance.
if (self.prev.items[self.prev.items[curr]] != self.next.items[curr] and orient2d( min_dist = d;
polygon.items[self.prev.items[self.prev.items[curr]]], ear = i;
polygon.items[self.prev.items[curr]], found = true;
polygon.items[self.next.items[curr]], }
) > 0) {
try self.ears.append(allocator, self.prev.items[curr]);
self.is_ear.items[self.prev.items[curr]] = true;
}
}
if (!self.is_ear.items[self.next.items[curr]] and self.next.items[curr] < size - 1) {
if (self.next.items[self.next.items[curr]] != self.prev.items[curr] and orient2d(
polygon.items[self.prev.items[curr]],
polygon.items[self.next.items[curr]],
polygon.items[self.next.items[self.next.items[curr]]],
) > 0) {
try self.ears.append(allocator, self.next.items[curr]);
self.is_ear.items[self.next.items[curr]] = true;
} }
if (next == begin) break;
i = next;
} }
if (!found) return;
if (begin == ear) begin = self.next.items[ear];
// Clip this ear.
// Create the triangle.
try out_triangles.append(allocator, self.prev.items[ear]);
try out_triangles.append(allocator, ear);
try out_triangles.append(allocator, self.next.items[ear]);
// Exclude the ear vertex from the polygon, connecting prev and next.
self.next.items[self.prev.items[ear]] = self.next.items[ear];
self.prev.items[self.next.items[ear]] = self.prev.items[ear];
} }
} }
pub fn sort(allocator: std.mem.Allocator, polygon: std.ArrayListUnmanaged(Vec2)) !std.ArrayListUnmanaged(Vec2) { fn dist(p0: Vec2, p1: Vec2) T {
var max_dist: f32 = 0; return std.math.hypot(T, p1[0] - p0[0], p1[1] - p0[1]);
var extrema_start: usize = undefined;
var extrema_end: usize = undefined;
var i: usize = 0;
while (i < polygon.items.len) : (i += 1) {
var next_index = (i + 1) % polygon.items.len;
var p0 = polygon.items[i];
var p1 = polygon.items[next_index];
var dist = std.math.hypot(T, p1[0] - p0[0], p1[1] - p0[1]);
if (dist > max_dist) {
max_dist = dist;
extrema_start = i;
extrema_end = next_index;
}
}
var sorted = std.ArrayListUnmanaged(Vec2){};
i = extrema_end;
while (i < polygon.items.len) : (i += 1) try sorted.append(allocator, polygon.items[i]);
i = 0;
while (i <= extrema_start) : (i += 1) try sorted.append(allocator, polygon.items[i]);
return sorted;
} }
/// Inexact geometric predicate. /// Inexact geometric predicate.
@ -153,6 +127,14 @@ pub fn Processor(comptime T: type) type {
const bcy = pb[1] - pc[1]; const bcy = pb[1] - pc[1];
return acx * bcy - acy * bcx; return acx * bcy - acy * bcx;
} }
fn triangleArea(a: Vec2, b: Vec2, c: Vec2) T {
const l1 = sqrt(pow(T, a[0] - b[0], 2) + pow(T, a[1] - b[1], 2));
const l2 = sqrt(pow(T, b[0] - c[0], 2) + pow(T, b[1] - c[1], 2));
const l3 = sqrt(pow(T, c[0] - a[0], 2) + pow(T, c[1] - a[1], 2));
const p = (l1 + l2 + l3) / 2;
return sqrt(p * (p - l1) * (p - l2) * (p - l3));
}
}; };
} }
@ -179,12 +161,12 @@ test "simple" {
// out_triangles has indices into polygon.items of our triangle vertices. // out_triangles has indices into polygon.items of our triangle vertices.
// If desired, call .reset() and call .process() again! Internal buffers will be reused. // If desired, call .reset() and call .process() again! Internal buffers will be reused.
try std.testing.expectEqual(@as(usize, 6), out_triangles.items.len); try std.testing.expectEqual(@as(usize, 6), out_triangles.items.len);
try std.testing.expectEqual(@as(u32, 1), out_triangles.items[0]); // bottom-right try std.testing.expectEqual(@as(u32, 3), out_triangles.items[0]); // top-left
try std.testing.expectEqual(@as(u32, 2), out_triangles.items[1]); // top-right try std.testing.expectEqual(@as(u32, 0), out_triangles.items[1]); // bottom-left
try std.testing.expectEqual(@as(u32, 3), out_triangles.items[2]); // top-left try std.testing.expectEqual(@as(u32, 1), out_triangles.items[2]); // bottom-right
try std.testing.expectEqual(@as(u32, 0), out_triangles.items[3]); // bottom-left try std.testing.expectEqual(@as(u32, 3), out_triangles.items[3]); // top-left
try std.testing.expectEqual(@as(u32, 1), out_triangles.items[4]); // bottom-right try std.testing.expectEqual(@as(u32, 1), out_triangles.items[4]); // bottom-right
try std.testing.expectEqual(@as(u32, 3), out_triangles.items[5]); // top-left try std.testing.expectEqual(@as(u32, 2), out_triangles.items[5]); // top-right
} }
test { test {