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