mach/libs/trimesh2d/src/main.zig
Stephen Gutekanst 43e1dcbb50 trimesh2d: clip ears with smallest triangle area first
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
2022-10-18 13:42:38 -07:00

174 lines
7.1 KiB
Zig

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.
pub fn Processor(comptime T: type) type {
return struct {
const Vec2 = @Vector(2, T);
// Doubly linked list for fast polygon inspection
prev: std.ArrayListUnmanaged(u32) = .{},
next: std.ArrayListUnmanaged(u32) = .{},
// A list of the ears to be cut
ears: std.ArrayListUnmanaged(u32) = .{},
// Keeps track of ears (corners that were not ears at the beginning may become so later on.)
is_ear: std.ArrayListUnmanaged(bool) = .{},
/// Resets the processor, clearing the internal buffers and preparing it for processing a
/// new polygon.
pub fn reset(self: *@This()) void {
self.prev.clearRetainingCapacity();
self.next.clearRetainingCapacity();
self.ears.clearRetainingCapacity();
self.is_ear.clearRetainingCapacity();
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.prev.deinit(allocator);
self.next.deinit(allocator);
self.ears.deinit(allocator);
self.is_ear.deinit(allocator);
}
/// Processes a simple polygon (no holes) into triangles in linear time, writing the
/// triangles to out_triangles (indices into polygon vertices list.)
///
/// The polygons must be sorted in counter-clockwise order.
pub fn process(
self: *@This(),
allocator: std.mem.Allocator,
// TODO(trimesh2d): make this a slice?
polygon: std.ArrayListUnmanaged(Vec2),
out_triangles: *std.ArrayListUnmanaged(u32),
) error{OutOfMemory}!void {
if (polygon.items.len < 3) {
return;
}
// Ensure our doubly linked list and ears list are large enough.
const size = polygon.items.len;
try self.prev.resize(allocator, size);
try self.next.resize(allocator, size);
try self.ears.ensureTotalCapacity(allocator, size);
try self.is_ear.resize(allocator, size);
for (self.is_ear.items) |*v| v.* = false;
// Fill prev list with prior-index values, e.g.:
// [4, 0, 1, 2, 3]
for (self.prev.items) |_, i| self.prev.items[i] = @intCast(u32, if (i == 0) size - 1 else i - 1);
// Fill next list with next-index values, e.g.:
// [1, 2, 3, 4, 0]
for (self.next.items) |_, i| self.next.items[i] = @intCast(u32, if (i == size - 1) 0 else i + 1);
// var length: usize = size;
var begin: usize = 0;
while (true) {
// length -= 1;
// if (length < 3) return; // last triangle
// 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];
}
}
fn dist(p0: Vec2, p1: Vec2) T {
return std.math.hypot(T, p1[0] - p0[0], p1[1] - p0[1]);
}
/// Inexact geometric predicate.
/// Basically Shewchuk's orient2dfast()
fn orient2d(
pa: Vec2,
pb: Vec2,
pc: Vec2,
) T {
const acx = pa[0] - pc[0];
const bcx = pb[0] - pc[0];
const acy = pa[1] - pc[1];
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));
}
};
}
test "simple" {
const allocator = std.testing.allocator;
const Vec2 = @Vector(2, f32);
var polygon = std.ArrayListUnmanaged(Vec2){};
defer polygon.deinit(allocator);
// CCW
try polygon.append(allocator, Vec2{ 0.0, 0.0 }); // bottom-left
try polygon.append(allocator, Vec2{ 1.0, 0.0 }); // bottom-right
try polygon.append(allocator, Vec2{ 1.0, 1.0 }); // top-right
try polygon.append(allocator, Vec2{ 0.0, 1.0 }); // top-left
var out_triangles = std.ArrayListUnmanaged(u32){};
defer out_triangles.deinit(allocator);
var processor = Processor(f32){};
defer processor.deinit(allocator);
// Process a polygon.
try processor.process(allocator, polygon, &out_triangles);
// 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, 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, 2), out_triangles.items[5]); // top-right
}
test {
std.testing.refAllDeclsRecursive(@This());
}