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>
66 lines
2.6 KiB
JavaScript
66 lines
2.6 KiB
JavaScript
// Orthographic projection of the relaxed 3D Goldberg sphere.
|
||
//
|
||
// Rotates the sphere so the current face's centroid points toward +z,
|
||
// then projects each vertex's (x, y) components directly to canvas.
|
||
// Faces on the far hemisphere (z < 0) are still included if in the
|
||
// neighbourhood — they'll just appear behind the centre.
|
||
//
|
||
// 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_ortho_3d(poly, neighborhood, root_face_index, cx, cy, face_size) {
|
||
// --- Determine projection centre from root face ---
|
||
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 = the outward direction we point toward +z.
|
||
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);
|
||
|
||
// Build orthonormal tangent frame {u, v, w=C}.
|
||
// u = perpendicular to C in the horizontal plane; v = C × u.
|
||
const up = Math.abs(C.y) < 0.9 ? new Vec3(0, 1, 0) : new Vec3(1, 0, 0);
|
||
const u = C.cross(up).normalize(); // points "right" on screen
|
||
const v = C.cross(u).normalize(); // points "up" on screen (then flipped for canvas)
|
||
|
||
// Project a Vec3 onto the tangent frame, returns [canvas_x, canvas_y, z_depth].
|
||
const project = (P) => [
|
||
P.dot(u), // tangent x
|
||
P.dot(v), // tangent y (flipped to canvas below)
|
||
P.dot(C), // depth (1 = facing viewer, -1 = back)
|
||
];
|
||
|
||
// Determine scale from root face's projected circumradius.
|
||
const root_proj = root_verts.map(project);
|
||
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 = verts.map(P => {
|
||
const [tx, ty] = project(P);
|
||
return [cx + tx * scale, cy - ty * scale]; // flip y: +v → up on canvas
|
||
});
|
||
placements.set(face.index, {
|
||
vertices_2d: pts,
|
||
centroid_2d: centroid_2d(pts),
|
||
depth: entry.depth,
|
||
});
|
||
}
|
||
|
||
return placements;
|
||
}
|