Builds the mesh that shows, exports, prints and renders by applying each modifier in turn to a copy of the mesh.
How the precision is kept
Each modifier maps the 64-bit vertex positions of a copy; the mesh itself is not changed. Copies of an array are placed from the original by i times the step, not by repeated addition. Layers of type f32 (UVs, creases, weights) are copied as 32-bit arrays unchanged.
Open item. Put merged mirror seam vertices on the mirror plane (coordinate 0) instead of leaving the kept vertex where it was.
Calculations under this policy
11 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.
Adds a mirrored copy across the x, y or z plane through the origin and merges the seam.
copy: coordinate k → −k; seam: vertices with |coordinate k| ≤ dist merged within max(2·dist, 1e-12)
Negation is exact. Seam vertices are merged with mergeByDistance, which keeps the first vertex of each pair where it is, so a seam vertex that was up to dist off the plane stays off it by that amount.
mesh/modifiers.js
lines 361 to 371
ModelCraft 0.8.1
case 'mirror': {
let cur = mesh;
for (const ax of ['x', 'y', 'z']) {
if (!p[ax]) continue;
const k = AX[ax];
const part = cur.clone();
cur = combine([{ mesh: cur }, { mesh: part, map: (q) => { const r = q.slice(); r[k] = -r[k]; return r; }, flip: true }]);
if (p.merge) { const onPlane = []; for (let v = 0; v < cur.nv; v++) if (!cur.vertDead(v) && Math.abs(cur.P[v * 3 + k]) <= p.dist) onPlane.push(v); if (onPlane.length) O.mergeByDistance(cur, Math.max(p.dist * 2, 1e-12), onPlane); }
}
return cur;
}
Radial array
64-bit
Places copies turned round an axis through the origin.
step = (angle·π ÷ 180) ÷ (n for a full circle, else n − 1); aᵢ = step·i; [u′, v′] = [u·cos aᵢ − v·sin aᵢ, u·sin aᵢ + v·cos aᵢ]
Copy i is rotated from the original by step × i, so errors do not add up from copy to copy. A full circle is recognized when ||angle| − 360| < 1e-9 degrees. The cosine and sine are computed once per copy.
mesh/modifiers.js
lines 375 to 381
ModelCraft 0.8.1
if (p.mode === 'radial') {
const k = AX[p.axis], full = Math.abs(Math.abs(p.angle) - 360) < 1e-9, step = ((p.angle * Math.PI) / 180) / (full ? n : Math.max(1, n - 1));
for (let i = 0; i < n; i++) {
const a = step * i, c = Math.cos(a), s = Math.sin(a);
parts.push({ mesh, map: (q) => { const r = q.slice(), i1 = (k + 1) % 3, i2 = (k + 2) % 3; r[i1] = q[i1] * c - q[i2] * s; r[i2] = q[i1] * s + q[i2] * c; return r; } });
}
} else if (p.mode === 'curve' && ref?.pts?.length > 1) {
Linear array
64-bit
Places copies at a step made of a fraction of the mesh's box size plus a fixed offset.
d = size ∘ rel + abs; copy i: p′ = p + d·i
The step is computed once from the bounding box and copy i is placed at p + d × i from the original, not by adding d repeatedly.
mesh/modifiers.js
lines 388 to 389
ModelCraft 0.8.1
const d = add([size[0] * p.rel[0], size[1] * p.rel[1], size[2] * p.rel[2]], p.abs);
for (let i = 0; i < n; i++) parts.push({ mesh, map: (q) => add(q, mul(d, i)) });
Frames along a polyline
64-bit
The point, tangent and two cross directions at a given length along a polyline; used by the curve modifier and the array along a curve.
Lᵢ = Lᵢ₋₁ + |ptsᵢ − ptsᵢ₋₁|; f = (s − Lᵢ₋₁) ÷ (Lᵢ − Lᵢ₋₁); p = ptsᵢ₋₁ + (ptsᵢ − ptsᵢ₋₁)·f; b = unit(t × up), n = b × t
Lengths are a running sum of Math.hypot segment lengths in order. The length asked for is clamped to 0…total, a segment of zero length divides by 1 instead, and the tangent is the segment's own direction, so it changes abruptly at each polyline point. The up vector is z unless |t_z| ≥ 0.99, then y.
mesh/modifiers.js
lines 287 to 303
ModelCraft 0.8.1
/** A polyline's frames by arc length: {length, at(s) → {p, t, n, b}} (n: the up direction made square to the tangent). */
export function curveFrames(pts) {
const L = [0];
for (let i = 1; i < pts.length; i++) L.push(L[i - 1] + len(sub(pts[i], pts[i - 1])));
const total = L[L.length - 1];
const at = (s) => {
const x = Math.max(0, Math.min(total, s));
let i = 1;
while (i < L.length - 1 && L[i] < x) i++;
const seg = L[i] - L[i - 1] || 1, f = (x - L[i - 1]) / seg, p = add(pts[i - 1], mul(sub(pts[i], pts[i - 1]), f));
const t = norm(sub(pts[i], pts[i - 1]));
let up = Math.abs(t[2]) < 0.99 ? [0, 0, 1] : [0, 1, 0];
const b = norm(cross(t, up)), n = cross(b, t);
return { p, t, n, b };
};
return { length: total, at };
}
Curve modifier
64-bit
Bends the mesh along a curve: each vertex's coordinate on the chosen axis becomes a length along the curve.
frame = F.at(q_k); p′ = frame.p − b·q_i1 + n·q_i2
The coordinate on the axis is used directly as arc length and clamped to the curve by curveFrames, so vertices before the start or past the end all land on the end frames. The curve is the polyline passed in; its sampling sets how closely the result follows the true curve.
mesh/modifiers.js
lines 471 to 475
ModelCraft 0.8.1
const m = mesh.clone(), F = curveFrames(ref.pts), k = AX[p.axis], i1 = (k + 1) % 3, i2 = (k + 2) % 3;
for (let v = 0; v < m.nv; v++) {
if (m.vertDead(v)) continue;
const q = P3(m, v), fr = F.at(q[k]), r = add(fr.p, add(mul(fr.b, -q[i1]), mul(fr.n, q[i2])));
m.P[v * 3] = r[0]; m.P[v * 3 + 1] = r[1]; m.P[v * 3 + 2] = r[2];
Solidify
64-bit
Gives a surface thickness by making two copies moved along the vertex normals.
o₁ − o₂ equals the thickness. Each vertex moves along its area-weighted unit normal by the same distance, so the wall measured square to a face is the thickness times the cosine of the angle between that face and the vertex normal: thinner at sharp corners.
mesh/modifiers.js
lines 412 to 416
ModelCraft 0.8.1
const N = vnormals(mesh), t = p.thickness, o1 = (t * (p.offset + 1)) / 2, o2 = (t * (p.offset - 1)) / 2;
const along = (d) => (q, v) => [q[0] + N[v * 3] * d, q[1] + N[v * 3 + 1] * d, q[2] + N[v * 3 + 2] * d];
// the vertex index is needed: shift copies by hand
const outer = mesh.clone(), inner = mesh.clone();
for (const [m, d] of [[outer, o1], [inner, o2]]) { const f = along(d); for (let v = 0; v < m.nv; v++) if (!m.vertDead(v)) { const q = f(P3(m, v), v); m.P[v * 3] = q[0]; m.P[v * 3 + 1] = q[1]; m.P[v * 3 + 2] = q[2]; } }
Displace noise
64-bit
A seeded smooth noise value between −1 and 1 at a point; the displace modifier multiplies it by the strength.
value = 2 × trilinear blend of 8 lattice values, with smoothstep weights t²(3 − 2t), − 1
Lattice values come from an integer hash as a 32-bit unsigned integer divided by 4294967295; the blend is 64-bit. The hash adds products of the lattice indices with large constants before a bitwise OR with 0: once an index passes about 7 million that sum exceeds 2⁵³ and its low bits are already rounded. The bitwise operators act on the hash, not on a measured value.
mesh/modifiers.js
lines 158 to 164
ModelCraft 0.8.1
export function noise3(x, y, z, seed = 1) {
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z), xf = x - xi, yf = y - yi, zf = z - zi;
const s = (t) => t * t * (3 - 2 * t), u = s(xf), v = s(yf), w = s(zf);
const L = (a, b, t) => a + (b - a) * t;
const c = (dx, dy, dz) => hash3(xi + dx, yi + dy, zi + dz, seed);
return 2 * L(L(L(c(0, 0, 0), c(1, 0, 0), u), L(c(0, 1, 0), c(1, 1, 0), u), v), L(L(c(0, 0, 1), c(1, 0, 1), u), L(c(0, 1, 1), c(1, 1, 1), u), v), w) - 1;
}
Lattice displacement
64-bit
The move of a point inside a cage whose boundary points were moved.
u = (x − min_x) ÷ (max_x − min_x) (same for v, w); d = Σ faces − Σ edges + Σ corners (transfinite interpolation)
Six face blends are added, twelve edge blends subtracted and eight corner terms added, each weighted by products of u, v, w and their complements. A zero box extent divides by 1 instead. u, v and w are not clamped, while the indices into the cage are, so a point outside the rest box is extrapolated with weights outside 0…1.
mesh/modifiers.js
lines 250 to 284
ModelCraft 0.8.1
/** cage = {res: [nx, ny, nz], min, max (the rest box), disp: Map 'i,j,k' → [dx, dy, dz] (the boundary points' moves)}. */
export function latticeDisplace(p, cage) {
const [nx, ny, nz] = cage.res, { min, max } = cage;
const u = (p[0] - min[0]) / ((max[0] - min[0]) || 1), v = (p[1] - min[1]) / ((max[1] - min[1]) || 1), w = (p[2] - min[2]) / ((max[2] - min[2]) || 1);
const D = (i, j, k) => cage.disp.get(i + ',' + j + ',' + k) || [0, 0, 0];
// a boundary face of the grid, bilinear: s, t in 0…1 across it
const face = (fixed, val, s, t) => {
const [na, nb] = fixed === 0 ? [ny, nz] : fixed === 1 ? [nx, nz] : [nx, ny];
const a = Math.max(0, Math.min(na - 1, s * (na - 1))), b = Math.max(0, Math.min(nb - 1, t * (nb - 1)));
const a0 = Math.min(na - 2, Math.floor(a)), b0 = Math.min(nb - 2, Math.floor(b)), fa = a - Math.max(0, a0), fb = b - Math.max(0, b0);
const A0 = Math.max(0, a0), B0 = Math.max(0, b0);
const at = (x, y) => (fixed === 0 ? D(val, x, y) : fixed === 1 ? D(x, val, y) : D(x, y, val));
const r = [0, 0, 0];
const cs = [[A0, B0, (1 - fa) * (1 - fb)], [A0 + 1, B0, fa * (1 - fb)], [A0, B0 + 1, (1 - fa) * fb], [A0 + 1, B0 + 1, fa * fb]];
for (const [x, y, k] of cs) { const d = at(Math.min(x, na - 1), Math.min(y, nb - 1)); r[0] += d[0] * k; r[1] += d[1] * k; r[2] += d[2] * k; }
return r;
};
const edge = (axis, a, b, t) => {
// an edge of the box along `axis`, the other two indices fixed at a (0 | last) and b; linear along it
const n = [nx, ny, nz][axis], x = Math.max(0, Math.min(n - 1, t * (n - 1))), x0 = Math.min(n - 2, Math.floor(x)), f = x - Math.max(0, x0), X0 = Math.max(0, x0);
const at = (i) => (axis === 0 ? D(i, a, b) : axis === 1 ? D(a, i, b) : D(a, b, i));
const d0 = at(X0), d1 = at(Math.min(X0 + 1, n - 1));
return [d0[0] + (d1[0] - d0[0]) * f, d0[1] + (d1[1] - d0[1]) * f, d0[2] + (d1[2] - d0[2]) * f];
};
const L = [nx - 1, ny - 1, nz - 1];
const r = [0, 0, 0], acc = (d, k) => { r[0] += d[0] * k; r[1] += d[1] * k; r[2] += d[2] * k; };
acc(face(0, 0, v, w), 1 - u); acc(face(0, L[0], v, w), u);
acc(face(1, 0, u, w), 1 - v); acc(face(1, L[1], u, w), v);
acc(face(2, 0, u, v), 1 - w); acc(face(2, L[2], u, v), w);
for (const [a, A] of [[0, 1 - v], [L[1], v]]) for (const [b, B] of [[0, 1 - w], [L[2], w]]) acc(edge(0, a, b, u), -A * B);
for (const [a, A] of [[0, 1 - u], [L[0], u]]) for (const [b, B] of [[0, 1 - w], [L[2], w]]) acc(edge(1, a, b, v), -A * B);
for (const [a, A] of [[0, 1 - u], [L[0], u]]) for (const [b, B] of [[0, 1 - v], [L[1], v]]) acc(edge(2, a, b, w), -A * B);
for (const [i, I] of [[0, 1 - u], [L[0], u]]) for (const [j, J] of [[0, 1 - v], [L[1], v]]) for (const [k, K] of [[0, 1 - w], [L[2], w]]) acc(D(i, j, k), I * J * K);
return r;
}
Closest point on a triangle
64-bit
The point of a triangle nearest a given point; used by shrinkwrap and by snapping onto a retopology target.
vertex, edge or interior region chosen by the signs of six dot products; interior: a + ab·(v_b ÷ (v_a + v_b + v_c)) + ac·(v_c ÷ (v_a + v_b + v_c))
The region tests compare dot products and their 2 × 2 determinants with zero directly, with no tolerance. The interior case divides by v_a + v_b + v_c without a guard; that sum is zero only for a triangle of zero area.
Finds the nearest point of a target's triangles to a point, with its distance.
r = size ÷ 64, doubled up to 30 times; best = min over triangles in the box p ± r of |closest − p|; accepted when best ≤ r
Triangle boxes are a Float64Array. A candidate found in the search box is accepted only when its distance is not more than the box's half size, otherwise the box is doubled, so a nearer triangle just outside the box is not missed. After 30 doublings with nothing accepted the function returns null and the caller leaves the vertex where it is.
mesh/modifiers.js
lines 323 to 344
ModelCraft 0.8.1
export function nearestOnTris(target) {
const { pos, tri } = target, n = tri.length / 3, boxes = new Float64Array(n * 6);
const V = (i) => [pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]];
for (let t = 0; t < n; t++) for (let k = 0; k < 3; k++) {
const xs = [pos[tri[t * 3] * 3 + k], pos[tri[t * 3 + 1] * 3 + k], pos[tri[t * 3 + 2] * 3 + k]];
boxes[t * 6 + k] = Math.min(...xs); boxes[t * 6 + 3 + k] = Math.max(...xs);
}
const tree = new BoxTree(boxes);
const size = tree.min ? len(sub(tree.max, tree.min)) || 1 : 1;
return (p) => {
let r = size / 64, best = null;
for (let tries = 0; tries < 30 && !best; tries++, r *= 2) {
tree.box(sub(p, [r, r, r]), add(p, [r, r, r]), (t) => {
const q = closestOnTri(p, V(tri[t * 3]), V(tri[t * 3 + 1]), V(tri[t * 3 + 2])), d = len(sub(q, p));
if (!best || d < best.d) best = { q, d, t };
return false;
});
if (best && best.d > r) best = null; // something nearer may sit just outside the box: look wider
}
return best;
};
}
In detail
Method
Per-vertex maps: multiply by factors, negate one coordinate, rotate or translate by a multiple of the step, move along the area-weighted vertex normal, add a transfinite interpolation of a cage's displacements, place on a polyline's frame by arc length, or move to the nearest point of a target's triangles (Ericson, Real-Time Collision Detection, 5.1.5, as cited in the code).
Number types
64-bit numbers and Float64Array positions; noise lattice values are 32-bit integers divided by 4294967295.
Tolerances
Mirror merge: |coordinate| ≤ dist picks the seam vertices, merged within max(2 × dist, 1e-12). Radial array: a full circle when ||angle| − 360| < 1e-9. Array along a curve: step at least 1e-9, copies while i × step ≤ length + 1e-9. Curve frame: the up vector switches from z to y when |t_z| ≥ 0.99. Parameter limits are set in MODS (for example scale factors 1e-4…1e4).
Limits
Solidify and shrinkwrap offsets are measured along vertex normals, not square to each face. Curve and array-along-curve use the polyline handed in, with one tangent per segment. Mirror does not move seam vertices onto the mirror plane.
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.