Files
websperiments/standalone/goldberg-sphere/placer_gnomonic_3d.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

78 lines
2.9 KiB
JavaScript

// Gnomonic projection of the relaxed 3D Goldberg sphere.
//
// Projects each vertex from the sphere's centre onto the tangent plane at
// the current face's centroid. Equivalent to placing your eye at the centre
// of the sphere and looking outward — geodesics project to straight lines
// and tile shapes stay natural even for faces away from centre.
// Near the 90° horizon the projection diverges; faces that far out are rare
// in typical neighbourhood depths so this is acceptable.
//
// Requires goldberg.relax_sphere() to have been called first.
// Falls back to vertices_3d if relaxed_vertices_3d is not set.
//
// Returns Map<face_index, { vertices_2d, centroid_2d, depth }>
import { Vec3, centroid_2d } from './geometry.mjs';
export function place_gnomonic_3d(poly, neighborhood, root_face_index, cx, cy, face_size) {
const root_face = poly.faces[root_face_index];
const root_verts = root_face.relaxed_vertices_3d ?? root_face.vertices_3d;
// Normalised centroid of root face = projection centre C.
let sx = 0, sy = 0, sz = 0;
for (const v of root_verts) { sx += v.x; sy += v.y; sz += v.z; }
const cl = Math.sqrt(sx*sx + sy*sy + sz*sz);
const C = new Vec3(sx / cl, sy / cl, sz / cl);
// Orthonormal tangent frame {u, v} at C.
const up = Math.abs(C.y) < 0.9 ? new Vec3(0, 1, 0) : new Vec3(1, 0, 0);
const u = C.cross(up).normalize();
const v = C.cross(u).normalize();
// Gnomonic project a unit-sphere point P onto the tangent plane at C.
// Returns [tx, ty] in tangent-plane coords, or null if behind the plane.
const project = (P) => {
const d = P.dot(C);
if (d <= 1e-6) { return null; }
// Point on tangent plane: P/d. Subtract C to get offset, decompose into u,v.
const inv_d = 1 / d;
const qx = P.x * inv_d - C.x;
const qy = P.y * inv_d - C.y;
const qz = P.z * inv_d - C.z;
return [qx * u.x + qy * u.y + qz * u.z,
qx * v.x + qy * v.y + qz * v.z];
};
// Determine canvas scale from root face's projected circumradius.
const root_proj = root_verts.map(p => project(p));
if (root_proj.some(p => p === null)) { return new Map(); }
const rcx = root_proj.reduce((s, p) => s + p[0], 0) / root_proj.length;
const rcy = root_proj.reduce((s, p) => s + p[1], 0) / root_proj.length;
const root_r = Math.max(...root_proj.map(([x, y]) =>
Math.sqrt((x - rcx) ** 2 + (y - rcy) ** 2)));
if (root_r < 1e-10) { return new Map(); }
const scale = face_size / root_r;
// Build placements.
const placements = new Map();
for (const entry of neighborhood) {
const face = entry.face;
const verts = face.relaxed_vertices_3d ?? face.vertices_3d;
const pts = [];
let ok = true;
for (const P of verts) {
const proj = project(P);
if (!proj) { ok = false; break; }
pts.push([cx + proj[0] * scale, cy - proj[1] * scale]);
}
if (!ok) { continue; }
placements.set(face.index, {
vertices_2d: pts,
centroid_2d: centroid_2d(pts),
depth: entry.depth,
});
}
return placements;
}