Author:ptrace
Comitter:ptrace
Date:2026-05-05 04:59:35 UTC
diff --git a/src/cells.jai b/src/cells.jai
new file mode 100644
index 0000000..8eb0395
--- /dev/null
+++ b/src/cells.jai
@@ -0,0 +1,204 @@
cells_init :: (tree_copy: []int) {
tree = tree_copy;
row_count, col_count = dimensions(tree.count);
}
cells_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
width := col_count * CELL_WIDTH_HEIGHT;
height := row_count * CELL_WIDTH_HEIGHT;
center := Vector2.{ width / 2.0, height / 2.0 };
zoom_x := (SCREEN_WIDTH * 0.7) / width;
zoom_y := (SCREEN_HEIGHT * 0.7) / height;
fit_zoom := ifx zoom_x < zoom_y then zoom_x else zoom_y;
cam := camera;
cam.zoom = fit_zoom;
cam.offset = { SCREEN_WIDTH / 2.0, SCREEN_HEIGHT / 2.0 };
cam.target = center;
#if update_camera then camera = cam;
return cam;
}
cells_draw_2d :: () {
code :: #code {
label: string;
color: Color;
if it == {
case 0;
label = "R";
color = COLOR_NODE_ROOT;
case NODE_INTERNAL;
label = "N";
color = COLOR_NODE_INTERNAL;
case NODE_EMPTY;
color = COLOR_NODE_EMPTY;
case;
label = tprint("%", it);
color = COLOR_NODE_VALUE;
}
if CheckCollisionPointRec(mouse_world, rect) {
color += Color.{ 30, 30, 30, 0 };
}
DrawRectangleRec(rect, color);
DrawRectangleLinesEx(rect, 1.0, COLOR_CELLS_BORDER);
do_text(label, { rect.x, rect.y }, 30, .CENTER, WHITE);
do_text(
tprint("%", it_index),
{ rect.x, rect.y },
10,
.NORTH_WEST,
WHITE
);
}
traverse_tree_array(code);
}
cells_draw_screen :: () {
info := to_c_string(tprint("Array Items %", tree.count));
DrawText(
info,
SCREEN_WIDTH - MeasureText(info, 18) - 16,
SCREEN_HEIGHT - 24,
18,
{ 160, 160, 200, 220 }
);
code :: #code {
if CheckCollisionPointRec(
mouse_world, rect
) {
text := tooltip_text(it_index, it);
tooltip(mouse_screen, text);
}
}
traverse_tree_array(code);
}
#scope_file
tree: []int;
row_count, col_count: int;
CELL_WIDTH_HEIGHT :: 100.0;
CELL_ASPECT_RATIO :float: #run 16.0 / 9.0;
CELL_GAP :: 5.0;
/** Overkill, but maybe we need it */
Anchor :: enum {
CENTER;
NORTH;
EAST;
SOUTH;
WEST;
NORTH_EAST;
NORTH_WEST;
SOUTH_EAST;
SOUTH_WEST;
}
dimensions :: (length: int) -> (row: int, col: int) {
flen := cast(float, length);
row := ceil(sqrt(flen / CELL_ASPECT_RATIO));
col := ceil(flen / row);
return cast(int, row), cast(int, col);
}
traverse_tree_array :: ($code1: Code) {
rect: Rectangle;
rect.x = 0.0;
rect.y = 0.0;
rect.width = CELL_WIDTH_HEIGHT;
rect.height = CELL_WIDTH_HEIGHT;
idx_col := col_count;
for tree {
if idx_col {
#insert,scope() code1;
rect.x += CELL_WIDTH_HEIGHT + CELL_GAP;
idx_col -= 1;
} else {
rect.y += CELL_WIDTH_HEIGHT + CELL_GAP;
rect.x = 0.0;
idx_col = col_count;
}
}
}
/** Overkill, but maybe we need it */
do_text :: (text: string, pos: Vector2, size: s32, anchor: Anchor, color: Color) {
label_c := to_c_string(text);
tw := MeasureTextEx(GetFontDefault(), label_c, xx size, 1);
local_pos: Vector2;
PADDING :: Vector2.{ 6.0, 4.0 };
if anchor == {
case .CENTER;
local_pos.x = pos.x + (CELL_WIDTH_HEIGHT / 2.0) - (tw.x / 2.0);
local_pos.y = pos.y + (CELL_WIDTH_HEIGHT / 2.0) - (tw.y / 2.0);
case .NORTH;
local_pos = pos + { CELL_WIDTH_HEIGHT / 2.0 - (tw.x / 2.0), PADDING.y };
case .EAST;
local_pos = pos + {
CELL_WIDTH_HEIGHT - (tw.x + PADDING.x),
CELL_WIDTH_HEIGHT / 2.0 - (tw.y / 2.0)
};
case .SOUTH;
local_pos = pos + {
CELL_WIDTH_HEIGHT / 2.0 - (tw.x / 2.0),
(CELL_WIDTH_HEIGHT - PADDING.y - tw.y)
};
case .WEST;
local_pos = pos + {
PADDING.x,
CELL_WIDTH_HEIGHT / 2.0 - (tw.y / 2.0)
};
case .NORTH_EAST;
local_pos = pos + { CELL_WIDTH_HEIGHT - PADDING.x - tw.x, PADDING.y };
case .NORTH_WEST;
local_pos = pos + PADDING;
case .SOUTH_EAST;
local_pos = pos + {
CELL_WIDTH_HEIGHT - PADDING.x - tw.x,
CELL_WIDTH_HEIGHT - PADDING.y - tw.y
};
case .SOUTH_WEST;
local_pos = pos + {
PADDING.x,
CELL_WIDTH_HEIGHT - PADDING.y - tw.y
};
}
DrawText(
label_c,
xx local_pos.x,
xx local_pos.y,
size,
color
);
}
diff --git a/src/gui.jai b/src/gui.jai
new file mode 100644
index 0000000..ddf4902
--- /dev/null
+++ b/src/gui.jai
@@ -0,0 +1,59 @@
gui_init :: () {
}
gui_draw_screen :: () {
using container;
x = (GetScreenWidth() / 2.0) - (WIDTH / 2.0);
y = GetScreenHeight() - HEIGHT;
width = WIDTH;
height = HEIGHT;
left := rcut_from_left(*container, 0.333333);
mid := rcut_from_left(*container, 0.5);
right := rcut_from_left(*container, 1.0);
SIZE :: 30.0;
draw_indicator(left, .TRAD_TREE);
draw_indicator(mid, .HYPER_TREE);
draw_indicator(right, .CELLS);
draw_text(left, "vTree", SIZE, .TRAD_TREE);
draw_text(mid, "hTree", SIZE, .HYPER_TREE);
draw_text(right, "Cells", SIZE, .CELLS);
}
#scope_file
container: Rectangle;
WIDTH :: 420.0;
HEIGHT :: 60.0;
draw_text :: (rect: Rectangle, text: string, size: float, m: Mode) {
color := ifx mode == m then COLOR_BACKGROUND else WHITE;
ctext := to_c_string(text);
mt := MeasureTextEx(GetFontDefault(), ctext, size, 1.0);
x := rect.x + (rect.width / 2.0) - (mt.x / 2.0) - 3.0;
y := rect.y + (rect.height / 2.0) - (mt.y / 2.0);
DrawText(ctext, xx x, xx y, xx size, color);
}
draw_indicator :: (rect: Rectangle, m: Mode) {
if mode == m
then DrawRectangleRec(rect, WHITE);
else DrawRectangleLinesEx(rect, 1.0, WHITE);
}
diff --git a/src/input.jai b/src/input.jai
new file mode 100644
index 0000000..9c5b5d5
--- /dev/null
+++ b/src/input.jai
@@ -0,0 +1,152 @@
Mode :: enum #specified {
TRAD_TREE :: 0;
HYPER_TREE :: 1;
CELLS :: 2;
}
input :: () {
/* ----------- *
* Zoom & Drag *
* ----------- */
if IsKeyDown(.LEFT_SHIFT) {
dragging = false;
if !selecting then SetMouseCursor(.DEFAULT);
if IsMouseButtonPressed(.LEFT) {
selecting = true;
selection.x = mouse_world.x;
selection.y = mouse_world.y;
SetMouseCursor(.RESIZE_NESW);
}
}
if IsMouseButtonPressed(.LEFT) {
drag_start = mouse_screen;
dragging = true;
SetMouseCursor(.RESIZE_ALL);
}
if IsMouseButtonReleased(.LEFT) {
dragging = false;
SetMouseCursor(.DEFAULT);
if selecting {
selecting = false;
zoom_to_selection();
}
}
if IsMouseButtonPressed(.RIGHT) {
if selecting then selecting = false;
}
if dragging {
camera.target.x -= (mouse_screen.x - drag_start.x) / camera.zoom;
camera.target.y -= (mouse_screen.y - drag_start.y) / camera.zoom;
drag_start = mouse_screen;
}
wheel := GetMouseWheelMove();
if wheel != 0.0 {
before := Vector2.{
camera.target.x + (mouse_screen.x - camera.offset.x) / camera.zoom,
camera.target.y + (mouse_screen.y - camera.offset.y) / camera.zoom
};
camera.zoom *= (1.0 + wheel * 0.1);
// TODO: Maybe lerp?
// t := 1.0 - exp(-8.0 * frame_time);
// camera.zoom *= lerp(thing1, thing2, t);
camera.zoom = clamp(camera.zoom, 0.1, 5.0);
camera.target.x = before.x - (mouse_screen.x - camera.offset.x) / camera.zoom;
camera.target.y = before.y - (mouse_screen.y - camera.offset.y) / camera.zoom;
}
/* ----- *
* Modes *
* ----- */
if IsKeyPressed(.F1) {
if mode == .TRAD_TREE {
vtree_fit_zoom();
} else {
mode = .TRAD_TREE;
state_switch(mode);
}
}
else if IsKeyPressed(.F2) {
if mode == .HYPER_TREE {
// tbd
} else {
mode = .HYPER_TREE;
// state_switch(mode);
}
}
else if IsKeyPressed(.F3) {
if mode == .CELLS {
cells_fit_zoom();
} else {
mode = .CELLS;
state_switch(mode);
}
}
}
input_draw_selection :: () {
if !selecting return;
using selection;
width = mouse_world.x - x;
height = mouse_world.y - y;
DrawRectangleLinesEx(selection, 2.0, GREEN);
}
// flag_has :: (flag: Mode) -> bool {
// return cast(bool, mode & flag);
// }
// flag_add :: (flag: Mode) {
// mode |= flag;
// }
// flag_remove :: (flag: Mode) {
// mode &= ~flag;
// }
#scope_file
drag_start: Vector2;
dragging: bool;
selection: Rectangle;
selecting: bool;
zoom_to_selection :: () {
using selection;
center := Vector2.{ x + width / 2.0, y + height / 2.0 };
zoom_x := cast(float, GetScreenWidth() / width);
zoom_y := cast(float, GetScreenHeight() / height);
new_zoom := min(zoom_x, zoom_y) * 0.9;
camera.offset = { GetScreenWidth() / 2.0, GetScreenHeight() / 2.0 };
camera.target = center;
camera.zoom = new_zoom;
}
diff --git a/src/main.jai b/src/main.jai
index 170e051..8a26c95 100644
--- a/src/main.jai
+++ b/src/main.jai
@@ -5,29 +5,26 @@
#import "Print_Vars";
#import "raylib";
#load "tree_viz.jai";
#load "rect_cut.jai";
#load "gui.jai";
#load "input.jai";
#load "tooltip.jai";
#load "state.jai";
#load "vtree.jai";
#load "cells.jai";
/*
(0)
/ \
(1) (2)
/ \
(3, 4) (5, 6)
l = 2 * i + 1
r = 2 * i + 2
parent = floor((i - 1) / 2)
[
0, 1, 2,
]
/** Ideas / Todo
- Check the CELLS array, it looks scuffed!
- Hot reload a binary dump
- Update the binary dump
- Half the size of the nodes with every depth
- Smooth zoom (maybe)
- Different font
- Generic approach for other types of info data in the tooltip
- Second option for visualizing the tree -> https://en.wikipedia.org/wiki/Hyperbolic_tree
*/
@@ -47,8 +44,14 @@ rects := Rectangle.[
tree: [..]int;
bvs: [..]Aabb;
mode: Mode;
camera: Camera2D;
frame_time: float;
mouse_screen: Vector2;
mouse_world: Vector2;
color_choice := Color.[
ORANGE,
YELLOW,
@@ -60,6 +63,13 @@ NODE_ROOT :: -9;
NODE_INTERNAL :: -7;
NODE_EMPTY :: -1;
COLOR_NODE_ROOT :: Color.{ 138, 62, 81, 255 };
COLOR_NODE_INTERNAL :: Color.{ 30, 60, 120, 255 };
COLOR_NODE_EMPTY :: #run COLOR_BACKGROUND + { 10, 10, 10, 0 };
COLOR_NODE_VALUE :: Color.{ 30, 110, 70, 255 };
COLOR_CELLS_BORDER :: #run COLOR_BACKGROUND + { 70, 70, 75, 0 };
COLOR_BACKGROUND :: Color.{ 42, 42, 60, 255 };
DRAW_BV :: true;
@@ -71,6 +81,7 @@ SCREEN_HEIGHT :: 1080;
TREE_VIEW :: true;
Aabb :: struct {
upper_bound: Vector2;
lower_bound: Vector2;
@@ -116,78 +127,81 @@ Node :: struct {
// Refit
}
bounds: Treev_Bounds;
tree_copy := array_copy(tree);
#if TREE_VIEW then treev_init(tree, *bounds);
vtree_init(tree_copy);
cells_init(tree_copy);
state_vtree: State;
state_cells: State;
{
using bounds;
camera.zoom = fit_zoom;
camera.offset = { SCREEN_WIDTH / 2.0, SCREEN_HEIGHT / 2.0 };
camera.target = { (min.x + max.x) / 2.0, (min.y + max.y) / 2.0 };
using state_cells;
id = .CELLS;
camera = cells_fit_zoom();
}
drag_start: Vector2;
dragging: bool;
{
using state_vtree;
id = .TRAD_TREE;
camera = vtree_fit_zoom();
}
state_init(mode, state_vtree, state_cells);
push_allocator(temp);
while !WindowShouldClose() {
if IsMouseButtonPressed(.LEFT) {
drag_start = GetMousePosition();
dragging = true;
}
if IsMouseButtonReleased(.LEFT) then dragging = false;
if dragging {
current := GetMousePosition();
camera.target.x -= (current.x - drag_start.x) / camera.zoom;
camera.target.y -= (current.y - drag_start.y) / camera.zoom;
drag_start = current;
}
gui_init();
wheel := GetMouseWheelMove();
if wheel != 0.0 {
mouse_screen := GetMousePosition();
before := Vector2.{
camera.target.x + (mouse_screen.x - camera.offset.x) / camera.zoom,
camera.target.y + (mouse_screen.y - camera.offset.y) / camera.zoom
};
camera.zoom *= (1.0 + wheel * 0.1);
if camera.zoom < 0.1 then camera.zoom = 0.1;
if camera.zoom > 5.0 then camera.zoom = 5.0;
push_allocator(temp);
while !WindowShouldClose() {
frame_time = GetFrameTime();
camera.target.x = before.x - (mouse_screen.x - camera.offset.x) / camera.zoom;
camera.target.y = before.y - (mouse_screen.y - camera.offset.y) / camera.zoom;
}
mouse_screen = GetMousePosition();
mouse_world = GetScreenToWorld2D(mouse_screen, camera);
input();
BeginDrawing();
defer EndDrawing();
ClearBackground({ 42, 42, 60, 255 });
ClearBackground(COLOR_BACKGROUND);
gx: s32;
while gx < SCREEN_WIDTH {
DrawLine(gx, 0, gx, SCREEN_HEIGHT, { 255,255,255,12 });
DrawLine(gx, 0, gx, SCREEN_HEIGHT, { 255, 255, 255, 12 });
gx += 40;
}
gy: s32;
while gy < SCREEN_HEIGHT {
DrawLine(0, gy, SCREEN_WIDTH, gy, { 255,255,255,12 });
DrawLine(0, gy, SCREEN_WIDTH, gy, { 255, 255, 255, 12 });
gy += 40;
}
{
BeginMode2D(camera);
defer EndMode2D();
#if TREE_VIEW then treev_draw();
if mode == {
case .TRAD_TREE; vtree_draw_2d();
case .HYPER_TREE; // Todo;
case .CELLS; cells_draw_2d();
}
input_draw_selection();
}
#if TREE_VIEW then treev_screen_draw();
if mode == {
case .TRAD_TREE; vtree_draw_screen();
case .HYPER_TREE; // Todo;
case .CELLS; cells_draw_screen();
}
DrawFPS(10, 10);
DrawText(TextFormat("Frame Time: % s", GetFrameTime()), 10, 40, 20, WHITE);
DrawText(TextFormat("Frame Time: % s", frame_time), 10, 40, 20, WHITE);
DrawText(TextFormat("Mouse (World): %", mouse_world), 10, 80, 20, WHITE);
DrawText(TextFormat("Mouse (Screen): %", mouse_screen), 10, 120, 20, WHITE);
gui_draw_screen();
reset_temporary_storage();
}
@@ -357,3 +371,14 @@ aabb_get_sa :: (a: Aabb, b: Aabb) -> float {
return sa;
}
operator + :: (a: Color, b: Color) -> Color {
c: Color = ---;
c.r = a.r + b.r;
c.g = a.g + b.g;
c.b = a.b + b.b;
c.a = a.a + b.a;
return c;
}
diff --git a/src/rect_cut.jai b/src/rect_cut.jai
new file mode 100644
index 0000000..fa2c8d0
--- /dev/null
+++ b/src/rect_cut.jai
@@ -0,0 +1,42 @@
/** C R E D I T
Idea from: https://halt.software/p/rectcut-for-dead-simple-ui-layouts
Code from: https://solarium.technology/
*/
rcut_from_right :: inline (using rect: *$T/interface Rectangle, percentage: float) -> T { return rcut_from_right_fixed (rect, rect.width * percentage); }
rcut_from_left :: inline (using rect: *$T/interface Rectangle, percentage: float) -> T { return rcut_from_left_fixed (rect, rect.width * percentage); }
rcut_from_top :: inline (using rect: *$T/interface Rectangle, percentage: float) -> T { return rcut_from_top_fixed (rect, rect.height * percentage); }
rcut_from_bottom :: inline (using rect: *$T/interface Rectangle, percentage: float) -> T { return rcut_from_bottom_fixed (rect, rect.height * percentage); }
rcut_from_right_fixed :: inline (using rect: *$T/interface Rectangle, n: float) -> T {
result := T.{ x + width - n, y, n, height };
width = max(width - n, 0);
return result;
}
rcut_from_left_fixed :: inline (using rect: *$T/interface Rectangle, n: float) -> T {
result := T.{ x, y, n, height };
x += n;
width = max(width - n, 0);
return result;
}
rcut_from_top_fixed :: inline (using rect: *$T/interface Rectangle, n: float) -> T {
result := T.{ x, y, width, n };
y += n;
height = max(height - n, 0);
return result;
}
rcut_from_bottom_fixed :: inline (using rect: *$T/interface Rectangle, n: float) -> T {
result := T.{ x, y + height - n, width, n };
height = max(height - n, 0);
return result;
}
rcut_shrink :: inline (using rect: $T/interface Rectangle, n: float) -> T {
return { x + n, y + n, width - n*2, height - n*2 };
}
diff --git a/src/state.jai b/src/state.jai
new file mode 100644
index 0000000..9d73a38
--- /dev/null
+++ b/src/state.jai
@@ -0,0 +1,43 @@
State :: struct {
id: Mode;
camera: Camera2D;
}
state_init :: (current_mode: Mode, st: ..State) {
for st array_add(*states, it);
current = state_find(current_mode);
}
state_switch :: (state_id: Mode) {
idx := state_find(state_id);
new_state := *states[idx];
old_state := *states[current];
old_state.camera = camera;
camera = new_state.camera;
current = idx;
}
#scope_file
states: [..]State;
current: int;
state_find :: (id: Mode) -> idx: int {
ok, idx := array_find(states, { id, {} }, find_by_id);
assert(ok, "Should not happen @ state_find");
return idx;
}
find_by_id :: (a: State, b: State) -> bool {
if a.id == b.id return true;
return false;
}
diff --git a/src/tooltip.jai b/src/tooltip.jai
new file mode 100644
index 0000000..af87eff
--- /dev/null
+++ b/src/tooltip.jai
@@ -0,0 +1,268 @@
tooltip :: (mouse: Vector2, text: string) {
update_text_render :: () #expand {
array_reset_keeping_memory(*`codepoints);
array_reset_keeping_memory(*`offsets);
`rect_padded = rect;
`rect_padded.x += PADDING;
`rect_padded.y += PADDING;
`rect_padded.width -= PADDING;
`text_height = wrap_text(
GetFontDefault(),
to_c_string(`text),
*`codepoints,
*`offsets,
`rect_padded,
`FONT_SIZE,
`SPACING,
`MY_COLOR
);
}
BACKGROUND :: Color.{ 34, 31, 45, 242 };
OFFSET :: Vector2.{ 24.0, 33.0 };
LINES :: Color.{ 99, 90, 122, 240 };
FONT_SIZE :: 20.0;
SPACING :: 4.0;
PADDING :: 16.0;
MY_COLOR :: WHITE;
rect: Rectangle;
rect.x = mouse.x + OFFSET.x;
rect.y = mouse.y + OFFSET.y;
rect.width = #run 14.0 * 34;
rect.height = #run PADDING * 1.5;
codepoints: [..]s32;
offsets: [..]Vector2;
rect_padded: Rectangle;
text_height: float;
update_text_render();
rect.height += text_height;
screen_w := cast(float) GetScreenWidth();
screen_h := cast(float) GetScreenHeight();
if rect.x + rect.width > screen_w {
rect.x = mouse.x - OFFSET.x - rect.width;
update_text_render();
}
if rect.y + rect.height > screen_h {
rect.y = mouse.y - OFFSET.y - rect.height;
update_text_render();
}
DrawRectangleRounded(rect, 0.1, 6, BACKGROUND);
DrawRectangleRoundedLinesEx(rect, 0.1, 6, 1.0, LINES);
draw_text(
GetFontDefault(),
codepoints,
offsets,
FONT_SIZE,
SPACING,
MY_COLOR
);
}
tooltip_text :: (index: int, value: int) -> string {
node_type: string;
bv_upper: string;
bv_lower: string;
sa_info: string;
if index == 0 {
node_type = "Root Node";
} else {
if value == {
case NODE_EMPTY;
node_type = "Empty Node";
case NODE_INTERNAL;
node_type = "Internal Node";
case;
node_type = "Child Node";
bv := bvs[value];
bv_upper = tprint("\nBV Upper Bound: %", bv.upper_bound);
bv_lower = tprint("\nBV Lower Bound: %", bv.lower_bound);
sa := aabb_bv_compute_surface_area(bv);
sa_info = tprint("\nSurface Area: %", sa);
}
}
sb: String_Builder;
print_to_builder(*sb, "Type: %\n", node_type);
print_to_builder(*sb, "Index: %\n", index);
append(*sb, bv_upper);
append(*sb, bv_lower);
append(*sb, sa_info);
text := builder_to_string(*sb);
return text;
}
#scope_file
/** C R E D I T
Original proc `wrap_text` is from:
- @Raysan (Original Author), https://github.com/raysan5
- @ahmedqarmout2 (Jai Adoption), https://github.com/ahmedqarmout2
*/
wrap_text :: (
font: Font,
text: *u8,
codepoints: *[..]s32,
offsets: *[..]Vector2,
rec: Rectangle,
font_size: float,
spacing: float,
tint: Color,
word_wrap: bool = true
)
-> text_height: float
{
length: s32 = cast(s32)TextLength(text);
text_offset_y: float = 0.0;
text_offset_x: float = 0.0;
scale_factor: float = font_size / cast(float)font.baseSize;
my_enum :: enum { MEASURE_STATE :: 0; DRAW_STATE :: 1; }
state: my_enum = ifx word_wrap then .MEASURE_STATE else .DRAW_STATE;
start_line: s32 = -1; // Index where to begin drawing (where a line begins)
end_line: s32 = -1; // Index where to stop drawing (where a line ends)
lastk: s32 = -1; // Holds last value of the character position
i: s32 = 0;
k: s32 = 0;
while i < length {
defer i += 1;
defer k += 1;
codepoint_byte_count: s32 = 0;
codepoint: s32 = GetCodepoint(*text[i], *codepoint_byte_count);
index: s32 = GetGlyphIndex(font, codepoint);
if codepoint == 0x3f then codepoint_byte_count = 1;
i += (codepoint_byte_count - 1);
glyph_width: float = 0;
if codepoint != #char "\n" {
glyph_width = ifx (font.glyphs[index].advanceX == 0)
then font.recs[index].width * scale_factor
else font.glyphs[index].advanceX * scale_factor;
if (i + 1 < length) glyph_width = glyph_width + spacing;
}
if (state == .MEASURE_STATE) {
if codepoint == #char " "
|| codepoint == #char "\t"
|| codepoint == #char "\n"
then end_line = i;
if text_offset_x + glyph_width > rec.width {
end_line = ifx (end_line < 1) then i else end_line;
if i == end_line then end_line -= codepoint_byte_count;
if (start_line + codepoint_byte_count) == end_line
then end_line = (i - codepoint_byte_count);
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
else if i + 1 == length
{
end_line = i;
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
else if codepoint == #char "\n"
{
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
if state == .DRAW_STATE {
text_offset_x = 0;
i = start_line;
glyph_width = 0;
tmp: s32 = lastk;
lastk = k - 1;
k = tmp;
}
} else {
if codepoint == #char "\n" {
if !word_wrap {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
}
} else {
if !word_wrap && text_offset_x + glyph_width > rec.width {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
}
if codepoint != #char " " && codepoint != #char "\t" {
array_add(codepoints, codepoint);
array_add(offsets, { rec.x + text_offset_x, rec.y + text_offset_y });
}
}
if word_wrap && i == end_line {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
start_line = end_line;
end_line = -1;
glyph_width = 0;
k = lastk;
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
}
if text_offset_x != 0 || codepoint != #char " " then text_offset_x += glyph_width;
}
return text_offset_y;
}
draw_text :: (
font: Font,
codepoints: [..]s32,
offsets: [..]Vector2,
font_size: float,
spacing: float,
tint: Color
) {
assert(codepoints.count == offsets.count);
for codepoints {
x := offsets[it_index].x;
y := offsets[it_index].y;
DrawTextCodepoint(
font,
it,
{ x, y },
font_size,
tint
);
}
}
diff --git a/src/tree_viz.jai b/src/tree_viz.jai
deleted file mode 100644
index 29a543a..0000000
--- a/src/tree_viz.jai
+++ /dev/null
@@ -1,482 +0,0 @@
Treev_Bounds :: struct {
min, max: Vector2;
fit_zoom: float = 1.0;
}
treev_init :: (t: [..]int, bounds: *Treev_Bounds) {
tree = array_copy(t);
tree_size = tree.count;
assign_positions(0, xx PADDING, xx (SCREEN_WIDTH - PADDING), 0);
// for i: 0..node_count-1 log_vars(nodes[i]);
min, max: Vector2;
min.x = nodes[0].x - NODE_RADIUS;
max.x = nodes[0].x + NODE_RADIUS;
min.y = nodes[0].y - NODE_RADIUS;
max.y = nodes[0].y + NODE_RADIUS;
for i: 1..node_count-1 {
if nodes[i].x - NODE_RADIUS < min.x then min.x = nodes[i].x - NODE_RADIUS;
if nodes[i].x + NODE_RADIUS > max.x then max.x = nodes[i].x + NODE_RADIUS;
if nodes[i].y - NODE_RADIUS < min.y then min.y = nodes[i].y - NODE_RADIUS;
if nodes[i].y + NODE_RADIUS > max.y then max.y = nodes[i].y + NODE_RADIUS;
}
tree_w := max.x - min.x;
tree_h := max.y - min.y;
zoom_x := (SCREEN_WIDTH * 0.90) / tree_w;
zoom_y := (SCREEN_HEIGHT * 0.90) / tree_h;
fit_zoom := ifx zoom_x < zoom_y then zoom_x else zoom_y;
bounds.min = min;
bounds.max = max;
bounds.fit_zoom = fit_zoom;
}
treev_draw :: () {
mouse := GetMousePosition();
mouse_world := GetScreenToWorld2D(mouse, camera);
for i: 0..node_count-1 {
n := nodes[i];
idx_left := 2 * n.index + 1;
idx_right := 2 * n.index + 2;
found_l, lc := find_node(idx_left);
found_r, rc := find_node(idx_right);
if found_l {
DrawLineEx(
{ n.x, n.y }, { lc.x, lc.y }, 2.0, { 100, 180, 255, 160 }
);
}
if found_r {
DrawLineEx(
{ n.x, n.y }, { rc.x, rc.y }, 2.0, { 255, 140, 100, 160 }
);
}
tn_x := cast(s32, n.x);
tn_y := cast(s32, n.y);
is_internal := n.value == NODE_INTERNAL;
glow := ifx is_internal
then Color.{ 80, 160, 255, 60 }
else Color.{ 80, 255, 160, 60 };
DrawCircle(tn_x, tn_y, NODE_RADIUS + 6.0, glow);
fill := ifx is_internal
then Color.{ 30, 60, 120, 255 }
else Color.{ 30, 110, 70, 255 };
if CheckCollisionPointCircle(mouse_world, { n.x, n.y }, NODE_RADIUS) {
fill += Color.{ 30, 30, 30, 0 };
}
DrawCircle(tn_x, tn_y, xx NODE_RADIUS, fill);
border := ifx is_internal
then Color.{ 100, 180, 255, 255 }
else Color.{ 80, 255, 160, 255 };
DrawCircleLines(tn_x, tn_y, xx NODE_RADIUS, border);
label := ifx is_internal then "N" else tprint("%", n.value);
label_c := to_c_string(label);
fs := cast(s32, FONT1_SIZE);
tw := MeasureTextEx(GetFontDefault(), label_c, FONT1_SIZE, 1);
DrawText(
label_c,
xx (tn_x - tw.x / 2),
xx (tn_y - tw.y / 2),
FONT1_SIZE,
WHITE
);
idx_label := to_c_string(tprint("[%]", n.index));
sw := MeasureTextEx(GetFontDefault(), idx_label, FONT2_SIZE, 1);
DrawText(
idx_label,
xx (tn_x - sw.x / 2),
xx (tn_y - (NODE_RADIUS * 2)),
FONT2_SIZE,
{ 180, 180, 180, 180 }
);
}
}
treev_screen_draw :: () {
info := to_c_string(tprint("Nodes %", node_count));
DrawText(
info,
SCREEN_WIDTH - MeasureText(info, 14) - 16,
SCREEN_HEIGHT - 24,
14,
{ 160, 160, 200, 220 }
);
mouse := GetMousePosition();
for i: 0..node_count-1 {
n := nodes[i];
n_screen := GetWorldToScreen2D({ n.x, n.y }, camera);
if CheckCollisionPointCircle(mouse, n_screen, NODE_RADIUS * camera.zoom) {
text := tooltip_text(n);
tooltip(mouse, text);
}
}
}
#scope_file
tree: []int;
tree_size: int;
nodes: [NODES_MAX]Treev_Node;
node_count: int;
NODES_MAX :: 128;
NODE_RADIUS :: 28;
FONT1_SIZE :: 34;
FONT2_SIZE :: 16;
LEVEL_GAP_Y :: 160;
LEVEL_GAP_X :: 2;
PADDING :: 60;
Treev_Node :: struct {
index: int;
value: int;
x, y: float;
depth: int;
}
Range :: struct {
left: float;
right: float;
}
get_depth :: (idx: int) -> int {
d: int;
while idx > 0 {
idx = (idx - 1) / 2;
d += 1;
}
return d;
}
subtree_width :: (idx: int) -> int {
if idx >= tree_size || tree[idx] == NODE_EMPTY then return 0;
return 1 + subtree_width(2 * idx + 1) + subtree_width(2 * idx + 2);
}
assign_positions :: (idx: int, left: float, right: float, depth: int) {
if idx >= tree_size || tree[idx] == NODE_EMPTY return;
cx := (left + right) / 2.0;
cy := 80.0 + depth;
if node_count < NODES_MAX {
nodes[node_count].index = idx;
nodes[node_count].value = tree[idx];
nodes[node_count].x = cx * LEVEL_GAP_X;
nodes[node_count].y = cy * LEVEL_GAP_Y;
nodes[node_count].depth = depth;
node_count += 1;
}
mid := (left + right) / 2.0;
assign_positions(2 * idx + 1, left, mid, depth + 1);
assign_positions(2 * idx + 2, mid, right, depth + 1);
}
find_node :: (idx: int) -> (found: bool, node: Treev_Node) {
for nodes {
if it.index == idx return true, it;
}
return false, {};
}
tooltip_text :: (tvn: Treev_Node) -> string {
node_type: string;
bv_upper: string;
bv_lower: string;
sa_info: string;
if tvn.index == 0 {
node_type = "Root Node";
} else {
if tvn.value == {
case NODE_EMPTY; node_type = "Empty Node";
case NODE_INTERNAL;
node_type = "Internal Node";
case;
node_type = "Child Node";
bv := bvs[tvn.value];
bv_upper = tprint("\nBV Upper Bound: %", bv.upper_bound);
bv_lower = tprint("\nBV Lower Bound: %", bv.lower_bound);
sa := aabb_bv_compute_surface_area(bv);
sa_info = tprint("\nSurface Area: %", sa);
}
}
sb: String_Builder;
print_to_builder(*sb, "Type: %\n", node_type);
print_to_builder(*sb, "Index: %\n", tvn.index);
append(*sb, bv_upper);
append(*sb, bv_lower);
append(*sb, sa_info);
text := builder_to_string(*sb);
return text;
}
tooltip :: (mouse: Vector2, text: string) {
update_text_render :: () #expand {
array_reset_keeping_memory(*`codepoints);
array_reset_keeping_memory(*`offsets);
`rect_padded = rect;
`rect_padded.x += PADDING;
`rect_padded.y += PADDING;
`rect_padded.width -= PADDING;
`text_height = wrap_text(
GetFontDefault(),
to_c_string(`text),
*`codepoints,
*`offsets,
`rect_padded,
`FONT_SIZE,
`SPACING,
`MY_COLOR
);
}
BACKGROUND :: Color.{ 34, 31, 45, 242 };
OFFSET :: Vector2.{ 24.0, 33.0 };
LINES :: Color.{ 99, 90, 122, 240 };
FONT_SIZE :: 20.0;
SPACING :: 4.0;
PADDING :: 16.0;
MY_COLOR :: WHITE;
rect: Rectangle;
rect.x = mouse.x + OFFSET.x;
rect.y = mouse.y + OFFSET.y;
rect.width = #run 14.0 * 34;
rect.height = #run PADDING * 1.5;
codepoints: [..]s32;
offsets: [..]Vector2;
rect_padded: Rectangle;
text_height: float;
update_text_render();
rect.height += text_height;
screen_w := cast(float) GetScreenWidth();
screen_h := cast(float) GetScreenHeight();
if rect.x + rect.width > screen_w
rect.x = mouse.x - OFFSET.x - rect.width;
if rect.y + rect.height > screen_h
rect.y = mouse.y - OFFSET.y - rect.height;
update_text_render();
DrawRectangleRounded(rect, 0.1, 6, BACKGROUND);
DrawRectangleRoundedLinesEx(rect, 0.1, 6, 1.0, LINES);
draw_text(
GetFontDefault(),
codepoints,
offsets,
FONT_SIZE,
SPACING,
MY_COLOR
);
}
/** C R E D I T
Original proc `wrap_text` is from:
- @Raysan (Original Author), https://github.com/raysan5
- @ahmedqarmout2 (Jai Adoption), https://github.com/ahmedqarmout2
*/
wrap_text :: (
font: Font,
text: *u8,
codepoints: *[..]s32,
offsets: *[..]Vector2,
rec: Rectangle,
font_size: float,
spacing: float,
tint: Color,
word_wrap: bool = true
)
-> text_height: float
{
length: s32 = cast(s32)TextLength(text);
text_offset_y: float = 0.0;
text_offset_x: float = 0.0;
scale_factor: float = font_size / cast(float)font.baseSize;
my_enum :: enum { MEASURE_STATE :: 0; DRAW_STATE :: 1; }
state: my_enum = ifx word_wrap then .MEASURE_STATE else .DRAW_STATE;
start_line: s32 = -1; // Index where to begin drawing (where a line begins)
end_line: s32 = -1; // Index where to stop drawing (where a line ends)
lastk: s32 = -1; // Holds last value of the character position
i: s32 = 0;
k: s32 = 0;
while i < length {
defer i += 1;
defer k += 1;
codepoint_byte_count: s32 = 0;
codepoint: s32 = GetCodepoint(*text[i], *codepoint_byte_count);
index: s32 = GetGlyphIndex(font, codepoint);
if codepoint == 0x3f then codepoint_byte_count = 1;
i += (codepoint_byte_count - 1);
glyph_width: float = 0;
if codepoint != #char "\n" {
glyph_width = ifx (font.glyphs[index].advanceX == 0)
then font.recs[index].width * scale_factor
else font.glyphs[index].advanceX * scale_factor;
if (i + 1 < length) glyph_width = glyph_width + spacing;
}
if (state == .MEASURE_STATE) {
if codepoint == #char " "
|| codepoint == #char "\t"
|| codepoint == #char "\n"
then end_line = i;
if text_offset_x + glyph_width > rec.width {
end_line = ifx (end_line < 1) then i else end_line;
if i == end_line then end_line -= codepoint_byte_count;
if (start_line + codepoint_byte_count) == end_line
then end_line = (i - codepoint_byte_count);
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
else if i + 1 == length
{
end_line = i;
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
else if codepoint == #char "\n"
{
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
if state == .DRAW_STATE {
text_offset_x = 0;
i = start_line;
glyph_width = 0;
tmp: s32 = lastk;
lastk = k - 1;
k = tmp;
}
} else {
if codepoint == #char "\n" {
if !word_wrap {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
}
} else {
if !word_wrap && text_offset_x + glyph_width > rec.width {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
}
if codepoint != #char " " && codepoint != #char "\t" {
array_add(codepoints, codepoint);
array_add(offsets, { rec.x + text_offset_x, rec.y + text_offset_y });
}
}
if word_wrap && i == end_line {
text_offset_y += (font.baseSize + font.baseSize / 2) * scale_factor;
text_offset_x = 0;
start_line = end_line;
end_line = -1;
glyph_width = 0;
k = lastk;
state = ifx (state == .MEASURE_STATE) then .DRAW_STATE else .MEASURE_STATE;
}
}
if text_offset_x != 0 || codepoint != #char " " then text_offset_x += glyph_width;
}
return text_offset_y;
}
draw_text :: (
font: Font,
codepoints: [..]s32,
offsets: [..]Vector2,
font_size: float,
spacing: float,
tint: Color
) {
assert(codepoints.count == offsets.count);
for codepoints {
x := offsets[it_index].x;
y := offsets[it_index].y;
DrawTextCodepoint(
font,
it,
{ x, y },
font_size,
tint
);
}
}
operator + :: (a: Color, b: Color) -> Color {
c: Color = ---;
c.r = a.r + b.r;
c.g = a.g + b.g;
c.b = a.b + b.b;
c.a = a.a + b.a;
return c;
}
diff --git a/src/vtree.jai b/src/vtree.jai
new file mode 100644
index 0000000..309b448
--- /dev/null
+++ b/src/vtree.jai
@@ -0,0 +1,219 @@
vtree_init :: (tree_copy: []int) {
tree = tree_copy;
assign_positions(0, -(SCREEN_WIDTH / 2), SCREEN_WIDTH - (SCREEN_WIDTH / 2), 0);
}
vtree_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
min, max: Vector2;
min.x = nodes[0].x - NODE_RADIUS;
max.x = nodes[0].x + NODE_RADIUS;
min.y = nodes[0].y - NODE_RADIUS;
max.y = nodes[0].y + NODE_RADIUS;
for i: 1..node_count-1 {
if nodes[i].x - NODE_RADIUS < min.x then min.x = nodes[i].x - NODE_RADIUS;
if nodes[i].x + NODE_RADIUS > max.x then max.x = nodes[i].x + NODE_RADIUS;
if nodes[i].y - NODE_RADIUS < min.y then min.y = nodes[i].y - NODE_RADIUS;
if nodes[i].y + NODE_RADIUS > max.y then max.y = nodes[i].y + NODE_RADIUS;
}
tree_w := max.x - min.x;
tree_h := max.y - min.y;
zoom_x := (SCREEN_WIDTH * 0.9) / tree_w;
zoom_y := (SCREEN_HEIGHT * 0.9) / tree_h;
fit_zoom := ifx zoom_x < zoom_y then zoom_x else zoom_y;
cam := camera;
cam.zoom = fit_zoom;
cam.offset = { SCREEN_WIDTH / 2.0, SCREEN_HEIGHT / 2.0 };
cam.target = { (min.x + max.x) / 2.0, (min.y + max.y) / 2.0 };
#if update_camera then camera = cam;
return cam;
}
vtree_draw_2d :: () {
for i: 0..node_count-1 {
n := nodes[i];
idx_left := 2 * n.index + 1;
idx_right := 2 * n.index + 2;
found_l, lc := find_node(idx_left);
found_r, rc := find_node(idx_right);
if found_l {
DrawLineEx(
{ n.x, n.y }, { lc.x, lc.y }, 2.0, { 100, 180, 255, 160 }
);
}
if found_r {
DrawLineEx(
{ n.x, n.y }, { rc.x, rc.y }, 2.0, { 255, 140, 100, 160 }
);
}
// CLANK
node_scale := pow(DEPTH_SCALE_NODE, cast(float)n.depth);
font_scale := pow(DEPTH_SCALE_FONT, cast(float)n.depth);
node_r := NODE_RADIUS * node_scale;
font1_sz := max(6.0, FONT1_SIZE * font_scale);
font2_sz := max(4.0, FONT2_SIZE * font_scale);
tn_x := cast(s32, n.x);
tn_y := cast(s32, n.y);
is_internal := n.value == NODE_INTERNAL;
glow := ifx is_internal
then Color.{ 80, 160, 255, 60 }
else Color.{ 80, 255, 160, 60 };
DrawCircle(tn_x, tn_y, node_r + 6.0, glow);
fill := ifx is_internal
then COLOR_NODE_INTERNAL
else COLOR_NODE_VALUE;
if CheckCollisionPointCircle(mouse_world, { n.x, n.y }, node_r) {
fill += Color.{ 30, 30, 30, 0 };
}
DrawCircle(tn_x, tn_y, xx node_r, fill);
border := ifx is_internal
then Color.{ 100, 180, 255, 255 }
else Color.{ 80, 255, 160, 255 };
DrawCircleLines(tn_x, tn_y, xx node_r, border);
label := ifx is_internal then "N" else tprint("%", n.value);
label_c := to_c_string(label);
tw := MeasureTextEx(GetFontDefault(), label_c, xx font1_sz, 1);
DrawText(
label_c,
xx (tn_x - tw.x / 2),
xx (tn_y - tw.y / 2),
xx font1_sz,
WHITE
);
idx_label := to_c_string(tprint("[%]", n.index));
sw := MeasureTextEx(GetFontDefault(), idx_label, xx font2_sz, 1);
DrawText(
idx_label,
xx (tn_x - sw.x / 2),
xx (tn_y - (node_r * 2)),
xx font2_sz,
{ 180, 180, 180, 180 }
);
}
}
vtree_draw_screen :: () {
info := to_c_string(tprint("Nodes %", node_count));
DrawText(
info,
SCREEN_WIDTH - MeasureText(info, 18) - 16,
SCREEN_HEIGHT - 24,
18,
{ 160, 160, 200, 220 }
);
for i: 0..node_count-1 {
n := nodes[i];
// CLANK
node_scale := pow(DEPTH_SCALE_NODE, cast(float)n.depth);
node_r := NODE_RADIUS * node_scale;
n_screen := GetWorldToScreen2D({ n.x, n.y }, camera);
if CheckCollisionPointCircle(mouse_screen, n_screen, node_r * camera.zoom) {
text := tooltip_text(n.index, n.value);
tooltip(mouse_screen, text);
}
}
}
#scope_file
camera_local: Camera2D;
tree: []int;
nodes: [NODES_MAX]VTree_Node;
node_count: int;
NODES_MAX :: 128;
NODE_RADIUS :: 52;
FONT1_SIZE :: 69;
FONT2_SIZE :: 30;
LEVEL_GAP_Y :: 110;
LEVEL_GAP_X :: 2;
PADDING :: 60;
DEPTH_SCALE_NODE :: 0.8;
DEPTH_SCALE_FONT :: 0.75;
VTree_Node :: struct {
index: int;
value: int;
x, y: float;
depth: int;
}
Range :: struct {
left: float;
right: float;
}
get_depth :: (idx: int) -> int {
d: int;
while idx > 0 {
idx = (idx - 1) / 2;
d += 1;
}
return d;
}
subtree_width :: (idx: int) -> int {
if idx >= tree.count || tree[idx] == NODE_EMPTY then return 0;
return 1 + subtree_width(2 * idx + 1) + subtree_width(2 * idx + 2);
}
assign_positions :: (idx: int, left: float, right: float, depth: int) {
if idx >= tree.count || tree[idx] == NODE_EMPTY return;
cx := (left + right) / 2.0;
cy := cast(float)depth;
if node_count < NODES_MAX {
nodes[node_count].index = idx;
nodes[node_count].value = tree[idx];
nodes[node_count].x = cx * LEVEL_GAP_X;
nodes[node_count].y = cy * LEVEL_GAP_Y;
nodes[node_count].depth = depth;
node_count += 1;
}
mid := (left + right) / 2.0;
assign_positions(2 * idx + 1, left, mid, depth + 1);
assign_positions(2 * idx + 2, mid, right, depth + 1);
}
find_node :: (idx: int) -> (found: bool, node: VTree_Node) {
for nodes {
if it.index == idx return true, it;
}
return false, {};
}