trimesh2d: clip ears with smallest triangle area first
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
ae699565bb
commit
43e1dcbb50
2 changed files with 61 additions and 77 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue