Point and normal under the pointer
64-bitThe clicked point and the face normal in the face's own coordinates, and where on the material's tile the point lies.
p = M⁻¹ × hit; n = (b − a) × (c − a), made unit; tile uv = planar uv ÷ tile size
The point and the triangle corners go through the inverse placement matrix in 64-bit numbers. For a mesh, the triangle is chosen by the smallest plane distance under 1e-3 with weights not below −1e-4, both absolute and not scaled to the mesh size. The point and normal are handed to a new decal when one is placed.
/** What lies under screen point ev: the face, its placement path, the point and normal in its definition's coordinates, its paint,
* and where on its material's tile it is (tile units: a mesh's UVs, else metres along the face ÷ the tile) | null. */
export function surfaceAt(h, ev) {
const hit = h.scene().hit(h.cam.ray(ev.x, ev.y));
if (!hit) return null;
const path = hit.f.path, ctx = h.model.context(path);
if (!ctx) return null;
const def = ctx.def, inv = invert(ctx.m), pL = apply(inv, hit.p);
const [a, b, c] = hit.f.tris[0].map((q) => apply(inv, q)), nL = norm(cross(sub(b, a), sub(c, a)));
// the placement's paint: the innermost placement that has one (as the renderers walk them)
let paint = null, d = h.model.root;
for (const iid of path) { const inst = d.insts.get(iid); if (!inst) break; if (inst.color) paint = inst.color; d = h.model.def(inst.def); }
let face = null, uv = null;
if (def.mesh) {
// the shown mesh's triangle holding the point: its corners' UVs, weighted
const M = def.shown || def.mesh, T = M.triangulate(), P = M.P, hV = M.hV, uvA = M.attrArray('uv');
let best = null;
for (let i = 0; i < T.n; i++) {
const q = [0, 1, 2].map((k) => { const v = hV[T.tri[i * 3 + k]] * 3; return [P[v], P[v + 1], P[v + 2]]; });
const w = bary(pL, q[0], q[1], q[2]);
if (!w || w.dist > (best ? best.dist : 1e-3) || Math.min(w.a, w.b, w.c) < -1e-4) continue;
best = { i, ...w };
}
if (best) {
face = M.facePaint(T.face[best.i]);
if (uvA) { const hs = [0, 1, 2].map((k) => T.tri[best.i * 3 + k]); uv = [0, 1].map((j) => best.a * uvA[hs[0] * 2 + j] + best.b * uvA[hs[1] * 2 + j] + best.c * uvA[hs[2] * 2 + j]); }
}
} else {
face = def.faces.get(hit.f.id)?.color || null;
}
const p = face || paint, mid = isMaterialPaint(p) ? p.slice(4) : null;
const m = mid && materials.has(mid) ? libMat(mid) : null;
let tuv = null;
if (m) {
if (def.mesh) tuv = uv;
else { const w = planarUV(pL, nL); tuv = [w[0] / m.size[0], w[1] / m.size[1]]; }
}
return { hit, path, def, pL, nL, paint: p, material: m ? mid : null, tuv };
}