diff --git a/examples/gkurve/LICENSE.tesselator b/examples/gkurve/LICENSE.tesselator new file mode 100644 index 00000000..b7e66b1d --- /dev/null +++ b/examples/gkurve/LICENSE.tesselator @@ -0,0 +1 @@ +TODO: add license, the tesselator implementation comes from https://github.com/fubark/cosmic/blob/master/graphics/src/tessellator.zig diff --git a/examples/gkurve/atlas.zig b/examples/gkurve/atlas.zig index 0e1d3f12..d13d2476 100644 --- a/examples/gkurve/atlas.zig +++ b/examples/gkurve/atlas.zig @@ -25,7 +25,7 @@ const Node = struct { width: u32, }; -const Error = error{ +pub const Error = error{ /// Atlas cannot fit the desired region. You must enlarge the atlas. AtlasFull, }; diff --git a/examples/gkurve/data_structures/bit_array_list.zig b/examples/gkurve/data_structures/bit_array_list.zig new file mode 100644 index 00000000..c2c2311c --- /dev/null +++ b/examples/gkurve/data_structures/bit_array_list.zig @@ -0,0 +1,67 @@ +const std = @import("std"); + +// std.DynamicBitSet doesn't behave like std.ArrayList, it will realloc on every resize. +// For now provide a bitset api and use std.ArrayList(bool) as the implementation. +pub const BitArrayList = struct { + const Self = @This(); + + buf: std.ArrayList(bool), + + pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .buf = std.ArrayList(bool).init(alloc), + }; + } + + pub fn deinit(self: Self) void { + self.buf.deinit(); + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.buf.clearRetainingCapacity(); + } + + pub fn appendUnset(self: *Self) !void { + try self.buf.append(false); + } + + pub fn appendSet(self: *Self) !void { + try self.buf.append(true); + } + + pub fn isSet(self: Self, idx: usize) bool { + return self.buf.items[idx]; + } + + pub fn set(self: *Self, idx: usize) void { + self.buf.items[idx] = true; + } + + pub fn unset(self: *Self, idx: usize) void { + self.buf.items[idx] = false; + } + + pub fn setRange(self: *Self, start: usize, end: usize) void { + std.mem.set(bool, self.buf.items[start..end], true); + } + + pub fn unsetRange(self: *Self, start: usize, end: usize) void { + std.mem.set(bool, self.buf.items[start..end], false); + } + + pub fn resize(self: *Self, size: usize) !void { + try self.buf.resize(size); + } + + pub fn resizeFillNew(self: *Self, size: usize, comptime fill: bool) !void { + const start = self.buf.items.len; + try self.resize(size); + if (self.buf.items.len > start) { + if (fill) { + self.setRange(start, self.buf.items.len); + } else { + self.unsetRange(start, self.buf.items.len); + } + } + } +}; diff --git a/examples/gkurve/data_structures/compact.zig b/examples/gkurve/data_structures/compact.zig new file mode 100644 index 00000000..0b94c1e2 --- /dev/null +++ b/examples/gkurve/data_structures/compact.zig @@ -0,0 +1,780 @@ +const std = @import("std"); +const t = std.testing; +const BitArrayList = @import("bit_array_list.zig").BitArrayList; +const log = std.log.scoped(.compact); + +/// Useful for keeping elements closer together in memory when you're using a bunch of insert/delete, +/// while keeping realloc to a minimum and preserving the element's initial insert index. +/// Backed by std.ArrayList. +/// Item ids are reused once removed. +/// Items are assigned an id and have O(1) access time by id. +/// TODO: Iterating can be just as fast as a dense array if CompactIdGenerator kept a sorted list of freed id ranges. +/// Although that also means delete ops would need to be O(logn). +pub fn CompactUnorderedList(comptime Id: type, comptime T: type) type { + if (@typeInfo(Id).Int.signedness != .unsigned) { + @compileError("Unsigned id type required."); + } + return struct { + id_gen: CompactIdGenerator(Id), + + // TODO: Rename to buf. + data: std.ArrayList(T), + + // Keep track of whether an item exists at id in order to perform iteration. + // TODO: Rename to exists. + // TODO: Maybe the user should provide this if it's important. It would also simplify the api and remove optional return types. It also means iteration won't be possible. + data_exists: BitArrayList, + + const Self = @This(); + const Iterator = struct { + // The current id should reflect the id of the value returned from next or nextPtr. + cur_id: Id, + list: *const Self, + + fn init(list: *const Self) @This() { + return .{ + .cur_id = std.math.maxInt(Id), + .list = list, + }; + } + + pub fn reset(self: *@This()) void { + self.idx = std.math.maxInt(Id); + } + + pub fn nextPtr(self: *@This()) ?*T { + self.cur_id +%= 1; + while (true) { + if (self.cur_id < self.list.data.items.len) { + if (!self.list.data_exists.isSet(self.cur_id)) { + self.cur_id += 1; + continue; + } else { + return &self.list.data.items[self.cur_id]; + } + } else { + return null; + } + } + } + + pub fn next(self: *@This()) ?T { + self.cur_id +%= 1; + while (true) { + if (self.cur_id < self.list.data.items.len) { + if (!self.list.data_exists.isSet(self.cur_id)) { + self.cur_id += 1; + continue; + } else { + return self.list.data.items[self.cur_id]; + } + } else { + return null; + } + } + } + }; + + pub fn init(alloc: std.mem.Allocator) @This() { + const new = @This(){ + .id_gen = CompactIdGenerator(Id).init(alloc, 0), + .data = std.ArrayList(T).init(alloc), + .data_exists = BitArrayList.init(alloc), + }; + return new; + } + + pub fn deinit(self: Self) void { + self.id_gen.deinit(); + self.data.deinit(); + self.data_exists.deinit(); + } + + pub fn iterator(self: *const Self) Iterator { + return Iterator.init(self); + } + + // Returns the id of the item. + pub fn add(self: *Self, item: T) !Id { + const new_id = self.id_gen.getNextId(); + errdefer self.id_gen.deleteId(new_id); + + if (new_id >= self.data.items.len) { + try self.data.resize(new_id + 1); + try self.data_exists.resize(new_id + 1); + } + self.data.items[new_id] = item; + self.data_exists.set(new_id); + return new_id; + } + + pub fn set(self: *Self, id: Id, item: T) void { + self.data.items[id] = item; + } + + pub fn remove(self: *Self, id: Id) void { + self.data_exists.unset(id); + self.id_gen.deleteId(id); + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.data_exists.clearRetainingCapacity(); + self.id_gen.clearRetainingCapacity(); + self.data.clearRetainingCapacity(); + } + + pub fn get(self: Self, id: Id) ?T { + if (self.has(id)) { + return self.data.items[id]; + } else return null; + } + + pub fn getNoCheck(self: Self, id: Id) T { + return self.data.items[id]; + } + + pub fn getPtr(self: *const Self, id: Id) ?*T { + if (self.has(id)) { + return &self.data.items[id]; + } else return null; + } + + pub fn getPtrNoCheck(self: Self, id: Id) *T { + return &self.data.items[id]; + } + + pub fn has(self: Self, id: Id) bool { + return self.data_exists.isSet(id); + } + + pub fn size(self: Self) usize { + return self.data.items.len - self.id_gen.next_ids.count; + } + }; +} + +test "CompactUnorderedList" { + { + // General test. + var arr = CompactUnorderedList(u32, u8).init(t.allocator); + defer arr.deinit(); + + _ = try arr.add(1); + const id = try arr.add(2); + _ = try arr.add(3); + arr.remove(id); + // Test adding to a removed slot. + _ = try arr.add(4); + const id2 = try arr.add(5); + // Test iterator skips removed slot. + arr.remove(id2); + + var iter = arr.iterator(); + try t.expectEqual(iter.next(), 1); + try t.expectEqual(iter.next(), 4); + try t.expectEqual(iter.next(), 3); + try t.expectEqual(iter.next(), null); + try t.expectEqual(arr.size(), 3); + } + { + // Empty test. + var arr = CompactUnorderedList(u32, u8).init(t.allocator); + defer arr.deinit(); + var iter = arr.iterator(); + try t.expectEqual(iter.next(), null); + try t.expectEqual(arr.size(), 0); + } +} + +/// Buffer is a CompactUnorderedList. +pub fn CompactSinglyLinkedList(comptime Id: type, comptime T: type) type { + const Null = CompactNull(Id); + const Node = CompactSinglyLinkedListNode(Id, T); + return struct { + const Self = @This(); + + first: Id, + nodes: CompactUnorderedList(Id, Node), + + pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .first = Null, + .nodes = CompactUnorderedList(Id, Node).init(alloc), + }; + } + + pub fn deinit(self: Self) void { + self.nodes.deinit(); + } + + pub fn insertAfter(self: *Self, id: Id, data: T) !Id { + if (self.nodes.has(id)) { + const new = try self.nodes.add(.{ + .next = self.nodes.getNoCheck(id).next, + .data = data, + }); + self.nodes.getPtrNoCheck(id).next = new; + return new; + } else return error.NoElement; + } + + pub fn removeNext(self: *Self, id: Id) !bool { + if (self.nodes.has(id)) { + const at = self.nodes.getPtrNoCheck(id); + if (at.next != Null) { + const next = at.next; + at.next = self.nodes.getNoCheck(next).next; + self.nodes.remove(next); + return true; + } else return false; + } else return error.NoElement; + } + + pub fn getNode(self: *const Self, id: Id) ?Node { + return self.nodes.get(id); + } + + pub fn getNodeAssumeExists(self: *const Self, id: Id) Node { + return self.nodes.getNoCheck(id); + } + + pub fn get(self: *const Self, id: Id) ?T { + if (self.nodes.has(id)) { + return self.nodes.getNoCheck(id).data; + } else return null; + } + + pub fn getNoCheck(self: *const Self, id: Id) T { + return self.nodes.getNoCheck(id).data; + } + + pub fn getAt(self: *const Self, idx: usize) Id { + var i: u32 = 0; + var cur = self.first.?; + while (i != idx) : (i += 1) { + cur = self.getNext(cur).?; + } + return cur; + } + + pub fn getFirst(self: *const Self) Id { + return self.first; + } + + pub fn getNext(self: Self, id: Id) ?Id { + if (self.nodes.has(id)) { + return self.nodes.getNoCheck(id).next; + } else return null; + } + + pub fn prepend(self: *Self, data: T) !Id { + const node = Node{ + .next = self.first, + .data = data, + }; + self.first = try self.nodes.add(node); + return self.first; + } + + pub fn removeFirst(self: *Self) bool { + if (self.first != Null) { + const next = self.getNodeAssumeExists(self.first).next; + self.nodes.remove(self.first); + self.first = next; + return true; + } else return false; + } + }; +} + +test "CompactSinglyLinkedList" { + const Null = CompactNull(u32); + { + // General test. + var list = CompactSinglyLinkedList(u32, u8).init(t.allocator); + defer list.deinit(); + + const first = try list.prepend(1); + var last = first; + last = try list.insertAfter(last, 2); + last = try list.insertAfter(last, 3); + // Test remove next. + _ = try list.removeNext(first); + // Test remove first. + _ = list.removeFirst(); + + var id = list.getFirst(); + try t.expectEqual(list.get(id), 3); + id = list.getNext(id).?; + try t.expectEqual(id, Null); + } + { + // Empty test. + var list = CompactSinglyLinkedList(u32, u8).init(t.allocator); + defer list.deinit(); + try t.expectEqual(list.getFirst(), Null); + } +} + +/// Id should be an unsigned integer type. +/// Max value of Id is used to indicate null. (An optional would increase the struct size.) +pub fn CompactSinglyLinkedListNode(comptime Id: type, comptime T: type) type { + return struct { + next: Id, + data: T, + }; +} + +pub fn CompactNull(comptime Id: type) Id { + return comptime std.math.maxInt(Id); +} + +/// Stores multiple linked lists together in memory. +pub fn CompactManySinglyLinkedList(comptime ListId: type, comptime Index: type, comptime T: type) type { + const Node = CompactSinglyLinkedListNode(Index, T); + const Null = CompactNull(Index); + return struct { + const Self = @This(); + + const List = struct { + head: ?Index, + }; + + nodes: CompactUnorderedList(Index, Node), + lists: CompactUnorderedList(ListId, List), + + pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .nodes = CompactUnorderedList(Index, Node).init(alloc), + .lists = CompactUnorderedList(ListId, List).init(alloc), + }; + } + + pub fn deinit(self: Self) void { + self.nodes.deinit(); + self.lists.deinit(); + } + + // Returns detached item. + pub fn detachAfter(self: *Self, id: Index) !Index { + if (self.nodes.has(id)) { + const item = self.getNodePtrAssumeExists(id); + const detached = item.next; + item.next = Null; + return detached; + } else return error.NoElement; + } + + pub fn insertAfter(self: *Self, id: Index, data: T) !Index { + if (self.nodes.has(id)) { + const new = try self.nodes.add(.{ + .next = self.nodes.getNoCheck(id).next, + .data = data, + }); + self.nodes.getPtrNoCheck(id).next = new; + return new; + } else return error.NoElement; + } + + pub fn setDetachedToEnd(self: *Self, id: Index, detached_id: Index) void { + const item = self.nodes.getPtr(id).?; + item.next = detached_id; + } + + pub fn addListWithDetachedHead(self: *Self, id: Index) !ListId { + return self.lists.add(.{ .head = id }); + } + + pub fn addListWithHead(self: *Self, data: T) !ListId { + const item_id = try self.addDetachedItem(data); + return self.addListWithDetachedHead(item_id); + } + + pub fn addEmptyList(self: *Self) !ListId { + return self.lists.add(.{ .head = Null }); + } + + pub fn addDetachedItem(self: *Self, data: T) !Index { + return try self.nodes.add(.{ + .next = Null, + .data = data, + }); + } + + pub fn prepend(self: *Self, list_id: ListId, data: T) !Index { + const list = self.getList(list_id); + const item = Node{ + .next = list.first, + .data = data, + }; + list.first = try self.nodes.add(item); + return list.first.?; + } + + pub fn removeFirst(self: *Self, list_id: ListId) bool { + const list = self.getList(list_id); + if (list.first == null) { + return false; + } else { + const next = self.getNext(list.first.?); + self.nodes.remove(list.first.?); + list.first = next; + return true; + } + } + + pub fn removeNext(self: *Self, id: Index) !bool { + if (self.nodes.has(id)) { + const at = self.nodes.getPtrNoCheck(id); + if (at.next != Null) { + const next = at.next; + at.next = self.nodes.getNoCheck(next).next; + self.nodes.remove(next); + return true; + } else return false; + } else return error.NoElement; + } + + pub fn removeDetached(self: *Self, id: Index) void { + self.nodes.remove(id); + } + + pub fn getListPtr(self: *const Self, id: ListId) *List { + return self.lists.getPtr(id); + } + + pub fn getListHead(self: *const Self, id: ListId) ?Index { + if (self.lists.has(id)) { + return self.lists.getNoCheck(id).head; + } else return null; + } + + pub fn findInList(self: Self, list_id: ListId, ctx: anytype, pred: fn (ctx: @TypeOf(ctx), buf: Self, item_id: Index) bool) ?Index { + var id = self.getListHead(list_id) orelse return null; + while (id != Null) { + if (pred(ctx, self, id)) { + return id; + } + id = self.getNextIdNoCheck(id); + } + return null; + } + + pub fn has(self: Self, id: Index) bool { + return self.nodes.has(id); + } + + pub fn getNode(self: Self, id: Index) ?Node { + return self.nodes.get(id); + } + + pub fn getNodeAssumeExists(self: Self, id: Index) Node { + return self.nodes.getNoCheck(id); + } + + pub fn getNodePtr(self: Self, id: Index) ?*Node { + return self.nodes.getPtr(id); + } + + pub fn getNodePtrAssumeExists(self: Self, id: Index) *Node { + return self.nodes.getPtrNoCheck(id); + } + + pub fn get(self: Self, id: Index) ?T { + if (self.nodes.has(id)) { + return self.nodes.getNoCheck(id).data; + } else return null; + } + + pub fn getNoCheck(self: Self, id: Index) T { + return self.nodes.getNoCheck(id).data; + } + + pub fn getIdAt(self: Self, list_id: ListId, idx: usize) Index { + var i: u32 = 0; + var cur: Index = self.getListHead(list_id).?; + while (i != idx) : (i += 1) { + cur = self.getNextId(cur).?; + } + return cur; + } + + pub fn getPtr(self: Self, id: Index) ?*T { + if (self.nodes.has(id)) { + return &self.nodes.getPtrNoCheck(id).data; + } else return null; + } + + pub fn getPtrNoCheck(self: Self, id: Index) *T { + return &self.nodes.getPtrNoCheck(id).data; + } + + pub fn getNextId(self: Self, id: Index) ?Index { + if (self.nodes.get(id)) |node| { + return node.next; + } else return null; + } + + pub fn getNextIdNoCheck(self: Self, id: Index) Index { + return self.nodes.getNoCheck(id).next; + } + + pub fn getNextNode(self: Self, id: Index) ?Node { + if (self.getNext(id)) |next| { + return self.getNode(next); + } else return null; + } + + pub fn getNextData(self: *const Self, id: Index) ?T { + if (self.getNext(id)) |next| { + return self.get(next); + } else return null; + } + }; +} + +test "CompactManySinglyLinkedList" { + const Null = CompactNull(u32); + var lists = CompactManySinglyLinkedList(u32, u32, u32).init(t.allocator); + defer lists.deinit(); + + const list_id = try lists.addListWithHead(10); + const head = lists.getListHead(list_id).?; + + // Test detachAfter. + const after = try lists.insertAfter(head, 20); + try t.expectEqual(lists.getNextIdNoCheck(head), after); + try t.expectEqual(lists.detachAfter(head), after); + try t.expectEqual(lists.getNextIdNoCheck(head), Null); +} + +/// Reuses deleted ids. +/// Uses a fifo id buffer to get the next id if not empty, otherwise it uses the next id counter. +pub fn CompactIdGenerator(comptime T: type) type { + return struct { + const Self = @This(); + + start_id: T, + next_default_id: T, + next_ids: std.fifo.LinearFifo(T, .Dynamic), + + pub fn init(alloc: std.mem.Allocator, start_id: T) Self { + return .{ + .start_id = start_id, + .next_default_id = start_id, + .next_ids = std.fifo.LinearFifo(T, .Dynamic).init(alloc), + }; + } + + pub fn peekNextId(self: Self) T { + if (self.next_ids.readableLength() == 0) { + return self.next_default_id; + } else { + return self.next_ids.peekItem(0); + } + } + + pub fn getNextId(self: *Self) T { + if (self.next_ids.readableLength() == 0) { + defer self.next_default_id += 1; + return self.next_default_id; + } else { + return self.next_ids.readItem().?; + } + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.next_default_id = self.start_id; + self.next_ids.head = 0; + self.next_ids.count = 0; + } + + pub fn deleteId(self: *Self, id: T) void { + self.next_ids.writeItem(id) catch unreachable; + } + + pub fn deinit(self: Self) void { + self.next_ids.deinit(); + } + }; +} + +test "CompactIdGenerator" { + var gen = CompactIdGenerator(u16).init(t.allocator, 1); + defer gen.deinit(); + try t.expectEqual(gen.getNextId(), 1); + try t.expectEqual(gen.getNextId(), 2); + gen.deleteId(1); + try t.expectEqual(gen.getNextId(), 1); + try t.expectEqual(gen.getNextId(), 3); +} + +/// Holds linked lists in a compact buffer. Does not keep track of list heads. +/// This might replace CompactManySinglyLinkedList. +pub fn CompactSinglyLinkedListBuffer(comptime Id: type, comptime T: type) type { + const Null = comptime CompactNull(Id); + const OptId = Id; + return struct { + const Self = @This(); + + pub const Node = CompactSinglyLinkedListNode(Id, T); + + nodes: CompactUnorderedList(Id, Node), + + pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .nodes = CompactUnorderedList(Id, Node).init(alloc), + }; + } + + pub fn deinit(self: Self) void { + self.nodes.deinit(); + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.nodes.clearRetainingCapacity(); + } + + pub fn getNode(self: Self, idx: Id) ?Node { + return self.nodes.get(idx); + } + + pub fn getNodeNoCheck(self: Self, idx: Id) Node { + return self.nodes.getNoCheck(idx); + } + + pub fn getNodePtrNoCheck(self: Self, idx: Id) *Node { + return self.nodes.getPtrNoCheck(idx); + } + + pub fn iterator(self: Self) CompactUnorderedList(Id, Node).Iterator { + return self.nodes.iterator(); + } + + pub fn iterFirstNoCheck(self: Self) Id { + var iter = self.nodes.iterator(); + _ = iter.next(); + return iter.cur_id; + } + + pub fn iterFirstValueNoCheck(self: Self) T { + var iter = self.nodes.iterator(); + return iter.next().?.data; + } + + pub fn size(self: Self) usize { + return self.nodes.size(); + } + + pub fn getLast(self: Self, id: Id) ?Id { + if (id == Null) { + return null; + } + if (self.nodes.has(id)) { + var cur = id; + while (cur != Null) { + const next = self.getNextNoCheck(cur); + if (next == Null) { + return cur; + } + cur = next; + } + unreachable; + } else return null; + } + + pub fn get(self: Self, id: Id) ?T { + if (self.nodes.has(id)) { + return self.nodes.getNoCheck(id).data; + } else return null; + } + + pub fn getNoCheck(self: Self, idx: Id) T { + return self.nodes.getNoCheck(idx).data; + } + + pub fn getPtrNoCheck(self: Self, idx: Id) *T { + return &self.nodes.getPtrNoCheck(idx).data; + } + + pub fn getNextNoCheck(self: Self, id: Id) OptId { + return self.nodes.getNoCheck(id).next; + } + + pub fn getNext(self: Self, id: Id) ?OptId { + if (self.nodes.has(id)) { + return self.nodes.getNoCheck(id).next; + } else return null; + } + + /// Adds a new head node. + pub fn add(self: *Self, data: T) !Id { + return try self.nodes.add(.{ + .next = Null, + .data = data, + }); + } + + pub fn insertBeforeHead(self: *Self, head_id: Id, data: T) !Id { + if (self.nodes.has(head_id)) { + return try self.nodes.add(.{ + .next = head_id, + .data = data, + }); + } else return error.NoElement; + } + + pub fn insertBeforeHeadNoCheck(self: *Self, head_id: Id, data: T) !Id { + return try self.nodes.add(.{ + .next = head_id, + .data = data, + }); + } + + pub fn insertAfter(self: *Self, id: Id, data: T) !Id { + if (self.nodes.has(id)) { + const new = try self.nodes.add(.{ + .next = self.nodes.getNoCheck(id).next, + .data = data, + }); + self.nodes.getPtrNoCheck(id).next = new; + return new; + } else return error.NoElement; + } + + pub fn removeAfter(self: *Self, id: Id) !void { + if (self.nodes.has(id)) { + const next = self.getNextNoCheck(id); + if (next != Null) { + const next_next = self.getNextNoCheck(next); + self.nodes.getNoCheck(id).next = next_next; + self.nodes.remove(next); + } + } else return error.NoElement; + } + + pub fn removeAssumeNoPrev(self: *Self, id: Id) !void { + if (self.nodes.has(id)) { + self.nodes.remove(id); + } else return error.NoElement; + } + }; +} + +test "CompactSinglyLinkedListBuffer" { + var buf = CompactSinglyLinkedListBuffer(u32, u32).init(t.allocator); + defer buf.deinit(); + + const head = try buf.add(1); + try t.expectEqual(buf.get(head).?, 1); + try t.expectEqual(buf.getNoCheck(head), 1); + try t.expectEqual(buf.getNode(head).?.data, 1); + try t.expectEqual(buf.getNodeNoCheck(head).data, 1); + + const second = try buf.insertAfter(head, 2); + try t.expectEqual(buf.getNodeNoCheck(head).next, second); + try t.expectEqual(buf.getNoCheck(second), 2); + + try buf.removeAssumeNoPrev(head); + try t.expectEqual(buf.get(head), null); +} diff --git a/examples/gkurve/data_structures/rb_tree.zig b/examples/gkurve/data_structures/rb_tree.zig new file mode 100644 index 00000000..b93eece8 --- /dev/null +++ b/examples/gkurve/data_structures/rb_tree.zig @@ -0,0 +1,1390 @@ +const std = @import("std"); +// const t = stdx.testing; +const t = std.testing; + +const log = std.log.scoped(.rb_tree); + +const Color = enum(u1) { + Black, + Red, +}; + +const CompactUnorderedList = @import("compact.zig").CompactUnorderedList; +const CompactNull = @import("compact.zig").CompactNull; + +/// Based on Zig's rb node pointer based implementation: +/// https://github.com/ziglang/std-lib-orphanage/blob/master/std/rb.zig +/// Deletion logic was redone from https://www.youtube.com/watch?v=CTvfzU_uNKE as guidance. +/// Correction for case 5 in the video: it should not have a parent black condition. +/// Visualize: https://www.cs.usfca.edu/~galles/visualization/RedBlack.html +pub fn RbTree(comptime Id: type, comptime Value: type, comptime Context: type, comptime Compare: fn (Value, Value, Context) std.math.Order) type { + return struct { + root: OptId, + buf: CompactUnorderedList(Id, Node), + /// The current number of nodes. Does not count detached nodes. + size: usize, + ctx: Context, + + const Self = @This(); + const OptId = Id; + const NullId = CompactNull(Id); + + const Node = struct { + left: OptId, + right: OptId, + val: Value, + + parent: OptId, + color: Color, + + fn getParentOpt(self: Node) ?Id { + if (self.parent == NullId) { + return null; + } else { + return self.parent; + } + } + + fn isRoot(self: Node) bool { + return self.parent == NullId; + } + + fn setChild(self: *Node, child: Id, is_left: bool) void { + if (is_left) { + self.left = child; + } else { + self.right = child; + } + } + }; + + pub fn init(alloc: std.mem.Allocator, ctx: Context) Self { + return .{ + .root = NullId, + .buf = CompactUnorderedList(Id, Node).init(alloc), + .size = 0, + .ctx = ctx, + }; + } + + pub fn deinit(self: *Self) void { + self.buf.deinit(); + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.buf.clearRetainingCapacity(); + self.root = NullId; + self.size = 0; + } + + /// Re-sorts a tree with a compare function. + pub fn sort(self: *Self, ctx: anytype, cmp: fn (Value, Value, @TypeOf(ctx)) std.math.Order) !void { + self.root = NullId; + var iter = self.buf.iterator(); + while (iter.next()) |node| { + self.buf.remove(iter.cur_id); + _ = try self.insertCustom(node.val, ctx, cmp); + } + } + + pub fn first(self: Self) ?Id { + if (self.root == NullId) { + return null; + } + var id = self.root; + var node = self.buf.getNoCheck(id); + while (node.left != NullId) { + id = node.left; + node = self.buf.getNoCheck(id); + } + return id; + } + + pub fn firstFrom(self: Self, node_id: Id) Id { + var id = node_id; + var node = self.buf.getNoCheck(id); + while (node.left != NullId) { + id = node.left; + node = self.buf.getNoCheck(id); + } + return id; + } + + pub fn last(self: Self) ?Id { + if (self.root == NullId) { + return null; + } + var id = self.root; + var node = self.buf.getNoCheck(id); + while (node.right != NullId) { + id = node.right; + node = self.buf.getNoCheck(id); + } + return id; + } + + pub fn allocValuesInOrder(self: Self, alloc: std.mem.Allocator) []const Value { + var vals = std.ArrayList(Value).initCapacity(alloc, self.size) catch unreachable; + var cur = self.first(); + while (cur) |id| { + vals.appendAssumeCapacity(self.get(id).?); + cur = self.getNext(id); + } + return vals.toOwnedSlice(); + } + + pub fn allocNodeIdsInOrder(self: Self, alloc: std.mem.Allocator) []const Id { + var node_ids = std.ArrayList(Id).initCapacity(alloc, self.size) catch unreachable; + var cur = self.first(); + while (cur) |id| { + node_ids.appendAssumeCapacity(id); + cur = self.getNext(id); + } + return node_ids.toOwnedSlice(); + } + + /// User code typically wouldn't need the node, but it is available for testing. + pub fn getNode(self: Self, id: Id) ?Node { + return self.buf.get(id); + } + + pub fn get(self: Self, id: Id) ?Value { + if (self.buf.get(id)) |node| { + return node.val; + } else return null; + } + + pub fn getNoCheck(self: Self, id: Id) Value { + return self.buf.getNoCheck(id).val; + } + + pub fn getPtr(self: Self, id: Id) ?*Value { + if (self.buf.getPtr(id)) |node| { + return &node.val; + } else return null; + } + + pub fn getPtrNoCheck(self: Self, id: Id) *Value { + return &self.buf.getPtrNoCheck(id).val; + } + + fn getSibling(self: Self, parent: *Node, child_id: Id) OptId { + _ = self; + if (parent.left == child_id) { + return parent.right; + } else { + return parent.left; + } + } + + // Replacement node (with it's children) are relinked in place of node . Node (with it's children) become detached from tree. + fn transplant(self: *Self, node_id: Id, node: *Node, r_id: Id, mb_rnode: ?*Node) void { + if (node_id == self.root) { + self.root = r_id; + } else { + const parent = self.buf.getPtrNoCheck(node.parent); + parent.setChild(r_id, parent.left == node_id); + } + if (mb_rnode) |rnode| { + rnode.parent = node.parent; + } + } + + pub fn removeDetached(self: *Self, node_id: Id) void { + self.buf.remove(node_id); + } + + pub fn remove(self: *Self, node_id: Id) anyerror!void { + try self.detach(node_id); + self.buf.remove(node_id); + } + + /// If node is not part of tree, error is returned. + /// Detaches the node from the tree, rebalances tree, but does not remove it. The caller is responsible for calling removeDetached later on. + pub fn detach(self: *Self, node_id: Id) anyerror!void { + try self.detachInternal(node_id); + self.size -= 1; + } + + fn detachInternal(self: *Self, node_id: Id) anyerror!void { + var node = self.buf.getPtr(node_id) orelse return error.DoesNotExist; + + var to_fix_nid: OptId = undefined; + + if (node.left == NullId) { + const mb_rnode: ?*Node = if (node.right != NullId) self.buf.getPtrNoCheck(node.right) else null; + self.transplant(node_id, node, node.right, mb_rnode); + to_fix_nid = node.right; + } else if (node.right == NullId) { + const mb_rnode: ?*Node = if (node.left != NullId) self.buf.getPtrNoCheck(node.left) else null; + self.transplant(node_id, node, node.left, mb_rnode); + to_fix_nid = node.left; + } else { + const r_id = self.firstFrom(node.right); + const r_node = self.buf.getPtrNoCheck(r_id); + + // Normally this would be a value copy but since ids are tied to their nodes, the replacement node is relinked in place of the target node. + // Transplant only relinks parent of replacement node. + const r_parent = r_node.parent; + self.transplant(node_id, node, r_id, r_node); + if (r_parent != node_id) { + const rp = self.buf.getPtrNoCheck(r_parent); + rp.setChild(node_id, rp.left == r_id); + node.parent = r_parent; + } else { + node.parent = r_id; + } + + // Swap colors. + const tmp_color = r_node.color; + r_node.color = node.color; + node.color = tmp_color; + + // Copy r_node value to node; a recursive call to delete node will be called. + var orig_val = node.val; + node.val = r_node.val; + defer { + // Revert back to node's original val in case the user wanted to only detach the node (and not remove it). + node.val = orig_val; + } + + // Relink r_node left and node left. + // Note: r_node shouldn't have a left child since firstFrom would have returned it the child instead. + self.buf.getPtrNoCheck(node.left).parent = r_id; + r_node.left = node.left; + node.left = NullId; + + // Relink r_node right and node right. + // Note: tmp_right can't be null since node should have two children. + const tmp_right = node.right; + if (r_node.right != NullId) { + self.buf.getPtrNoCheck(r_node.right).parent = node_id; + } + node.right = r_node.right; + if (tmp_right != r_id) { + self.buf.getPtrNoCheck(tmp_right).parent = r_id; + r_node.right = tmp_right; + } + + // Reduced to zero or one children case. Recurse once. + try self.detachInternal(node_id); + return; + } + + // If red was removed, it is done. + if (node.color == .Red) { + return; + } else { + // If black was removed and replacement is red. Paint replacement black and we are done. + if (to_fix_nid == NullId) { + // Double black. Perform fix. + self.detachFixUp(to_fix_nid, node.parent); + } else { + const r_node = self.buf.getPtrNoCheck(to_fix_nid); + if (r_node.color == .Red) { + // Can mark black and be done since a black was removed. + r_node.color = .Black; + } else { + // Double black. Perform fix. + self.detachFixUp(to_fix_nid, r_node.parent); + } + } + } + } + + /// Handle double black cases. + /// Assumes node is a double black. Since the node could be null, the current parent is also required. + fn detachFixUp(self: *Self, node_id: OptId, parent_id: OptId) void { + // Case 1: Root case. + if (parent_id == NullId) { + return; + } + + const parent = self.buf.getPtrNoCheck(parent_id); + const s_id = self.getSibling(parent, node_id); + const is_right_sibling = parent.left == node_id; + // Sibling must exist since node is a double black. + const sibling = self.buf.getPtrNoCheck(s_id); + const s_left: ?*Node = if (sibling.left != NullId) self.buf.getPtrNoCheck(sibling.left) else null; + const s_right: ?*Node = if (sibling.right != NullId) self.buf.getPtrNoCheck(sibling.right) else null; + const s_left_black = s_left == null or s_left.?.color == .Black; + const s_right_black = s_right == null or s_right.?.color == .Black; + + if (parent.color == .Black and sibling.color == .Red) { + if (s_left_black and s_right_black) { + if (is_right_sibling) { + // Case 2: parent is black, right sibling is red and has two black children. + self.rotateLeft(parent_id, parent); + } else { + // Case 2: parent is black, left sibling is red and has two black children. + self.rotateRight(parent_id, parent); + } + parent.color = .Red; + sibling.color = .Black; + self.detachFixUp(node_id, parent_id); + return; + } + } + + if (parent.color == .Black and sibling.color == .Black) { + // Case 3: left or right sibling with both black children. + if (s_left_black and s_right_black) { + sibling.color = .Red; + // Recurse at parent. + self.detachFixUp(parent_id, parent.parent); + return; + } + } + + if (parent.color == .Red and sibling.color == .Black) { + // Case 4: left or right sibling with both black children. + if (s_left_black and s_right_black) { + parent.color = .Black; + sibling.color = .Red; + return; + } + } + + if (sibling.color == .Black) { + // Case 5: parent is black, right sibling is black, sibling has red left child and black right child. + if (is_right_sibling and s_left != null and s_left.?.color == .Red and s_right_black) { + self.rotateRight(s_id, sibling); + sibling.color = .Red; + s_left.?.color = .Black; + // Call again to check for case 6. + self.detachFixUp(node_id, parent_id); + return; + } + // Case 5: parent is black, left sibiling is black, sibling has red right child and black left child. + if (!is_right_sibling and s_right != null and s_right.?.color == .Red and s_left_black) { + self.rotateLeft(s_id, sibling); + sibling.color = .Red; + s_right.?.color = .Black; + // Call again to check for case 6. + self.detachFixUp(node_id, parent_id); + return; + } + // Case 6: right sibling with red right child. + if (is_right_sibling and s_right != null and s_right.?.color == .Red) { + self.rotateLeft(parent_id, parent); + sibling.color = parent.color; + parent.color = .Black; + s_right.?.color = .Black; + return; + } + // Case 6: left sibling with red left child. + if (!is_right_sibling and s_left != null and s_left.?.color == .Red) { + self.rotateRight(parent_id, parent); + sibling.color = parent.color; + parent.color = .Black; + s_left.?.color = .Black; + return; + } + } + } + + pub fn insert(self: *Self, val: Value) !Id { + return try self.insertCustom(val, self.ctx, Compare); + } + + /// Duplicate keys are not allowed. + pub fn insertCustom(self: *Self, val: Value, ctx: anytype, cmp: fn (Value, Value, @TypeOf(ctx)) std.math.Order) !Id { + var maybe_id: ?Id = undefined; + var maybe_parent: ?Id = undefined; + var is_left: bool = undefined; + + maybe_id = self.doLookup(val, &maybe_parent, &is_left, ctx, cmp); + if (maybe_id) |_| { + return error.DuplicateKey; + } + + const new_id = try self.buf.add(.{ + .left = NullId, + .right = NullId, + .color = .Red, + .parent = maybe_parent orelse NullId, + .val = val, + }); + self.size += 1; + + if (maybe_parent) |parent| { + self.buf.getPtrNoCheck(parent).setChild(new_id, is_left); + } else { + self.root = new_id; + } + + var node_id = new_id; + while (true) { + var node = self.buf.getPtrNoCheck(node_id); + const parent_id = node.getParentOpt() orelse break; + var parent = self.buf.getPtrNoCheck(parent_id); + if (parent.color == .Black) { + // Current is red, parent is black. Nothing left to do. + break; + } + // If parent is red, there must be a grand parent that is black. + var grandpa_id = parent.getParentOpt() orelse unreachable; + var grandpa = self.buf.getPtrNoCheck(grandpa_id); + + if (parent_id == grandpa.left) { + var opt_psibling = grandpa.right; + const mb_psibling: ?*Node = if (opt_psibling != NullId) self.buf.getPtrNoCheck(opt_psibling) else null; + if (mb_psibling == null or mb_psibling.?.color == .Black) { + // Case #5, parent is red, parent sibling is black, node is inner grandchild. Rotate left first. + if (node_id == parent.right) { + self.rotateLeft(parent_id, parent); + // Make sure reattached right is black since parent is red. + if (parent.right != NullId) { + self.buf.getPtrNoCheck(parent.right).color = .Black; + } + parent = node; + } + parent.color = .Black; + grandpa.color = .Red; + self.rotateRight(grandpa_id, grandpa); + // Make sure reattached left is black since grandpa is red. + if (grandpa.left != NullId) { + self.buf.getPtrNoCheck(grandpa.left).color = .Black; + } + } else { + // parent and parent sibling are both red. Set both to black, and grand parent to red. + parent.color = .Black; + mb_psibling.?.color = .Black; + grandpa.color = .Red; + node_id = grandpa_id; + } + } else { + var opt_psibling = grandpa.left; + const mb_psibling: ?*Node = if (opt_psibling != NullId) self.buf.getPtrNoCheck(opt_psibling) else null; + if (mb_psibling == null or mb_psibling.?.color == .Black) { + // Case #5, parent is red, parent sibling is black, node is inner grandchild. Rotate right first. + if (node_id == parent.left) { + self.rotateRight(parent_id, parent); + // Make sure reattached left is black since parent is red. + if (parent.left != NullId) { + self.buf.getPtrNoCheck(parent.left).color = .Black; + } + parent = node; + } + parent.color = .Black; + grandpa.color = .Red; + self.rotateLeft(grandpa_id, grandpa); + // Make sure reattached right is black since grandpa is red. + if (grandpa.right != NullId) { + self.buf.getPtrNoCheck(grandpa.right).color = .Black; + } + } else { + // parent and parent sibling are both red. Set both to black, and grand parent to red. + parent.color = .Black; + mb_psibling.?.color = .Black; + grandpa.color = .Red; + node_id = grandpa_id; + } + } + } + // This was an insert, there is at least one node. + self.buf.getPtrNoCheck(self.root).color = .Black; + return new_id; + } + + /// lookup searches for the value of key, using binary search. It will + /// return a pointer to the node if it is there, otherwise it will return null. + /// Complexity guaranteed O(log n), where n is the number of nodes book-kept + /// by tree. + pub fn lookup(self: Self, val: Value) ?Id { + var parent: ?Id = undefined; + var is_left: bool = undefined; + return self.doLookup(val, &parent, &is_left, self.ctx, Compare); + } + + /// Lookup with a different compare function. + pub fn lookupCustom(self: Self, val: Value, ctx: anytype, cmp: fn (Value, Value, @TypeOf(ctx)) std.math.Order) ?Id { + var parent: ?Id = undefined; + var is_left: bool = undefined; + return self.doLookup(val, &parent, &is_left, ctx, cmp); + } + + /// Lookup that also sets the parent and is_left to indicate where it was found or where it would be inserted. + pub fn lookupCustomLoc(self: Self, val: Value, ctx: anytype, cmp: fn (Value, Value, @TypeOf(ctx)) std.math.Order, parent: *?Id, is_left: *bool) ?Id { + return self.doLookup(val, parent, is_left, ctx, cmp); + } + + /// If there is a match, the Id is returned. + /// Otherwise, the insert slot is provided by pparent and is_left. + fn doLookup(self: Self, val: Value, pparent: *?Id, is_left: *bool, ctx: anytype, cmp: fn (Value, Value, @TypeOf(ctx)) std.math.Order) ?Id { + var opt_id = self.root; + + pparent.* = null; + is_left.* = false; + + while (opt_id != NullId) { + const node = self.buf.getNoCheck(opt_id); + const res = cmp(val, node.val, ctx); + if (res == .eq) { + return opt_id; + } + pparent.* = opt_id; + switch (res) { + .lt => { + is_left.* = true; + opt_id = node.left; + }, + .gt => { + is_left.* = false; + opt_id = node.right; + }, + .eq => unreachable, // handled above + } + } + return null; + } + + /// e + /// / + /// (a) + /// / \ + /// b c + /// / + /// d + /// Given a, rotate a to b and c to a. a's parent becomes c and right becomes d. c's parent bcomes e and left becomes a. + fn rotateLeft(self: *Self, node_id: Id, node: *Node) void { + if (node.right == NullId) { + unreachable; + } + var right = self.buf.getPtrNoCheck(node.right); + if (!node.isRoot()) { + var parent = self.buf.getPtrNoCheck(node.parent); + if (parent.left == node_id) { + parent.left = node.right; + } else { + parent.right = node.right; + } + right.parent = node.parent; + } else { + self.root = node.right; + right.parent = NullId; + } + node.parent = node.right; + node.right = right.left; + if (node.right != NullId) { + self.buf.getPtrNoCheck(node.right).parent = node_id; + } + right.left = node_id; + } + + /// Works similarily to rotateLeft for the right direction. + fn rotateRight(self: *Self, node_id: Id, node: *Node) void { + if (node.left == NullId) { + unreachable; + } + var left = self.buf.getPtrNoCheck(node.left); + if (!node.isRoot()) { + var parent = self.buf.getPtrNoCheck(node.parent); + if (parent.left == node_id) { + parent.left = node.left; + } else { + parent.right = node.left; + } + left.parent = node.parent; + } else { + self.root = node.left; + left.parent = NullId; + } + node.parent = node.left; + node.left = left.right; + if (node.left != NullId) { + self.buf.getPtrNoCheck(node.left).parent = node_id; + } + left.right = node_id; + } + + pub fn getPrev(self: Self, id: Id) ?Id { + var node = self.buf.getNoCheck(id); + if (node.left != NullId) { + var cur = node.left; + node = self.buf.getNoCheck(cur); + while (node.right != NullId) { + cur = node.right; + node = self.buf.getNoCheck(cur); + } + return cur; + } + var cur = id; + while (true) { + if (node.parent != NullId) { + var p = self.buf.getNoCheck(node.parent); + if (cur != p.left) { + return node.parent; + } + cur = node.parent; + node = p; + } else { + return null; + } + } + } + + pub fn getNext(self: Self, id: Id) ?Id { + var node = self.buf.getNoCheck(id); + if (node.right != NullId) { + var cur = node.right; + node = self.buf.getNoCheck(cur); + while (node.left != NullId) { + cur = node.left; + node = self.buf.getNoCheck(cur); + } + return cur; + } + var cur = id; + while (true) { + if (node.parent != NullId) { + var p = self.buf.getNoCheck(node.parent); + if (cur != p.right) { + return node.parent; + } + cur = node.parent; + node = p; + } else { + return null; + } + } + } + + /// Checks whether the tree is a valid red black tree. + pub fn isValid(self: Self) bool { + if (self.root == NullId) { + return true; + } + const root = self.buf.getNoCheck(self.root); + if (root.color != .Black) { + return false; + } + var black_cnt: u32 = undefined; + return self.isValid2(self.root, &black_cnt); + } + + fn isValid2(self: Self, id: OptId, out_black_cnt: *u32) bool { + if (id == NullId) { + out_black_cnt.* = 1; + return true; + } + const node = self.buf.getNoCheck(id); + if (node.color == .Red) { + // Red node. Both children must be black. + if (node.left != NullId) { + const left = self.buf.getNoCheck(node.left); + if (left.color != .Black) { + return false; + } + } + if (node.right != NullId) { + const right = self.buf.getNoCheck(node.right); + if (right.color != .Black) { + return false; + } + } + } + var left_black_cnt: u32 = undefined; + if (!self.isValid2(node.left, &left_black_cnt)) { + return false; + } + var right_black_cnt: u32 = undefined; + if (!self.isValid2(node.right, &right_black_cnt)) { + return false; + } + if (left_black_cnt != right_black_cnt) { + return false; + } + if (node.color == .Black) { + out_black_cnt.* = left_black_cnt + 1; + } else { + out_black_cnt.* = left_black_cnt; + } + return true; + } + + fn dump(self: Self) void { + self.dump2(self.root); + } + + fn dump2(self: Self, id: Id) void { + if (id == NullId) { + return; + } + const node = self.buf.getNoCheck(id); + log.debug("{}-{} -> {} {}", .{ id, node.color, node.left, node.right }); + self.dump2(node.left); + self.dump2(node.right); + } + + fn insertDetached(self: *Self, val: Value, color: Color) !Id { + return try self.buf.add(.{ + .left = NullId, + .right = NullId, + .color = color, + .parent = NullId, + .val = val, + }); + } + + fn attachLeft(self: *Self, attached_parent: Id, detached: Id) !void { + const parent = self.buf.getPtrNoCheck(attached_parent); + if (parent.left != NullId) { + return error.CantAttach; + } + parent.left = detached; + const left = self.buf.getPtrNoCheck(detached); + left.parent = attached_parent; + self.size += 1; + } + + fn attachRight(self: *Self, attached_parent: Id, detached: Id) !void { + const parent = self.buf.getPtrNoCheck(attached_parent); + if (parent.right != NullId) { + return error.CantAttach; + } + parent.right = detached; + const right = self.buf.getPtrNoCheck(detached); + right.parent = attached_parent; + self.size += 1; + } + }; +} + +fn testCompare(left: u32, right: u32, _: void) std.math.Order { + if (left < right) { + return .lt; + } else if (left == right) { + return .eq; + } else if (left > right) { + return .gt; + } + unreachable; +} + +fn testCompareReverse(left: u32, right: u32, _: void) std.math.Order { + return testCompare(right, left, {}); +} + +test "getNext, getPrev" { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + + try t.expectEqual(tree.getNext(b).?, a); + try t.expectEqual(tree.getNext(a).?, c); + try t.expectEqual(tree.getNext(c), null); + + try t.expectEqual(tree.getPrev(b), null); + try t.expectEqual(tree.getPrev(a).?, b); + try t.expectEqual(tree.getPrev(c).?, a); +} + +test "Insert where rotations need to set reattached nodes to black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insertDetached(9, .Black); + const c = try tree.insertDetached(20, .Red); + const d = try tree.insertDetached(8, .Red); + const e = try tree.insertDetached(19, .Black); + const f = try tree.insertDetached(21, .Black); + const g = try tree.insertDetached(17, .Red); + try tree.attachLeft(a, b); + try tree.attachRight(a, c); + try tree.attachLeft(b, d); + try tree.attachLeft(c, e); + try tree.attachRight(c, f); + try tree.attachLeft(e, g); + try t.expectEqual(tree.isValid(), true); + + const h = try tree.insert(18); + try t.expectEqual(tree.isValid(), true); + + const vals = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(vals); + try t.expectEqualSlices(TestId, vals, &.{ d, b, a, g, h, e, c, f }); +} + +test "Insert case #1: current node parent is black" { + var tree = initTestTree(); + defer tree.deinit(); + + const root = try tree.insert(10); + const node = try tree.insert(9); + + try t.expectEqual(tree.getNode(root).?.color, .Black); + try t.expectEqual(tree.getNode(root).?.left, node); + try t.expectEqual(tree.getNode(node).?.color, .Red); + try t.expectEqual(tree.getNode(node).?.parent, root); +} + +test "Insert case #2/#3/#4: both parent and parent sibling are red" { + var tree = initTestTree(); + defer tree.deinit(); + + const root = try tree.insert(10); + const parent = try tree.insert(9); + const psibling = try tree.insert(11); + const node = try tree.insert(8); + + try t.expectEqual(tree.getNode(root).?.color, .Black); + try t.expectEqual(tree.getNode(root).?.left, parent); + try t.expectEqual(tree.getNode(root).?.right, psibling); + try t.expectEqual(tree.getNode(parent).?.color, .Black); + try t.expectEqual(tree.getNode(parent).?.left, node); + try t.expectEqual(tree.getNode(psibling).?.color, .Black); + try t.expectEqual(tree.getNode(node).?.color, .Red); + try t.expectEqual(tree.getNode(node).?.parent, parent); +} + +test "Insert case #5: parent is red but parent sibling is black, node is inner grandchild" { + var tree = initTestTree(); + defer tree.deinit(); + + const root = try tree.insert(100); + const parent = try tree.insert(50); + _ = try tree.insert(150); + _ = try tree.insert(25); + const node = try tree.insert(75); + _ = try tree.insert(80); + _ = try tree.insert(70); + _ = try tree.insert(60); + + try t.expectEqual(tree.getNode(node).?.parent, TestNullId); + try t.expectEqual(tree.getNode(node).?.color, .Black); + try t.expectEqual(tree.getNode(node).?.right, root); + try t.expectEqual(tree.getNode(node).?.left, parent); + + try t.expectEqual(tree.getNode(root).?.color, .Red); + try t.expectEqual(tree.getNode(root).?.parent, node); + + try t.expectEqual(tree.getNode(parent).?.color, .Red); + try t.expectEqual(tree.getNode(parent).?.parent, node); +} + +test "Insert case #6: parent is red but parent sibling is black, node is outer grandchild" { + var tree = initTestTree(); + defer tree.deinit(); + + const root = try tree.insert(100); + const parent = try tree.insert(50); + _ = try tree.insert(150); + const node = try tree.insert(25); + _ = try tree.insert(70); + _ = try tree.insert(20); + _ = try tree.insert(30); + _ = try tree.insert(15); + + try t.expectEqual(tree.getNode(parent).?.parent, TestNullId); + try t.expectEqual(tree.getNode(parent).?.color, .Black); + try t.expectEqual(tree.getNode(parent).?.right, root); + try t.expectEqual(tree.getNode(parent).?.left, node); + + try t.expectEqual(tree.getNode(root).?.color, .Red); + try t.expectEqual(tree.getNode(root).?.parent, parent); + + try t.expectEqual(tree.getNode(node).?.color, .Red); + try t.expectEqual(tree.getNode(node).?.parent, parent); +} + +test "Insert in order." { + var tree = initTestTree(); + defer tree.deinit(); + + _ = try tree.insert(1); + _ = try tree.insert(2); + _ = try tree.insert(3); + _ = try tree.insert(4); + _ = try tree.insert(5); + _ = try tree.insert(6); + _ = try tree.insert(7); + _ = try tree.insert(8); + _ = try tree.insert(9); + _ = try tree.insert(10); + + const vals = tree.allocValuesInOrder(t.allocator); + defer t.allocator.free(vals); + try t.expectEqualSlices(TestId, vals, &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); +} + +test "Insert in reverse order." { + var tree = initTestTree(); + defer tree.deinit(); + + _ = try tree.insert(10); + _ = try tree.insert(9); + _ = try tree.insert(8); + _ = try tree.insert(7); + _ = try tree.insert(6); + _ = try tree.insert(5); + _ = try tree.insert(4); + _ = try tree.insert(3); + _ = try tree.insert(2); + _ = try tree.insert(1); + + const vals = tree.allocValuesInOrder(t.allocator); + defer t.allocator.free(vals); + try t.expectEqualSlices(TestId, vals, &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); +} + +test "inserting and looking up" { + var tree = initTestTree(); + defer tree.deinit(); + const orig: u32 = 1000; + const node_id = try tree.insert(orig); + // Assert that identical value finds the same pointer + try t.expectEqual(tree.lookup(1000), node_id); + // Assert that insert duplicate returns error. + try t.expectError(error.DuplicateKey, tree.insert(1000)); + try t.expectEqual(tree.lookup(1000), node_id); + try t.expectEqual(tree.get(node_id).?, orig); + // Assert that if looking for a non-existing value, return null. + try t.expectEqual(tree.lookup(1234), null); +} + +test "multiple inserts, followed by calling first and last" { + // if (@import("builtin").arch == .aarch64) { + // // TODO https://github.com/ziglang/zig/issues/3288 + // return error.SkipZigTest; + // } + var tree = initTestTree(); + defer tree.deinit(); + + _ = try tree.insert(0); + _ = try tree.insert(1); + _ = try tree.insert(2); + const third_id = try tree.insert(3); + try t.expectEqual(tree.get(tree.first().?).?, 0); + try t.expectEqual(tree.get(tree.last().?).?, 3); + try t.expectEqual(tree.lookup(3), third_id); + tree.sort({}, testCompareReverse) catch unreachable; + try t.expectEqual(tree.get(tree.first().?).?, 3); + try t.expectEqual(tree.get(tree.last().?).?, 0); + try t.expectEqual(tree.lookupCustom(3, {}, testCompareReverse), third_id); +} + +test "Remove root with no children." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + try t.expectEqual(tree.root, a); + + try tree.remove(a); + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{}); +} + +test "Remove root with left red child." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.left, b); + try t.expectEqual(tree.getNode(b).?.color, .Red); + + try tree.remove(a); + try t.expectEqual(tree.getNode(b).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{b}); +} + +test "Remove root with right red child." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(15); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.right, b); + try t.expectEqual(tree.getNode(b).?.color, .Red); + + try tree.remove(a); + try t.expectEqual(tree.getNode(b).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{b}); +} + +test "Remove root with two red children." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.left, b); + try t.expectEqual(tree.getNode(a).?.right, c); + try t.expectEqual(tree.getNode(b).?.color, .Red); + try t.expectEqual(tree.getNode(c).?.color, .Red); + + try tree.remove(a); + try t.expectEqual(tree.root, c); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.left, b); + try t.expectEqual(tree.getNode(b).?.color, .Red); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, c }); +} + +test "Remove red non-root." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.left, b); + try t.expectEqual(tree.getNode(b).?.color, .Red); + + try tree.remove(b); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{a}); +} + +test "Remove black non-root with left red child." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(3); + const b = try tree.insert(2); + const c = try tree.insert(4); + const d = try tree.insert(1); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(b); + + try t.expectEqual(tree.getNode(d).?.parent, a); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ d, a, c }); +} + +test "Remove black non-root with right red child." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(3); + const b = try tree.insert(2); + const c = try tree.insert(4); + const d = try tree.insert(5); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(c); + + try t.expectEqual(tree.getNode(d).?.parent, a); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, a, d }); +} + +test "Remove non-root with double black case: Parent is red, right sibling is black, and sibling's children are black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(15); + const b = try tree.insert(10); + const c = try tree.insert(20); + const d = try tree.insert(17); + const e = try tree.insert(23); + const f = try tree.insert(25); + try tree.remove(f); + try t.expectEqual(tree.getNode(c).?.color, .Red); + try t.expectEqual(tree.getNode(d).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.left, TestNullId); + try t.expectEqual(tree.getNode(e).?.right, TestNullId); + + try tree.remove(d); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.left, TestNullId); + try t.expectEqual(tree.getNode(e).?.color, .Red); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, a, c, e }); +} + +test "Remove non-root with double black case: Parent is red, left sibling is black, and sibling's children are black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(15); + const b = try tree.insert(10); + const c = try tree.insert(20); + const d = try tree.insert(13); + const e = try tree.insert(7); + const f = try tree.insert(5); + try tree.remove(f); + try t.expectEqual(tree.getNode(b).?.color, .Red); + try t.expectEqual(tree.getNode(d).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.left, TestNullId); + try t.expectEqual(tree.getNode(e).?.right, TestNullId); + + try tree.remove(d); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.right, TestNullId); + try t.expectEqual(tree.getNode(e).?.color, .Red); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ e, b, a, c }); +} + +test "Remove non-root with double black case: Right sibling is black, and sibling's right child is red." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(20); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.right, d); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(b); + try t.expectEqual(tree.root, c); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ a, c, d }); +} + +test "Remove non-root with double black case: Left sibling is black, and sibling's left child is red." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(4); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.left, d); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(c); + try t.expectEqual(tree.root, b); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ d, b, a }); +} + +test "Remove non-root case 5: parent is red, right sibling is black, sibling's left child is red, sibling's right child is black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insertDetached(10, .Red); + const c = try tree.insertDetached(10, .Black); + const d = try tree.insertDetached(10, .Black); + const e = try tree.insertDetached(10, .Black); + const f = try tree.insertDetached(10, .Red); + try tree.attachLeft(a, b); + try tree.attachRight(a, c); + try tree.attachLeft(b, d); + try tree.attachRight(b, e); + try tree.attachLeft(e, f); + try t.expectEqual(tree.isValid(), true); + + t.log_level = .debug; + + try tree.remove(d); + try t.expectEqual(tree.isValid(), true); + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, f, e, a, c }); +} + +test "Remove non-root with double black case: Parent is black, right sibling is red, sibling's children are black" { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(20); + const e = try tree.insert(25); + const f = try tree.insert(30); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Red); + try t.expectEqual(tree.getNode(d).?.left, c); + try t.expectEqual(tree.getNode(d).?.right, e); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.color, .Black); + + try tree.remove(b); + try t.expectEqual(tree.root, d); + try t.expectEqual(tree.getNode(c).?.color, .Red); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ a, c, d, e, f }); +} + +test "Remove non-root with double black case: Parent is black, left sibling is red, sibling's children are black" { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(4); + const e = try tree.insert(3); + const f = try tree.insert(2); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Red); + try t.expectEqual(tree.getNode(d).?.left, e); + try t.expectEqual(tree.getNode(d).?.right, b); + try t.expectEqual(tree.getNode(e).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + + try tree.remove(c); + try t.expectEqual(tree.root, d); + try t.expectEqual(tree.getNode(b).?.color, .Red); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + try t.expectEqual(tree.getNode(e).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ f, e, d, b, a }); +} + +test "Remove non-root with double black case: Parent is black, right sibling is black, sibling's left child is red, sibling's right child is black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(13); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.left, d); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(b); + try t.expectEqual(tree.root, d); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ a, d, c }); +} + +test "Remove non-root with double black case: Parent is black, left sibling is black, sibling's right child is red, sibling's left child is black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(10); + const b = try tree.insert(5); + const c = try tree.insert(15); + const d = try tree.insert(7); + try t.expectEqual(tree.getNode(c).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.right, d); + try t.expectEqual(tree.getNode(d).?.color, .Red); + + try tree.remove(c); + try t.expectEqual(tree.root, d); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(d).?.color, .Black); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, d, a }); +} + +test "Remove non-root with double black case: Parent is black, right sibling is black, and sibling's children are black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(15); + const b = try tree.insert(10); + const c = try tree.insert(20); + const d = try tree.insert(5); + try tree.remove(d); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Black); + + try tree.remove(b); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Red); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ a, c }); +} + +test "Remove non-root with double black case: Parent is black, left sibling is black, and sibling's children are black." { + var tree = initTestTree(); + defer tree.deinit(); + + const a = try tree.insert(15); + const b = try tree.insert(10); + const c = try tree.insert(20); + const d = try tree.insert(5); + try tree.remove(d); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Black); + try t.expectEqual(tree.getNode(c).?.color, .Black); + + try tree.remove(c); + try t.expectEqual(tree.root, a); + try t.expectEqual(tree.getNode(a).?.color, .Black); + try t.expectEqual(tree.getNode(b).?.color, .Red); + + const node_ids = tree.allocNodeIdsInOrder(t.allocator); + defer t.allocator.free(node_ids); + try t.expectEqualSlices(TestId, node_ids, &.{ b, a }); +} + +fn initTestTree() RbTree(TestId, u32, void, testCompare) { + return RbTree(TestId, u32, void, testCompare).init(t.allocator, {}); +} + +const TestId = u32; +const TestNullId = @import("compact.zig").CompactNull(TestId); diff --git a/examples/gkurve/text.zig b/examples/gkurve/label.zig similarity index 73% rename from examples/gkurve/text.zig rename to examples/gkurve/label.zig index dcb1b92f..35b6d57a 100644 --- a/examples/gkurve/text.zig +++ b/examples/gkurve/label.zig @@ -4,6 +4,7 @@ const std = @import("std"); const ft = @import("freetype"); const zigimg = @import("zigimg"); const Atlas = @import("atlas.zig").Atlas; +const AtlasErr = @import("atlas.zig").Error; const UVData = @import("atlas.zig").UVData; const App = @import("main.zig").App; const draw = @import("draw.zig"); @@ -29,7 +30,8 @@ const WriterContext = struct { position: Vec2, text_color: Vec4, }; -const Writer = std.io.Writer(WriterContext, ft.Error, write); +const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr; +const Writer = std.io.Writer(WriterContext, WriterError, write); pub fn writer(label: *Label, app: *App, position: Vec2, text_color: Vec4) Writer { return Writer{ @@ -56,8 +58,7 @@ pub fn deinit(label: *Label) void { label.char_map.deinit(); } -// FIXME: union ft.error and hashmap error -fn write(ctx: WriterContext, bytes: []const u8) ft.Error!usize { +fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize { var offset = Vec2{ 0, 0 }; for (bytes) |char| { switch (char) { @@ -66,7 +67,7 @@ fn write(ctx: WriterContext, bytes: []const u8) ft.Error!usize { offset[1] -= @intToFloat(f32, ctx.label.face.size().metrics().height >> 6); }, ' ' => { - const v = ctx.label.char_map.getOrPut(char) catch unreachable; + const v = try ctx.label.char_map.getOrPut(char); if (!v.found_existing) { try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); try ctx.label.face.loadChar(char, .{ .render = true }); @@ -79,7 +80,7 @@ fn write(ctx: WriterContext, bytes: []const u8) ft.Error!usize { offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); }, else => { - const v = ctx.label.char_map.getOrPut(char) catch unreachable; + const v = try ctx.label.char_map.getOrPut(char); if (!v.found_existing) { try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); try ctx.label.face.loadChar(char, .{ .render = true }); @@ -87,31 +88,47 @@ fn write(ctx: WriterContext, bytes: []const u8) ft.Error!usize { const glyph_bitmap = glyph.bitmap(); const glyph_width = glyph_bitmap.width(); const glyph_height = glyph_bitmap.rows(); - var glyph_data = ctx.label.allocator.alloc(zigimg.color.Rgba32, glyph_width * glyph_height) catch unreachable; + + // Add 1 pixel padding to texture to avoid bleeding over other textures + var glyph_data = try ctx.label.allocator.alloc(zigimg.color.Rgba32, (glyph_width + 2) * (glyph_height + 2)); defer ctx.label.allocator.free(glyph_data); const glyph_buffer = glyph_bitmap.buffer(); for (glyph_data) |*data, i| { - const x = i % glyph_width; - const y = i / glyph_width; - const glyph_col = glyph_buffer[y * glyph_width + x]; + const x = i % (glyph_width + 2); + const y = i / (glyph_width + 2); + + // zig fmt: off + const glyph_col = + if (x == 0 or x == (glyph_width + 1) or y == 0 or y == (glyph_height + 1)) + 0 + else + glyph_buffer[(y - 1) * glyph_width + (x - 1)]; + // zig fmt: on + data.* = zigimg.color.Rgba32.initRGB(glyph_col, glyph_col, glyph_col); } - const glyph_atlas_region = ctx.app.texture_atlas_data.reserve(ctx.label.allocator, glyph_width, glyph_height) catch unreachable; - const glyph_uv_data = glyph_atlas_region.getUVData(@intToFloat(f32, ctx.app.texture_atlas_data.size)); + var glyph_atlas_region = try ctx.app.texture_atlas_data.reserve(ctx.label.allocator, glyph_width + 2, glyph_height + 2); ctx.app.texture_atlas_data.set(glyph_atlas_region, glyph_data); + glyph_atlas_region.x += 1; + glyph_atlas_region.y += 1; + glyph_atlas_region.width -= 2; + glyph_atlas_region.height -= 2; + const glyph_uv_data = glyph_atlas_region.getUVData(@intToFloat(f32, ctx.app.texture_atlas_data.size)); + v.value_ptr.* = GlyphInfo{ .uv_data = glyph_uv_data, .metrics = glyph.metrics(), }; } - draw.quad( + + try draw.quad( ctx.app, ctx.position + offset + Vec2{ @intToFloat(f32, v.value_ptr.metrics.horiBearingX >> 6), @intToFloat(f32, (v.value_ptr.metrics.horiBearingY - v.value_ptr.metrics.height) >> 6) }, .{ @intToFloat(f32, v.value_ptr.metrics.width >> 6), @intToFloat(f32, v.value_ptr.metrics.height >> 6) }, .{ .blend_color = ctx.text_color }, v.value_ptr.uv_data, - ) catch unreachable; + ); offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); }, } diff --git a/examples/gkurve/main.zig b/examples/gkurve/main.zig index a88cf587..70fd8b21 100644 --- a/examples/gkurve/main.zig +++ b/examples/gkurve/main.zig @@ -11,7 +11,8 @@ const glfw = @import("glfw"); const draw = @import("draw.zig"); const Atlas = @import("atlas.zig").Atlas; const ft = @import("freetype"); -const Label = @import("text.zig"); +const Label = @import("label.zig"); +const ResizableLabel = @import("resizable_label.zig"); pub const App = @This(); @@ -75,7 +76,11 @@ pub fn init(app: *App, engine: *mach.Engine) !void { } const white_tex_scale = 80; - const atlas_white_region = try app.texture_atlas_data.reserve(engine.allocator, white_tex_scale, white_tex_scale); + var atlas_white_region = try app.texture_atlas_data.reserve(engine.allocator, white_tex_scale, white_tex_scale); + atlas_white_region.x += 1; + atlas_white_region.y += 1; + atlas_white_region.width -= 2; + atlas_white_region.height -= 2; const white_texture_uv_data = atlas_white_region.getUVData(atlas_float_size); var white_tex_data = try engine.allocator.alloc(zigimg.color.Rgba32, white_tex_scale * white_tex_scale); std.mem.set(zigimg.color.Rgba32, white_tex_data, zigimg.color.Rgba32.initRGB(0xff, 0xff, 0xff)); @@ -88,10 +93,16 @@ pub fn init(app: *App, engine: *mach.Engine) !void { const lib = try ft.Library.init(); defer lib.deinit(); - var label = try Label.init(lib, "freetype/upstream/assets/FiraSans-Regular.ttf", 0, 40, engine.allocator); + const size_multiplier = 5; + const character = 'e'; + var label = try Label.init(lib, "freetype/upstream/assets/FiraSans-Regular.ttf", 0, 110 * size_multiplier, engine.allocator); defer label.deinit(); + try label.print(app, &.{character}, .{}, @Vector(2, f32){ 50 * size_multiplier, 40 }, @Vector(4, f32){ 1, 1, 1, 1 }); - try label.print(app, "All your game's bases are belong to us", .{}, @Vector(2, f32){ 0, 420 }, @Vector(4, f32){ 1, 1, 1, 1 }); + var resizable_label: ResizableLabel = undefined; + try resizable_label.init(lib, "freetype/upstream/assets/FiraSans-Regular.ttf", 0, engine.allocator, white_texture_uv_data); + defer resizable_label.deinit(); + try resizable_label.print(app, &.{character}, .{}, @Vector(4, f32){ 0, 40, 0, 0 }, @Vector(4, f32){ 1, 1, 1, 1 }, 80 * size_multiplier); queue.writeTexture( &.{ .texture = texture }, @@ -206,8 +217,8 @@ pub fn init(app: *App, engine: *mach.Engine) !void { }); const sampler = engine.device.createSampler(&.{ - .mag_filter = .linear, - .min_filter = .linear, + // .mag_filter = .linear, + // .min_filter = .linear, }); const bind_group = engine.device.createBindGroup( diff --git a/examples/gkurve/resizable_label.zig b/examples/gkurve/resizable_label.zig new file mode 100644 index 00000000..8716d716 --- /dev/null +++ b/examples/gkurve/resizable_label.zig @@ -0,0 +1,442 @@ +//! TODO: Refactor the API, maybe use a handle that contains the lib and other things and controls init and deinit of ft.Lib and other things + +const std = @import("std"); +const ft = @import("freetype"); +const zigimg = @import("zigimg"); +const Atlas = @import("atlas.zig").Atlas; +const AtlasErr = @import("atlas.zig").Error; +const UVData = @import("atlas.zig").UVData; +const App = @import("main.zig").App; +const draw = @import("draw.zig"); +const Vertex = draw.Vertex; +const Tessellator = @import("tessellator.zig").Tessellator; + +// If true, show the filled triangles green, the concave beziers blue and the convex ones red +const debug_colors = false; + +pub const ResizableLabel = @This(); + +const Vec2 = @Vector(2, f32); +const Vec4 = @Vector(4, f32); +const VertexList = std.ArrayList(Vertex); + +// All the data that a single character needs to be rendered +// TODO: hori/vert advance, write file format +const CharVertices = struct { + filled_vertices: VertexList, + filled_vertices_indices: std.ArrayList(u16), + // Concave vertices belong to the filled_vertices list, so just index them + concave_vertices: std.ArrayList(u16), + // The point outside of the convex bezier, doesn't belong to the filled vertices, + // But the other two points do, so put those in the indices + convex_vertices: VertexList, + convex_vertices_indices: std.ArrayList(u16), +}; + +face: ft.Face, +char_map: std.AutoHashMap(u8, CharVertices), +allocator: std.mem.Allocator, +tessellator: Tessellator, +white_texture: UVData, + +// The data that the write function needs +// TODO: move twxture here, don't limit to just white_texture +const WriterContext = struct { + label: *ResizableLabel, + app: *App, + position: Vec4, + text_color: Vec4, + text_size: u32, +}; +const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr; +const Writer = std.io.Writer(WriterContext, WriterError, write); + +pub fn writer(label: *ResizableLabel, app: *App, position: Vec4, text_color: Vec4, text_size: u32) Writer { + return Writer{ + .context = .{ + .label = label, + .app = app, + .position = position, + .text_color = text_color, + .text_size = text_size, + }, + }; +} + +pub fn init(self: *ResizableLabel, lib: ft.Library, font_path: []const u8, face_index: i32, allocator: std.mem.Allocator, white_texture: UVData) !void { + self.* = ResizableLabel{ + .face = try lib.newFace(font_path, face_index), + .char_map = std.AutoHashMap(u8, CharVertices).init(allocator), + .allocator = allocator, + .tessellator = undefined, + .white_texture = white_texture, + }; + self.tessellator.init(self.allocator); +} + +pub fn deinit(label: *ResizableLabel) void { + label.face.deinit(); + label.tessellator.deinit(); + // FIXME: + // std.debug.todo("valueIterator() doesn't stop? How do we deallocate the values?"); + // while (label.char_map.valueIterator().next()) |value| { + // _ = value; + // value.filled_vertices.deinit(); + // value.filled_vertices_indices.deinit(); + // value.convex_vertices.deinit(); + // value.convex_vertices_indices.deinit(); + // value.concave_vertices.deinit(); + // } + label.char_map.deinit(); +} + +// TODO: handle offsets +// FIXME: many useless allocations for the arraylists +fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize { + var offset = Vec4{ 0, 0, 0, 0 }; + for (bytes) |char| { + switch (char) { + '\n' => { + offset[0] = 0; + offset[1] -= @intToFloat(f32, ctx.label.face.sizeMetrics().?.height >> 6); + std.debug.todo("New line not implemented yet"); + }, + ' ' => { + std.debug.todo("Space character not implemented yet"); + // const v = try ctx.label.char_map.getOrPut(char); + // if (!v.found_existing) { + // try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + // try ctx.label.face.loadChar(char, .{ .render = true }); + // const glyph = ctx.label.face.glyph; + // v.value_ptr.* = GlyphInfo{ + // .uv_data = undefined, + // .metrics = glyph.metrics(), + // }; + // } + // offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); + }, + else => { + const v = try ctx.label.char_map.getOrPut(char); + if (!v.found_existing) { + try ctx.label.face.loadChar(char, .{ .no_scale = true, .no_bitmap = true }); + const glyph = ctx.label.face.glyph; + + // Use a big scale and then scale to the actual text size + const multiplier = 1024 << 6; + const matrix = ft.Matrix{ + .xx = 1 * multiplier, + .xy = 0 * multiplier, + .yx = 0 * multiplier, + .yy = 1 * multiplier, + }; + glyph.outline().?.transform(matrix); + + v.value_ptr.* = CharVertices{ + .filled_vertices = VertexList.init(ctx.label.allocator), + .filled_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator), + .concave_vertices = std.ArrayList(u16).init(ctx.label.allocator), + .convex_vertices = VertexList.init(ctx.label.allocator), + .convex_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator), + }; + + var outline_ctx = OutlineContext{ + .outline_verts = std.ArrayList(std.ArrayList(Vec2)).init(ctx.label.allocator), + .inside_verts = std.ArrayList(Vec2).init(ctx.label.allocator), + .concave_vertices = std.ArrayList(Vec2).init(ctx.label.allocator), + .convex_vertices = std.ArrayList(Vec2).init(ctx.label.allocator), + }; + defer outline_ctx.outline_verts.deinit(); + defer { + for (outline_ctx.outline_verts.items) |*item| { + item.deinit(); + } + } + defer outline_ctx.inside_verts.deinit(); + defer outline_ctx.concave_vertices.deinit(); + defer outline_ctx.convex_vertices.deinit(); + + const callbacks = ft.Outline.OutlineFuncs(*OutlineContext){ + .move_to = moveToFunction, + .line_to = lineToFunction, + .conic_to = conicToFunction, + .cubic_to = cubicToFunction, + .shift = 0, + .delta = 0, + }; + try ctx.label.face.glyph.outline().?.decompose(&outline_ctx, callbacks); + + uniteOutsideAndInsideVertices(&outline_ctx); + + // Tessellator.triangulatePolygons() doesn't seem to work, so just + // call triangulatePolygon() for each polygon, and put the results all + // in all_outlines and all_indices + var all_outlines = std.ArrayList(Vec2).init(ctx.label.allocator); + defer all_outlines.deinit(); + var all_indices = std.ArrayList(u16).init(ctx.label.allocator); + defer all_indices.deinit(); + var idx_offset: u16 = 0; + for (outline_ctx.outline_verts.items) |item| { + ctx.label.tessellator.triangulatePolygon(item.items); + defer ctx.label.tessellator.clearBuffers(); + try all_outlines.appendSlice(ctx.label.tessellator.out_verts.items); + for (ctx.label.tessellator.out_idxes.items) |idx| { + try all_indices.append(idx + idx_offset); + } + idx_offset += @intCast(u16, ctx.label.tessellator.out_verts.items.len); + } + + for (all_outlines.items) |item| { + // FIXME: The uv_data is wrong, should be pushed up by the lowest a character can be + const vertex_uv = item / @splat(2, @as(f32, 1024 << 6)); + const vertex_pos = Vec4{ item[0], item[1], 0, 1 }; + try v.value_ptr.filled_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv }); + } + try v.value_ptr.filled_vertices_indices.appendSlice(all_indices.items); + + // FIXME: instead of finding the closest vertex and use its index maybe use indices directly in the moveTo,... functions + var i: usize = 0; + while (i < outline_ctx.concave_vertices.items.len) : (i += 1) { + for (all_outlines.items) |item, j| { + const dist = @reduce(.Add, (item - outline_ctx.concave_vertices.items[i]) * (item - outline_ctx.concave_vertices.items[i])); + if (dist < 0.1) { + try v.value_ptr.concave_vertices.append(@truncate(u16, j)); + break; + } + } + } + + i = 0; + while (i < outline_ctx.convex_vertices.items.len) : (i += 3) { + const vert = outline_ctx.convex_vertices.items[i]; + const vertex_uv = vert / @splat(2, @as(f32, 1024 << 6)); + const vertex_pos = Vec4{ vert[0], vert[1], 0, 1 }; + try v.value_ptr.convex_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv }); + + for (all_outlines.items) |item, j| { + const dist1 = @reduce(.Add, (item - outline_ctx.convex_vertices.items[i + 1]) * (item - outline_ctx.convex_vertices.items[i + 1])); + if (dist1 < 0.1) { + try v.value_ptr.convex_vertices_indices.append(@truncate(u16, j)); + } + + const dist2 = @reduce(.Add, (item - outline_ctx.convex_vertices.items[i + 2]) * (item - outline_ctx.convex_vertices.items[i + 2])); + if (dist2 < 0.1) { + try v.value_ptr.convex_vertices_indices.append(@truncate(u16, j)); + } + } + } + + ctx.label.tessellator.clearBuffers(); + } + + // Read the data and apply resizing of pos and uv + var filled_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.filled_vertices.items.len); + defer ctx.label.allocator.free(filled_vertices_after_offset); + for (filled_vertices_after_offset) |*vert, i| { + vert.* = v.value_ptr.filled_vertices.items[i]; + vert.pos *= Vec4{ @intToFloat(f32, ctx.text_size) / 1024, @intToFloat(f32, ctx.text_size) / 1024, 0, 1 }; + vert.pos += ctx.position + offset; + vert.uv = vert.uv * ctx.label.white_texture.width_and_height + ctx.label.white_texture.bottom_left; + } + + var actual_filled_vertices_to_use = try ctx.label.allocator.alloc(Vertex, v.value_ptr.filled_vertices_indices.items.len); + defer ctx.label.allocator.free(actual_filled_vertices_to_use); + for (actual_filled_vertices_to_use) |*vert, i| { + vert.* = filled_vertices_after_offset[v.value_ptr.filled_vertices_indices.items[i]]; + } + try ctx.app.vertices.appendSlice(actual_filled_vertices_to_use); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = .{ 0, 1, 0, 1 } }, actual_filled_vertices_to_use.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = ctx.text_color }, actual_filled_vertices_to_use.len / 3); + } + + var convex_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.convex_vertices.items.len + v.value_ptr.convex_vertices_indices.items.len); + defer ctx.label.allocator.free(convex_vertices_after_offset); + var j: u16 = 0; + var k: u16 = 0; + while (j < convex_vertices_after_offset.len) : (j += 3) { + convex_vertices_after_offset[j] = v.value_ptr.convex_vertices.items[j / 3]; + convex_vertices_after_offset[j].pos *= Vec4{ @intToFloat(f32, ctx.text_size) / 1024, @intToFloat(f32, ctx.text_size) / 1024, 0, 1 }; + convex_vertices_after_offset[j].pos += ctx.position + offset; + convex_vertices_after_offset[j].uv = convex_vertices_after_offset[j].uv * ctx.label.white_texture.width_and_height + ctx.label.white_texture.bottom_left; + + convex_vertices_after_offset[j + 1] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k]]; + convex_vertices_after_offset[j + 2] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k + 1]]; + k += 2; + } + try ctx.app.vertices.appendSlice(convex_vertices_after_offset); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .convex, .blend_color = .{ 1, 0, 0, 1 } }, convex_vertices_after_offset.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .convex, .blend_color = ctx.text_color }, convex_vertices_after_offset.len / 3); + } + + var concave_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.concave_vertices.items.len); + defer ctx.label.allocator.free(concave_vertices_after_offset); + for (concave_vertices_after_offset) |*vert, i| { + vert.* = filled_vertices_after_offset[v.value_ptr.concave_vertices.items[i]]; + } + try ctx.app.vertices.appendSlice(concave_vertices_after_offset); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .concave, .blend_color = .{ 0, 0, 1, 1 } }, concave_vertices_after_offset.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .concave, .blend_color = ctx.text_color }, concave_vertices_after_offset.len / 3); + } + + ctx.app.update_vertex_buffer = true; + ctx.app.update_frag_uniform_buffer = true; + + // offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); + }, + } + } + return bytes.len; +} + +// First move to initialize the outline, (first point), +// After many Q L or C, we will come back to the first point and then call M again if we need to hollow +// On the second M, we instead use an L to connect the first point to the start of the hollow path. +// We then follow like normal and at the end of the hollow path we use another L to close the path. + +// This is basically how an o would be drawn, each ┌... character is a Vertex +// ┌--------┐ +// | | +// | | +// | | +// | ┌----┐ | +// └-┘ | | Consider the vertices here and below to be at the same height, they are coincident +// ┌-┐ | | +// | └----┘ | +// | | +// | | +// | | +// └--------┘ + +const OutlineContext = struct { + // There may be more than one polygon (for example with 'i' we have the polygon of the base and another for the circle) + outline_verts: std.ArrayList(std.ArrayList(Vec2)), + + // The internal outline, used for carving the shape (for example in a, we would first get the outline of the a, but if we stopped there, it woul + // be filled, so we need another outline for carving the filled polygon) + inside_verts: std.ArrayList(Vec2), + + // For the concave and convex beziers + concave_vertices: std.ArrayList(Vec2), + convex_vertices: std.ArrayList(Vec2), +}; + +// If there are elements in inside_verts, unite them with the outline_verts, effectively carving the shape +fn uniteOutsideAndInsideVertices(ctx: *OutlineContext) void { + if (ctx.inside_verts.items.len != 0) { + // Check which point of outline is closer to the first of inside + var last_outline = &ctx.outline_verts.items[ctx.outline_verts.items.len - 1]; + const closest_to_inside: usize = blk: { + const first_point_inside = ctx.inside_verts.items[0]; + var min: f32 = std.math.f32_max; + var closest_index: usize = undefined; + + for (last_outline.items) |item, i| { + const dist = @reduce(.Add, (item - first_point_inside) * (item - first_point_inside)); + if (dist < min) { + min = dist; + closest_index = i; + } + } + break :blk closest_index; + }; + + ctx.inside_verts.append(last_outline.items[closest_to_inside]) catch unreachable; + last_outline.insertSlice(closest_to_inside + 1, ctx.inside_verts.items) catch unreachable; + ctx.inside_verts.clearRetainingCapacity(); + } +} +// TODO: Return also allocation error +fn moveToFunction(ctx: *OutlineContext, _to: ft.Vector) ft.Error!void { + // std.log.info("M {} {}", .{ to.x, to.y }); + uniteOutsideAndInsideVertices(ctx); + + const to = Vec2{ @intToFloat(f32, _to.x), @intToFloat(f32, _to.y) }; + + // TODO: Use raycasting of the edges for better accuracy on wether a point is inside the outline or not + const new_point_is_inside = blk: { + if (ctx.outline_verts.items.len == 0) { + break :blk false; + } + + var minx: f32 = std.math.f32_max; + var maxx: f32 = std.math.f32_min; + var miny: f32 = std.math.f32_max; + var maxy: f32 = std.math.f32_min; + for (ctx.outline_verts.items[ctx.outline_verts.items.len - 1].items) |item| { + minx = @minimum(item[0], minx); + maxx = @maximum(item[0], maxx); + miny = @minimum(item[1], miny); + maxy = @maximum(item[1], maxy); + } + + break :blk (to[0] >= minx) and (to[0] <= maxx) and (to[1] >= miny) and (to[1] <= maxy); + }; + + // If the point is inside, put it in the inside verts + if (new_point_is_inside) { + ctx.inside_verts.append(to) catch unreachable; + } else { + // Otherwise create a new polygon + var new_outline_list = std.ArrayList(Vec2).init(ctx.outline_verts.allocator); + new_outline_list.append(to) catch unreachable; + ctx.outline_verts.append(new_outline_list) catch unreachable; + } +} + +fn lineToFunction(ctx: *OutlineContext, to: ft.Vector) ft.Error!void { + // std.log.info("L {} {}", .{ to.x, to.y }); + + // If inside_verts is not empty, we need to fill it + if (ctx.inside_verts.items.len != 0) { + ctx.inside_verts.append(.{ @intToFloat(f32, to.x), @intToFloat(f32, to.y) }) catch unreachable; + } else { + // Otherwise append the new point to the last polygon + ctx.outline_verts.items[ctx.outline_verts.items.len - 1].append(.{ @intToFloat(f32, to.x), @intToFloat(f32, to.y) }) catch unreachable; + } +} + +fn conicToFunction(ctx: *OutlineContext, _control: ft.Vector, _to: ft.Vector) ft.Error!void { + // std.log.info("C {} {} {} {}", .{ control.x, control.y, to.x, to.y }); + const control = Vec2{ @intToFloat(f32, _control.x), @intToFloat(f32, _control.y) }; + const to = Vec2{ @intToFloat(f32, _to.x), @intToFloat(f32, _to.y) }; + + // Either the inside verts or the outine ones + var verts_to_write = if (ctx.inside_verts.items.len != 0) &ctx.inside_verts else &ctx.outline_verts.items[ctx.outline_verts.items.len - 1]; + const previous_point = verts_to_write.items[verts_to_write.items.len - 1]; + + const vertices = [_]Vec2{ control, to, previous_point }; + + const vec1 = control - previous_point; + const vec2 = to - control; + + // if ccw, it's concave, else it's convex + if ((vec1[0] * vec2[1] - vec1[1] * vec2[0]) > 0) { + ctx.concave_vertices.appendSlice(&vertices) catch unreachable; + verts_to_write.append(control) catch unreachable; + } else { + ctx.convex_vertices.appendSlice(&vertices) catch unreachable; + } + verts_to_write.append(to) catch unreachable; +} + +// Doesn't seem to be used much +fn cubicToFunction(ctx: *OutlineContext, control_0: ft.Vector, control_1: ft.Vector, to: ft.Vector) ft.Error!void { + _ = ctx; + _ = control_0; + _ = control_1; + _ = to; + @panic("TODO: search how to approximate cubic bezier with quadratic ones"); +} + +pub fn print(label: *ResizableLabel, app: *App, comptime fmt: []const u8, args: anytype, position: Vec4, text_color: Vec4, text_size: u32) !void { + const w = writer(label, app, position, text_color, text_size); + try w.print(fmt, args); +} diff --git a/examples/gkurve/tessellator.zig b/examples/gkurve/tessellator.zig new file mode 100644 index 00000000..3293e3dc --- /dev/null +++ b/examples/gkurve/tessellator.zig @@ -0,0 +1,1857 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const t = std.testing; +const Vec2 = @Vector(2, f32); +const zm = @import("zmath"); +const RbTree = @import("data_structures/rb_tree.zig").RbTree; + +inline fn cross(v1: Vec2, v2: Vec2) f32 { + return v1[0] * v2[1] - v1[1] * v2[0]; +} + +pub fn dassert(pred: bool) void { + if (builtin.mode == .Debug) { + std.debug.assert(pred); + } +} +const CompactSinglyLinkedListBuffer = @import("data_structures/compact.zig").CompactSinglyLinkedListBuffer; + +const trace = @import("tracy.zig").trace; + +const log_ = std.log.scoped(.tessellator); + +const DeferredVertexNodeId = u16; +const NullId = @import("data_structures/compact.zig").CompactNull(DeferredVertexNodeId); + +const EventQueue = std.PriorityQueue(u32, *std.ArrayList(Event), compareEventIdx); + +const debug = false and builtin.mode == .Debug; + +pub fn log(comptime format: []const u8, args: anytype) void { + if (debug) { + log_.debug(format, args); + } +} + +pub const Tessellator = struct { + /// Buffers. + verts: std.ArrayList(InternalVertex), + events: std.ArrayList(Event), + event_q: EventQueue, + sweep_edges: RbTree(u16, SweepEdge, Event, compareSweepEdge), + deferred_verts: CompactSinglyLinkedListBuffer(DeferredVertexNodeId, DeferredVertexNode), + + /// Verts to output. + /// No duplicate verts will be outputed to reduce size footprint. + /// Since some verts won't be discovered until during the processing of events (edge intersections), + /// verts are added as the events are processed. As a result, the verts are also in order y-asc and x-asc. + out_verts: std.ArrayList(Vec2), + + /// Triangles to output are triplets of indexes that point to verts. They are in ccw direction. + out_idxes: std.ArrayList(u16), + + cur_x: f32, + cur_y: f32, + cur_out_vert_idx: u16, + cur_polys: []const []const Vec2, + + const Self = @This(); + + pub fn init(self: *Self, alloc: std.mem.Allocator) void { + self.* = .{ + .verts = std.ArrayList(InternalVertex).init(alloc), + .events = std.ArrayList(Event).init(alloc), + .event_q = undefined, + .sweep_edges = RbTree(u16, SweepEdge, Event, compareSweepEdge).init(alloc, undefined), + .deferred_verts = CompactSinglyLinkedListBuffer(DeferredVertexNodeId, DeferredVertexNode).init(alloc), + .out_verts = std.ArrayList(Vec2).init(alloc), + .out_idxes = std.ArrayList(u16).init(alloc), + .cur_x = undefined, + .cur_y = undefined, + .cur_out_vert_idx = undefined, + .cur_polys = undefined, + }; + self.event_q = EventQueue.init(alloc, &self.events); + } + + pub fn deinit(self: *Self) void { + self.verts.deinit(); + self.events.deinit(); + self.event_q.deinit(); + self.sweep_edges.deinit(); + self.deferred_verts.deinit(); + self.out_verts.deinit(); + self.out_idxes.deinit(); + } + + pub fn clearBuffers(self: *Self) void { + self.verts.clearRetainingCapacity(); + self.events.clearRetainingCapacity(); + self.event_q.len = 0; + self.sweep_edges.clearRetainingCapacity(); + self.deferred_verts.clearRetainingCapacity(); + self.out_verts.clearRetainingCapacity(); + self.out_idxes.clearRetainingCapacity(); + } + + pub fn triangulatePolygon(self: *Self, polygon: []const Vec2) void { + self.triangulatePolygons(&.{polygon}); + } + + /// Perform a plane sweep to triangulate a complex polygon in one pass. + /// The output returns ccw triangle vertices and indexes ready to be fed into the gpu. + /// This uses Bentley-Ottmann to handle self intersecting edges. + /// Rules are followed to partition into y-monotone polygons and triangulate them. + /// This is ported from the JS implementation (tessellator.js) where it is easier to prototype. + /// Since the number of verts and indexes is not known beforehand, the output is an ArrayList. + /// TODO: See if inline callbacks would be faster to directly push data to the batcher buffer. + pub fn triangulatePolygons(self: *Self, polygons: []const []const Vec2) void { + // Construct the initial events by traversing the polygon. + self.initEvents(polygons); + + self.cur_x = std.math.f32_min; + self.cur_y = std.math.f32_min; + self.cur_out_vert_idx = std.math.maxInt(u16); + self.cur_polys = polygons; + + // Process events. + while (self.event_q.removeOrNull()) |e_id| { + self.processEvent(e_id); + } + } + + /// Initializes the events only. + pub fn debugTriangulatePolygons(self: *Self, polygons: []const []const Vec2) void { + self.initEvents(polygons); + self.cur_x = std.math.f32_min; + self.cur_y = std.math.f32_min; + self.cur_out_vert_idx = std.math.maxInt(u16); + self.cur_polys = polygons; + } + + /// Process the next event. This can be used with debugTriangulatePolygons. + pub fn debugProcessNext(self: *Self, alloc: std.mem.Allocator) ?DebugTriangulateStepResult { + const e_id = self.event_q.removeOrNull() orelse return null; + self.processEvent(e_id); + + return DebugTriangulateStepResult{ + .has_result = true, + .event = self.events.items[e_id], + .sweep_edges = self.sweep_edges.allocValuesInOrder(alloc), + .out_verts = alloc.dupe(Vec2, self.out_verts.items) catch unreachable, + .out_idxes = alloc.dupe(u16, self.out_idxes.items) catch unreachable, + .verts = alloc.dupe(InternalVertex, self.verts.items) catch unreachable, + .deferred_verts = alloc.dupe(CompactSinglyLinkedListBuffer(DeferredVertexNodeId, DeferredVertexNode).Node, self.deferred_verts.nodes.data.items) catch unreachable, + }; + } + + fn processEvent(self: *Self, e_id: u32) void { + const t_ = trace(@src()); + defer t_.end(); + const sweep_edges = &self.sweep_edges; + + const e = self.events.items[e_id]; + + // If the point changed, allocate a new out vertex index. + if (e.vert_x != self.cur_x or e.vert_y != self.cur_y) { + self.out_verts.append(Vec2{ e.vert_x, e.vert_y }) catch unreachable; + self.cur_out_vert_idx +%= 1; + self.cur_x = e.vert_x; + self.cur_y = e.vert_y; + } + + // Set the out vertex index on the event. + self.verts.items[e.vert_idx].out_idx = self.cur_out_vert_idx; + + if (debug) { + const tag_str: []const u8 = if (e.tag == .Start) "Start" else "End"; + log("--process event {}, ({},{}) {s} {s}", .{ e.vert_idx, e.vert_x, e.vert_y, tag_str, edgeToString(e.edge) }); + log("sweep edges: ", .{}); + var mb_cur = sweep_edges.first(); + while (mb_cur) |cur| { + const se = sweep_edges.get(cur).?; + log("{} -> {}", .{ se.edge.start_idx, se.edge.end_idx }); + mb_cur = sweep_edges.getNext(cur); + } + } + + if (e.tag == .Start) { + // Check to remove a previous left edge and continue it's sub-polygon vertex queue. + sweep_edges.ctx = e; + const new_id = sweep_edges.insert(SweepEdge.init(e, self.verts.items)) catch unreachable; + const new = sweep_edges.getPtr(new_id).?; + + log("start new sweep edge {}", .{new_id}); + + var mb_left_id = sweep_edges.getPrev(new_id); + // Update winding based on what is to the left. + if (mb_left_id == null) { + new.interior_is_left = false; + } else { + new.interior_is_left = !sweep_edges.get(mb_left_id.?).?.interior_is_left; + } + + if (new.interior_is_left) { + log("initially interior to the left", .{}); + // Initially appears to be a right edge but if it connects to the previous left, it becomes a left edge. + var left = sweep_edges.getPtrNoCheck(mb_left_id.?); + const e_vert_out_idx = self.verts.items[e.vert_idx].out_idx; + if (left.end_event_vert_uniq_idx == e_vert_out_idx) { + // Remove the previous ended edge, and takes it's place as the left edge. + defer sweep_edges.remove(mb_left_id.?) catch unreachable; + new.interior_is_left = false; + log("continuation from prev edge, interior to the right", .{}); + + // Previous left edge and this new edge forms a regular left angle. + // Check for bad up cusp. + if (left.bad_up_cusp_uniq_idx != NullId) { + // This monotone polygon (a) should already have run it's triangulate steps from the left edge's end event. + // \ / + // a \/ b + // ^ Bad cusp. + // \_ + // ^ End event from left edge happened before, currently processing start event for the new connected edge. + + // A line is connected from this vertex to the bad cusp to ensure that polygon (a) is monotone and polygon (b) is monotone. + // Since polygon (a) already ran it's triangulate step, it's done from this side of the polygon. + // This start event will create a new sweep edge, so transfer the deferred queue from the bad cusp's right side. (it is now the queue for this new left edge). + const bad_right = sweep_edges.getNoCheck(left.bad_up_cusp_right_sweep_edge_id); + bad_right.dumpQueue(self); + new.deferred_queue = bad_right.deferred_queue; + new.deferred_queue_size = bad_right.deferred_queue_size; + new.cur_side = bad_right.cur_side; + + // Also run triangulate on polygon (b) for the new vertex since the end event was already run for polygon (a). + self.triangulateLeftStep(new, self.verts.items[e.vert_idx]); + + left.bad_up_cusp_uniq_idx = NullId; + sweep_edges.removeDetached(left.bad_up_cusp_right_sweep_edge_id); + + log("FIX BAD UP CUSP", .{}); + new.dumpQueue(self); + } else { + new.deferred_queue = left.deferred_queue; + new.deferred_queue_size = left.deferred_queue_size; + new.cur_side = left.cur_side; + } + } else if (left.start_event_vert_uniq_idx == e_vert_out_idx) { + // Down cusp. + log("DOWN CUSP", .{}); + } + } else { + log("initially interior to the right", .{}); + // Initially appears to be a left edge but if it connects to a previous right, it becomes a right edge. + if (mb_left_id != null) { + const vert = self.verts.items[e.vert_idx]; + + // left edge has interior to the left. + var left = sweep_edges.get(mb_left_id.?).?; + if (left.end_event_vert_uniq_idx == vert.out_idx) { + // Remove previous ended edge. + sweep_edges.remove(mb_left_id.?) catch unreachable; + new.interior_is_left = true; + log("changed to interior is left", .{}); + } else if (left.start_event_vert_uniq_idx == vert.out_idx) { + // Linked to previous start event's vertex. + // Handle bad down cusp. + // \ \/ + // \ b + // \--a + // \ v Bad down cusp. + // \ /\ / + // \ / \ / + + // The bad cusp is linked to the lowest visible vertex seen from the left edge. If vertex (a) exists that would be connected to the cusp. + // If it didn't exist the next lowest would be (b) a bad up cusp. + + const left_left_id = sweep_edges.getPrev(mb_left_id.?).?; + const left_left = sweep_edges.getPtrNoCheck(left_left_id); + log("handle bad down cusp {}", .{left_left.lowest_right_vert_idx}); + if (left_left.lowest_right_vert_idx == NullId) { + log("expected lowest right vert", .{}); + unreachable; + } + + const low_poly_edge = sweep_edges.getPtrNoCheck(left_left.lowest_right_vert_sweep_edge_id); + if (left_left_id == left_left.lowest_right_vert_sweep_edge_id) { + // Lowest right point does NOT have a right side monotone polygon. + + if (left_left.lowest_right_vert_idx == left_left.vert_idx) { + // Pass on the vertex queue. + new.deferred_queue = low_poly_edge.deferred_queue; + new.deferred_queue_size = low_poly_edge.deferred_queue_size; + new.cur_side = low_poly_edge.cur_side; + low_poly_edge.deferred_queue = NullId; + low_poly_edge.deferred_queue_size = 0; + + self.triangulateLeftStep(new, vert); + + // Cut off the existing left monotone polygon. + left_left.deferred_queue = NullId; + left_left.deferred_queue_size = 0; + left_left.cur_side = .Right; + left_left.enqueueDeferred(self.verts.items[left_left.lowest_right_vert_idx], self); + if (vert.out_idx != self.verts.items[left_left.lowest_right_vert_idx].out_idx) { + left_left.enqueueDeferred(vert, self); + } + left_left.lowest_right_vert_idx = e.vert_idx; + } else { + // Initialize a new right side monotone polygon. + new.enqueueDeferred(self.verts.items[left_left.lowest_right_vert_idx], self); + new.enqueueDeferred(vert, self); + new.cur_side = .Left; + + // Triangulate on the monotone polygon to the left of the lowest right point. + self.triangulateRightStep(left_left, vert); + + left_left.lowest_right_vert_idx = e.vert_idx; + } + } else { + // Lowest right point does have a right side monotone polygon. + + // Most likely a bad up cusp as well, so reset it since connecting to the lowest right fixes it. + left_left.bad_up_cusp_uniq_idx = NullId; + + self.triangulateLeftStep(low_poly_edge, vert); + + // Pass on the vertex queue. + new.deferred_queue = low_poly_edge.deferred_queue; + new.deferred_queue_size = low_poly_edge.deferred_queue_size; + new.cur_side = low_poly_edge.cur_side; + low_poly_edge.deferred_queue = NullId; + low_poly_edge.deferred_queue_size = 0; + + // Triangulate on the monotone polygon to the left of the lowest right point. + self.triangulateRightStep(left_left, vert); + + left_left.lowest_right_vert_idx = e.vert_idx; + left_left.lowest_right_vert_sweep_edge_id = left_left_id; + } + } + } + } + + if (!new.interior_is_left) { + // Even-odd rule. + // Interior is to the right. + + const vert = self.verts.items[e.vert_idx]; + + // Initialize the deferred queue. + if (new.deferred_queue == NullId) { + new.enqueueDeferred(vert, self); + new.cur_side = .Left; + + log("initialize queue", .{}); + new.dumpQueue(self); + } + + // The lowest right vert is set initializes to itself. + new.lowest_right_vert_idx = e.vert_idx; + new.lowest_right_vert_sweep_edge_id = new_id; + } else { + // Interior is to the left. + } + + // Check intersection with the left. Fetch left again since it could be removed from an up cusp. + mb_left_id = sweep_edges.getPrev(new_id); + if (mb_left_id != null) { + log("check left intersect", .{}); + var left = sweep_edges.getPtrNoCheck(mb_left_id.?); + const res = computeTwoEdgeIntersect(new.edge, left.edge); + log("{},{}->{},{} {},{}->{},{} {} {}", .{ new.edge.start_pos[0], new.edge.start_pos[1], new.edge.end_pos[0], new.edge.end_pos[1], left.edge.start_pos[0], left.edge.start_pos[1], left.edge.end_pos[0], left.edge.end_pos[1], res.has_intersect, res.t }); + if (res.has_intersect and res.t > 0 and res.t < 1) { + self.handleIntersectForStartEvent(new, left, res, Vec2{ e.vert_x, e.vert_y }); + } + } + const mb_right_id = sweep_edges.getNext(new_id); + if (mb_right_id != null) { + // Check for intersection with the edge to the right. + log("check right intersect", .{}); + + // TODO: Is there a preliminary check to avoid doing the math? One idea is to check the x_slopes but it would need to know + // if the compared edge is pointing down or up. + const right = sweep_edges.getPtrNoCheck(mb_right_id.?); + const res = computeTwoEdgeIntersect(new.edge, right.edge); + if (res.has_intersect and res.t > 0 and res.t < 1) { + self.handleIntersectForStartEvent(new, right, res, Vec2{ e.vert_x, e.vert_y }); + } + } + } else { + // End event. + + if (e.invalidated) { + // This end event was invalidated from an intersection event. + return; + } + + const active_id = findSweepEdgeForEndEvent(sweep_edges, e) orelse { + log("polygons: {any}", .{self.cur_polys}); + @panic("expected active edge"); + }; + const active = sweep_edges.getPtrNoCheck(active_id); + log("active {} {}", .{ active.vert_idx, active.to_vert_idx }); + + const vert = self.verts.items[e.vert_idx]; + + if (active.interior_is_left) { + // Interior is to the left. + + log("interior to the left {}", .{active_id}); + + const left_id = sweep_edges.getPrev(active_id).?; + const left = sweep_edges.getPtrNoCheck(left_id); + + // Check if it has closed the polygon. (up cusp) + if (vert.out_idx == left.end_event_vert_uniq_idx) { + // Check for bad up cusp. + if (left.bad_up_cusp_uniq_idx != NullId) { + const bad_right = sweep_edges.getPtrNoCheck(left.bad_up_cusp_right_sweep_edge_id); + // Close off monotone polygon to the right of the bad up cusp. + self.triangulateLeftStep(bad_right, vert); + sweep_edges.removeDetached(left.bad_up_cusp_right_sweep_edge_id); + left.bad_up_cusp_uniq_idx = NullId; + left.lowest_right_vert_idx = e.vert_idx; + left.lowest_right_vert_sweep_edge_id = left_id; + if (bad_right.deferred_queue_size >= 3) { + log("{any}", .{self.out_idxes.items}); + bad_right.dumpQueue(self); + @panic("did not expect left over vertices"); + } + } + if (left.deferred_queue_size >= 3) { + log("{} {any}", .{ left_id, self.out_idxes.items }); + left.dumpQueue(self); + @panic("did not expect left over vertices"); + } + // Remove the left edge and this edge. + sweep_edges.remove(left_id) catch unreachable; + sweep_edges.remove(active_id) catch unreachable; + } else { + // Regular right side vertex. + + // Check for bad up cusp. + if (left.bad_up_cusp_uniq_idx != NullId) { + const bad_right = sweep_edges.getPtrNoCheck(left.bad_up_cusp_right_sweep_edge_id); + // Close off monotone polygon to the right of the bad up cusp. + self.triangulateRightStep(bad_right, vert); + left.lowest_right_vert_idx = vert.idx; + left.lowest_right_vert_sweep_edge_id = left_id; + sweep_edges.removeDetached(left.bad_up_cusp_right_sweep_edge_id); + left.bad_up_cusp_uniq_idx = NullId; + if (bad_right.deferred_queue_size >= 3) { + log("{any}", .{self.out_idxes.items}); + bad_right.dumpQueue(self); + @panic("did not expect left over vertices"); + } + } + // Left belongs to the same monotone polygon. + self.triangulateRightStep(left, vert); + + // Edge is only removed by the next connecting edge. + active.end_event_vert_uniq_idx = vert.out_idx; + } + } else { + // Interior is to the right. + log("interior to the right {}", .{active_id}); + + const mb_left_id = sweep_edges.getPrev(active_id); + + // Check to fix a bad up cusp to the right. + if (active.bad_up_cusp_uniq_idx != NullId) { + const bad_right = sweep_edges.getPtrNoCheck(active.bad_up_cusp_right_sweep_edge_id); + // Close off monotone polygon in between this active edge and the bad up cusp. + self.triangulateLeftStep(active, vert); + if (active.deferred_queue_size >= 3) { + log("{any}", .{self.out_idxes.items}); + active.dumpQueue(self); + @panic("did not expect left over vertices"); + } + active.lowest_right_vert_idx = NullId; + active.lowest_right_vert_sweep_edge_id = NullId; + sweep_edges.removeDetached(active.bad_up_cusp_right_sweep_edge_id); + active.bad_up_cusp_uniq_idx = NullId; + // Extend the monotone polygon to the right of the bad up cusp to this vertex. + self.triangulateLeftStep(bad_right, vert); + active.deferred_queue = bad_right.deferred_queue; + active.deferred_queue_size = bad_right.deferred_queue_size; + } else { + active.dumpQueue(self); + self.triangulateLeftStep(active, vert); + active.dumpQueue(self); + } + + // Check if this forms a bad up cusp with the right edge to the left monotone polygon. + var removed = false; + if (mb_left_id != null) { + log("check to set bad up cusp", .{}); + const left = sweep_edges.getNoCheck(mb_left_id.?); + // dump(edgeToString(left_right_edge.edge)) + if (vert.out_idx == left.end_event_vert_uniq_idx) { + const left_left_id = sweep_edges.getPrev(mb_left_id.?).?; + const left_left = sweep_edges.getPtrNoCheck(left_left_id); + // Bad up cusp. + left_left.bad_up_cusp_uniq_idx = vert.out_idx; + left_left.bad_up_cusp_right_sweep_edge_id = active_id; + left_left.lowest_right_vert_idx = e.vert_idx; + left_left.lowest_right_vert_sweep_edge_id = active_id; + + // Remove the left edge. + sweep_edges.remove(mb_left_id.?) catch unreachable; + // Detach this edge, remove it when the bad up cusp is fixed. + sweep_edges.detach(active_id) catch unreachable; + removed = true; + // Continue. + } + } + + if (!removed) { + // Don't remove the left edge of this sub-polygon yet. + // Record the end event's vert so the next start event that continues from this vert can persist the deferred vertices and remove this sweep edge. + // It can also be removed by a End right edge. + active.end_event_vert_uniq_idx = vert.out_idx; + } + } + } + } + + inline fn addTriangle(self: *Self, v1_out: u16, v2_out: u16, v3_out: u16) void { + log("triangle {} {} {}", .{ v1_out, v2_out, v3_out }); + self.out_idxes.appendSlice(&.{ v1_out, v2_out, v3_out }) catch unreachable; + } + + /// Parses the polygon pts and adds the initial events into the priority queue. + fn initEvents(self: *Self, polygons: []const []const Vec2) void { + const t_ = trace(@src()); + defer t_.end(); + for (polygons) |polygon| { + // Find the starting point that is not equal to the last vertex point. + // Since we are adding events to a priority queue, we need to make sure each add is final. + var start_idx: u16 = 0; + const last_pt = polygon[polygon.len - 1]; + while (start_idx < polygon.len) : (start_idx += 1) { + const pt = polygon[start_idx]; + + // Add internal vertex even though we are skipping events for it to keep the idxes consistent with the input. + const v = InternalVertex{ + .pos = pt, + .idx = start_idx, + }; + self.verts.append(v) catch unreachable; + + if (@fabs(last_pt[0] - pt[0]) > 1e-4 or @fabs(last_pt[1] - pt[1]) > 1e-4) { + break; + } else { + self.verts.items[self.verts.items.len - 1].idx = NullId; + } + } + + var last_v = self.verts.items[start_idx]; + var last_v_idx = start_idx; + + var i: u16 = start_idx + 1; + while (i < polygon.len) : (i += 1) { + const v_idx = @intCast(u16, self.verts.items.len); + const v = InternalVertex{ + .pos = polygon[i], + .idx = i, + }; + self.verts.append(v) catch unreachable; + + if (@fabs(last_v.pos[0] - v.pos[0]) < 1e-4 and @fabs(last_v.pos[1] - v.pos[1]) < 1e-4) { + // Don't connect two vertices that are on top of each other. + // Allowing this would require edge cases during event processing to make sure things don't break. + // Push the vertex in anyway so there is consistency with the input. + self.verts.items[self.verts.items.len - 1].idx = NullId; + continue; + } + + const prev_edge = Edge.init(last_v_idx, last_v, v_idx, v); + const event1_idx = @intCast(u32, self.events.items.len); + var event1: Event = undefined; + var event2: Event = undefined; + if (Event.isStartEvent(last_v_idx, prev_edge, self.verts.items)) { + event1 = Event.init(last_v, prev_edge, .Start); + event2 = Event.init(v, prev_edge, .End); + event1.end_event_idx = event1_idx + 1; + } else { + event1 = Event.init(last_v, prev_edge, .End); + event2 = Event.init(v, prev_edge, .Start); + event2.end_event_idx = event1_idx; + } + self.events.append(event1) catch unreachable; + self.event_q.add(event1_idx) catch unreachable; + self.events.append(event2) catch unreachable; + self.event_q.add(event1_idx + 1) catch unreachable; + last_v = v; + last_v_idx = v_idx; + } + // Link last pt to start pt. + const edge = Edge.init(last_v_idx, last_v, start_idx, self.verts.items[start_idx]); + const event1_idx = @intCast(u32, self.events.items.len); + var event1: Event = undefined; + var event2: Event = undefined; + if (Event.isStartEvent(last_v_idx, edge, self.verts.items)) { + event1 = Event.init(last_v, edge, .Start); + event2 = Event.init(self.verts.items[start_idx], edge, .End); + event1.end_event_idx = event1_idx + 1; + } else { + event1 = Event.init(last_v, edge, .End); + event2 = Event.init(self.verts.items[start_idx], edge, .Start); + event2.end_event_idx = event1_idx; + } + self.events.append(event1) catch unreachable; + self.event_q.add(event1_idx) catch unreachable; + self.events.append(event2) catch unreachable; + self.event_q.add(event1_idx + 1) catch unreachable; + } + } + + fn triangulateLeftStep(self: *Self, left: *SweepEdge, vert: InternalVertex) void { + if (left.cur_side == .Left) { + log("same left side", .{}); + left.dumpQueue(self); + + // Same side. + if (left.deferred_queue_size >= 2) { + var last_id = left.deferred_queue; + var last = self.deferred_verts.getNoCheck(last_id); + if (last.vert_out_idx == vert.out_idx) { + // Ignore this point since it is the same as the last. + return; + } + var cur_id = self.deferred_verts.getNextNoCheck(last_id); + var i: u16 = 0; + while (i < left.deferred_queue_size - 1) : (i += 1) { + log("check to add inward tri {} {}", .{ last_id, cur_id }); + const cur = self.deferred_verts.getNoCheck(cur_id); + const cxp = cross(Vec2{ last.vert_x - cur.vert_x, last.vert_y - cur.vert_y }, Vec2{ vert.pos[0] - last.vert_x, vert.pos[1] - last.vert_y }); + if (cxp < 0) { + // Bends inwards. Fill triangles until we aren't bending inward. + self.addTriangle(vert.out_idx, cur.vert_out_idx, last.vert_out_idx); + self.deferred_verts.removeAssumeNoPrev(last_id) catch unreachable; + } else { + break; + } + last_id = cur_id; + last = cur; + cur_id = self.deferred_verts.getNextNoCheck(cur_id); + } + if (i > 0) { + const d_vert = self.deferred_verts.insertBeforeHeadNoCheck(last_id, DeferredVertexNode.init(vert)) catch unreachable; + left.deferred_queue = d_vert; + left.deferred_queue_size = left.deferred_queue_size - i + 1; + } else { + left.enqueueDeferred(vert, self); + } + } else { + left.enqueueDeferred(vert, self); + } + } else { + log("changed to left side", .{}); + // Changed to left side. + // Automatically create queue size - 1 triangles. + var last_id = left.deferred_queue; + var last = self.deferred_verts.getNoCheck(last_id); + var cur_id = self.deferred_verts.getNextNoCheck(last_id); + var i: u32 = 0; + while (i < left.deferred_queue_size - 1) : (i += 1) { + const cur = self.deferred_verts.getNoCheck(cur_id); + self.addTriangle(vert.out_idx, last.vert_out_idx, cur.vert_out_idx); + last_id = cur_id; + last = cur; + cur_id = self.deferred_verts.getNextNoCheck(cur_id); + // Delete last after it's assigned to current. + self.deferred_verts.removeAssumeNoPrev(last_id) catch unreachable; + } + left.dumpQueue(self); + self.deferred_verts.getNodePtrNoCheck(left.deferred_queue).next = NullId; + left.deferred_queue_size = 1; + left.enqueueDeferred(vert, self); + left.cur_side = .Left; + left.dumpQueue(self); + } + } + + fn triangulateRightStep(self: *Self, left: *SweepEdge, vert: InternalVertex) void { + if (left.cur_side == .Right) { + log("right side", .{}); + // Same side. + if (left.deferred_queue_size >= 2) { + var last_id = left.deferred_queue; + var last = self.deferred_verts.getNoCheck(last_id); + if (last.vert_out_idx == vert.out_idx) { + // Ignore this point since it is the same as the last. + return; + } + var cur_id = self.deferred_verts.getNextNoCheck(last_id); + var i: u16 = 0; + while (i < left.deferred_queue_size - 1) : (i += 1) { + const cur = self.deferred_verts.getNoCheck(cur_id); + const cxp = cross(Vec2{ last.vert_x - cur.vert_x, last.vert_y - cur.vert_y }, Vec2{ vert.pos[0] - last.vert_x, vert.pos[1] - last.vert_y }); + if (cxp > 0) { + // Bends inwards. Fill triangles until we aren't bending inward. + self.addTriangle(vert.out_idx, last.vert_out_idx, cur.vert_out_idx); + self.deferred_verts.removeAssumeNoPrev(last_id) catch unreachable; + } else { + break; + } + last_id = cur_id; + last = cur; + cur_id = self.deferred_verts.getNextNoCheck(cur_id); + } + if (i > 0) { + const d_vert = self.deferred_verts.insertBeforeHeadNoCheck(last_id, DeferredVertexNode.init(vert)) catch unreachable; + left.deferred_queue = d_vert; + left.deferred_queue_size = left.deferred_queue_size - i + 1; + } else { + left.enqueueDeferred(vert, self); + } + } else { + left.enqueueDeferred(vert, self); + } + } else { + log("changed to right side", .{}); + var last_id = left.deferred_queue; + var last = self.deferred_verts.getNoCheck(last_id); + var cur_id = self.deferred_verts.getNextNoCheck(last_id); + var i: u32 = 0; + while (i < left.deferred_queue_size - 1) : (i += 1) { + const cur = self.deferred_verts.getNoCheck(cur_id); + self.addTriangle(vert.out_idx, cur.vert_out_idx, last.vert_out_idx); + last_id = cur_id; + last = cur; + cur_id = self.deferred_verts.getNextNoCheck(cur_id); + // Delete last after it's assigned to current. + self.deferred_verts.removeAssumeNoPrev(last_id) catch unreachable; + } + self.deferred_verts.getNodePtrNoCheck(left.deferred_queue).next = NullId; + left.deferred_queue_size = 1; + left.enqueueDeferred(vert, self); + left.cur_side = .Right; + left.dumpQueue(self); + } + } + + /// Splits two edges at an intersect point. + /// Assumes sweep edges have not processed their end events so they can be reinserted. + /// Does not add new events if an event already exists to the intersect point. + fn handleIntersectForStartEvent(self: *Self, sweep_edge_a: *SweepEdge, sweep_edge_b: *SweepEdge, intersect: IntersectResult, sweep_vert: Vec2) void { + log("split intersect {}", .{intersect}); + + // The intersect point must lie after the sweep_vert. + if (intersect.y < sweep_vert[1] or (intersect.y == sweep_vert[1] and intersect.x <= sweep_vert[0])) { + return; + } + + var added_events = false; + + // Create new intersect vertex. + const intersect_idx = @intCast(u16, self.verts.items.len); + const intersect_v = InternalVertex{ + .pos = Vec2{ intersect.x, intersect.y }, + .idx = intersect_idx, + }; + self.verts.append(intersect_v) catch unreachable; + + // TODO: Account for floating point error. + const a_to_vert = self.verts.items[sweep_edge_a.to_vert_idx]; + if (a_to_vert.pos[0] != intersect.x or a_to_vert.pos[1] != intersect.y) { + // log(edgeToString(sweep_edge_a.edge)) + log("adding edge a {} to {},{}", .{ sweep_edge_a.vert_idx, intersect.x, intersect.y }); + added_events = true; + + // Invalidate sweep_edge_a's end event since the priority queue can not be modified. + self.events.items[sweep_edge_a.end_event_idx].invalidated = true; + + // Keep original edge orientation when doing the split. + var first_edge: Edge = undefined; + var second_edge: Edge = undefined; + const start = self.verts.items[sweep_edge_a.edge.start_idx]; + const end = self.verts.items[sweep_edge_a.edge.end_idx]; + if (sweep_edge_a.to_vert_idx == sweep_edge_a.edge.start_idx) { + first_edge = Edge.init(intersect_idx, intersect_v, sweep_edge_a.edge.end_idx, end); + second_edge = Edge.init(sweep_edge_a.edge.start_idx, start, intersect_idx, intersect_v); + } else { + first_edge = Edge.init(sweep_edge_a.edge.start_idx, start, intersect_idx, intersect_v); + second_edge = Edge.init(intersect_idx, intersect_v, sweep_edge_a.edge.end_idx, end); + } + + // Update sweep_edge_a to end at the intersect. + sweep_edge_a.edge = first_edge; + const a_orig_to_vert = sweep_edge_a.to_vert_idx; + sweep_edge_a.to_vert_idx = intersect_idx; + + // Insert new sweep_edge_a end event. + const evt_idx = @intCast(u32, self.events.items.len); + var new_evt = Event.init(intersect_v, first_edge, .End); + self.events.append(new_evt) catch unreachable; + self.event_q.add(evt_idx) catch unreachable; + + // Insert start/end event from the intersect to the end of the original sweep_edge_a. + var event1: Event = undefined; + var event2: Event = undefined; + if (Event.isStartEvent(intersect_idx, second_edge, self.verts.items)) { + event1 = Event.init(intersect_v, second_edge, .Start); + event2 = Event.init(self.verts.items[a_orig_to_vert], second_edge, .End); + event1.end_event_idx = evt_idx + 2; + } else { + event1 = Event.init(intersect_v, second_edge, .End); + event2 = Event.init(self.verts.items[a_orig_to_vert], second_edge, .Start); + event2.end_event_idx = evt_idx + 1; + } + self.events.append(event1) catch unreachable; + self.event_q.add(evt_idx + 1) catch unreachable; + self.events.append(event2) catch unreachable; + self.event_q.add(evt_idx + 2) catch unreachable; + } + + const b_to_vert = self.verts.items[sweep_edge_b.to_vert_idx]; + if (b_to_vert.pos[0] != intersect.x or b_to_vert.pos[1] != intersect.y) { + log("adding edge b {} to {},{}", .{ sweep_edge_b.vert_idx, intersect.x, intersect.y }); + added_events = true; + + // Invalidate sweep_edge_b's end event since the priority queue can not be modified. + log("invalidate: {} {}", .{ sweep_edge_b.end_event_idx, self.events.items.len }); + self.events.items[sweep_edge_b.end_event_idx].invalidated = true; + + // Keep original edge orientation when doing the split. + var first_edge: Edge = undefined; + var second_edge: Edge = undefined; + const start = self.verts.items[sweep_edge_b.edge.start_idx]; + const end = self.verts.items[sweep_edge_b.edge.end_idx]; + if (sweep_edge_b.to_vert_idx == sweep_edge_b.edge.start_idx) { + first_edge = Edge.init(intersect_idx, intersect_v, sweep_edge_b.edge.end_idx, end); + second_edge = Edge.init(sweep_edge_b.edge.start_idx, start, intersect_idx, intersect_v); + } else { + first_edge = Edge.init(sweep_edge_b.edge.start_idx, start, intersect_idx, intersect_v); + second_edge = Edge.init(intersect_idx, intersect_v, sweep_edge_b.edge.end_idx, end); + } + + // Update sweep_edge_b to end at the intersect. + sweep_edge_b.edge = first_edge; + const b_orig_to_vert = sweep_edge_b.to_vert_idx; + sweep_edge_b.to_vert_idx = intersect_idx; + + // Insert new sweep_edge_b end event. + const evt_idx = @intCast(u32, self.events.items.len); + var new_evt = Event.init(intersect_v, first_edge, .End); + self.events.append(new_evt) catch unreachable; + self.event_q.add(evt_idx) catch unreachable; + + // Insert start/end event from the intersect to the end of the original sweep_edge_b. + var event1: Event = undefined; + var event2: Event = undefined; + if (Event.isStartEvent(intersect_idx, second_edge, self.verts.items)) { + event1 = Event.init(intersect_v, second_edge, .Start); + event2 = Event.init(self.verts.items[b_orig_to_vert], second_edge, .End); + event1.end_event_idx = evt_idx + 2; + } else { + event1 = Event.init(intersect_v, second_edge, .End); + event2 = Event.init(self.verts.items[b_orig_to_vert], second_edge, .Start); + event2.end_event_idx = evt_idx + 1; + } + self.events.append(event1) catch unreachable; + self.event_q.add(evt_idx + 1) catch unreachable; + self.events.append(event2) catch unreachable; + self.event_q.add(evt_idx + 2) catch unreachable; + } + + if (!added_events) { + // No events were added, revert adding intersect point. + _ = self.verts.pop(); + } + } +}; + +fn edgeToString(edge: Edge) []const u8 { + const S = struct { + var buf: [100]u8 = undefined; + }; + return std.fmt.bufPrint(&S.buf, "{} ({},{}) -> {} ({},{})", .{ edge.start_idx, edge.start_pos.x, edge.start_pos.y, edge.end_idx, edge.end_pos.x, edge.end_pos.y }) catch unreachable; +} + +fn compareEventIdx(events: *std.ArrayList(Event), a: u32, b: u32) std.math.Order { + return compareEvent(events.items[a], events.items[b]); +} + +/// Sort verts by y asc then x asc. Resolve same position by looking at the edge's xslope and whether it is active. +fn compareEvent(a: Event, b: Event) std.math.Order { + if (a.vert_y < b.vert_y) { + return .lt; + } else if (a.vert_y > b.vert_y) { + return .gt; + } else { + if (a.vert_x < b.vert_x) { + return .lt; + } else if (a.vert_x > b.vert_x) { + return .gt; + } else { + if (a.tag == .End and b.tag == .Start) { + return .lt; + } else if (a.tag == .Start and b.tag == .End) { + return .gt; + } else if (a.tag == .End and b.tag == .End) { + if (a.edge.x_slope > b.edge.x_slope) { + return .lt; + } else if (a.edge.x_slope < b.edge.x_slope) { + return .gt; + } else { + return .eq; + } + } else { + if (a.edge.x_slope < b.edge.x_slope) { + return .lt; + } else if (a.edge.x_slope > b.edge.x_slope) { + return .gt; + } else { + return .eq; + } + } + } + } +} + +/// Compare SweepEdges for insertion. Each sweep edge should be unique since the rb tree doesn't support duplicate values. +/// The slopes from the current sweep edges are used to find their x-intersect along the event's y line. +/// If compared sweep edge is a horizontal line, return gt so it's inserted after it. The horizontal edge can be assumed to intersect with the target event or it wouldn't be in the sweep edges. +fn compareSweepEdge(_: SweepEdge, b: SweepEdge, evt: Event) std.math.Order { + if (!b.edge.is_horiz) { + const x_intersect = b.edge.x_slope * (evt.vert_y - b.edge.start_pos[1]) + b.edge.start_pos[0]; + if (@fabs(evt.vert_x - x_intersect) < SweepEdgeApproxEpsilon) { + // Since there is a chance of having floating point error, check with an epsilon. + // Always return .gt so the left sweep edge can be reliably checked for a joining edge. + return .gt; + } else { + if (evt.vert_x < x_intersect) { + return .lt; + } else if (evt.vert_x > x_intersect) { + return .gt; + } else { + unreachable; + } + } + } else { + return .gt; + } +} + +fn findSweepEdgeForEndEvent(sweep_edges: *RbTree(u16, SweepEdge, Event, compareSweepEdge), e: Event) ?u16 { + const target = Vec2{ e.vert_x, e.vert_y }; + const dummy = SweepEdge{ + .edge = undefined, + .start_event_vert_uniq_idx = undefined, + .vert_idx = undefined, + .to_vert_idx = undefined, + .end_event_vert_uniq_idx = undefined, + .deferred_queue = undefined, + .deferred_queue_size = undefined, + .cur_side = undefined, + .bad_up_cusp_uniq_idx = undefined, + .bad_up_cusp_right_sweep_edge_id = undefined, + .lowest_right_vert_idx = undefined, + .lowest_right_vert_sweep_edge_id = undefined, + .interior_is_left = undefined, + .end_event_idx = undefined, + }; + log("find {},{}", .{ target[0], target[1] }); + var mb_parent: ?u16 = undefined; + var is_left: bool = undefined; + var start_id: u16 = undefined; + if (sweep_edges.lookupCustomLoc(dummy, target, compareSweepEdgeApprox, &mb_parent, &is_left)) |id| { + start_id = id; + } else { + // It's possible that we can't find the sweep edge based on x intersect due to floating point error. + // Start linear search at the parent. + if (mb_parent) |parent| { + start_id = parent; + } else { + return null; + } + } + + // Given a start index where a group of verts could have approx the same x-intersect value, find the one with the exact vert and to_vert. + // The search ends on left/right when the x-intersect suddenly becomes greater than the epsilon. + var se = sweep_edges.getNoCheck(start_id); + if (se.to_vert_idx == e.vert_idx and se.vert_idx == e.to_vert_idx) { + return start_id; + } + log("skip edge {} -> {}", .{ se.vert_idx, se.to_vert_idx }); + // Search left. + var mb_cur = sweep_edges.getPrev(start_id); + while (mb_cur) |cur| { + se = sweep_edges.getNoCheck(cur); + const x_intersect = getXIntersect(se.edge, target); + if (@fabs(e.vert_x - x_intersect) > SweepEdgeApproxEpsilon) { + break; + } else if (se.to_vert_idx == e.vert_idx and se.vert_idx == e.to_vert_idx) { + return cur; + } + mb_cur = sweep_edges.getPrev(cur); + } + // Search right. + mb_cur = sweep_edges.getNext(start_id); + while (mb_cur) |cur| { + se = sweep_edges.getNoCheck(cur); + const x_intersect = getXIntersect(se.edge, target); + if (@fabs(e.vert_x - x_intersect) > SweepEdgeApproxEpsilon) { + break; + } else if (se.to_vert_idx == e.vert_idx and se.vert_idx == e.to_vert_idx) { + return cur; + } + mb_cur = sweep_edges.getNext(cur); + } + return null; +} + +const SweepEdgeApproxEpsilon: f32 = 1e-4; + +/// Finds the first edge with x-intersect that approximates the provided target vert's x. +/// This is needed since floating point error can lead to inconsistent divide and conquer for x-intersects that are close together (eg. two edges stemming from one vertex) +/// A follow up routine to find the exact edge should be run afterwards. +fn compareSweepEdgeApprox(_: SweepEdge, b: SweepEdge, target: Vec2) std.math.Order { + const x_intersect = getXIntersect(b.edge, target); + // Relax the epsilon since larger floating point error can happen here. In the end it's surroundings is verified by linear search. + if (@fabs(target[0] - x_intersect) < SweepEdgeApproxEpsilon) { + return .eq; + } else if (target[0] < x_intersect) { + return .lt; + } else { + return .gt; + } +} + +// Assumes there is an intersect. +fn getXIntersect(edge: Edge, target: Vec2) f32 { + if (!edge.is_horiz) { + return edge.x_slope * (target[1] - edge.start_pos[1]) + edge.start_pos[0]; + } else { + return target[0]; + } +} + +const EventTag = enum(u1) { + Start = 0, + End = 1, +}; + +/// Polygon verts and edges are reduced to events. +/// Each event is centered on a vertex and has an outgoing or incoming edge. +/// If the edge is above the vertex, it's considered a End event. +/// If the edge is below the vertex, it's considered a Start event. +/// Events are sorted by y asc and the x asc. +/// To order events on the same vertex, the event with an active edge has priority. +/// If both events have active edges (both End events), the one that has a greater x-slope comes first. This means an active edge to the left comes first. +/// This helps end processing since the left edge still exists and contains state information about that fill region. +/// If both events have non active edges (both Start events), the one that has a lesser x-slope comes first. +const Event = struct { + const Self = @This(); + + /// The idx of the internal vertex that this event fires on. + vert_idx: u16, + + /// Duped vert pos for compare func. + vert_x: f32, + vert_y: f32, + + tag: EventTag, + + edge: Edge, + + to_vert_idx: u16, + + /// If this is a start event, end_event_idx will point to the corresponding end event. + end_event_idx: u32, + + /// Since an intersection creates new events and can not modify the priority queue, + /// this flag is used to invalid an existing end event. + invalidated: bool, + + fn init(vert: InternalVertex, edge: Edge, tag: EventTag) Self { + var new = Self{ + .vert_idx = vert.idx, + .vert_x = vert.pos[0], + .vert_y = vert.pos[1], + .edge = edge, + .tag = tag, + .to_vert_idx = undefined, + .end_event_idx = std.math.maxInt(u32), + .invalidated = false, + }; + if (edge.start_idx == vert.idx) { + new.to_vert_idx = edge.end_idx; + } else { + new.to_vert_idx = edge.start_idx; + } + return new; + } + + fn isStartEvent(vert_idx: u16, edge: Edge, verts: []const InternalVertex) bool { + const vert = verts[vert_idx]; + // The start and end vertex of an edge is not to be confused with the EventType. + // It is used to determine if the edge is above or below the vertex point. + if (edge.start_idx == vert_idx) { + const end_v = verts[edge.end_idx]; + if (end_v.pos[1] < vert.pos[1] or (end_v.pos[1] == vert.pos[1] and end_v.pos[0] < vert.pos[0])) { + return false; + } else { + return true; + } + } else { + const start_v = verts[edge.start_idx]; + if (start_v.pos[1] < vert.pos[1] or (start_v.pos[1] == vert.pos[1] and start_v.pos[0] < vert.pos[0])) { + return false; + } else { + return true; + } + } + } +}; + +const Side = enum(u1) { + Left = 0, + Right = 1, +}; + +pub const SweepEdge = struct { + const Self = @This(); + + edge: Edge, + start_event_vert_uniq_idx: u16, + vert_idx: u16, + to_vert_idx: u16, + + end_event_idx: u32, + + /// The End event that marks this edge for removal. This is set in the End event. + end_event_vert_uniq_idx: u16, + + /// Points to the head vertex. + deferred_queue: DeferredVertexNodeId, + + /// Current size of the queue. + deferred_queue_size: u16, + + /// The current side being processed for monotone triangulation. + cur_side: Side, + + /// Last seen bad up cusp. + bad_up_cusp_uniq_idx: u16, + bad_up_cusp_right_sweep_edge_id: u16, + lowest_right_vert_idx: u16, + lowest_right_vert_sweep_edge_id: u16, + + /// Store the winding. This would be used by the fill rule to determine if the interior is to the left or right. + interior_is_left: bool, + + /// A sweep edge is created from a Start event. + fn init(e: Event, verts: []const InternalVertex) Self { + const e_vert = verts[e.vert_idx]; + return .{ + .edge = e.edge, + .start_event_vert_uniq_idx = e_vert.out_idx, + .vert_idx = e.vert_idx, + .to_vert_idx = e.to_vert_idx, + .end_event_idx = e.end_event_idx, + .end_event_vert_uniq_idx = std.math.maxInt(u16), + .deferred_queue = NullId, + .deferred_queue_size = 0, + .cur_side = .Left, + .bad_up_cusp_uniq_idx = NullId, + .bad_up_cusp_right_sweep_edge_id = NullId, + .lowest_right_vert_idx = NullId, + .lowest_right_vert_sweep_edge_id = NullId, + .interior_is_left = false, + }; + } + + fn enqueueDeferred(self: *Self, vert: InternalVertex, tess: *Tessellator) void { + const node = DeferredVertexNode.init(vert); + self.deferred_queue = tess.deferred_verts.insertBeforeHeadNoCheck(self.deferred_queue, node) catch unreachable; + self.deferred_queue_size += 1; + } + + fn verifySize(self: Self, tess: *Tessellator) void { + var cur = self.deferred_queue; + var act_size: u32 = 0; + while (cur != NullId) { + act_size += 1; + cur = tess.deferred_verts.getNextNoCheck(cur); + } + if (self.deferred_queue_size != act_size) { + log("expected {}, actual {}", .{ self.deferred_queue_size, act_size }); + unreachable; + } + } + + fn dumpQueue(self: Self, tess: *Tessellator) void { + var buf: [200]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + var writer = buf_stream.writer(); + var cur_id = self.deferred_queue; + while (cur_id != NullId) { + const cur = tess.deferred_verts.getNoCheck(cur_id); + std.fmt.format(writer, "{},", .{cur.vert_idx}) catch unreachable; + const last = cur_id; + cur_id = tess.deferred_verts.getNextNoCheck(cur_id); + if (last == cur_id) { + std.fmt.format(writer, "repeat pt - bad state", .{}) catch unreachable; + break; + } + } + var side_str: []const u8 = if (self.cur_side == .Right) "right" else "left"; + log("size: {}, side: {s}, idxes: {s}", .{ self.deferred_queue_size, side_str, buf[0..buf_stream.pos] }); + } +}; + +/// This contains a vertex that still needs to be triangulated later when it can. +/// It is linked together in a singly linked list queue and is designed to behave like the monotone triangulation queue. +/// Since the triangulator does everything in one pass for complex polygons, every monotone polygon span in the sweep edges +/// needs to keep track of their deferred vertices since the edges are short lived. +pub const DeferredVertexNode = struct { + const Self = @This(); + + vert_idx: u16, + + // Duped vars. + vert_out_idx: u16, + vert_x: f32, + vert_y: f32, + + fn init(vert: InternalVertex) Self { + return .{ + .vert_idx = vert.idx, + .vert_out_idx = vert.out_idx, + .vert_x = vert.pos[0], + .vert_y = vert.pos[1], + }; + } +}; + +/// Contains start/end InternalVertex. +pub const Edge = struct { + const Self = @This(); + + /// Start vertex index. + start_idx: u16, + /// End vertex index. + end_idx: u16, + + /// Duped start_pos for compareSweepEdge, getXIntersect. + start_pos: Vec2, + end_pos: Vec2, + + /// Vector from start to end pos. + vec: Vec2, + + /// Change of x with respect to y. + x_slope: f32, + + /// Whether the edge is horizontal. + /// TODO: This may not be needed anymore. + is_horiz: bool, + + fn init(start_idx: u16, start_v: InternalVertex, end_idx: u16, end_v: InternalVertex) Self { + var new = Self{ + .start_idx = start_idx, + .end_idx = end_idx, + .start_pos = start_v.pos, + .end_pos = end_v.pos, + .vec = end_v.pos - start_v.pos, + .x_slope = undefined, + .is_horiz = undefined, + }; + if (new.vec[1] != 0) { + new.x_slope = new.vec[0] / new.vec[1]; + new.is_horiz = false; + } else { + new.x_slope = std.math.f32_max; + new.is_horiz = true; + } + return new; + } +}; + +/// Internal verts. During triangulation, these are using for event computations so there can be duplicates points. +pub const InternalVertex = struct { + pos: Vec2, + /// This is the index of the vertex from the given polygon. + idx: u16, + /// The vertex index of the resulting buffer. Set during process event. + out_idx: u16 = undefined, +}; + +/// Avoids division by zero. +/// https://stackoverflow.com/questions/563198 +/// For segments: p, p + r, q, q + s +/// To find t, u for p + tr, q + us +/// t = (q − p) X s / (r X s) +/// u = (q − p) X r / (r X s) +fn computeTwoEdgeIntersect(p: Edge, q: Edge) IntersectResult { + const t__ = trace(@src()); + defer t__.end(); + const r_s = cross(p.vec, q.vec); + if (r_s == 0) { + return IntersectResult.initNull(); + } + const qmp = Vec2{ q.start_pos[0] - p.start_pos[0], q.start_pos[1] - p.start_pos[1] }; + const qmp_r = cross(qmp, p.vec); + const u = qmp_r / r_s; + if (u >= 0 and u <= 1) { + // Must check intersect point is also on p. + const qmp_s = cross(qmp, q.vec); + const t_ = qmp_s / r_s; + if (t_ >= 0 and t_ <= 1) { + return .{ + .x = q.start_pos[0] + q.vec[0] * u, + .y = q.start_pos[1] + q.vec[1] * u, + .t = t_, + .u = u, + .has_intersect = true, + }; + } else { + return IntersectResult.initNull(); + } + } else { + return IntersectResult.initNull(); + } +} + +const IntersectResult = struct { + x: f32, + y: f32, + t: f32, + u: f32, + has_intersect: bool, + + fn initNull() IntersectResult { + return .{ + .x = undefined, + .y = undefined, + .t = undefined, + .u = undefined, + .has_intersect = false, + }; + } +}; + +/// Just check triangle count. +/// TODO: Verify all triangles take up the entire space. +fn testLarge(polygon: []const f32, exp_tri_count: u32) !void { + var polygon_buf = std.ArrayList(Vec2).init(t.allocator); + defer polygon_buf.deinit(); + var i: u32 = 0; + while (i < polygon.len) : (i += 2) { + polygon_buf.append(Vec2{ polygon[i], polygon[i + 1] }) catch unreachable; + } + var tessellator: Tessellator = undefined; + tessellator.init(t.allocator); + defer tessellator.deinit(); + tessellator.triangulatePolygon(polygon_buf.items); + try t.expectEqual(tessellator.out_idxes.items.len / 3, exp_tri_count); +} + +fn testSimple(polygon: []const f32, exp_verts: []const f32, exp_idxes: []const u16) !void { + var polygon_buf = std.ArrayList(Vec2).init(t.allocator); + defer polygon_buf.deinit(); + var i: u32 = 0; + while (i < polygon.len) : (i += 2) { + polygon_buf.append(Vec2{ polygon[i], polygon[i + 1] }) catch unreachable; + } + var tessellator: Tessellator = undefined; + tessellator.init(t.allocator); + defer tessellator.deinit(); + tessellator.triangulatePolygon(polygon_buf.items); + + var exp_verts_buf = std.ArrayList(Vec2).init(t.allocator); + defer exp_verts_buf.deinit(); + i = 0; + while (i < exp_verts.len) : (i += 2) { + exp_verts_buf.append(Vec2{ exp_verts[i], exp_verts[i + 1] }) catch unreachable; + } + try t.expectEqualSlices(Vec2, tessellator.out_verts.items, exp_verts_buf.items); + // log("{any}", .{tessellator.out_idxes.items}); + try t.expectEqualSlices(u16, tessellator.out_idxes.items, exp_idxes); +} + +test "One triangle ccw." { + try testSimple(&.{ + 100, 0, + 0, 0, + 0, 100, + }, &.{ + 0, 0, + 100, 0, + 0, 100, + }, &.{ 2, 1, 0 }); +} + +test "One triangle cw." { + try testSimple(&.{ + 100, 0, + 0, 100, + 0, 0, + }, &.{ + 0, 0, + 100, 0, + 0, 100, + }, &.{ 2, 1, 0 }); +} + +test "Square." { + try testSimple(&.{ + 0, 0, + 100, 0, + 100, 100, + 0, 100, + }, &.{ + 0, 0, + 100, 0, + 0, 100, + 100, 100, + }, &.{ + 2, 1, 0, + 3, 1, 2, + }); +} + +test "Pentagon." { + try testSimple(&.{ + 100, 0, + 200, 100, + 200, 200, + 0, 200, + 0, 100, + }, &.{ + 100, 0, + 0, 100, + 200, 100, + 0, 200, + 200, 200, + }, &.{ + 2, 0, 1, + 3, 2, 1, + 4, 2, 3, + }); +} + +test "Hexagon." { + try testSimple(&.{ + 100, 0, + 200, 100, + 200, 200, + 100, 300, + 0, 200, + 0, 100, + }, &.{ + 100, 0, + 0, 100, + 200, 100, + 0, 200, + 200, 200, + 100, 300, + }, &.{ + 2, 0, 1, + 3, 2, 1, + 4, 2, 3, + 5, 4, 3, + }); +} + +test "Octagon." { + try testSimple(&.{ + 100, 0, + 200, 0, + 300, 100, + 300, 200, + 200, 300, + 100, 300, + 0, 200, + 0, 100, + }, &.{ + 100, 0, + 200, 0, + 0, 100, + 300, 100, + 0, 200, + 300, 200, + 100, 300, + 200, 300, + }, &.{ + 2, 1, 0, + 3, 1, 2, + 4, 3, 2, + 5, 3, 4, + 6, 5, 4, + 7, 5, 6, + }); +} + +test "Rhombus." { + try testSimple(&.{ + 100, 0, + 200, 100, + 100, 200, + 0, 100, + }, &.{ + 100, 0, + 0, 100, + 200, 100, + 100, 200, + }, &.{ + 2, 0, 1, + 3, 2, 1, + }); +} + +// Tests monotone partition with bad up cusp and valid right angle. +test "Square with concave top side." { + try testSimple(&.{ + 0, 0, + 100, 100, + 200, 0, + 200, 200, + 0, 200, + }, &.{ + 0, 0, + 200, 0, + 100, 100, + 0, 200, + 200, 200, + }, &.{ + 3, 2, 0, + 4, 2, 3, + 4, 1, 2, + }); +} + +// Tests monotone partition with bad down cusp and valid right angle. +test "Square with concave bottom side." { + try testSimple(&.{ + 0, 0, + 200, 0, + 200, 200, + 100, 100, + 0, 200, + }, &.{ + 0, 0, + 200, 0, + 100, 100, + 0, 200, + 200, 200, + }, &.{ + 2, 1, 0, + 3, 2, 0, + 4, 1, 2, + }); +} + +// Tests monotone partition with bad up cusp and valid up cusp. +test "V shape." { + try testSimple(&.{ + 0, 0, + 100, 100, + 200, 0, + 100, 200, + }, &.{ + 0, 0, + 200, 0, + 100, 100, + 100, 200, + }, &.{ + 3, 2, 0, + 3, 1, 2, + }); +} + +// Tests monotone partition with bad down cusp and valid up cusp. +test "Upside down V shape." { + try testSimple(&.{ + 100, 0, + 200, 200, + 100, 100, + 0, 200, + }, &.{ + 100, 0, + 100, 100, + 0, 200, + 200, 200, + }, &.{ + 2, 1, 0, + 3, 0, 1, + }); +} + +// Tests the sweep line with alternating interior/exterior sides. +test "Clockwise spiral." { + try testSimple(&.{ + 0, 0, + 500, 0, + 500, 500, + 200, 500, + 200, 200, + 300, 200, + 300, 400, + 400, 400, + 400, 100, + 100, 100, + 100, 500, + 0, 500, + }, &.{ + 0, 0, + 500, 0, + 100, 100, + 400, 100, + 200, 200, + 300, 200, + 300, 400, + 400, 400, + 0, 500, + 100, 500, + 200, 500, + 500, 500, + }, &.{ + 2, 1, 0, + 3, 1, 2, + 6, 5, 4, + 7, 1, 3, + 8, 2, 0, + 9, 2, 8, + 10, 7, 6, + 10, 6, 4, + 11, 7, 10, + 11, 1, 7, + }); +} + +// Tests the sweep line with alternating interior/exterior sides. +test "CCW spiral." { + try testSimple(&.{ + 0, 0, + 500, 0, + 500, 500, + 400, 500, + 400, 100, + 100, 100, + 100, 400, + 200, 400, + 200, 200, + 300, 200, + 300, 500, + 0, 500, + }, &.{ + 0, 0, + 500, 0, + 100, 100, + 400, 100, + 200, 200, + 300, 200, + 100, 400, + 200, 400, + 0, 500, + 300, 500, + 400, 500, + 500, 500, + }, &.{ + 2, 1, 0, + 3, 1, 2, + 6, 2, 0, + 7, 5, 4, + 8, 7, 6, + 8, 6, 0, + 9, 7, 8, + 9, 5, 7, + 10, 1, 3, + 11, 1, 10, + }); +} + +test "Overlapping point." { + try testSimple(&.{ + 0, 0, + 100, 100, + 200, 0, + 200, 200, + 100, 100, + 0, 200, + }, &.{ + 0, 0, + 200, 0, + 100, 100, + 0, 200, + 200, 200, + }, &.{ + 3, 2, 0, + 4, 1, 2, + }); +} + +// Different windings on split polygons. +test "Self intersecting polygon." { + try testSimple(&.{ + 0, 200, + 0, 100, + 200, 100, + 200, 0, + }, &.{ + 200, 0, + 0, 100, + 100, 100, + 200, 100, + 0, 200, + }, &.{ + 3, 0, 2, + 4, 2, 1, + }); +} + +// Test evenodd rule. +test "Overlapping triangles." { + try testSimple(&.{ + 0, 100, + 200, 0, + 200, 200, + 0, 100, + 250, 75, + 250, 125, + 0, 100, + }, &.{ + 200, 0, + 250, 75, + 200, 80, + 0, 100, + 200, 120, + 250, 125, + 200, 200, + }, &.{ + 3, 2, 0, + 4, 1, 2, + 5, 1, 4, + 6, 4, 3, + }); +} + +// Begin mapbox test cases. + +test "bad-diagonals.json" { + try testSimple(&.{ + 440, 4152, + 440, 4208, + 296, 4192, + 368, 4192, + 400, 4200, + 400, 4176, + 368, 4192, + 296, 4192, + 264, 4200, + 288, 4160, + 296, 4192, + }, &.{ + 440, 4152, + 288, 4160, + 400, 4176, + 296, 4192, + 368, 4192, + 264, 4200, + 400, 4200, + 440, 4208, + }, &.{ + 3, 2, 0, + 4, 2, 3, + 5, 3, 1, + 6, 4, 3, + 6, 0, 2, + 7, 6, 3, + 7, 0, 6, + }); +} + +// TODO: dude.json + +// Case by case examples. + +test "Rectangle with bottom-left wedge." { + try testSimple(&.{ + 56, 22, + 111, 22, + 111, 44, + 37, 44, + 56, 32, + }, &.{ + 56, 22, + 111, 22, + 56, 32, + 37, 44, + 111, 44, + }, &.{ + 2, 1, 0, + 3, 1, 2, + 4, 1, 3, + }); +} + +test "Tiger big part." { + try testLarge(&.{ + -129.83, 103.06, -129.83, 103.06, -128.36, 113.62, -126.60, 118.80, -126.60, 118.80, -127.33, 125.99, -125.81, 135.32, -121.40, 144.40, -121.40, 144.40, -121.18, 151.03, -120.20, 154.80, -120.20, 154.80, -115.56, 161.53, -111.40, 164.00, -111.40, 164.00, -99.95, 166.93, -88.93, 169.12, -88.93, 169.12, -82.12, 176.08, -77.01, 183.74, -74.79, 190.23, -75.00, 196.00, -75.00, 196.00, -76.74, 210.10, -79.00, 214.00, -79.00, 214.00, -73.96, 210.34, -73.22, 211.25, -77.00, 219.60, -81.40, 238.40, -81.40, 238.40, -67.64, 228.08, -66.39, 228.12, -71.40, 235.20, -81.40, 261.20, -81.40, 261.20, -67.93, 249.24, -67.39, 249.03, -69.00, 251.20, -72.20, 260.00, -72.20, 260.00, -53.39, 249.64, -48.97, 248.73, -49.31, 250.85, -59.80, 262.40, -59.80, 262.40, -52.31, 260.54, -47.40, 261.60, -47.40, 261.60, -41.92, 261.27, -41.40, 262.00, -41.40, 262.00, -49.70, 267.43, -57.61, 274.87, -62.93, 282.61, -65.80, 290.80, -65.80, 290.80, -61.30, 286.73, -59.99, 287.03, -60.60, 291.60, -60.20, 303.20, -60.20, 303.20, -58.30, 297.14, -57.37, 298.26, -56.60, 319.20, -56.60, 319.20, -48.49, 312.82, -45.76, 312.03, -45.35, 313.82, -49.00, 322.00, -49.00, 338.80, -49.00, 338.80, -40.26, 330.76, -38.63, 330.61, -40.20, 335.20, -40.20, 335.20, -36.26, 332.85, -34.22, 332.98, -33.27, 335.13, -34.20, 341.60, -34.20, 341.60, -33.94, 345.73, -33.17, 345.79, -30.60, 340.80, -30.60, 340.80, -22.95, 328.26, -20.19, 325.79, -19.29, 327.04, -20.60, 336.40, -20.60, 336.40, -20.14, 345.51, -19.15, 346.31, -16.60, 340.80, -16.60, 340.80, -15.40, 346.50, -12.14, 353.01, -7.00, 358.40, -7.00, 358.40, -6.27, 339.53, -4.46, 332.02, -2.77, 330.70, -0.27, 332.77, 4.60, 343.60, 8.60, 360.00, 8.60, 360.00, 10.64, 351.18, 11.00, 345.60, 19.00, 353.60, 19.00, 353.60, 28.79, 341.06, 31.17, 340.03, 31.00, 344.00, 31.00, 344.00, 25.45, 358.75, 25.00, 364.80, 25.00, 364.80, 43.00, 328.40, 43.00, 328.40, 43.39, 345.38, 44.82, 349.24, 46.77, 347.92, 51.80, 334.80, 51.80, 334.80, 54.49, 342.22, 55.39, 347.96, 54.60, 351.20, 54.60, 351.20, 60.76, 343.58, 61.80, 340.00, 61.80, 340.00, 64.54, 337.57, 66.77, 338.71, 69.20, 345.40, 69.20, 345.40, 71.39, 352.02, 72.60, 351.60, 72.60, 351.60, 75.37, 362.09, 76.42, 362.30, 77.80, 352.80, 77.80, 352.80, 77.75, 344.98, 75.91, 335.70, 72.20, 327.60, 72.20, 327.60, 72.12, 324.56, 70.20, 320.40, 70.20, 320.40, 75.70, 327.30, 77.91, 328.09, 78.69, 325.45, 76.60, 313.20, 76.60, 313.20, 89.00, 321.20, 89.00, 321.20, 82.70, 308.82, 81.23, 303.45, 81.82, 302.23, 84.20, 302.80, 84.20, 302.80, 83.54, 299.86, 84.53, 298.67, 87.95, 299.28, 97.00, 304.40, 97.00, 304.40, 91.03, 297.34, 90.41, 295.37, 91.64, 294.95, 98.60, 298.00, 98.60, 298.00, 101.93, 300.03, 102.23, 299.50, 99.00, 294.40, 99.00, 294.40, 94.40, 288.21, 95.27, 288.03, 106.60, 296.40, 106.60, 296.40, 116.61, 311.24, 119.00, 315.60, 119.00, 315.60, 108.86, 290.10, 104.60, 283.60, 104.60, 283.60, 108.02, 275.28, 114.09, 267.22, 121.76, 261.79, 130.82, 259.23, 141.00, 259.30, 154.20, 262.80, 154.20, 262.80, 157.75, 268.68, 160.36, 270.08, 162.73, 268.60, 165.40, 261.60, 165.40, 261.60, 170.08, 261.04, 176.25, 263.49, 182.75, 270.16, 189.40, 282.80, 189.40, 282.80, 192.36, 270.67, 192.60, 266.40, 192.60, 266.40, 198.19, 266.89, 198.60, 266.40, 198.60, 266.40, 210.19, 269.77, 213.00, 270.00, 213.00, 270.00, 217.51, 273.62, 219.47, 274.23, 220.20, 273.20, 220.20, 273.20, 225.75, 274.22, 227.51, 273.79, 227.40, 272.40, 227.40, 272.40, 234.76, 286.58, 236.60, 291.60, 239.00, 277.60, 241.00, 280.40, 241.00, 280.40, 242.01, 273.69, 241.80, 271.60, 241.80, 271.60, 242.83, 271.72, 249.79, 275.48, 257.44, 282.09, 263.14, 289.99, 266.60, 299.20, 268.60, 307.60, 268.60, 307.60, 272.87, 294.06, 273.00, 288.80, 273.00, 288.80, 277.01, 290.73, 278.60, 294.00, 278.60, 294.00, 280.13, 278.97, 279.59, 269.28, 277.80, 264.80, 277.80, 264.80, 281.47, 265.30, 283.40, 267.60, 283.40, 260.40, 283.40, 260.40, 289.30, 260.14, 290.60, 258.80, 290.60, 258.80, 293.21, 257.36, 295.38, 257.54, 297.00, 259.60, 297.00, 259.60, 293.38, 246.31, 293.06, 239.85, 294.31, 238.04, 296.82, 238.39, 303.00, 243.60, 303.00, 243.60, 306.29, 246.69, 307.45, 245.38, 306.60, 235.60, 306.60, 235.60, 302.46, 219.68, 301.73, 215.52, 303.80, 214.80, 303.80, 214.80, 303.85, 211.76, 302.60, 209.60, 302.60, 209.60, 301.93, 208.90, 303.80, 209.60, 303.80, 209.60, 305.11, 209.64, 305.81, 206.07, 303.40, 191.60, 303.40, 191.60, 304.82, 190.48, 304.47, 183.66, 297.80, 164.00, 297.80, 164.00, 298.73, 160.69, 296.60, 153.20, 296.60, 153.20, 303.95, 156.14, 307.40, 156.00, 307.40, 156.00, 307.15, 155.40, 303.80, 150.40, 303.80, 150.40, 295.80, 126.68, 293.83, 115.79, 294.65, 112.78, 296.76, 112.66, 302.60, 117.60, 302.60, 117.60, 307.07, 121.27, 309.11, 121.32, 309.97, 118.48, 308.05, 108.35, 308.05, 108.35, 300.79, 86.65, 299.72, 80.04, -129.83, 103.06, + }, 227); +} + +// Test points that are close to each other with higher precision. +test "Tiger whisker." { + try testLarge(&.{ -109.01000, 110.07000, -109.01000, 110.07000, -108.34000, 111.97000, -108.34000, 111.97000, -123.56751, 104.91863, -141.35422, 98.68091, -153.21875, 96.99554, -161.26077, 98.11440, -166.87000, 101.68000, -166.87000, 101.68000, -163.45908, 98.55489, -156.28145, 96.28941, -145.48354, 96.58372, -130.09291, 100.65533, -109.00999, 110.07000 }, 9); +} + +// Tests zig zag shape. +test "Tiger part." { + try testLarge(&.{ -54.20, 176.40, -54.20, 176.40, -51.54, 180.01, -50.04, 187.53, -51.51, 198.82, -57.40, 214.80, -51.00, 212.40, -51.00, 212.40, -52.75, 222.12, -55.00, 226.00, -47.80, 222.80, -47.80, 222.80, -45.85, 227.62, -45.49, 232.20, -47.00, 235.60, -47.00, 235.60, -37.04, 241.57, -32.01, 246.52, -31.00, 250.00, -31.00, 250.00, -28.25, 245.06, -27.27, 239.91, -28.60, 235.60, -28.60, 235.60, -32.06, 232.22, -36.59, 228.60, -38.53, 223.88, -39.00, 214.80, -47.80, 218.00, -47.80, 218.00, -43.55, 209.33, -42.20, 202.80, -50.20, 205.20, -50.20, 205.20, -43.67, 191.40, -41.68, 182.92, -42.47, 178.91, -45.40, 177.20, -45.40, 177.20, -53.55, 176.38, -54.20, 176.40 }, 29); +} + +test "Tiger whisker #2." { + try testLarge(&.{ 50.60, 84.00, 50.60, 84.00, 36.68, 72.16, 27.20, 65.81, 22.20, 64.00, 22.20, 64.00, 7.18, 63.89, -7.84, 66.44, -19.01, 71.16, -27.00, 78.00, -27.00, 78.00, -18.60, 70.90, -7.02, 64.99, 5.17, 62.33, 18.20, 63.20, 18.20, 63.20, 4.15, 61.23, -7.70, 60.89, -15.80, 62.00, -42.20, 76.00, -45.00, 80.80, -45.00, 80.80, -42.44, 75.17, -37.28, 68.75, -31.28, 64.00, -22.60, 60.00, -22.60, 60.00, -7.92, 58.05, 3.78, 58.19, 11.00, 60.00, 11.00, 60.00, -3.17, 56.40, -14.12, 54.88, -20.60, 55.20, -20.60, 55.20, -30.04, 55.69, -41.27, 58.57, -50.82, 63.73, -57.83, 70.06, -63.80, 79.20, -63.80, 79.20, -60.27, 71.59, -53.70, 63.52, -45.00, 57.60, -45.00, 57.60, -36.54, 53.82, -24.25, 51.26, -11.00, 51.60, -11.00, 51.60, 2.14, 54.95, 8.60, 57.20, 8.60, 57.20, 11.58, 58.03, 11.26, 56.98, 4.20, 52.00, 4.20, 52.00, 0.05, 47.36, -6.79, 43.57, -15.40, 42.40, -15.40, 42.40, -36.18, 45.32, -52.83, 49.44, -63.12, 53.75, -68.60, 58.00, -68.60, 58.00, -54.94, 48.57, -44.60, 44.00, -44.60, 44.00, -23.74, 37.92, -13.80, 36.80, -13.80, 36.80, 8.76, 36.15, 18.60, 33.80, 18.60, 33.80, 12.30, 37.54, 10.11, 40.15, 10.60, 42.00, 10.60, 42.00, 18.76, 51.11, 20.60, 54.00, 20.60, 54.00, 28.20, 61.89, 48.40, 81.70, 50.60, 84.00 }, 61); +} + +test "Tiger big part #2." { + try testLarge(&.{ 143.80, 259.60, 143.80, 259.60, 156.61, 257.34, 165.99, 254.14, 171.00, 250.80, 175.40, 254.40, 193.00, 216.00, 196.60, 221.20, 196.60, 221.20, 204.92, 211.13, 209.33, 203.27, 210.20, 198.40, 210.20, 198.40, 210.92, 196.01, 214.22, 196.97, 223.00, 204.40, 223.00, 204.40, 223.74, 198.82, 225.70, 197.45, 229.40, 199.60, 229.40, 199.60, 229.48, 191.67, 231.36, 189.69, 235.40, 192.00, 235.40, 192.00, 233.02, 182.21, 233.43, 178.04, 234.91, 177.19, 238.62, 178.91, 247.40, 187.60, 247.40, 187.60, 250.17, 190.36, 248.60, 187.20, 248.60, 187.20, 238.82, 166.34, 235.69, 155.55, 236.21, 151.81, 238.37, 150.90, 244.20, 153.60, 244.20, 153.60, 245.37, 133.23, 245.00, 126.40, 245.00, 126.40, 242.11, 109.37, 239.39, 98.81, 237.00, 94.40, 237.00, 94.40, 235.10, 91.16, 235.84, 89.80, 238.54, 89.93, 243.00, 92.80, 243.00, 92.80, 238.96, 81.92, 238.77, 78.05, 240.20, 77.49, 245.00, 80.80, 245.00, 80.80, 240.58, 67.71, 237.00, 62.80, 237.00, 62.80, 236.04, 56.18, 237.21, 53.39, 240.05, 52.95, 246.60, 56.40, 246.60, 56.40, 241.98, 45.41, 239.00, 40.80, 239.00, 40.80, 232.68, 22.48, 232.55, 17.75, 234.60, 18.00, 239.00, 21.60, 239.00, 21.60, 235.94, 13.32, 236.27, 11.21, 238.60, 12.00, 238.60, 12.00, 244.87, 15.98, 245.00, 16.00, 245.00, 16.00, 236.54, 0.65, 235.41, -4.10, 236.94, -4.65, 244.20, 0.40, 244.20, 0.40, 232.60, -20.40, 232.60, -20.40, 224.56, -30.47, 222.78, -34.51, 223.63, -35.63, 228.20, -34.40, 233.00, -32.80, 233.00, -32.80, 223.84, -40.87, 216.20, -44.40, 216.20, -44.40, 213.35, -45.94, 214.21, -47.98, 219.17, -50.41, 225.00, -50.40, 225.00, -50.40, 247.00, -40.80, 247.00, -40.80, 259.33, -24.93, 263.80, -21.60, 263.80, -21.60, 251.96, -24.82, 248.79, -24.16, 249.80, -21.20, 249.80, -21.20, 257.74, -11.96, 258.98, -8.44, 257.00, -7.60, 257.00, -7.60, 254.67, -3.13, 253.96, 2.58, 255.80, 8.40, 255.80, 8.40, 248.73, 2.56, 246.65, 2.15, 246.70, 4.49, 252.20, 15.60, 259.00, 32.00, 259.00, 32.00, 247.61, 21.93, 243.68, 20.11, 242.85, 21.48, 245.80, 29.20, 245.80, 29.20, 261.57, 49.78, 265.00, 53.20, 265.00, 53.20, 267.03, 55.03, 271.40, 62.40, 267.00, 60.40, 272.20, 69.20, 272.20, 69.20, 266.48, 64.35, 265.25, 64.72, 267.00, 70.40, 272.60, 84.80, 272.60, 84.80, 264.54, 77.74, 261.68, 77.09, 261.24, 79.97, 265.80, 92.40, 265.80, 92.40, 259.71, 91.77, 256.50, 93.34, 255.61, 96.92, 258.20, 104.40, 258.20, 104.40, 257.00, 125.60, 257.00, 125.60, 257.37, 146.74, 256.16, 160.73, 254.20, 167.20, 254.20, 167.20, 253.12, 172.65, 255.06, 182.19, 262.20, 198.40, 262.20, 198.40, 264.65, 206.19, 263.74, 207.59, 259.00, 204.00, 259.00, 204.00, 253.94, 199.29, 253.84, 200.28, 256.60, 209.20, 256.60, 209.20, 262.81, 231.18, 263.80, 239.20, 263.80, 239.20, 262.65, 239.33, 259.40, 236.80, 259.40, 236.80, 251.57, 226.52, 247.90, 223.71, 246.46, 224.22, 246.20, 228.40, 246.20, 228.40, 241.80, 245.20, 241.80, 245.20, 239.77, 250.25, 239.04, 250.56, 238.60, 247.20, 238.60, 247.20, 235.84, 237.79, 234.13, 236.00, 232.60, 238.00, 232.60, 238.00, 227.70, 248.46, 223.40, 254.00, 223.40, 254.00, 221.77, 253.32, 217.15, 243.71, 215.18, 241.23, 214.20, 244.00, 214.20, 244.00, 208.81, 240.31, 204.14, 239.64, 200.46, 241.80, 197.40, 248.00, 185.80, 264.40, 185.80, 264.40, 184.89, 256.37, 184.20, 258.00, 184.20, 258.00, 164.60, 260.78, 150.89, 261.06, 143.80, 259.60 }, 157); +} + +pub const DebugTriangulateStepResult = struct { + has_result: bool, + sweep_edges: []const SweepEdge, + event: Event, + out_verts: []const Vec2, + out_idxes: []const u16, + verts: []const InternalVertex, + deferred_verts: []const CompactSinglyLinkedListBuffer(DeferredVertexNodeId, DeferredVertexNode).Node, + + pub fn deinit(self: @This(), alloc: std.mem.Allocator) void { + alloc.free(self.sweep_edges); + alloc.free(self.out_verts); + alloc.free(self.out_idxes); + alloc.free(self.verts); + alloc.free(self.deferred_verts); + } +}; + +// test "Simple breaking shape" { +// var polygon_buf = std.ArrayList(Vec2).init(t.allocator); +// defer polygon_buf.deinit(); +// try polygon_buf.appendSlice(&[_]Vec2{ +// Vec2{ 0, 0 }, +// Vec2{ 10, 0 }, +// Vec2{ 10, 10 }, +// Vec2{ 0, 10 }, + +// Vec2{ 0, 0 }, +// Vec2{ 0, 0 }, +// Vec2{ 2, 2 }, +// Vec2{ 8, 2 }, +// Vec2{ 8, 8 }, +// Vec2{ 2, 8 }, +// }); + +// var tessellator: Tessellator = undefined; +// tessellator.init(t.allocator); +// defer tessellator.deinit(); +// tessellator.triangulatePolygons(&.{ polygon_buf.items[0..5], polygon_buf.items[5..] }); +// // tessellator.triangulatePolygons(&.{polygon_buf.items[0..4]}); +// } diff --git a/examples/gkurve/tracy.zig b/examples/gkurve/tracy.zig new file mode 100644 index 00000000..00ff9fa8 --- /dev/null +++ b/examples/gkurve/tracy.zig @@ -0,0 +1,314 @@ +// Copied from zig/src/tracy.zig + +const std = @import("std"); +const builtin = @import("builtin"); +// TODO: integrate with tracy? +// const build_options = @import("build_options"); + +// pub const enable = if (builtin.is_test) false else build_options.enable_tracy; +// pub const enable_allocation = enable and build_options.enable_tracy_allocation; +// pub const enable_callstack = enable and build_options.enable_tracy_callstack; +pub const enable = false; +pub const enable_allocation = enable and false; +pub const enable_callstack = enable and false; + +// TODO: make this configurable +const callstack_depth = 10; + +const ___tracy_c_zone_context = extern struct { + id: u32, + active: c_int, + + pub inline fn end(self: @This()) void { + ___tracy_emit_zone_end(self); + } + + pub inline fn addText(self: @This(), text: []const u8) void { + ___tracy_emit_zone_text(self, text.ptr, text.len); + } + + pub inline fn setName(self: @This(), name: []const u8) void { + ___tracy_emit_zone_name(self, name.ptr, name.len); + } + + pub inline fn setColor(self: @This(), color: u32) void { + ___tracy_emit_zone_color(self, color); + } + + pub inline fn setValue(self: @This(), value: u64) void { + ___tracy_emit_zone_value(self, value); + } +}; + +pub const Ctx = if (enable) ___tracy_c_zone_context else struct { + pub inline fn end(self: @This()) void { + _ = self; + } + + pub inline fn addText(self: @This(), text: []const u8) void { + _ = self; + _ = text; + } + + pub inline fn setName(self: @This(), name: []const u8) void { + _ = self; + _ = name; + } + + pub inline fn setColor(self: @This(), color: u32) void { + _ = self; + _ = color; + } + + pub inline fn setValue(self: @This(), value: u64) void { + _ = self; + _ = value; + } +}; + +pub inline fn trace(comptime src: std.builtin.SourceLocation) Ctx { + if (!enable) return .{}; + + if (enable_callstack) { + return ___tracy_emit_zone_begin_callstack(&.{ + .name = null, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, callstack_depth, 1); + } else { + return ___tracy_emit_zone_begin(&.{ + .name = null, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, 1); + } +} + +pub inline fn traceNamed(comptime src: std.builtin.SourceLocation, comptime name: [:0]const u8) Ctx { + if (!enable) return .{}; + + if (enable_callstack) { + return ___tracy_emit_zone_begin_callstack(&.{ + .name = name.ptr, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, callstack_depth, 1); + } else { + return ___tracy_emit_zone_begin(&.{ + .name = name.ptr, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, 1); + } +} + +pub fn tracyAllocator(allocator: std.mem.Allocator) TracyAllocator(null) { + return TracyAllocator(null).init(allocator); +} + +pub fn TracyAllocator(comptime name: ?[:0]const u8) type { + return struct { + parent_allocator: std.mem.Allocator, + + const Self = @This(); + + pub fn init(parent_allocator: std.mem.Allocator) Self { + return .{ + .parent_allocator = parent_allocator, + }; + } + + pub fn allocator(self: *Self) std.mem.Allocator { + return std.mem.Allocator.init(self, allocFn, resizeFn, freeFn); + } + + fn allocFn(self: *Self, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) std.mem.Allocator.Error![]u8 { + const result = self.parent_allocator.rawAlloc(len, ptr_align, len_align, ret_addr); + if (result) |data| { + if (data.len != 0) { + if (name) |n| { + allocNamed(data.ptr, data.len, n); + } else { + alloc(data.ptr, data.len); + } + } + } else |_| { + messageColor("allocation failed", 0xFF0000); + } + return result; + } + + fn resizeFn(self: *Self, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize { + if (self.parent_allocator.rawResize(buf, buf_align, new_len, len_align, ret_addr)) |resized_len| { + if (name) |n| { + freeNamed(buf.ptr, n); + allocNamed(buf.ptr, resized_len, n); + } else { + free(buf.ptr); + alloc(buf.ptr, resized_len); + } + + return resized_len; + } + + // during normal operation the compiler hits this case thousands of times due to this + // emitting messages for it is both slow and causes clutter + return null; + } + + fn freeFn(self: *Self, buf: []u8, buf_align: u29, ret_addr: usize) void { + self.parent_allocator.rawFree(buf, buf_align, ret_addr); + // this condition is to handle free being called on an empty slice that was never even allocated + // example case: `std.process.getSelfExeSharedLibPaths` can return `&[_][:0]u8{}` + if (buf.len != 0) { + if (name) |n| { + freeNamed(buf.ptr, n); + } else { + free(buf.ptr); + } + } + } + }; +} + +// This function only accepts comptime known strings, see `messageCopy` for runtime strings +pub inline fn message(comptime msg: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_messageL(msg.ptr, if (enable_callstack) callstack_depth else 0); +} + +// This function only accepts comptime known strings, see `messageColorCopy` for runtime strings +pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void { + if (!enable) return; + ___tracy_emit_messageLC(msg.ptr, color, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn messageCopy(msg: []const u8) void { + if (!enable) return; + ___tracy_emit_message(msg.ptr, msg.len, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn messageColorCopy(msg: [:0]const u8, color: u32) void { + if (!enable) return; + ___tracy_emit_messageC(msg.ptr, msg.len, color, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn frameMark() void { + if (!enable) return; + ___tracy_emit_frame_mark(null); +} + +pub inline fn frameMarkNamed(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark(name.ptr); +} + +pub inline fn namedFrame(comptime name: [:0]const u8) Frame(name) { + frameMarkStart(name); + return .{}; +} + +pub fn Frame(comptime name: [:0]const u8) type { + return struct { + pub fn end(_: @This()) void { + frameMarkEnd(name); + } + }; +} + +inline fn frameMarkStart(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark_start(name.ptr); +} + +inline fn frameMarkEnd(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark_end(name.ptr); +} + +extern fn ___tracy_emit_frame_mark_start(name: [*:0]const u8) void; +extern fn ___tracy_emit_frame_mark_end(name: [*:0]const u8) void; + +inline fn alloc(ptr: [*]u8, len: usize) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_alloc_callstack(ptr, len, callstack_depth, 0); + } else { + ___tracy_emit_memory_alloc(ptr, len, 0); + } +} + +inline fn allocNamed(ptr: [*]u8, len: usize, comptime name: [:0]const u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_alloc_callstack_named(ptr, len, callstack_depth, 0, name.ptr); + } else { + ___tracy_emit_memory_alloc_named(ptr, len, 0, name.ptr); + } +} + +inline fn free(ptr: [*]u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_free_callstack(ptr, callstack_depth, 0); + } else { + ___tracy_emit_memory_free(ptr, 0); + } +} + +inline fn freeNamed(ptr: [*]u8, comptime name: [:0]const u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_free_callstack_named(ptr, callstack_depth, 0, name.ptr); + } else { + ___tracy_emit_memory_free_named(ptr, 0, name.ptr); + } +} + +extern fn ___tracy_emit_zone_begin( + srcloc: *const ___tracy_source_location_data, + active: c_int, +) ___tracy_c_zone_context; +extern fn ___tracy_emit_zone_begin_callstack( + srcloc: *const ___tracy_source_location_data, + depth: c_int, + active: c_int, +) ___tracy_c_zone_context; +extern fn ___tracy_emit_zone_text(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void; +extern fn ___tracy_emit_zone_name(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void; +extern fn ___tracy_emit_zone_color(ctx: ___tracy_c_zone_context, color: u32) void; +extern fn ___tracy_emit_zone_value(ctx: ___tracy_c_zone_context, value: u64) void; +extern fn ___tracy_emit_zone_end(ctx: ___tracy_c_zone_context) void; +extern fn ___tracy_emit_memory_alloc(ptr: *const anyopaque, size: usize, secure: c_int) void; +extern fn ___tracy_emit_memory_alloc_callstack(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int) void; +extern fn ___tracy_emit_memory_free(ptr: *const anyopaque, secure: c_int) void; +extern fn ___tracy_emit_memory_free_callstack(ptr: *const anyopaque, depth: c_int, secure: c_int) void; +extern fn ___tracy_emit_memory_alloc_named(ptr: *const anyopaque, size: usize, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_alloc_callstack_named(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_free_named(ptr: *const anyopaque, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_free_callstack_named(ptr: *const anyopaque, depth: c_int, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void; +extern fn ___tracy_emit_messageL(txt: [*:0]const u8, callstack: c_int) void; +extern fn ___tracy_emit_messageC(txt: [*]const u8, size: usize, color: u32, callstack: c_int) void; +extern fn ___tracy_emit_messageLC(txt: [*:0]const u8, color: u32, callstack: c_int) void; +extern fn ___tracy_emit_frame_mark(name: ?[*:0]const u8) void; + +const ___tracy_source_location_data = extern struct { + name: ?[*:0]const u8, + function: [*:0]const u8, + file: [*:0]const u8, + line: u32, + color: u32, +};