forked from mikael-lovqvist/websperiments
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>
98 lines
3.2 KiB
JavaScript
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;
|
|
}
|