Move Odin renderer into gui/ with per-project Makefiles
CI / Detect changed paths (pull_request) Failing after 1s
CI / Infra unit tests and vet (pull_request) Failing after 1s
CI / Odin unit tests and build (pull_request) Has been skipped
CI / API unit tests and lint (pull_request) Has been skipped

Each sub-project (gui, api, infra) now owns its own Makefile, and the
root Makefile is a lean delegator for the base commands (run, build,
test, clean, fmt). Odin source, deps, and web assets live under gui/
(gui/src, gui/lib, gui/www), with scripts kept under gui/scripts for
build tooling only.
This commit is contained in:
2026-08-31 13:45:58 -06:00
parent 175bb4acad
commit bd8a7da379
16 changed files with 223 additions and 122 deletions
+46
View File
@@ -0,0 +1,46 @@
# gui/Makefile — the Odin + raylib renderer (the interactive 3D universe map).
#
# This is the Odin sub-project. It is normally invoked from the repo root via
# `make run`, `make build`, etc. (which delegate here through the root
# Makefile), but it can also be driven directly with `make -C gui <target>`.
#
# `lib/` (third-party Odin deps) lives under `gui/lib`; the `bin/` / `build/`
# output dirs live at the repo root and are referenced through `$(ROOT)`.
ODIN ?= odin
ROOT := ..
BIN := $(ROOT)/bin
BINARY := $(BIN)/desi_explorer
ODIN_FLAGS := -collection:lib=lib/local
WASM_DEFINE := RAYLIB_WASM_LIB=env.o
.PHONY: help run build build-debug build-web test clean fmt
help: ## List available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
run: ## Run the native app
@mkdir -p lib/local
$(ODIN) run src $(ODIN_FLAGS)
build: ## Release build -> bin/desi_explorer
@mkdir -p lib/local $(BIN)
$(ODIN) build src $(ODIN_FLAGS) -o:speed -out:$(BINARY)
build-debug: ## Debug build -> bin/desi_explorer
@mkdir -p lib/local $(BIN)
$(ODIN) build src $(ODIN_FLAGS) -o:none -debug -out:$(BINARY)
build-web: ## WebAssembly build -> build/web (needs emscripten)
@scripts/build_web.sh
test: ## Run Odin unit tests
@mkdir -p lib/local
$(ODIN) test src $(ODIN_FLAGS)
clean: ## Remove build artifacts
rm -rf $(BIN) build
fmt: ## Format Odin sources with odinfmt
odinfmt src
+11
View File
@@ -0,0 +1,11 @@
# Libraries
Third-party libraries for the DESI Explorer renderer live here, either as git
submodules or vendored directly and wired in via `-collection:lib=lib/local`
(as resolved from the `gui/` directory).
- `raylib` is bundled with Odin and imported as `vendor:raylib` — it does **not**
live in this directory.
No external dependencies yet. `local/` is the default `-collection:lib` target
and is currently empty.
+1
View File
@@ -0,0 +1 @@
# Intentionally empty; third-party Odin deps are wired in via -collection:lib=lib/local.
+5
View File
@@ -0,0 +1,5 @@
{
"character_width": 80,
"tabs": true,
"tabs_width": 2
}
+15
View File
@@ -0,0 +1,15 @@
{
"collections": [
{ "name": "app", "path": "./src" },
{ "name": "lib", "path": "./lib" }
],
"enable_semantic_tokens": false,
"enable_document_symbols": true,
"enable_hover": true,
"enable_snippets": true,
"profiles": [
{
"checker_path": "src/main.odin"
}
]
}
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Builds the WebAssembly ("web") build of DESI Explorer with Odin
# (js_wasm32) + emscripten. Output: build/web/index.html (+ index.wasm,
# odin.js) at the repo root. Run via `make -C gui build-web`.
#
# Requires:
# - `odin` on PATH with the js_wasm32 target
# - emcc / emscripten active in PATH
# Optional:
# - ODIN_ROOT (defaults to `odin root`)
# - OUT_DIR (defaults to <repo>/build/web)
set -euo pipefail
# Resolve the gui/ project root so this script works regardless of CWD.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GUI_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
ROOT_DIR="$(cd "${GUI_DIR}/.." && pwd)"
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/build/web}"
ODIN_ROOT="${ODIN_ROOT:-$(odin root)}"
mkdir -p "${GUI_DIR}/lib/local"
mkdir -p "$OUT_DIR"
# The Odin compiler emits a wasm object; raylib's C code is linked later by
# emcc. `-define:RAYLIB_WASM_LIB=env.o` stops the compiler from trying to
# link the raylib lib itself (emcc does that; it ends up in the final module).
odin build "${GUI_DIR}/src" \
-target:js_wasm32 \
-build-mode:obj \
-define:RAYLIB_WASM_LIB=env.o \
-o:speed \
-collection:lib="${GUI_DIR}/lib/local" \
-out:"${OUT_DIR}/game"
# Odin's JS runtime glue; the generated module imports functions from it.
cp "${ODIN_ROOT}/core/sys/wasm/js/odin.js" "${OUT_DIR}/odin.js"
# Link with emscripten against raylib's prebuilt web library. Flags mirror the
# known-good karl-zylinski/odin-raylib-web template.
emcc \
-o "${OUT_DIR}/index.html" \
"${OUT_DIR}/game.obj" \
"${ODIN_ROOT}/vendor/raylib/wasm/libraylib.web.a" \
--shell-file "${GUI_DIR}/www/index_template.html" \
-sEXPORTED_RUNTIME_METHODS="['HEAPF32']" \
-sUSE_GLFW=3 \
-sWASM_BIGINT \
-sWARN_ON_UNDEFINED_SYMBOLS=0 \
-sASSERTIONS
rm -f "${OUT_DIR}/game.obj"
echo "Web build created in ${OUT_DIR}"
+113
View File
@@ -0,0 +1,113 @@
package main
import "core:math"
import "core:math/rand"
import rl "vendor:raylib"
WIDTH :: 1280
HEIGHT :: 720
// Number of procedurally generated points standing in for the DESI catalog.
// This is replaced by real survey data once ingestion lands.
POINT_COUNT :: 4_000
WORLD_RADIUS :: f32(500.0)
Galaxy :: struct {
position: rl.Vector3,
color: rl.Color,
}
camera: rl.Camera3D
universe: [dynamic]Galaxy
// Deterministic generator so the placeholder sky is stable between runs.
rng: rand.Default_Random_State
main :: proc() {
rng = rand.create(0xDE51_0000)
context.random_generator = rand.default_random_generator(&rng)
rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT, .MSAA_4X_HINT})
rl.InitWindow(WIDTH, HEIGHT, "DESI Explorer")
defer rl.CloseWindow()
rl.SetTargetFPS(60)
camera = make_camera()
make_universe(&universe, POINT_COUNT)
defer delete(universe)
for !rl.WindowShouldClose() {
update()
draw()
}
}
make_camera :: proc() -> rl.Camera3D {
return {
position = {0, 220, 220},
target = {0, 0, 0},
up = {0, 1, 0},
fovy = 60,
projection = .PERSPECTIVE,
}
}
make_universe :: proc(u: ^[dynamic]Galaxy, count: int) {
clear(u)
reserve(u, count)
for _ in 0 ..< count {
pos := random_sphere_point(WORLD_RADIUS)
append(u, Galaxy{position = pos, color = color_for_position(pos)})
}
}
// Uniformly distributed random point inside the scene's bounding sphere.
random_sphere_point :: proc(radius: f32) -> rl.Vector3 {
for {
x := rand.float32_range(-1, 1)
y := rand.float32_range(-1, 1)
z := rand.float32_range(-1, 1)
len_sq := x * x + y * y + z * z
if len_sq > 0.0001 && len_sq < 1.0 {
return {x * radius, y * radius, z * radius}
}
}
}
// Maps distance from the origin to a blue (near) -> red (far) gradient, a
// stand-in for redshift until real colors are computed from survey metadata.
color_for_position :: proc(p: rl.Vector3) -> rl.Color {
d := math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z)
t := math.clamp(d / WORLD_RADIUS, 0, 1)
return rl.Color{u8(40 + 180 * t), 120, u8(255 - 180 * t), 255}
}
update :: proc() {
// Orbital camera: drag to rotate, scroll to zoom, right-drag / shift to pan.
rl.UpdateCamera(&camera, .ORBITAL)
}
draw :: proc() {
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground({5, 5, 15, 255})
rl.BeginMode3D(camera)
defer rl.EndMode3D()
rl.DrawGrid(20, 50.0)
for galaxy in universe {
rl.DrawPoint3D(galaxy.position, galaxy.color)
}
rl.DrawFPS(10, 10)
rl.DrawText(
"DESI Explorer — drag to rotate, scroll to zoom",
10,
34,
18,
rl.RAYWHITE,
)
}
+50
View File
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DESI Explorer</title>
<meta name="viewport" content="width=device-width">
<style>
body {
margin: 0;
overflow: hidden;
background: #05050f;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
<!-- Odin's JS runtime glue; required by the generated wasm module. -->
<script src="odin.js"></script>
<script>
var odinMemoryInterface = new odin.WasmMemoryInterface();
odinMemoryInterface.setIntSize(4);
var odinImports = odin.setupDefaultImports(odinMemoryInterface);
var Module = {
instantiateWasm: function(imports, successCallback) {
var newImports = Object.assign({}, odinImports, imports);
return WebAssembly.instantiateStreaming(fetch("index.wasm"), newImports).then(
function(output) {
var e = output.instance.exports;
odinMemoryInterface.setExports(e);
odinMemoryInterface.setMemory(e.memory);
return successCallback(output.instance);
}
);
},
onRuntimeInitialized: function() {
var e = wasmExports;
if (e._start) { e._start(); }
},
canvas: document.getElementById("canvas")
};
</script>
{{{ SCRIPT }}}
</body>
</html>