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,4 +1,6 @@
|
|||
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
|
||||
/// (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]
|
||||
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).
|
||||
// This amounts to finding all convex vertices but the endpoints of the constrained edge
|
||||
var curr: u32 = 1;
|
||||
while (curr < size - 1) : (curr += 1) {
|
||||
// NOTE: the polygon may contain dangling edges
|
||||
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;
|
||||
}
|
||||
}
|
||||
// var length: usize = size;
|
||||
var begin: usize = 0;
|
||||
while (true) {
|
||||
// length -= 1;
|
||||
// if (length < 3) return; // last triangle
|
||||
|
||||
// Progressively delete all ears, updating the data structure
|
||||
while (self.ears.items.len > 0) {
|
||||
curr = self.ears.pop();
|
||||
|
||||
// make new tri
|
||||
try out_triangles.append(allocator, self.prev.items[curr]);
|
||||
try out_triangles.append(allocator, curr);
|
||||
try out_triangles.append(allocator, self.next.items[curr]);
|
||||
|
||||
// exclude curr from the polygon, connecting prev and next
|
||||
self.next.items[self.prev.items[curr]] = self.next.items[curr];
|
||||
self.prev.items[self.next.items[curr]] = self.prev.items[curr];
|
||||
|
||||
// check if prev and next have become new ears
|
||||
if (!self.is_ear.items[self.prev.items[curr]] and self.prev.items[curr] != 0) {
|
||||
if (self.prev.items[self.prev.items[curr]] != self.next.items[curr] and orient2d(
|
||||
polygon.items[self.prev.items[self.prev.items[curr]]],
|
||||
polygon.items[self.prev.items[curr]],
|
||||
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;
|
||||
// Find the convex ear in the polygon that has the shortest distance between two
|
||||
// vertices we would need to connect in order to clip the ear.
|
||||
var ear: u32 = undefined;
|
||||
var min_dist = std.math.floatMax(T);
|
||||
var i: u32 = @intCast(u32, begin);
|
||||
var found: bool = false;
|
||||
while (true) {
|
||||
const prev = self.prev.items[i];
|
||||
const next = self.next.items[i];
|
||||
if (orient2d(polygon.items[prev], polygon.items[i], polygon.items[next]) > 0) {
|
||||
// Convex
|
||||
// const d = dist(polygon.items[prev], polygon.items[next]);
|
||||
const d = triangleArea(polygon.items[prev], polygon.items[i], polygon.items[next]);
|
||||
if (d < min_dist) {
|
||||
// Smaller distance.
|
||||
min_dist = d;
|
||||
ear = i;
|
||||
found = 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) {
|
||||
var max_dist: f32 = 0;
|
||||
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;
|
||||
fn dist(p0: Vec2, p1: Vec2) T {
|
||||
return std.math.hypot(T, p1[0] - p0[0], p1[1] - p0[1]);
|
||||
}
|
||||
|
||||
/// Inexact geometric predicate.
|
||||
|
|
@ -153,6 +127,14 @@ pub fn Processor(comptime T: type) type {
|
|||
const bcy = pb[1] - pc[1];
|
||||
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.
|
||||
// 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(u32, 1), out_triangles.items[0]); // bottom-right
|
||||
try std.testing.expectEqual(@as(u32, 2), out_triangles.items[1]); // top-right
|
||||
try std.testing.expectEqual(@as(u32, 3), out_triangles.items[2]); // top-left
|
||||
try std.testing.expectEqual(@as(u32, 0), out_triangles.items[3]); // bottom-left
|
||||
try std.testing.expectEqual(@as(u32, 3), out_triangles.items[0]); // top-left
|
||||
try std.testing.expectEqual(@as(u32, 0), out_triangles.items[1]); // bottom-left
|
||||
try std.testing.expectEqual(@as(u32, 1), out_triangles.items[2]); // bottom-right
|
||||
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, 3), out_triangles.items[5]); // top-left
|
||||
try std.testing.expectEqual(@as(u32, 2), out_triangles.items[5]); // top-right
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue