nodes
This commit is contained in:
17
CONTEXT.md
17
CONTEXT.md
@@ -24,13 +24,22 @@ Sourced from books by Aajonus Vonderplanitz. Curated by the owner only — no co
|
||||
|
||||
Game-engine style — custom rendering, animations, smooth transitions, physics-based graph layouts. Not a traditional web UI.
|
||||
|
||||
## Tech Stack — Under Consideration
|
||||
## Tech Stack
|
||||
|
||||
- **Raylib + C++ + Emscripten** — leading candidate. Mature WASM story, true native binary, lean output.
|
||||
- **Raylib + Zig** — elegant `@embedFile` for data embedding, but friction between Zig's WASM target and raylib's Emscripten dependency.
|
||||
- **Mach (Zig game engine)** — native + WASM via WebGPU, but pre-1.0 and still maturing.
|
||||
- **Zig + raylib-zig + Emscripten** — chosen. Native binary and WASM from the same codebase. Package managed via `build.zig.zon`.
|
||||
- ~~Raylib + C++ + Emscripten~~ — considered, but Zig's `@embedFile` and compile-time data embedding are a better fit.
|
||||
- ~~Mach (Zig game engine)~~ — discarded, pre-1.0 and still maturing.
|
||||
- ~~Three.js + Electron~~ — discarded. Electron too heavy, sidesteps WASM.
|
||||
|
||||
## Current Implementation State
|
||||
|
||||
Graph navigation is working (`src/main.zig`):
|
||||
|
||||
- **Data model**: nodes of five kinds — `body_part`, `ailment`, `recipe`, `ingredient`, `property` — with typed link lists, all hardcoded in the binary.
|
||||
- **Navigation**: root view shows body systems in a radial ring; clicking any node drills into it, placing it at the centre with its linked nodes in orbit. Right-click or `B` navigates back. Nav history kept in a fixed-size stack.
|
||||
- **Rendering**: colour-coded circles per node kind, glow on hover, selection ring on the active centre node, edge lines to linked nodes, breadcrumb trail, colour legend, navigation hints.
|
||||
- **WASM path**: `emscripten_set_main_loop` hook in place; same `drawFrame` callback used for both targets.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No widget toolkits — fully custom rendered
|
||||
|
||||
280
src/main.zig
280
src/main.zig
@@ -1,18 +1,279 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const builtin = @import("builtin");
|
||||
const math = std.math;
|
||||
|
||||
const screen_width = 800;
|
||||
const screen_height = 450;
|
||||
const SCREEN_W: i32 = 1024;
|
||||
const SCREEN_H: i32 = 768;
|
||||
const CX: f32 = @as(f32, @floatFromInt(SCREEN_W)) / 2.0;
|
||||
const CY: f32 = @as(f32, @floatFromInt(SCREEN_H)) / 2.0;
|
||||
const NODE_R: f32 = 42.0;
|
||||
const ORBIT_R: f32 = 220.0;
|
||||
|
||||
// ── Data model ───────────────────────────────────────────────────────────────
|
||||
|
||||
const NodeKind = enum { body_part, ailment, recipe, ingredient, property };
|
||||
|
||||
const NodeDef = struct {
|
||||
id: u8,
|
||||
name: [:0]const u8,
|
||||
kind: NodeKind,
|
||||
links: []const u8,
|
||||
};
|
||||
|
||||
/// All content is hardcoded and will be embedded in the binary at compile time.
|
||||
const all_nodes = [_]NodeDef{
|
||||
// Body systems (0–4)
|
||||
.{ .id = 0, .name = "Digestive System", .kind = .body_part, .links = &.{ 10, 11, 12 } },
|
||||
.{ .id = 1, .name = "Immune System", .kind = .body_part, .links = &.{ 13, 14 } },
|
||||
.{ .id = 2, .name = "Nervous System", .kind = .body_part, .links = &.{15} },
|
||||
.{ .id = 3, .name = "Circulatory", .kind = .body_part, .links = &.{ 13, 14 } },
|
||||
.{ .id = 4, .name = "Endocrine", .kind = .body_part, .links = &.{ 14, 15 } },
|
||||
// Ailments (10–15)
|
||||
.{ .id = 10, .name = "Poor Digestion", .kind = .ailment, .links = &.{ 20, 21 } },
|
||||
.{ .id = 11, .name = "Constipation", .kind = .ailment, .links = &.{ 20, 22 } },
|
||||
.{ .id = 12, .name = "Bloating", .kind = .ailment, .links = &.{21} },
|
||||
.{ .id = 13, .name = "Inflammation", .kind = .ailment, .links = &.{ 22, 23 } },
|
||||
.{ .id = 14, .name = "Fatigue", .kind = .ailment, .links = &.{ 21, 23 } },
|
||||
.{ .id = 15, .name = "Brain Fog", .kind = .ailment, .links = &.{23} },
|
||||
// Recipes (20–23)
|
||||
.{ .id = 20, .name = "Meat Smoothie", .kind = .recipe, .links = &.{ 30, 31, 32 } },
|
||||
.{ .id = 21, .name = "Raw Milk Kefir", .kind = .recipe, .links = &.{ 33, 34 } },
|
||||
.{ .id = 22, .name = "Raw Egg Blend", .kind = .recipe, .links = &.{ 31, 32, 34 } },
|
||||
.{ .id = 23, .name = "Honey-Butter Mix", .kind = .recipe, .links = &.{ 35, 36 } },
|
||||
// Ingredients (30–36)
|
||||
.{ .id = 30, .name = "Raw Meat", .kind = .ingredient, .links = &.{ 40, 41 } },
|
||||
.{ .id = 31, .name = "Raw Eggs", .kind = .ingredient, .links = &.{ 40, 42 } },
|
||||
.{ .id = 32, .name = "Lemon Juice", .kind = .ingredient, .links = &.{43} },
|
||||
.{ .id = 33, .name = "Raw Milk", .kind = .ingredient, .links = &.{ 42, 44 } },
|
||||
.{ .id = 34, .name = "Kefir Grains", .kind = .ingredient, .links = &.{44} },
|
||||
.{ .id = 35, .name = "Raw Honey", .kind = .ingredient, .links = &.{ 41, 43 } },
|
||||
.{ .id = 36, .name = "Raw Butter", .kind = .ingredient, .links = &.{ 41, 44 } },
|
||||
// Properties (40–44)
|
||||
.{ .id = 40, .name = "High Protein", .kind = .property, .links = &.{} },
|
||||
.{ .id = 41, .name = "Energy Dense", .kind = .property, .links = &.{} },
|
||||
.{ .id = 42, .name = "Probiotic", .kind = .property, .links = &.{} },
|
||||
.{ .id = 43, .name = "Alkalizing", .kind = .property, .links = &.{} },
|
||||
.{ .id = 44, .name = "Enzyme Rich", .kind = .property, .links = &.{} },
|
||||
};
|
||||
|
||||
const root_ids = [_]u8{ 0, 1, 2, 3, 4 };
|
||||
|
||||
fn findNode(id: u8) ?*const NodeDef {
|
||||
for (&all_nodes) |*n| {
|
||||
if (n.id == id) return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn kindColor(kind: NodeKind) rl.Color {
|
||||
return switch (kind) {
|
||||
.body_part => rl.Color.init(70, 150, 215, 255),
|
||||
.ailment => rl.Color.init(210, 75, 65, 255),
|
||||
.recipe => rl.Color.init(75, 190, 110, 255),
|
||||
.ingredient => rl.Color.init(215, 170, 50, 255),
|
||||
.property => rl.Color.init(170, 90, 215, 255),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Navigation state ──────────────────────────────────────────────────────────
|
||||
|
||||
const VisNode = struct { id: u8, x: f32, y: f32, is_center: bool };
|
||||
|
||||
const MAX_STACK = 16;
|
||||
const MAX_VIS = 24;
|
||||
|
||||
var nav_stack: [MAX_STACK]u8 = undefined;
|
||||
var nav_depth: usize = 0;
|
||||
var vis: [MAX_VIS]VisNode = undefined;
|
||||
var vis_count: usize = 0;
|
||||
var hovered_id: ?u8 = null;
|
||||
var ready: bool = false;
|
||||
|
||||
fn buildVis() void {
|
||||
vis_count = 0;
|
||||
if (nav_depth == 0) {
|
||||
// Root view: body systems arranged in a circle
|
||||
const n = root_ids.len;
|
||||
const r: f32 = 200.0;
|
||||
for (root_ids, 0..) |id, i| {
|
||||
const angle = (2.0 * math.pi * @as(f32, @floatFromInt(i))) /
|
||||
@as(f32, @floatFromInt(n)) - math.pi / 2.0;
|
||||
vis[vis_count] = .{
|
||||
.id = id,
|
||||
.x = CX + r * @cos(angle),
|
||||
.y = CY + r * @sin(angle),
|
||||
.is_center = false,
|
||||
};
|
||||
vis_count += 1;
|
||||
}
|
||||
} else {
|
||||
// Drill-down view: selected node at centre, its links in orbit
|
||||
const cur_id = nav_stack[nav_depth - 1];
|
||||
const cur = findNode(cur_id) orelse return;
|
||||
vis[vis_count] = .{ .id = cur_id, .x = CX, .y = CY, .is_center = true };
|
||||
vis_count += 1;
|
||||
const n = cur.links.len;
|
||||
if (n > 0) {
|
||||
for (cur.links, 0..) |link_id, i| {
|
||||
const angle = (2.0 * math.pi * @as(f32, @floatFromInt(i))) /
|
||||
@as(f32, @floatFromInt(n)) - math.pi / 2.0;
|
||||
vis[vis_count] = .{
|
||||
.id = link_id,
|
||||
.x = CX + ORBIT_R * @cos(angle),
|
||||
.y = CY + ORBIT_R * @sin(angle),
|
||||
.is_center = false,
|
||||
};
|
||||
vis_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visNodeAt(mx: f32, my: f32) ?u8 {
|
||||
for (vis[0..vis_count]) |vn| {
|
||||
const dx = mx - vn.x;
|
||||
const dy = my - vn.y;
|
||||
if (dx * dx + dy * dy <= NODE_R * NODE_R) return vn.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Per-frame logic ───────────────────────────────────────────────────────────
|
||||
|
||||
fn update() void {
|
||||
const mx: f32 = @floatFromInt(rl.getMouseX());
|
||||
const my: f32 = @floatFromInt(rl.getMouseY());
|
||||
hovered_id = visNodeAt(mx, my);
|
||||
|
||||
if (rl.isMouseButtonPressed(.left)) {
|
||||
if (hovered_id) |id| {
|
||||
const is_cur = nav_depth > 0 and nav_stack[nav_depth - 1] == id;
|
||||
if (!is_cur and nav_depth < MAX_STACK) {
|
||||
nav_stack[nav_depth] = id;
|
||||
nav_depth += 1;
|
||||
buildVis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rl.isMouseButtonPressed(.right) or rl.isKeyPressed(.b)) {
|
||||
if (nav_depth > 0) {
|
||||
nav_depth -= 1;
|
||||
buildVis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawScene() void {
|
||||
// Edges from centre to orbit
|
||||
if (nav_depth > 0 and vis_count > 1) {
|
||||
const c = vis[0];
|
||||
for (vis[1..vis_count]) |vn| {
|
||||
rl.drawLineEx(
|
||||
.{ .x = c.x, .y = c.y },
|
||||
.{ .x = vn.x, .y = vn.y },
|
||||
1.5,
|
||||
rl.Color.init(55, 55, 65, 255),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Nodes
|
||||
for (vis[0..vis_count]) |vn| {
|
||||
const node = findNode(vn.id) orelse continue;
|
||||
const col = kindColor(node.kind);
|
||||
const hov = hovered_id != null and hovered_id.? == vn.id;
|
||||
const r = if (hov) NODE_R + 5.0 else NODE_R;
|
||||
const ix: i32 = @intFromFloat(vn.x);
|
||||
const iy: i32 = @intFromFloat(vn.y);
|
||||
|
||||
// Soft glow on hover
|
||||
if (hov) {
|
||||
rl.drawCircle(ix, iy, r + 12.0, rl.Color.init(col.r, col.g, col.b, 45));
|
||||
}
|
||||
rl.drawCircle(ix, iy, r, col);
|
||||
|
||||
// White ring marks the current centre node
|
||||
if (vn.is_center) {
|
||||
rl.drawCircleLines(ix, iy, r + 5.0, rl.Color.white);
|
||||
}
|
||||
|
||||
// Label below the circle
|
||||
const fsz: i32 = if (vn.is_center) 14 else 12;
|
||||
const tw = rl.measureText(node.name, fsz);
|
||||
const lx = ix - @divTrunc(tw, 2);
|
||||
const ly = iy + @as(i32, @intFromFloat(r)) + 5;
|
||||
rl.drawText(node.name, lx, ly, fsz, rl.Color.ray_white);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawHUD() void {
|
||||
// Breadcrumb trail
|
||||
var bx: i32 = 10;
|
||||
const by: i32 = 10;
|
||||
rl.drawText("Body Systems", bx, by, 15, rl.Color.gray);
|
||||
bx += rl.measureText("Body Systems", 15);
|
||||
for (nav_stack[0..nav_depth]) |id| {
|
||||
rl.drawText(" > ", bx, by, 15, rl.Color.dark_gray);
|
||||
bx += rl.measureText(" > ", 15);
|
||||
const node = findNode(id) orelse continue;
|
||||
rl.drawText(node.name, bx, by, 15, rl.Color.ray_white);
|
||||
bx += rl.measureText(node.name, 15);
|
||||
}
|
||||
|
||||
// Navigation hint at bottom
|
||||
const hint: [:0]const u8 = if (nav_depth > 0)
|
||||
"[B] / right-click to go back"
|
||||
else
|
||||
"Click a body system to explore";
|
||||
rl.drawText(hint, 10, SCREEN_H - 28, 13, rl.Color.dark_gray);
|
||||
|
||||
// Colour legend (top-right)
|
||||
const legend = [_]struct { kind: NodeKind, label: [:0]const u8 }{
|
||||
.{ .kind = .body_part, .label = "Body System" },
|
||||
.{ .kind = .ailment, .label = "Ailment" },
|
||||
.{ .kind = .recipe, .label = "Recipe" },
|
||||
.{ .kind = .ingredient, .label = "Ingredient" },
|
||||
.{ .kind = .property, .label = "Property" },
|
||||
};
|
||||
const lx: i32 = SCREEN_W - 140;
|
||||
var ly: i32 = 10;
|
||||
for (legend) |e| {
|
||||
rl.drawCircle(lx + 8, ly + 8, 8.0, kindColor(e.kind));
|
||||
rl.drawText(e.label, lx + 22, ly + 1, 13, rl.Color.light_gray);
|
||||
ly += 22;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn drawFrame() callconv(.c) void {
|
||||
if (!ready) {
|
||||
buildVis();
|
||||
ready = true;
|
||||
}
|
||||
update();
|
||||
|
||||
rl.beginDrawing();
|
||||
defer rl.endDrawing();
|
||||
|
||||
rl.clearBackground(rl.Color.init(12, 12, 18, 255));
|
||||
drawScene();
|
||||
drawHUD();
|
||||
}
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
rl.initWindow(screen_width, screen_height, "primalwiki");
|
||||
rl.initWindow(SCREEN_W, SCREEN_H, "primalwiki");
|
||||
defer rl.closeWindow();
|
||||
|
||||
if (builtin.os.tag == .emscripten) {
|
||||
// Browser: emscripten's main loop drives the frame callback
|
||||
const em = struct {
|
||||
extern fn emscripten_set_main_loop(func: *const fn () callconv(.c) void, fps: c_int, simulate_infinite_loop: c_int) void;
|
||||
extern fn emscripten_set_main_loop(
|
||||
func: *const fn () callconv(.c) void,
|
||||
fps: c_int,
|
||||
simulate_infinite_loop: c_int,
|
||||
) void;
|
||||
};
|
||||
em.emscripten_set_main_loop(&drawFrame, 0, 1);
|
||||
} else {
|
||||
@@ -22,12 +283,3 @@ pub fn main() anyerror!void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawFrame() callconv(.c) void {
|
||||
rl.beginDrawing();
|
||||
defer rl.endDrawing();
|
||||
|
||||
rl.clearBackground(rl.Color.black);
|
||||
rl.drawText("primalwiki poc", 190, 180, 30, rl.Color.ray_white);
|
||||
rl.drawText("zig + raylib", 240, 230, 20, rl.Color.dark_gray);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user