464 lines
12 KiB
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));
|
|
}
|