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 @@ -
-

Maze

-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-

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; +}