diff --git a/standalone/goldberg-sphere/index.html b/standalone/goldberg-sphere/index.html
index 938b628..44ed0f2 100644
--- a/standalone/goldberg-sphere/index.html
+++ b/standalone/goldberg-sphere/index.html
@@ -132,54 +132,6 @@
-
Paint
diff --git a/standalone/goldberg-sphere/maze.mjs b/standalone/goldberg-sphere/maze.mjs
deleted file mode 100644
index 3357a06..0000000
--- a/standalone/goldberg-sphere/maze.mjs
+++ /dev/null
@@ -1,336 +0,0 @@
-import { make_buffer } from './gl_utils.mjs';
-
-// ---------------------------------------------------------------------------
-// Face adjacency
-// ---------------------------------------------------------------------------
-
-// Build adjacency list for Goldberg faces via shared edges.
-// Uses unrelaxed vertex positions as canonical keys — topology is stable across stages.
-export function build_goldberg_adjacency(goldberg) {
- const edge_to_entries = new Map();
- for (let fi = 0; fi < goldberg.faces.length; fi++) {
- const verts = goldberg.faces[fi].vertices_3d;
- const n = verts.length;
- for (let i = 0; i < n; i++) {
- const a = verts[i], b = verts[(i + 1) % n];
- const ka = `${a.x.toFixed(5)},${a.y.toFixed(5)},${a.z.toFixed(5)}`;
- const kb = `${b.x.toFixed(5)},${b.y.toFixed(5)},${b.z.toFixed(5)}`;
- const key = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
- if (!edge_to_entries.has(key)) { edge_to_entries.set(key, []); }
- edge_to_entries.get(key).push({ fi, edge_idx: i });
- }
- }
- const adj = Array.from({ length: goldberg.faces.length }, () => []);
- for (const entries of edge_to_entries.values()) {
- if (entries.length === 2) {
- const [e0, e1] = entries;
- adj[e0.fi].push({ fj: e1.fi, fi_edge_idx: e0.edge_idx });
- adj[e1.fi].push({ fj: e0.fi, fi_edge_idx: e1.edge_idx });
- }
- }
- return adj;
-}
-
-// ---------------------------------------------------------------------------
-// Prim island maze
-// ---------------------------------------------------------------------------
-
-function grow_island(seed, max_cells, adj, cell_region, region_id) {
- cell_region[seed] = region_id;
- const frontier = [];
- const add_frontier = fi => {
- for (const { fj, fi_edge_idx } of adj[fi]) {
- if (cell_region[fj] === -1) { frontier.push({ fi, fj, fi_edge_idx }); }
- }
- };
- add_frontier(seed);
-
- const passage_pairs = new Set();
- let count = 1;
- while (frontier.length > 0 && count < max_cells) {
- const idx = Math.floor(Math.random() * frontier.length);
- const { fi, fj, fi_edge_idx } = frontier[idx];
- frontier[idx] = frontier[frontier.length - 1];
- frontier.pop();
- if (cell_region[fj] !== -1) { continue; }
- const pk = fi < fj ? `${fi},${fj}` : `${fj},${fi}`;
- passage_pairs.add(pk);
- cell_region[fj] = region_id;
- count++;
- add_frontier(fj);
- }
- return passage_pairs;
-}
-
-function generate_maze_walls(goldberg, seed_count, max_cells) {
- const adj = build_goldberg_adjacency(goldberg);
- const n = goldberg.faces.length;
- const cell_region = new Int32Array(n).fill(-1);
- const all_passages = new Set();
-
- const order = Array.from({ length: n }, (_, i) => i);
- for (let i = n - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [order[i], order[j]] = [order[j], order[i]];
- }
-
- let region_id = 0;
- for (let s = 0; s < seed_count && s < n; s++) {
- const seed = order[s];
- if (cell_region[seed] !== -1) { continue; }
- const passages = grow_island(seed, max_cells, adj, cell_region, region_id);
- for (const pk of passages) { all_passages.add(pk); }
- region_id++;
- }
-
- const wall_edges = [];
- for (let fi = 0; fi < n; fi++) {
- if (cell_region[fi] === -1) { continue; }
- for (const { fj, fi_edge_idx } of adj[fi]) {
- if (fi >= fj) { continue; }
- if (cell_region[fj] === -1) {
- wall_edges.push({ fi, edge_idx: fi_edge_idx });
- } else if (cell_region[fj] === cell_region[fi]) {
- const pk = `${fi},${fj}`;
- if (!all_passages.has(pk)) { wall_edges.push({ fi, edge_idx: fi_edge_idx }); }
- }
- }
- }
- return wall_edges;
-}
-
-// ---------------------------------------------------------------------------
-// Edge-walker maze
-// ---------------------------------------------------------------------------
-
-function build_vertex_graph(goldberg) {
- const pos_to_id = new Map();
- let nv = 0;
- const get_vid = v => {
- const k = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
- if (!pos_to_id.has(k)) { pos_to_id.set(k, nv++); }
- return pos_to_id.get(k);
- };
- for (const face of goldberg.faces) {
- for (const v of face.vertices_3d) { get_vid(v); }
- }
- const vertex_adj = Array.from({ length: nv }, () => []);
- const edge_seen = new Set();
- const edge_data = new Map();
- for (let fi = 0; fi < goldberg.faces.length; fi++) {
- const verts = goldberg.faces[fi].vertices_3d;
- const n = verts.length;
- for (let i = 0; i < n; i++) {
- const va = get_vid(verts[i]);
- const vb = get_vid(verts[(i + 1) % n]);
- const ek = va < vb ? `${va},${vb}` : `${vb},${va}`;
- if (!edge_seen.has(ek)) {
- edge_seen.add(ek);
- edge_data.set(ek, { fi, edge_idx: i });
- vertex_adj[va].push({ vj: vb, ek });
- vertex_adj[vb].push({ vj: va, ek });
- }
- }
- }
- return { nv, vertex_adj, edge_data };
-}
-
-function generate_walker_walls(goldberg, walker_count, stop_prob, branch_prob, close_prob) {
- const { nv, vertex_adj, edge_data } = build_vertex_graph(goldberg);
- const walled = new Set();
- const vertex_walled = new Uint8Array(nv);
-
- const order = Array.from({ length: nv }, (_, i) => i);
- for (let i = nv - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [order[i], order[j]] = [order[j], order[i]];
- }
-
- const active = [];
- for (let w = 0; w < Math.min(walker_count, nv); w++) {
- active.push({ vi: order[w], prev_vi: -1 });
- }
-
- while (active.length > 0) {
- const wi = Math.floor(Math.random() * active.length);
- const walker = active[wi];
- const { vi, prev_vi } = walker;
- if (Math.random() < stop_prob) { active.splice(wi, 1); continue; }
- const candidates = [];
- for (const { vj, ek } of vertex_adj[vi]) {
- if (vj === prev_vi) { continue; }
- if (walled.has(ek)) { continue; }
- if (vertex_walled[vj] && Math.random() >= close_prob) { continue; }
- candidates.push({ vj, ek });
- }
- if (candidates.length === 0) { active.splice(wi, 1); continue; }
- const { vj, ek } = candidates[Math.floor(Math.random() * candidates.length)];
- walled.add(ek);
- vertex_walled[vi] = 1;
- vertex_walled[vj] = 1;
- if (Math.random() < branch_prob) { active.push({ vi, prev_vi }); }
- walker.vi = vj;
- walker.prev_vi = vi;
- }
-
- const wall_edges = [];
- for (const ek of walled) {
- const entry = edge_data.get(ek);
- if (entry) { wall_edges.push({ fi: entry.fi, edge_idx: entry.edge_idx }); }
- }
- return wall_edges;
-}
-
-// ---------------------------------------------------------------------------
-// Wall geometry
-// ---------------------------------------------------------------------------
-
-function build_wall_geo(goldberg, use_relaxed, wall_edges, wall_width) {
- if (wall_edges.length === 0) { return { wall_pos: new Float32Array(0), wall_verts: 0 }; }
-
- const LIFT = 1.010;
- const hw = wall_width / 2;
- const MITER_CLAMP = 4.0;
-
- const ev = wall_edges.map(({ fi, edge_idx }) => {
- const face = goldberg.faces[fi];
- const verts = use_relaxed ? (face.relaxed_vertices_3d ?? face.vertices_3d) : face.vertices_3d;
- return { a: verts[edge_idx], b: verts[(edge_idx + 1) % verts.length] };
- });
-
- const pk = v => `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
-
- const vtx_edges = new Map();
- for (let i = 0; i < ev.length; i++) {
- for (const v of [ev[i].a, ev[i].b]) {
- const k = pk(v);
- if (!vtx_edges.has(k)) { vtx_edges.set(k, []); }
- vtx_edges.get(k).push(i);
- }
- }
-
- const w_dir = (v, w) => {
- const ex = w.x-v.x, ey = w.y-v.y, ez = w.z-v.z;
- const er = Math.sqrt(ex*ex + ey*ey + ez*ez);
- if (er < 1e-10) { return [0, 0, 0]; }
- const vr = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
- const nx = v.x/vr, ny = v.y/vr, nz = v.z/vr;
- const edx = ex/er, edy = ey/er, edz = ez/er;
- return [ny*edz - nz*edy, nz*edx - nx*edz, nx*edy - ny*edx];
- };
-
- const miter_offset = (v, other_v, i) => {
- const vk = pk(v);
- const adj = vtx_edges.get(vk).filter(j => j !== i);
- const w1 = w_dir(v, other_v);
- if (adj.length === 0) { return [0, 0, 0]; }
- let sx = w1[0], sy = w1[1], sz = w1[2];
- for (const j of adj) {
- const { a, b } = ev[j];
- const far = pk(a) === vk ? b : a;
- const wj = w_dir(v, far);
- sx += wj[0]; sy += wj[1]; sz += wj[2];
- }
- const sr = Math.sqrt(sx*sx + sy*sy + sz*sz);
- if (sr < 1e-6) { return [w1[0]*hw, w1[1]*hw, w1[2]*hw]; }
- const m = [sx/sr, sy/sr, sz/sr];
- const d = w1[0]*m[0] + w1[1]*m[1] + w1[2]*m[2];
- if (d < 0.05) { return [w1[0]*hw, w1[1]*hw, w1[2]*hw]; }
- const len = Math.min(hw / d, hw * MITER_CLAMP);
- return [m[0]*len, m[1]*len, m[2]*len];
- };
-
- const pos = [];
- for (let i = 0; i < ev.length; i++) {
- const { a, b } = ev[i];
- const oa = miter_offset(a, b, i);
- const ob = miter_offset(b, a, i);
- const ax = a.x*LIFT, ay = a.y*LIFT, az = a.z*LIFT;
- const bx = b.x*LIFT, by = b.y*LIFT, bz = b.z*LIFT;
- const term_a = oa[0] === 0 && oa[1] === 0 && oa[2] === 0;
- const term_b = ob[0] === 0 && ob[1] === 0 && ob[2] === 0;
-
- if (term_a && term_b) {
- const mx = (a.x+b.x)/2, my = (a.y+b.y)/2, mz = (a.z+b.z)/2;
- const mr = Math.sqrt(mx*mx+my*my+mz*mz);
- const ex = b.x-a.x, ey = b.y-a.y, ez = b.z-a.z;
- const er = Math.sqrt(ex*ex+ey*ey+ez*ez);
- const wd = [
- (my/mr)*(ez/er) - (mz/mr)*(ey/er),
- (mz/mr)*(ex/er) - (mx/mr)*(ez/er),
- (mx/mr)*(ey/er) - (my/mr)*(ex/er),
- ];
- pos.push(
- ax-wd[0]*hw, ay-wd[1]*hw, az-wd[2]*hw,
- ax+wd[0]*hw, ay+wd[1]*hw, az+wd[2]*hw,
- bx+wd[0]*hw, by+wd[1]*hw, bz+wd[2]*hw,
- ax-wd[0]*hw, ay-wd[1]*hw, az-wd[2]*hw,
- bx+wd[0]*hw, by+wd[1]*hw, bz+wd[2]*hw,
- bx-wd[0]*hw, by-wd[1]*hw, bz-wd[2]*hw,
- );
- continue;
- }
-
- const v0 = [ax-oa[0], ay-oa[1], az-oa[2]];
- const v1 = [ax+oa[0], ay+oa[1], az+oa[2]];
- const v2 = [bx-ob[0], by-ob[1], bz-ob[2]];
- const v3 = [bx+ob[0], by+ob[1], bz+ob[2]];
-
- if (term_a) {
- pos.push(ax, ay, az, ...v2, ...v3);
- } else if (term_b) {
- pos.push(...v0, ...v1, bx, by, bz);
- } else {
- pos.push(...v0, ...v1, ...v2, ...v0, ...v2, ...v3);
- }
- }
- return { wall_pos: new Float32Array(pos), wall_verts: pos.length / 3 };
-}
-
-// ---------------------------------------------------------------------------
-// Maze_State
-// ---------------------------------------------------------------------------
-
-export class Maze_State {
- constructor() {
- this.wall_edges = null;
- this.pos_buf = null;
- this.vert_count = 0;
- }
-
- generate(goldberg, p) {
- if (p.algo === 'walker') {
- this.wall_edges = generate_walker_walls(goldberg, p.walkers, p.stop_prob, p.branch_prob, p.close_prob);
- } else {
- this.wall_edges = generate_maze_walls(goldberg, p.seeds, p.max_cells);
- }
- }
-
- upload(gl, goldberg, use_relaxed, wall_width) {
- if (!this.wall_edges) { return; }
- const geo = build_wall_geo(goldberg, use_relaxed, this.wall_edges, wall_width);
- if (this.pos_buf) { gl.deleteBuffer(this.pos_buf); }
- this.pos_buf = make_buffer(gl, geo.wall_pos);
- this.vert_count = geo.wall_verts;
- }
-
- invalidate(gl) {
- this.wall_edges = null;
- if (this.pos_buf) { gl.deleteBuffer(this.pos_buf); this.pos_buf = null; }
- this.vert_count = 0;
- }
-
- draw(gl, prog, mvp, show) {
- if (!show || !this.pos_buf || this.vert_count === 0) { return; }
- gl.useProgram(prog);
- gl.uniformMatrix4fv(gl.getUniformLocation(prog, 'u_mvp'), false, mvp);
- const ap = gl.getAttribLocation(prog, 'a_pos');
- gl.bindBuffer(gl.ARRAY_BUFFER, this.pos_buf);
- gl.enableVertexAttribArray(ap);
- gl.vertexAttribPointer(ap, 3, gl.FLOAT, false, 0, 0);
- gl.disable(gl.CULL_FACE);
- gl.drawArrays(gl.TRIANGLES, 0, this.vert_count);
- gl.enable(gl.CULL_FACE);
- gl.disableVertexAttribArray(ap);
- }
-}
diff --git a/standalone/goldberg-sphere/playground_main.mjs b/standalone/goldberg-sphere/playground_main.mjs
index cdd0ca9..9a88ea7 100644
--- a/standalone/goldberg-sphere/playground_main.mjs
+++ b/standalone/goldberg-sphere/playground_main.mjs
@@ -6,7 +6,7 @@ import { create_program, make_buffer } from './gl_utils.mjs';
import { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE, Paint_State } from './paint_state.mjs';
import { Spin_State } from './spin_state.mjs';
import { shape_distortion, build_triangle_geo, build_goldberg_geo } from './render_geo.mjs';
-import { build_goldberg_adjacency, Maze_State } from './maze.mjs';
+import { build_goldberg_adjacency } from './topology.mjs';
import { Undo_State } from './undo_state.mjs';
import { Env_State } from './env_state.mjs';
import { PALETTES, DEFAULT_PALETTE } from './palettes.mjs';
@@ -15,13 +15,11 @@ import { PALETTES, DEFAULT_PALETTE } from './palettes.mjs';
// Load shaders
// ---------------------------------------------------------------------------
-const [face_vert_src, face_frag_src, edge_vert_src, edge_frag_src, wall_vert_src, wall_frag_src] = await Promise.all([
+const [face_vert_src, face_frag_src, edge_vert_src, edge_frag_src] = await Promise.all([
fetch('./shaders/face.vert').then(r => r.text()),
fetch('./shaders/face.frag').then(r => r.text()),
fetch('./shaders/edge.vert').then(r => r.text()),
fetch('./shaders/edge.frag').then(r => r.text()),
- fetch('./shaders/wall.vert').then(r => r.text()),
- fetch('./shaders/wall.frag').then(r => r.text()),
]);
// ---------------------------------------------------------------------------
@@ -29,7 +27,6 @@ const [face_vert_src, face_frag_src, edge_vert_src, edge_frag_src, wall_vert_src
// ---------------------------------------------------------------------------
const cache = { depth: -1, ico: null, poly: null, goldberg: null, adj: null };
-const maze = new Maze_State();
const spin = new Spin_State();
const paint = new Paint_State();
const undo_state = new Undo_State();
@@ -53,40 +50,6 @@ function get_params() {
};
}
-function get_wall_width() {
- return parseFloat(document.getElementById('maze-width').value) || 0.015;
-}
-
-function get_maze_params() {
- const algo = document.getElementById('maze-algo').value;
- return {
- algo,
- seeds: parseInt(document.getElementById('maze-seeds').value, 10) || 12,
- max_cells: parseInt(document.getElementById('maze-cells').value, 10) || 40,
- walkers: parseInt(document.getElementById('maze-walkers').value, 10) || 20,
- stop_prob: (parseInt(document.getElementById('maze-stop').value, 10) || 0) / 100,
- branch_prob:(parseInt(document.getElementById('maze-branch').value, 10) || 0) / 100,
- close_prob: (parseInt(document.getElementById('maze-close').value, 10) || 0) / 100,
- };
-}
-
-function maze_applicable() {
- return current_stage === 'goldberg' || current_stage === 'relaxed';
-}
-
-function update_maze() {
- const show = document.getElementById('maze-show').checked;
- if (!show || !maze_applicable() || !cache.goldberg) {
- request_render();
- return;
- }
- if (!maze.wall_edges) {
- maze.generate(cache.goldberg, get_maze_params());
- }
- const use_relaxed = current_stage === 'relaxed';
- maze.upload(gl, cache.goldberg, use_relaxed, get_wall_width());
- request_render();
-}
const status_el = document.getElementById('status');
const stats_el = document.getElementById('stats');
@@ -122,7 +85,6 @@ gl.polygonOffset(1, 1);
const face_prog = create_program(gl, face_vert_src, face_frag_src);
const edge_prog = create_program(gl, edge_vert_src, edge_frag_src);
-const wall_prog = create_program(gl, wall_vert_src, wall_frag_src);
let face_pos_buf = null, face_col_buf = null, face_pbr_buf = null, face_noise_buf = null, edge_pos_buf = null;
let face_verts = 0, edge_verts = 0;
@@ -289,7 +251,6 @@ async function build() {
set_status('Building Goldberg dual…'); await yield_ui(30);
cache.goldberg = Goldberg_Polyhedron.from_subdivided(cache.poly);
cache.adj = null;
- maze.invalidate(gl);
undo_state.invalidate();
}
if (current_stage === 'goldberg') {
@@ -313,7 +274,6 @@ async function build() {
} finally {
building = false;
btn.disabled = false;
- update_maze();
snapshot_now();
}
}
@@ -480,7 +440,6 @@ function load_paint(file) {
document.getElementById('depth').value = data.depth;
document.getElementById('depth-val').textContent = data.depth;
cache.poly = null; cache.goldberg = null; cache.adj = null;
- maze.invalidate(gl);
}
paint.face_colors.clear();
paint.face_pbr.clear();
@@ -766,8 +725,6 @@ function render_frame(ts) {
gl.drawArrays(gl.LINES, 0, edge_verts);
gl.disableVertexAttribArray(ep);
- const show_maze = document.getElementById('maze-show').checked && maze_applicable();
- maze.draw(gl, wall_prog, mvp, show_maze);
gl.enable(gl.POLYGON_OFFSET_FILL);
}
@@ -811,7 +768,6 @@ document.getElementById('depth').addEventListener('input', () => {
cache.poly = null;
cache.goldberg = null;
cache.adj = null;
- maze.invalidate(gl);
});
document.getElementById('build-btn').addEventListener('click', () => {
@@ -822,15 +778,6 @@ document.getElementById('build-btn').addEventListener('click', () => {
build();
});
-document.getElementById('maze-algo').addEventListener('change', e => {
- const is_walker = e.target.value === 'walker';
- document.getElementById('prim-params').style.display = is_walker ? 'none' : '';
- document.getElementById('walker-params').style.display = is_walker ? '' : 'none';
-});
-
-document.getElementById('maze-show').addEventListener('change', () => { update_maze(); });
-document.getElementById('maze-width').addEventListener('input', () => { update_maze(); });
-document.getElementById('maze-btn').addEventListener('click', () => { maze.invalidate(gl); update_maze(); });
document.getElementById('spin-btn').addEventListener('click', () => {
const btn = document.getElementById('spin-btn');
diff --git a/standalone/goldberg-sphere/shaders/wall.frag b/standalone/goldberg-sphere/shaders/wall.frag
deleted file mode 100644
index f943577..0000000
--- a/standalone/goldberg-sphere/shaders/wall.frag
+++ /dev/null
@@ -1,2 +0,0 @@
-precision mediump float;
-void main() { gl_FragColor = vec4(1.0, 0.58, 0.08, 1.0); }
diff --git a/standalone/goldberg-sphere/shaders/wall.vert b/standalone/goldberg-sphere/shaders/wall.vert
deleted file mode 100644
index cf3741d..0000000
--- a/standalone/goldberg-sphere/shaders/wall.vert
+++ /dev/null
@@ -1,3 +0,0 @@
-attribute vec3 a_pos;
-uniform mat4 u_mvp;
-void main() { gl_Position = u_mvp * vec4(a_pos, 1.0); }
diff --git a/standalone/goldberg-sphere/topology.mjs b/standalone/goldberg-sphere/topology.mjs
index 52e41c7..5952412 100644
--- a/standalone/goldberg-sphere/topology.mjs
+++ b/standalone/goldberg-sphere/topology.mjs
@@ -717,3 +717,34 @@ function find_shared_outer_vertex(face_a, face_b, exclude_vi) {
}
return null;
}
+
+// ---------------------------------------------------------------------------
+// Face adjacency (shared-edge graph)
+// ---------------------------------------------------------------------------
+
+// Build adjacency list for Goldberg faces via shared edges.
+// Uses unrelaxed vertex positions as canonical keys — topology is stable across stages.
+export function build_goldberg_adjacency(goldberg) {
+ const edge_to_entries = new Map();
+ for (let fi = 0; fi < goldberg.faces.length; fi++) {
+ const verts = goldberg.faces[fi].vertices_3d;
+ const n = verts.length;
+ for (let i = 0; i < n; i++) {
+ const a = verts[i], b = verts[(i + 1) % n];
+ const ka = `${a.x.toFixed(5)},${a.y.toFixed(5)},${a.z.toFixed(5)}`;
+ const kb = `${b.x.toFixed(5)},${b.y.toFixed(5)},${b.z.toFixed(5)}`;
+ const key = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
+ if (!edge_to_entries.has(key)) { edge_to_entries.set(key, []); }
+ edge_to_entries.get(key).push({ fi, edge_idx: i });
+ }
+ }
+ const adj = Array.from({ length: goldberg.faces.length }, () => []);
+ for (const entries of edge_to_entries.values()) {
+ if (entries.length === 2) {
+ const [e0, e1] = entries;
+ adj[e0.fi].push({ fj: e1.fi, fi_edge_idx: e0.edge_idx });
+ adj[e1.fi].push({ fj: e0.fi, fi_edge_idx: e1.edge_idx });
+ }
+ }
+ return adj;
+}