zlurp/box.zig

464 lines
12 KiB
Zig

///! 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));
}