///! Box math, including Boxes, Point, Size and Selection const std = @import("std"); pub const Point = struct { const Self = @This(); x: i32 = 0, y: i32 = 0, pub fn length(self: Self) f32 { return @sqrt(@intToFloat(f32, self.x*self.x + self.y*self.y)); } pub fn distanceTo(self: Self, other: Self) f32 { const delta = self.added(other.inverted()); return delta.length(); } pub fn inverted(self: *const Self) Self { return Self{ .x = self.x * -1, .y = self.y * -1, }; } pub fn add(self: *Self, other: Self) void { self.x += other.x; self.y += other.y; } pub fn added(self: *const Self, other: Self) Self { var new = self.*; new.add(other); return new; } pub fn eq(self: Self, other: Self) bool { return self.x == other.x and self.y == other.y; } }; pub const Size = struct { const Self = @This(); width: u32 = 0, height: u32 = 0, pub fn size(self: *Self) usize { return self.width * self.height; } pub fn eq(self: Self, other: Self) bool { return self.width == other.width and self.height == other.height; } }; pub const Selection = struct { const Self = @This(); start: Point, end: Point, pub fn fromPoint(point: Point) Self { return .{ .start = point, .end = point, }; } pub fn asBox(self: Self) Box { return Box.fromCorners(self.start, self.end); } }; pub const Box = struct { const Self = @This(); position: Point = .{.x = 0, .y = 0,}, extents: Size = .{.width = 0, .height = 0,}, pub fn default() Self { return Self { .position = .{.x = 0, .y = 0,}, .extents = .{.width = 0, .height = 0,}, }; } pub fn eq(self: Self, other: Self) bool { return self.position.eq(other.position) and self.extents.eq(other.extents); } pub fn intersects(self: *const Self, other: *const Self) bool { const x1 = self.position.x; const y1 = self.position.y; const w1 = @intCast(isize, self.extents.width); const h1 = @intCast(isize, self.extents.height); const x2 = other.position.x; const y2 = other.position.y; const w2 = @intCast(isize, other.extents.width); const h2 = @intCast(isize, other.extents.height); // return self.x < other.x + other.width and // self.x + self.width > other.x and // self.y < other.y + other.height and // self.height + self.y > other.y; return x1 < x2 + w2 and x1 + w1 > x2 and y1 < y1 + h2 and y1 + h1 > y2; } pub fn fromCorners(a: Point, b: Point) Self { const top = @min(a.y, b.y); const bot = @max(a.y, b.y); const left = @min(a.x, b.x); const right = @max(a.x, b.x); const pos = Point{.x = left, .y = top,}; const extents = Size{ .width = @intCast(u32, right - left), .height = @intCast(u32, bot - top), }; return Self { .position = pos, .extents = extents, }; } pub fn getInner(comptime T: anytype, v: anytype) T { switch (@typeInfo(T)) { .Float => { return @intToFloat(T, v); }, .Int => { return @intCast(T, v); }, else => { @compileError("can only cast positional scalar to integer or floating point types"); }, } } pub fn getCenter(self: *const Self) Point { return .{ .x = self.position.x + @intCast(i32, self.extents.width / 2), .y = self.position.y + @intCast(i32, self.extents.height / 2), }; } pub fn getX(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.x); } pub fn getY(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.y); } /// lowest X-axis value pub fn getLeftMost(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.x); } /// highest X-axis value pub fn getRightMost(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.x + self.getWidth(isize)); } /// lowest Y-axis value pub fn getTopMost(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.y); } /// highest Y-axis value pub fn getBottomMost(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.y + self.getHeight(isize)); } pub fn getWidth(self: *const Self, comptime T: anytype) T { return getInner(T, self.extents.width); } pub fn getHeight(self: *const Self, comptime T: anytype) T { return getInner(T, self.extents.height); } pub fn translate(self: *Self, delta: Point) void { self.position.add(delta); } pub fn translated(self: Self, delta: Point) Self { var new = self; new.translate(delta); return new; } pub fn contains(self: *const Self, point: Point) bool { const x1 = self.position.x; const y1 = self.position.y; const w1 = @intCast(isize, self.extents.width); const h1 = @intCast(isize, self.extents.height); const x2 = point.x; const y2 = point.y; // return self.x <= point.x and self.x + self.width > point.x and // self.y <= point.y and self.y + self.height > point.y; return x1 <= x2 and x1 + w1 > x2 and y1 <= y2 and y1 + h1 > y2; } pub fn size(self: *Self) usize { return self.extents.size(); } pub fn parseFromStr(str: [:0]const u8) !struct { box: Box, label: ?[]u8, pub fn destroy(self: @This()) void { if (self.label) |label| { std.c.free(label.ptr); } } } { var box: Box = undefined; var label: ?[*] u8 = null; const Stdio = @cImport({@cInclude("stdio.h");@cInclude("string.h");}); if (Stdio.sscanf( str.ptr, "%d,%d %dx%d %m[^\n]", &box.position.x, &box.position.y, &box.extents.width, &box.extents.height, &label, ) < 4) { return error.ScanFFailed; } const lbl = if (label == null) null else label.?[0..Stdio.strlen(label)]; return .{ .box = box, .label = lbl, }; } }; pub const Boxes = struct { const Self = @This(); boxes: std.ArrayList(Box), const Iterator = struct { items: []Box, i: usize = 0, pub fn next(it: *@This()) ?*Box { if (it.i < it.items.len) { const item = &it.items[it.i]; it.i += 1; return item; } else { return null; } } }; const Containing = struct { iter: Iterator, point: Point, pub fn next(it: *@This()) ?*Box { while (it.iter.next()) |item| { if (item.contains(it.point)) { return item; } } return null; } }; pub fn init(alloc: std.mem.Allocator) Self { const boxes = std.ArrayList(Box).init(alloc); return Self { .boxes = boxes, }; } pub fn deinit(self: Self) void { self.boxes.deinit(); } pub fn iter(self: *const Self) Iterator { return .{ .items = self.boxes.items, }; } pub fn iterBoxesContaining(self: *Self, point: Point) Containing { return .{ .iter = self.iter(), .point = point, }; } fn findClosestItemTo(it_: anytype, point: Point) ?*Box { var closest: ?*Box = null; var closest_distance: ?f32 = null; var it = it_; while (it.next()) |box| { const distance = box.getCenter().distanceTo(point); if (closest_distance == null or distance < closest_distance.? or (distance == closest_distance.? and closest.?.extents.size() > box.extents.size()) ) { closest_distance = distance; closest = box; } } return closest; } pub fn findClosestBoxTo(self: *Self, point: Point) ?*Box { return findClosestItemTo(self.iter(), point); } pub fn findClosestBoxToAndContaining(self: *Self, point: Point) ?*Box { return findClosestItemTo(self.iterBoxesContaining(point), point); } }; test "boxes" { std.testing.refAllDecls(Boxes); const a1 = Box{.extents = .{.width = 10, .height = 10,}}; const a2 = Box{.extents = .{.width = 20, .height = 20,}}; const a3 = Box{ .position = .{.x = 0, .y = 0,}, .extents = .{.width = 20, .height = 20,}, }; const a4 = Box{ .position = .{.x = 5, .y = 5,}, .extents = .{.width = 10, .height = 10,}, }; var boxes = Boxes.init(std.testing.allocator); defer boxes.deinit(); try boxes.boxes.append(a1); try boxes.boxes.append(a2); { std.debug.print("iter boxes..\n", .{}); var it = boxes.iter(); while (it.next()) |next| { std.debug.print("item: {?}\n", .{next}); } } const point = Point{.x = 15, .y = 15}; { std.debug.print("iter boxes containing {?}..\n", .{point}); var it = boxes.iterBoxesContaining(point); while (it.next()) |next| { std.debug.print("item: {?}\n", .{next}); } } var it = boxes.iterBoxesContaining(.{.x = 18, .y = 15}); std.debug.assert(a2.contains(.{.x = 18, .y = 15})); std.debug.assert( it.next().?.eq(a2)); try boxes.boxes.append(a3); try boxes.boxes.append(a4); } test "parse box" { std.testing.refAllDecls(Box); { const expected = Box{ .position = .{.x = 10, .y = 10,}, .extents = .{.width = 100, .height = 100,}, }; const box = try Box.parseFromStr("10,10 100x100 cool_box"); defer box.destroy(); std.debug.assert(box.box.eq(expected)); std.debug.assert(std.mem.eql(u8, box.label.?, "cool_box")); } { const expected = Box{ .position = .{.x = 10, .y = -10,}, .extents = .{.width = 100, .height = 100,}, }; const box = try Box.parseFromStr("10,-10 100x100 cool_\nbox"); defer box.destroy(); std.debug.assert(box.box.eq(expected)); std.debug.assert(std.mem.eql(u8, box.label.?, "cool_")); } { const expected = Box{ .position = .{.x = 10, .y = 10,}, .extents = .{.width = 100, .height = 100,}, }; const box = try Box.parseFromStr("10,10 100x100 \n"); defer box.destroy(); std.debug.assert(box.box.eq(expected)); std.debug.assert(box.label == null); } } test "box" { std.testing.refAllDecls(Box); const a = Box.default(); const a2 = Box{.extents = .{.width = 1, .height = 1,}}; const b = Box.default(); const b2 = Box{.extents = .{.width = 1, .height = 1,}}; std.debug.assert(!a.contains(.{.x = 0, .y = 0})); std.debug.assert(!a.intersects(&b)); std.debug.assert(a2.contains(.{.x = 0, .y = 0})); std.debug.assert(a2.intersects(&b2)); const c1 = Box{ .position = .{.x = 1, .y = 1,}, .extents = .{.width = 2, .height = 2,}, }; const c2 = Box{ .position = .{.x = 1, .y = 1,}, .extents = .{.width = 1, .height = 1,}, }; std.debug.assert(!c1.intersects(&a2)); std.debug.assert(c1.intersects(&c2)); }