Files
websperiments/standalone/goldberg-sphere/placer_relax_loose.mjs
mikael-lovqvists-claude-agent 9600a2bc2a Add Goldberg Polyhedron Paint experiment
Interactive WebGL Goldberg polyhedron viewer and painter with PBR
shading, adjustable environment lighting, paint tools (pen, brush,
circle, fill, line, pick), undo/redo, colour palettes, and mesh
relaxation. Added to the standalone experiments index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:00:19 +00:00

98 lines
3.2 KiB
JavaScript

// Loose relaxation: starts from the unfolded layout, then runs spring
// iterations that only pull BFS parent→child shared edges together.
// Sibling-sibling edges are ignored, so gaps remain between depth rings.
// Kept because the partial closing produces an interesting visual effect.
//
// Returns Map<face_index, { vertices_2d, centroid_2d, depth }>
import { centroid_2d } from './geometry.mjs';
import { place_unfold } from './placer_unfold.mjs';
const RELAX_ITERS = 14;
const RELAX_ALPHA = 0.38;
export function place_relax_loose(poly, neighborhood, root_face_index, cx, cy, face_size) {
const placements = place_unfold(poly, neighborhood, root_face_index, cx, cy, face_size);
// Build BFS-tree-only adjacency (parent → child edges).
const adj = [];
for (const entry of neighborhood) {
if (entry.parent_index === null) { continue; }
const fi_b = entry.face.index;
const fi_a = entry.parent_index;
if (!placements.has(fi_a) || !placements.has(fi_b)) { continue; }
const face_a = poly.faces[fi_a];
const face_b = entry.face;
const ea = entry.parent_edge_index;
let eb = -1;
for (let j = 0; j < face_b.edge_neighbors.length; j++) {
if (face_b.edge_neighbors[j] === fi_a) { eb = j; break; }
}
if (eb === -1) { continue; }
adj.push({ fi_a, na: face_a.size, ea, fi_b, nb: face_b.size, eb });
}
// Pin center face vertices so layout stays anchored.
const pinned = new Set();
const center_pl = placements.get(root_face_index);
if (center_pl) {
for (let i = 0; i < center_pl.vertices_2d.length; i++) {
pinned.add(`${root_face_index}:${i}`);
}
}
for (let iter = 0; iter < RELAX_ITERS; iter++) {
const deltas = new Map();
const delta = (fi, vi) => {
const k = `${fi}:${vi}`;
if (!deltas.has(k)) { deltas.set(k, [0, 0]); }
return deltas.get(k);
};
for (const { fi_a, na, ea, fi_b, nb, eb } of adj) {
const verts_a = placements.get(fi_a).vertices_2d;
const verts_b = placements.get(fi_b).vertices_2d;
// A.v[ea] ↔ B.v[(eb+1)%nb]
const a0 = verts_a[ea], b0 = verts_b[(eb + 1) % nb];
const mx0 = (a0[0] + b0[0]) / 2, my0 = (a0[1] + b0[1]) / 2;
// A.v[(ea+1)%na] ↔ B.v[eb]
const a1 = verts_a[(ea + 1) % na], b1 = verts_b[eb];
const mx1 = (a1[0] + b1[0]) / 2, my1 = (a1[1] + b1[1]) / 2;
if (!pinned.has(`${fi_a}:${ea}`)) {
const d = delta(fi_a, ea); d[0] += mx0 - a0[0]; d[1] += my0 - a0[1];
}
if (!pinned.has(`${fi_b}:${(eb + 1) % nb}`)) {
const d = delta(fi_b, (eb + 1) % nb); d[0] += mx0 - b0[0]; d[1] += my0 - b0[1];
}
if (!pinned.has(`${fi_a}:${(ea + 1) % na}`)) {
const d = delta(fi_a, (ea + 1) % na); d[0] += mx1 - a1[0]; d[1] += my1 - a1[1];
}
if (!pinned.has(`${fi_b}:${eb}`)) {
const d = delta(fi_b, eb); d[0] += mx1 - b1[0]; d[1] += my1 - b1[1];
}
}
for (const [key, [dx, dy]] of deltas) {
const colon = key.lastIndexOf(':');
const fi = parseInt(key.slice(0, colon), 10);
const vi = parseInt(key.slice(colon + 1), 10);
const pl = placements.get(fi);
if (!pl) { continue; }
const v = pl.vertices_2d[vi];
pl.vertices_2d[vi] = [v[0] + dx * RELAX_ALPHA, v[1] + dy * RELAX_ALPHA];
}
for (const pl of placements.values()) {
pl.centroid_2d = centroid_2d(pl.vertices_2d);
}
}
return placements;
}