The 3D point a click gives when it snaps to a face, an edge, a midpoint, a face center, an axis direction or a locked direction. That point becomes model geometry.
How the precision is kept
Which thing the cursor snaps to is decided in screen pixels, but the point returned is computed in 64-bit model coordinates: the end or middle of an edge, a ray and triangle intersection, or the closest points of two 3D lines. End points are the stored vertices moved by their placement matrix.
Open item. Transform face normals with the inverse transpose (applyNormal in vec.js) so faces of unevenly scaled placements get their true plane.
Calculations under this policy
12 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
Where the cursor's ray meets one triangle of a face.
det = e₁ · (d × e₂); u, v barycentric; t = e₂ · (s × e₁) ÷ det
Möller–Trumbore on nine numbers of a Float64Array. A triangle is skipped when |det| < 1e-14, an absolute threshold on a value that scales with the triangle's area and the length of the ray direction. The barycentric limits have a slack of 1e-9.
m3d/infer.js
lines 257 to 270
CadCraft 0.8.2
/** rayTri on a triangle packed in a Float64Array at q (9 numbers). */
function rayTriA(o, d, T, q) {
const ax = T[q], ay = T[q + 1], az = T[q + 2];
const e1x = T[q + 3] - ax, e1y = T[q + 4] - ay, e1z = T[q + 5] - az, e2x = T[q + 6] - ax, e2y = T[q + 7] - ay, e2z = T[q + 8] - az;
const px = d[1] * e2z - d[2] * e2y, py = d[2] * e2x - d[0] * e2z, pz = d[0] * e2y - d[1] * e2x;
const det = e1x * px + e1y * py + e1z * pz;
if (Math.abs(det) < 1e-14) return null;
const inv = 1 / det, sx = o[0] - ax, sy = o[1] - ay, sz = o[2] - az, u = (sx * px + sy * py + sz * pz) * inv;
if (u < -1e-9 || u > 1 + 1e-9) return null;
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x, v = (d[0] * qx + d[1] * qy + d[2] * qz) * inv;
if (v < -1e-9 || u + v > 1 + 1e-9) return null;
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
return t > 0 ? t : null;
}
Point on the face under the cursor
64-bit
The nearest face the cursor's ray meets and the 3D point on it.
o_L = M⁻¹ · o; d_L = M⁻¹ · d; p = o + d × t
The ray is taken into each definition's own space with the inverse placement matrix and tested against the untransformed triangles; t is the same in both spaces. The point returned is o + d × t in world coordinates, so it lies on the ray; its distance from the placed face depends on the rounding of the inverse matrix.
m3d/infer.js
lines 203 to 222
CadCraft 0.8.2
/** The nearest face a ray meets (filter(f) → false skips a face): {t, f, p} | null. */
hit(ray, filter) {
let best = null;
const lim = { t: Infinity }, o = ray.o, d = ray.d;
for (const P of this.insts) {
const inv = P.inv, oL = apply(inv, o), dL = applyDir(inv, d), tri = P.L.tri;
P.L.ftree.ray(oL, dL, (i) => {
const q = i * 9;
const t = rayTriA(oL, dL, tri, q);
if (t === null || t >= lim.t) return false;
const f = P.face(P.L.triFace[i]);
if (!f || (filter && !filter(f))) return false;
const p = add(o, mul(d, t));
if (this.keep && !this.keep(p)) return false;
best = { t, f, p }; lim.t = t;
return false;
}, lim);
}
return best;
}
World plane of a placed face
64-bit
The triangles, normal and plane of a face in model coordinates, used to draw on that face.
n = normalize(M · n_local); d = n · (M · P)
The world normal is the local normal multiplied by the placement matrix's 3×3 part and normalized. That is the true normal for a rotation, mirror or uniform scale; for a placement scaled unevenly it is not, and the plane built from it is tilted. vec.js has applyNormal (inverse transpose) but this file does not import it.
m3d/infer.js
lines 162 to 172
CadCraft 0.8.2
/** World face i (null when the section cut hides it). */
face(i) {
let f = this.fo.get(i);
if (f !== undefined) return f;
const lf = this.L.faces[i], m = this.m;
const tris = lf.tris.map((t) => t.map((p) => apply(m, p)));
const n = norm(applyDir(m, lf.n));
f = this.keep && !tris.some((t) => t.some(this.keep)) ? null : { path: this.path, def: this.def.id, id: lf.id, tris, n, plane: { n, d: dot(n, apply(m, lf.P)) } };
this.fo.set(i, f);
return f;
}
Center of a face
64-bit
The center point offered as a snap on the face under the cursor.
c = Σ (a + b + d) × w ÷ 3 ÷ Σ w, w = |(b − a) × (d − a)|
The area-weighted centroid of the face's triangles. The weights come from edge vectors, so they do not depend on the distance from the origin; the weighted sum adds full coordinates and is divided once at the end.
m3d/infer.js
lines 417 to 425
CadCraft 0.8.2
/** The area centroid of a scene face (holes left out, since its triangles leave them out). */
export function faceCenter(f) {
let A = 0, c = [0, 0, 0];
for (const [a, b, d] of f.tris) {
const w = len(cross(sub(b, a), sub(d, a)));
A += w; c = add(c, mul(add(add(a, b), d), w / 3));
}
return A > 0 ? mul(c, 1 / A) : f.tris[0][0].slice();
}
Is a point on a face
64-bit
Whether a snapped point lies on the face under the cursor, so a shape started there takes the face's plane.
|n · p − d| ≤ tol and distance to a triangle ≤ tol, tol = 1e-6 × max(1, |x|, |y|, |z|)
The tolerance is 1 micrometer near the origin and grows in proportion to the largest coordinate of the point beyond 1 m.
m3d/infer.js
lines 426 to 432
CadCraft 0.8.2
/** Is p on scene face f: in its plane and inside its outline, or on it (within 1 µm, scaled with the size)? */
export function onFace(f, p) {
const tol = 1e-6 * Math.max(1, Math.abs(p[0]), Math.abs(p[1]), Math.abs(p[2]));
if (Math.abs(dot(f.n, p) - f.plane.d) > tol) return false;
for (const [a, b, c] of f.tris) if (triDist(p, a, b, c) <= tol) return true;
return false;
}
Distance from a point to a triangle
64-bit
The distance used to decide whether a point is inside a face's outline or on it.
Works through the vertex, edge and interior regions in turn. The interior case divides by va + vb + vc, which is zero for a triangle with no area; the result is then NaN and fails the comparison in onFace.
One addition per coordinate, which rounds once; the halving is exact.
m3d/infer.js
line 333
CadCraft 0.8.2
tryPt(mul(add(e.a, e.b), 0.5), 'mid', 0, { edge: e });
Point on an edge
64-bit
The point of an edge nearest the cursor, offered when no end or midpoint is in reach.
The closest point of the segment to the cursor's ray (rayToSegment in vec.js), clamped to the segment's ends. It is a point of the segment's own line, a + u × s.
m3d/infer.js
lines 384 to 387
CadCraft 0.8.2
const r = rayToSegment(ray.o, ray.d, e.a, e.b);
if (r.t < 0 || (sc.keep && !sc.keep(r.onSeg))) continue;
const d = sd(r.onSeg);
if (d < R * 0.75) ons.push({ p: r.onSeg, kind: 'edge', d, edge: e });
Point along an axis from the start point
64-bit
The point on a red, green or blue line (or one parallel or square to an edge) through the start point.
The point of the axis line nearest the cursor's ray, from the closest points of two 3D lines (lineLine in vec.js). A point within 1e-9 m of the start point is not offered.
m3d/infer.js
lines 371 to 376
CadCraft 0.8.2
for (const [kind, dir] of cands) {
const ll = lineLine(ray.o, ray.d, opts.from, dir);
if (!ll || ll.t < 0) continue;
const d = sd(ll.p2);
if (d < R && (!ax || d < ax.d) && dist(ll.p2, opts.from) > 1e-9) ax = { p: ll.p2, kind, d, dir: norm(dir) };
}
Where an axis line meets an edge
64-bit
The point where the line from the start point along an axis meets the edge or guide under the cursor.
The two 3D lines are intersected as closest points; the result is used when those points are within 1e-6 m (absolute) of each other, and the point returned is the one on the axis line.
m3d/infer.js
lines 400 to 404
CadCraft 0.8.2
// where the axis line meets the edge/guide: a strong point
const od = on.edge ? sub(on.edge.b, on.edge.a) : on.gline.d;
const oa = on.edge ? on.edge.a : on.gline.p;
const ll = lineLine(opts.from, ax.dir, oa, od);
if (ll && dist(ll.p1, ll.p2) < 1e-6 && sd(ll.p1) < R) return { p: ll.p1, kind: ax.kind, tip: t(on.kind === 'guide' ? 'm3d.tip.axis_guide' : 'm3d.tip.axis_edge', { axis: TIPS[ax.kind] }), dir: ax.dir, edge: on.edge };
Point on a locked direction
64-bit
The point kept on a line when a direction is locked with Shift or an arrow key.
p = from + d × ((q − from) · d) ÷ (d · d)
With a snap point q in reach the result is q dropped square onto the locked line. Otherwise it is the point of the locked line nearest the cursor's ray; when the two lines are parallel the start point is returned.
m3d/infer.js
lines 343 to 351
CadCraft 0.8.2
// a locked direction (Shift or an arrow key) keeps the point on that line
if (opts.from && opts.lockDir) {
const d = norm(opts.lockDir);
let p;
if (pt) p = closestOnLine(pt.p, opts.from, d).p;
else {
const ll = lineLine(ray.o, ray.d, opts.from, d);
p = ll ? ll.p2 : opts.from;
}
Distance along a line under the cursor
64-bit
The point on a line nearest the cursor and how far along it lies, used by push/pull and moves along an axis.
Returns the point on the line and its parameter s from lineLine in vec.js. s counts in lengths of dir, so it is a distance only when dir is a unit vector.
m3d/infer.js
lines 764 to 769
CadCraft 0.8.2
/** The point where the pick ray meets the line through `o` along `dir` (push/pull, move along an axis). */
export function alongLine(cam, x, y, o, dir) {
const ray = cam.ray(x, y);
const ll = lineLine(ray.o, ray.d, o, dir);
return ll ? { p: ll.p2, s: ll.s } : null;
}
In detail
Method
Möller–Trumbore ray and triangle test in each definition's own space; closest points of two lines; area-weighted centroid; point to triangle distance (the code cites Ericson, Real-Time Collision Detection 5.1.5).
Number types
64-bit numbers; triangles packed in a Float64Array.
Tolerances
Ray and triangle: |det| < 1e-14 (absolute) skips the triangle; barycentric slack 1e-9. Point on a face: 1e-6 × max(1, |x|, |y|, |z|). Axis line meets an edge line when their closest points are within 1e-6 m. A point within 1e-9 m of the start is not offered on an axis.
Limits
The world normal of a face in a placed group is the local normal times the placement matrix, which is the true normal only for a placement without uneven scale. The 1e-14 determinant test does not scale with the triangle.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.