174 lines
7.1 KiB
Zig
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());
|
|
}
|