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>
78 lines
2.9 KiB
JavaScript
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;
|
|
}
|