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