//! Based on: https://www.cs.umd.edu/class/spring2020/cmsc754/Lects/lect13-delaun-alg.pdf //! Optimizations involved: //! - Cached neighbors for traversal. //! - Minimal memory footprint. //! - Cached circumferences. //! - No circumference calculations for new subdivisions, - circumferences of neighbors are used instead. //! - Lazy circumference calculation, as some places might not be neighboring new subdivisions. //! - Extensive use of vectorization. //! - Care given to linear access of memory. // todo: This method allows zero area triangles, we need to eliminate them. // Points that lie on edges can be detected in pointRelation function by == 0 comparison. const std = @import("std"); // Could be redefined as pleased, but i consider these to be most sensical for given implementation. pub const VertexComponent = f32; pub const Vertex = @Vector(2, VertexComponent); pub const Index = u15; pub const Area = GenericArea(VertexComponent); pub const Builder = struct { triangles: std.ArrayList(Triangle), vertices: std.ArrayList(Vertex), allocator: std.mem.Allocator, // todo: init with expected amount of points to preallocate beforehand. pub fn init(allocator: std.mem.Allocator, area: Area) !@This() { var triangles = try std.ArrayList(Triangle).initCapacity(allocator, 2); errdefer triangles.deinit(); var vertices = try std.ArrayList(Vertex).initCapacity(allocator, 4); errdefer vertices.deinit(); try vertices.ensureUnusedCapacity(4); try triangles.ensureUnusedCapacity(2); for (area.corners()) |corner| vertices.append(corner) catch unreachable; triangles.append(Triangle{ .points = [3]Index{ 0, 2, 1 }, .neighbors = [3]?Index{ null, 1, null }, }) catch unreachable; triangles.append(Triangle{ .points = [3]Index{ 3, 1, 2 }, .neighbors = [3]?Index{ null, 0, null }, }) catch unreachable; return .{ .triangles = triangles, .vertices = vertices, .allocator = allocator, }; } pub fn insertAtRandom(self: *@This(), point: Vertex, generator: std.rand.Random) !void { // Find a triangle the point lies starting from some random triangle. var abc_index: Index = @intCast(generator.int(Index) % self.triangles.items.len); var abc = &self.triangles.items[abc_index]; var relation = abc.pointRelation(self.vertices, point); while (relation != .contained) { abc_index = abc.neighbors[@intCast(@intFromEnum(relation))].?; abc = &self.triangles.items[abc_index]; relation = abc.pointRelation(self.vertices, point); } // Allocate two new triangles, as well as new vertex. const new_vertex_index: Index = @intCast(self.vertices.items.len); try self.vertices.append(point); const pbc_index: Index = @intCast(self.triangles.items.len); const apc_index: Index = @intCast(self.triangles.items.len + 1); try self.triangles.ensureUnusedCapacity(2); // Divide the abc triangle into three. abc = &self.triangles.items[abc_index]; // Insert pbc. self.triangles.append(Triangle{ .points = [3]Index{ new_vertex_index, abc.points[1], abc.points[2] }, .neighbors = [3]?Index{ abc_index, abc.neighbors[1], apc_index }, }) catch unreachable; // Insert apc. self.triangles.append(Triangle{ .points = [3]Index{ abc.points[0], new_vertex_index, abc.points[2] }, .neighbors = [3]?Index{ abc_index, pbc_index, abc.neighbors[2] }, }) catch unreachable; // Update neighbors to be aware of new triangles. inline for (abc.neighbors[1..], [2]Index{ pbc_index, apc_index }) |n, e| if (n) |i| { const p = &self.triangles.items[i]; p.neighbors[p.neighborPosition(abc_index)] = e; }; // Existing abc is reused. abc.points[2] = new_vertex_index; abc.neighbors[1] = pbc_index; abc.neighbors[2] = apc_index; abc.circumference = null; // Recursively adjust edges of triangles so that circumferences are only encasing 3 points at a time. // todo: Try inlining initial calls via @call(.always_inline, ...). self.trySwapping(abc_index, 0); self.trySwapping(pbc_index, 1); self.trySwapping(apc_index, 2); } fn trySwapping(self: @This(), triangle_index: Index, edge: u2) void { // First find opposite to edge point that lies in neighbor. const triangle = &self.triangles.items[triangle_index]; const neighbor_index = triangle.neighbors[edge]; if (neighbor_index == null) return; const neighbor = &self.triangles.items[neighbor_index.?]; if (neighbor.circumference == null) neighbor.circumference = Triangle.Circumference.init(neighbor.*, self.vertices); // Position of neighbor's point opposide to shared with triangle edge. const point_order = neighbor.nextAfter(triangle.points[edge]); const point_index = neighbor.points[point_order]; const prev_edge = if (edge == 0) 2 else edge - 1; if (neighbor.doesFailIncircleTest(self.vertices.items[triangle.points[prev_edge]])) { // Incircle test failed, swap edges of two triangles and then try swapping newly swapped ones. const next_edge = (edge + 1) % 3; const next_point_order = (point_order + 1) % 3; const prev_point_order = if (point_order == 0) 2 else point_order - 1; // Update neighbors of triangles in which edge was swapped. if (triangle.neighbors[next_edge]) |i| { const n = &self.triangles.items[i]; n.neighbors[n.neighborPosition(triangle_index)] = neighbor_index.?; } if (neighbor.neighbors[prev_point_order]) |i| { const n = &self.triangles.items[i]; n.neighbors[n.neighborPosition(neighbor_index.?)] = triangle_index; } const neighbor_prev_point_order_neighbor_index_cache = neighbor.neighbors[prev_point_order]; neighbor.points[prev_point_order] = triangle.points[prev_edge]; neighbor.neighbors[next_point_order] = triangle.neighbors[next_edge]; neighbor.neighbors[prev_point_order] = triangle_index; neighbor.circumference = null; triangle.points[next_edge] = point_index; triangle.neighbors[next_edge] = neighbor_index.?; triangle.neighbors[edge] = neighbor_prev_point_order_neighbor_index_cache; triangle.circumference = null; self.trySwapping(triangle_index, edge); self.trySwapping(neighbor_index.?, point_order); } } }; const Triangle = struct { // References to vertices it's composed of, named abc, in CCW orientation. points: [3]Index, // References to triangles that are on other side of any edge, if any. // Order is: ab, bc, ca neighbors: [3]?Index, // Lazily calculated and cached for incircle tests. circumference: ?Circumference = null, pub const Circumference = struct { center: Vertex, radius_squared: VertexComponent, // todo: Way to get a type capable of holding squared values. pub fn init(triangle: Triangle, vertices: std.ArrayList(Vertex)) @This() { const a = vertices.items[triangle.points[0]]; const b = vertices.items[triangle.points[1]]; const c = vertices.items[triangle.points[2]]; const ab: Vertex = @splat(magnitudeSquared(a)); const cd: Vertex = @splat(magnitudeSquared(b)); const ef: Vertex = @splat(magnitudeSquared(c)); const cmb = @shuffle(VertexComponent, c - b, undefined, [2]i32{ 1, 0 }); const amc = @shuffle(VertexComponent, a - c, undefined, [2]i32{ 1, 0 }); const bma = @shuffle(VertexComponent, b - a, undefined, [2]i32{ 1, 0 }); const center = ((ab * cmb + cd * amc + ef * bma) / (a * cmb + b * amc + c * bma)) / @as(Vertex, @splat(2)); return .{ .center = center, .radius_squared = magnitudeSquared(a - center), }; } }; // todo: Try perpendicular dot product approach. pub fn pointRelation(self: @This(), vertices: std.ArrayList(Vertex), point: Vertex) enum(u2) { outside_ab = 0, outside_bc = 1, outside_ca = 2, contained = 3, } { const a = vertices.items[self.points[0]]; const b = vertices.items[self.points[1]]; const c = vertices.items[self.points[2]]; // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line const p = point; // Calculate cross products for all edges at once. const q = @Vector(12, VertexComponent){ b[0], b[1], c[0], c[1], a[0], a[1], p[1], p[0], p[1], p[0], p[1], p[0] }; const w = @Vector(12, VertexComponent){ a[0], a[1], b[0], b[1], c[0], c[1], a[1], a[0], b[1], b[0], c[1], c[0] }; const e = q - w; const r = @shuffle(VertexComponent, e, undefined, [6]i32{ 0, 1, 2, 3, 4, 5 }); const t = @shuffle(VertexComponent, e, undefined, [6]i32{ 6, 7, 8, 9, 10, 11 }); const y = r * t; const u = @shuffle(VertexComponent, y, undefined, [3]i32{ 0, 2, 4 }); const i = @shuffle(VertexComponent, y, undefined, [3]i32{ 1, 3, 5 }); const o = (u - i) > @Vector(3, VertexComponent){ 0, 0, 0 }; // const o = (u - i) <= @Vector(3, VertexComponent){ 0, 0, 0 }; // if (@reduce(.And, o)) // return .contained // else if (!o[0]) // return .outside_ab // else if (!o[1]) // return .outside_bc // else // return .outside_ca; const mask = @as(u3, @intFromBool(o[0])) << 2 | @as(u3, @intFromBool(o[1])) << 1 | @as(u3, @intFromBool(o[2])); return @enumFromInt(@clz(mask)); } pub inline fn doesFailIncircleTest(self: @This(), point: Vertex) bool { return magnitudeSquared(self.circumference.?.center - point) < self.circumference.?.radius_squared; } // todo: Shouldn't be here. pub inline fn magnitudeSquared(p: Vertex) VertexComponent { return @reduce(.Add, p * p); } // Finds which point comes after given one, by index, CCW. // Used to translate point names when traveling between neighbors. pub inline fn nextAfter(self: @This(), point_index: Index) u2 { inline for (self.points, 0..) |p, i| if (point_index == p) return @intCast((i + 1) % 3); unreachable; } pub inline fn neighborPosition(self: @This(), triangle_index: Index) usize { inline for (self.neighbors, 0..) |n, i| if (triangle_index == n) return i; unreachable; } }; pub fn GenericArea(comptime T: type) type { return struct { // note: Upper-left origin is assumed, if second point lies left or up of first it willn't work. xyxy: @Vector(4, T), /// Order: Upperleft, upperright, bottomleft, bottomright. pub fn corners(self: @This()) [4]@Vector(2, T) { return [4]@Vector(2, T){ @Vector(2, T){ self.xyxy[0], self.xyxy[1] }, @Vector(2, T){ self.xyxy[2], self.xyxy[1] }, @Vector(2, T){ self.xyxy[0], self.xyxy[3] }, @Vector(2, T){ self.xyxy[2], self.xyxy[3] }, }; } }; }