Author:ptrace
Comitter:ptrace
Date:2026-05-08 00:57:13 UTC
diff --git a/src/cells.jai b/src/cells.jai
index ea3bd6e..3cba949 100644
--- a/src/cells.jai
+++ b/src/cells.jai
@@ -3,12 +3,15 @@
cells_init :: (tree_copy: []int) {
tree = tree_copy;
row_count, col_count = dimensions(tree.count);
cells_init :: (tree_copy: []int, stat: *Statistics) {
row_count, col_count = dimensions(tree_copy.count);
create_cells(tree_copy, stat);
// Note: if we do hot reloading, we have to manage memory here!
this_allocation_is_not_a_leak(cells.data);
}
cells_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
cells_fit_zoom :: () -> Camera2D {
width := col_count * CELL_WIDTH_HEIGHT;
height := row_count * CELL_WIDTH_HEIGHT;
@@ -23,85 +26,184 @@ cells_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
cam.offset = { SCREEN_WIDTH / 2.0, SCREEN_HEIGHT / 2.0 };
cam.target = center;
#if update_camera then camera = cam;
camera = cam;
return cam;
}
cells_draw_2d :: (stats: *Statistics) {
code :: #code {
label: string;
color: Color;
cells_draw_2d :: () {
assert(cells.count > 0, "Cell array empty");
if it_index == 0 {
label = "R";
color = COLOR_NODE_ROOT;
stats.nodes += 1;
first_cell := cells[0].rect;
last_cell := cells[cells.count-1].rect;
last_cell_in_first_row := cells[col_count].rect;
whole_cell: Rectangle; {
using whole_cell;
x = first_cell.x;
y = first_cell.y;
width = (last_cell_in_first_row.x + last_cell_in_first_row.width) - first_cell.x;
height = (last_cell.y + last_cell.width) - first_cell.y;
}
is_mouse_inside_any_cell := CheckCollisionPointRec(mouse_world, whole_cell);
for cells {
using it;
new_color := color;
inactive_color := color - { 0, 0, 0, 130 };
inactive_border := COLOR_CELLS_BORDER - { 0, 0, 0, 100 };
border_thickness := CELL_BORDER_THICK_NORMAL;
is_mouseover := CheckCollisionPointRec(mouse_world, rect);
if is_mouseover {
new_color += { 30, 30, 30, 0 };
border_thickness = CELL_BORDER_THICK_ACTIVE;
}
else {
if it == {
case NODE_INTERNAL;
label = "N";
color = COLOR_NODE_INTERNAL;
stats.nodes += 1;
case NODE_EMPTY;
color = COLOR_NODE_EMPTY;
stats.empty += 1;
case;
label = tprint("%", it);
color = COLOR_NODE_VALUE;
stats.children += 1;
if is_mouse_inside_any_cell {
if is_mouseover {
DrawRectangleRec(rect, new_color);
DrawRectangleLinesEx(rect, border_thickness, COLOR_CELLS_BORDER_HIGHLIGHT);
} else {
DrawRectangleRec(rect, inactive_color);
DrawRectangleLinesEx(rect, border_thickness, inactive_border);
}
} else {
DrawRectangleRec(rect, new_color);
DrawRectangleLinesEx(rect, border_thickness, COLOR_CELLS_BORDER);
}
}
{
parent :: #code {
new_color := cell.color + COLOR_HIGHLIGHT0;
DrawRectangleRec(cell.rect, new_color);
}
if CheckCollisionPointRec(mouse_world, rect) {
color += Color.{ 30, 30, 30, 0 };
child_left :: #code {
new_color := cell.color + COLOR_HIGHLIGHT0;
DrawRectangleRec(cell.rect, new_color);
}
DrawRectangleRec(rect, color);
DrawRectangleLinesEx(rect, 1.0, COLOR_CELLS_BORDER);
child_right :: #code {
new_color := cell.color + COLOR_HIGHLIGHT0;
DrawRectangleRec(cell.rect, new_color);
}
draw_hover_cells(parent, child_left, child_right);
}
{
parent :: #code {
border_color := cell.color + COLOR_HIGHLIGHT2;
DrawRectangleLinesEx(
cell.rect,
CELL_BORDER_THICK_ACTIVE,
border_color
);
do_text(
"P",
{ cell.rect.x, cell.rect.y },
CELL_INFO_FONT_SIZE,
.SOUTH_WEST,
border_color
);
}
child_left :: #code {
border_color := cell.color + COLOR_HIGHLIGHT2;
DrawRectangleLinesEx(
cell.rect,
CELL_BORDER_THICK_ACTIVE,
border_color
);
do_text(
"L",
{ cell.rect.x, cell.rect.y },
CELL_INFO_FONT_SIZE,
.SOUTH_WEST,
border_color
);
}
child_right :: #code {
border_color := cell.color + COLOR_HIGHLIGHT2;
DrawRectangleLinesEx(
cell.rect,
CELL_BORDER_THICK_ACTIVE,
border_color
);
do_text(
"R",
{ cell.rect.x, cell.rect.y },
CELL_INFO_FONT_SIZE,
.SOUTH_WEST,
border_color
);
}
draw_hover_cells(parent, child_left, child_right);
}
for cells {
using it;
do_text(label, { rect.x, rect.y }, 30, .CENTER, WHITE);
do_text(
tprint("%", it_index),
tprint("%", it.index),
{ rect.x, rect.y },
10,
.NORTH_WEST,
WHITE
);
}
traverse_tree_array(code, stats);
}
cells_draw_screen :: (stats: *Statistics) {
gui_statistics(stats);
code :: #code {
if CheckCollisionPointRec(
mouse_world, rect
) {
text := tooltip_text(it_index, it);
if !flag_has(.TOOLTIP) return;
for cells {
if !it.label continue;
if CheckCollisionPointRec(mouse_world, it.rect) {
text := tooltip_text(it_index, it.value);
tooltip(mouse_screen, text);
}
}
traverse_tree_array(code);
}
#scope_file
tree: []int;
cells: [..]Cell;
row_count, col_count: int;
CELL_INFO_FONT_SIZE :: 20;
CELL_BORDER_THICK_NORMAL :: 1.0;
CELL_BORDER_THICK_ACTIVE :: 2.0;
CELL_WIDTH_HEIGHT :: 100.0;
CELL_ASPECT_RATIO :float: #run 16.0 / 9.0;
CELL_GAP :: 5.0;
CELL_GAP :: 4.0;
Cell :: struct {
index: int;
value: int;
rect: Rectangle;
label: string;
color: Color;
}
/** Overkill, but maybe we need it */
Anchor :: enum {
@@ -124,31 +226,98 @@ dimensions :: (length: int) -> (row: int, col: int) {
return cast(int, row), cast(int, col);
}
traverse_tree_array :: ($code1: Code, stats: *Statistics = null) {
cell_pos_from_index :: (index: int) -> Rectangle {
col := index % col_count;
row := index / col_count;
return .{
x = col * (CELL_WIDTH_HEIGHT + CELL_GAP),
y = row * (CELL_WIDTH_HEIGHT + CELL_GAP),
width = CELL_WIDTH_HEIGHT,
height = CELL_WIDTH_HEIGHT,
};
}
create_cells :: (tree: []int, stat: *Statistics) {
CELL_WITH_GAP :: #run CELL_WIDTH_HEIGHT + CELL_GAP;
rect: Rectangle;
rect.x = 0.0;
rect.y = 0.0;
rect.width = CELL_WIDTH_HEIGHT;
rect.height = CELL_WIDTH_HEIGHT;
rect.x = -CELL_WITH_GAP;
idx_col := col_count;
idx_col := col_count + 1;
for tree {
if idx_col {
cell: Cell;
#insert,scope() code1;
rect.x += CELL_WIDTH_HEIGHT + CELL_GAP;
if idx_col {
rect.x += CELL_WITH_GAP;
idx_col -= 1;
} else {
rect.y += CELL_WIDTH_HEIGHT + CELL_GAP;
rect.x = 0.0;
idx_col = col_count;
rect.x = 0.0;
rect.y += CELL_WITH_GAP;
idx_col = col_count;
}
if it_index == 0 {
cell.label = "R";
cell.color = COLOR_NODE_ROOT;
stat.nodes += 1;
} else {
if it == {
case NODE_INTERNAL;
cell.label = "N";
cell.color = COLOR_NODE_INTERNAL;
stat.nodes += 1;
case NODE_EMPTY;
cell.color = COLOR_NODE_EMPTY;
stat.empty += 1;
case;
cell.label = tprint("%", it);
cell.color = COLOR_NODE_VALUE;
stat.children += 1;
}
}
cell.rect = rect;
cell.index = it_index;
cell.value = it;
array_add(*cells, cell);
}
}
draw_hover_cells :: (
$parent: Code,
$child_left: Code,
$child_right: Code,
) {
for cells {
using it;
if CheckCollisionPointRec(mouse_world, rect) && label {
parent_idx := (it_index - 1) / 2;
left_idx := 2 * it_index + 1;
right_idx := 2 * it_index + 2;
n := tree.count;
if it_index > 0 && cells[parent_idx].label {
cell := cells[parent_idx];
#insert,scope() parent;
}
if left_idx < n && cells[left_idx].label {
cell := cells[left_idx];
#insert,scope() child_left;
}
if right_idx < n && cells[right_idx].label {
cell := cells[right_idx];
#insert,scope() child_right;
}
}
}
}
/** 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);
diff --git a/src/htree.jai b/src/htree.jai
new file mode 100644
index 0000000..9cb7195
--- /dev/null
+++ b/src/htree.jai
@@ -0,0 +1,27 @@
// Continue: hyper hyper!
htree_init :: (tree_copy: []int, stat: *Statistics) {
}
htree_fit_zoom :: () {
}
htree_draw_2d :: () {
}
htree_draw_screen :: (stat: *Statistics) {
}
#scope_file
diff --git a/src/input.jai b/src/input.jai
index 9c5b5d5..340178e 100644
--- a/src/input.jai
+++ b/src/input.jai
@@ -6,6 +6,10 @@ Mode :: enum #specified {
CELLS :: 2;
}
Gui_Feature :: enum_flags {
TOOLTIP;
}
input :: () {
@@ -87,10 +91,10 @@ input :: () {
}
else if IsKeyPressed(.F2) {
if mode == .HYPER_TREE {
// tbd
// htree_fit_zoom();
} else {
mode = .HYPER_TREE;
// state_switch(mode);
state_switch(mode);
}
}
else if IsKeyPressed(.F3) {
@@ -101,6 +105,15 @@ input :: () {
state_switch(mode);
}
}
/* ---------------------- *
* Context Based Features *
* ---------------------- */
if mode == {
case .CELLS;
if IsKeyPressed(.KEY_T) then flag_toggle(.TOOLTIP);
}
}
input_draw_selection :: () {
@@ -110,20 +123,25 @@ input_draw_selection :: () {
width = mouse_world.x - x;
height = mouse_world.y - y;
DrawRectangleLinesEx(selection, 2.0, GREEN);
selection_normalized = normalize_selection(selection);
DrawRectangleLinesEx(selection_normalized, 2.0, GREEN);
}
// flag_has :: (flag: Mode) -> bool {
// return cast(bool, mode & flag);
// }
flag_has :: (flag: Gui_Feature) -> bool {
return cast(bool, gui_feature & flag);
}
// flag_add :: (flag: Mode) {
// mode |= flag;
// }
flag_add :: (flag: Gui_Feature) {
gui_feature |= flag;
}
// flag_remove :: (flag: Mode) {
// mode &= ~flag;
// }
flag_remove :: (flag: Gui_Feature) {
gui_feature &= ~flag;
}
flag_toggle :: (flag: Gui_Feature) {
gui_feature ^= flag;
}
#scope_file
@@ -133,11 +151,19 @@ drag_start: Vector2;
dragging: bool;
selection: Rectangle;
selection_normalized: Rectangle;
selecting: bool;
normalize_selection :: (using selection: Rectangle) -> Rectangle {
nx := ifx width < 0 then x + width else x;
ny := ifx height < 0 then y + height else y;
return { nx, ny, abs(width), abs(height) };
}
zoom_to_selection :: () {
using selection;
using selection_normalized;
center := Vector2.{ x + width / 2.0, y + height / 2.0 };
zoom_x := cast(float, GetScreenWidth() / width);
diff --git a/src/main.jai b/src/main.jai
index 21348c7..a901f5f 100644
--- a/src/main.jai
+++ b/src/main.jai
@@ -11,20 +11,19 @@
#load "tooltip.jai";
#load "state.jai";
#load "vtree.jai";
#load "htree.jai";
#load "cells.jai";
/** Ideas / Todo
- Check the CELLS array, it looks scuffed!
- Second option for visualizing the tree -> https://en.wikipedia.org/wiki/Hyperbolic_tree
- Different font
- 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
- Smooth zoom (maybe)
*/
@@ -45,6 +44,7 @@ tree: [..]int;
bvs: [..]Aabb;
mode: Mode;
gui_feature: Gui_Feature;
camera: Camera2D;
frame_time: float;
@@ -68,8 +68,12 @@ 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 };
COLOR_HIGHLIGHT0 :: Color.{ 30, 30, 30, 0 };
COLOR_HIGHLIGHT1 :: Color.{ 70, 70, 70, 0 };
COLOR_HIGHLIGHT2 :: Color.{ 110, 110, 110, 0 };
COLOR_CELLS_BORDER :: #run COLOR_BACKGROUND + COLOR_HIGHLIGHT1;
COLOR_CELLS_BORDER_HIGHLIGHT :: #run COLOR_CELLS_BORDER + COLOR_HIGHLIGHT1;
DRAW_BV :: true;
@@ -104,8 +108,6 @@ Statistics :: struct {
#if true main :: () {
#if MEMORY_DEBUGGER_ENABLED defer report_memory_leaks();
log("Hello, Sailor!");
SetTraceLogLevel(.LOG_NONE);
SetConfigFlags(.FLAG_MSAA_4X_HINT);
@@ -119,7 +121,6 @@ Statistics :: struct {
log_vars(bvs.count-1);
array_add(*tree, NODE_INTERNAL, 0, 1);
log_vars(tree);
for i: 2..bvs.count-1 {
// Find sibling
@@ -136,11 +137,17 @@ Statistics :: struct {
tree_copy := array_copy(tree);
vtree_init(tree_copy);
cells_init(tree_copy);
state_vtree: State;
state_htree: State;
state_cells: State;
stats_vtree: Statistics;
stats_htree: Statistics;
stats_cells: Statistics;
vtree_init(tree_copy, *stats_vtree);
htree_init(tree_copy, *stats_htree);
cells_init(tree_copy, *stats_cells);
{
using state_cells;
@@ -149,21 +156,24 @@ Statistics :: struct {
}
{
using state_htree;
id = .HYPER_TREE;
// camera = htree_fit_zoom();
}
{
using state_vtree;
id = .TRAD_TREE;
camera = vtree_fit_zoom();
}
state_init(mode, state_vtree, state_cells);
state_init(mode, state_vtree, state_htree, state_cells);
gui_init();
push_allocator(temp);
while !WindowShouldClose() {
stat_vtree: Statistics;
stat_cells: Statistics;
frame_time = GetFrameTime();
mouse_screen = GetMousePosition();
mouse_world = GetScreenToWorld2D(mouse_screen, camera);
@@ -191,9 +201,9 @@ Statistics :: struct {
defer EndMode2D();
if mode == {
case .TRAD_TREE; vtree_draw_2d(*stat_vtree);
case .HYPER_TREE; // Todo;
case .CELLS; cells_draw_2d(*stat_cells);
case .TRAD_TREE; vtree_draw_2d();
case .HYPER_TREE; htree_draw_2d();
case .CELLS; cells_draw_2d();
}
input_draw_selection();
@@ -202,9 +212,9 @@ Statistics :: struct {
gui_draw_screen();
if mode == {
case .TRAD_TREE; vtree_draw_screen(*stat_vtree);
case .HYPER_TREE; // Todo;
case .CELLS; cells_draw_screen(*stat_cells);
case .TRAD_TREE; vtree_draw_screen(*stats_vtree);
case .HYPER_TREE; htree_draw_screen(*stats_htree);
case .CELLS; cells_draw_screen(*stats_cells);
}
DrawFPS(10, 10);
@@ -384,10 +394,19 @@ aabb_get_sa :: (a: Aabb, b: Aabb) -> float {
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;
c.r = clamp(a.r + b.r, 0, U8_MAX);
c.g = clamp(a.g + b.g, 0, U8_MAX);
c.b = clamp(a.b + b.b, 0, U8_MAX);
c.a = clamp(a.a + b.a, 0, U8_MAX);
return c;
}
operator - :: (a: Color, b: Color) -> Color {
c: Color = ---;
c.r = clamp(a.r - b.r, 0, U8_MAX);
c.g = clamp(a.g - b.g, 0, U8_MAX);
c.b = clamp(a.b - b.b, 0, U8_MAX);
c.a = clamp(a.a - b.a, 0, U8_MAX);
return c;
}
diff --git a/src/vtree.jai b/src/vtree.jai
index 4ca972f..2cb5d1f 100644
--- a/src/vtree.jai
+++ b/src/vtree.jai
@@ -1,13 +1,15 @@
NODES_MAX :: 256;
vtree_init :: (tree_copy: []int) {
vtree_init :: (tree_copy: []int, stat: *Statistics) {
tree = tree_copy;
assign_positions(0, -(SCREEN_WIDTH / 2), SCREEN_WIDTH - (SCREEN_WIDTH / 2), 0);
create_stats(stat);
}
vtree_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
vtree_fit_zoom :: () -> Camera2D {
min, max: Vector2;
min.x = nodes[0].x - NODE_RADIUS;
max.x = nodes[0].x + NODE_RADIUS;
@@ -32,22 +34,14 @@ vtree_fit_zoom :: ($update_camera: bool = true) -> Camera2D {
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;
camera = cam;
return cam;
}
vtree_draw_2d :: (stat: *Statistics) {
vtree_draw_2d :: () {
for i: 0..node_count-1 {
n := nodes[i];
if n.value == {
case NODE_EMPTY; stat.empty += 1;
case NODE_INTERNAL; stat.nodes += 1;
case;
stat.children += 1;
}
idx_left := 2 * n.index + 1;
idx_right := 2 * n.index + 2;
@@ -59,6 +53,7 @@ vtree_draw_2d :: (stat: *Statistics) {
{ 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 }
@@ -75,29 +70,47 @@ vtree_draw_2d :: (stat: *Statistics) {
tn_x := cast(s32, n.x);
tn_y := cast(s32, n.y);
is_root := n.index == 0;
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: Color;
glow: Color;
border: Color;
if is_root {
fill = COLOR_NODE_ROOT;
} else {
if is_internal
then fill = COLOR_NODE_INTERNAL;
else fill = COLOR_NODE_VALUE;
}
glow = fill + { 40, 40, 40, 0 };
glow -= { 0, 0, 0, 190 };
fill := ifx is_internal
then COLOR_NODE_INTERNAL
else COLOR_NODE_VALUE;
border = fill + { 80, 80, 80, 0 };
DrawCircle(tn_x, tn_y, node_r + 6.0, glow);
if CheckCollisionPointCircle(mouse_world, { n.x, n.y }, node_r) {
fill += Color.{ 30, 30, 30, 0 };
fill += { 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: string;
if is_root {
label = "R";
}
else if is_internal {
label = "N";
}
else {
label = tprint("%", n.value);
}
label_c := to_c_string(label);
tw := MeasureTextEx(GetFontDefault(), label_c, xx font1_sz, 1);
@@ -130,6 +143,7 @@ vtree_draw_screen :: (stat: *Statistics) {
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);
@@ -148,7 +162,6 @@ nodes: [NODES_MAX]VTree_Node;
node_count: int;
NODES_MAX :: 128;
NODE_RADIUS :: 52;
FONT1_SIZE :: 69;
FONT2_SIZE :: 30;
@@ -203,6 +216,9 @@ assign_positions :: (idx: int, left: float, right: float, depth: int) {
node_count += 1;
}
// TODO: Error handling
assert(node_count < NODES_MAX, "Too many nodes");
mid := (left + right) / 2.0;
assign_positions(2 * idx + 1, left, mid, depth + 1);
assign_positions(2 * idx + 2, mid, right, depth + 1);
@@ -215,3 +231,14 @@ find_node :: (idx: int) -> (found: bool, node: VTree_Node) {
return false, {};
}
create_stats :: (stat: *Statistics) {
for i: 0..node_count-1 {
if nodes[i].value == {
case NODE_INTERNAL; stat.nodes += 1;
case NODE_EMPTY; stat.empty += 1;
case;
stat.children += 1;
}
}
}