Turns a shape description (spheres, boxes, beams, revolved profiles, lattices, booleans, offsets, shells) into a signed distance at any point.
How the precision is kept
Primitive distances are closed-form 64-bit expressions. Three are estimates by design: a tapered beam, a smooth blend, and a triply periodic surface, whose distance is g ÷ |∇g| improved by two projection steps. Sine and cosine come from detmath.js so every engine gives the same bits.
Open item. Guard the divisions by a zero length in the plane, cylinder and rotation cases.
Calculations under this policy
17 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.
A sum of squares, one square root and one subtraction. Near the surface the subtraction takes two nearly equal numbers, so the distance there carries the rounding of the square root, about one unit in the last place of r.
field/field.js
lines 123 to 126
PartCraft 0.8.1
case 'sphere': {
const [cx, cy, cz] = spec.c, r = spec.r;
return leaf((x, y, z) => Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - r, 1, boxBounds(spec.c, r));
}
Box with rounded edges
64-bit
Signed distance to a box, optionally with rounded edges and corners.
Outside, the length of the positive parts of q; inside, the largest component of q. Additions, comparisons and one square root; the rounding radius is subtracted last.
t = (p − m)·u; d_r = |p − m − t·u| − r; d_t = |t| − L ÷ 2; d = |max(d_r, d_t, 0)| + min(max(d_r, d_t), 0)
The axis is divided by its length L with no check for L = 0. The radial offset is the point minus its projection on the axis, a subtraction that loses digits when the point is far along the axis compared with its distance from it.
field/field.js
lines 138 to 149
PartCraft 0.8.1
case 'cylinder': {
const [ax, ay, az] = spec.a, [bx, by, bz] = spec.b, r = spec.r;
const dx = bx - ax, dy = by - ay, dz = bz - az, L2 = dx * dx + dy * dy + dz * dz, L = Math.sqrt(L2), hl = L / 2;
const ux = dx / L, uy = dy / L, uz = dz / L, mx = (ax + bx) / 2, my = (ay + by) / 2, mz = (az + bz) / 2;
return leaf((x, y, z) => {
const px = x - mx, py = y - my, pz = z - mz, t = px * ux + py * uy + pz * uz;
const rx = px - t * ux, ry = py - t * uy, rz = pz - t * uz;
const dr = Math.sqrt(rx * rx + ry * ry + rz * rz) - r, dt = Math.abs(t) - hl;
const o1 = dr > 0 ? dr : 0, o2 = dt > 0 ? dt : 0;
return Math.sqrt(o1 * o1 + o2 * o2) + Math.min(Math.max(dr, dt), 0);
}, 1, boxBounds([mx, my, mz], hl + r));
}
Torus
64-bit
Signed distance to a torus about the z axis.
d = √((√(x² + y²) − R)² + z²) − r
Two square roots and two subtractions, each of which takes nearly equal numbers close to the tube's surface.
field/field.js
lines 150 to 154
PartCraft 0.8.1
case 'torus': {
const [cx, cy, cz] = spec.c, R = spec.R, r = spec.r;
return leaf((x, y, z) => { const px = x - cx, py = y - cy, pz = z - cz; const q = Math.sqrt(px * px + py * py) - R; return Math.sqrt(q * q + pz * pz) - r; }, 1,
[cx - R - r, cy - R - r, cz - r, cx + R + r, cy + R + r, cz + r]);
}
Half space
64-bit
Signed distance to a plane.
d = (n·p − d₀) ÷ |n|
The normal and the offset are divided by the normal's length once, with no check for a zero normal. Then one dot product and one subtraction per point.
field/field.js
lines 155 to 158
PartCraft 0.8.1
case 'plane': {
let [nx, ny, nz] = spec.n; const n = Math.sqrt(nx * nx + ny * ny + nz * nz); nx /= n; ny /= n; nz /= n; const d = spec.d / n;
return leaf((x, y, z) => nx * x + ny * y + nz * z - d, 1, [-INF, -INF, -INF, INF, INF, INF]);
}
Solid of revolution
64-bit
Signed distance to the solid under a profile r(x) turned about the x axis.
ρ = √(y² + z²); d = least distance from (x, ρ) to the profile's segments; inside when ρ ≤ r(x)
Squared distances to each segment are compared and one square root is taken at the end. The sign comes from comparing ρ with the profile's radius at x, interpolated linearly between the two profile points. Profile points must have increasing x, so the interpolation's divisor is positive.
field/field.js
lines 165 to 191
PartCraft 0.8.1
case 'revolve': {
const P = spec.pts, n = P.length;
let rmax = 0; for (const q of P) rmax = Math.max(rmax, q[1]);
const segs = [[P[0][0], 0, P[0][0], P[0][1]]]; // the flat first end
for (let i = 0; i + 1 < n; i++) segs.push([P[i][0], P[i][1], P[i + 1][0], P[i + 1][1]]);
segs.push([P[n - 1][0], P[n - 1][1], P[n - 1][0], 0]); // the flat last end
const f = (x, y, z) => {
const rho = Math.sqrt(y * y + z * z);
let d2 = INF;
for (let i = 0; i < segs.length; i++) {
const s0 = segs[i], ax = s0[0], ay = s0[1], dx = s0[2] - ax, dy = s0[3] - ay, L2 = dx * dx + dy * dy;
const px = x - ax, py = rho - ay;
let t = L2 > 0 ? (px * dx + py * dy) / L2 : 0; t = t < 0 ? 0 : t > 1 ? 1 : t;
const qx = px - t * dx, qy = py - t * dy, q2 = qx * qx + qy * qy;
if (q2 < d2) d2 = q2;
}
let inside = false;
if (x >= P[0][0] && x <= P[n - 1][0]) {
let i = 0; while (i + 2 < n && x > P[i + 1][0]) i++;
const u = (x - P[i][0]) / (P[i + 1][0] - P[i][0]), r = P[i][1] + (P[i + 1][1] - P[i][1]) * u;
inside = rho <= r;
}
const d = Math.sqrt(d2);
return inside ? -d : d;
};
return leaf(f, 1, [P[0][0], -rmax, -rmax, P[n - 1][0], rmax, rmax]);
}
Union, intersection and subtraction
64-bit
Combines shapes by union, intersection or subtraction.
min and max return one of their operands unchanged, so a boolean adds no rounding of its own. The intersect and subtract cases that follow use max the same way.
field/field.js
lines 194 to 198
PartCraft 0.8.1
case 'union': {
const k = spec.a.map(compile), fs = k.map((n) => n.f), n = fs.length;
return {
f: n === 2 ? (x, y, z) => Math.min(fs[0](x, y, z), fs[1](x, y, z)) : (x, y, z) => { let m = INF; for (let i = 0; i < n; i++) { const v = fs[i](x, y, z); if (v < m) m = v; } return m; },
bounds: mergeBounds(k.map((q) => q.bounds)),
Smooth minimum
64-bit
Blends two shapes with a radius k.
h = clamp(½ + (b − a) ÷ 2k, 0, 1); s = b + (a − b)h − k·h(1 − h)
For k ≤ 0 it is the plain minimum, so the division by k never sees zero. The blend lowers the value by at most k ÷ 4, which the bounds and the brick classification allow for.
field/field.js
lines 276 to 280
PartCraft 0.8.1
function smin(a, b, k) { // polynomial smooth minimum
if (!(k > 0)) return Math.min(a, b);
const h0 = 0.5 + 0.5 * (b - a) / k, hh = h0 < 0 ? 0 : h0 > 1 ? 1 : h0;
return b + (a - b) * hh - k * hh * (1 - hh);
}
Offset and shell
64-bit
Grows or shrinks a shape, or hollows it to a wall thickness.
offset: d − δ; shell inward: max(d, −d − t); shell centered: |d| − t ÷ 2
One subtraction for an offset. A shell is the larger of two shifted copies of the distance, or |d| − t ÷ 2 for a centered wall.
field/field.js
lines 240 to 247
PartCraft 0.8.1
case 'offset': {
const A = compile(spec.a), fa = A.f, d = spec.d;
return { f: (x, y, z) => fa(x, y, z) - d, bounds: A.bounds.map((v, i) => (i < 3 ? v - d : v + d)), cls: (cx, cy, cz, r, band) => A.cls(cx, cy, cz, r, band + d) };
}
case 'shell': {
const A = compile(spec.a), fa = A.f, th = spec.th, side = spec.side || 'in';
const f = side === 'mid' ? (x, y, z) => Math.abs(fa(x, y, z)) - th / 2 : side === 'out' ? (x, y, z) => { const v = fa(x, y, z); return Math.max(-v, v - th); } : (x, y, z) => { const v = fa(x, y, z); return Math.max(v, -v - th); };
const pad = side === 'out' ? th : side === 'mid' ? th / 2 : 0;
Move, turn and scale
64-bit
A shape under a rigid motion and a uniform scale.
q = Rᵀ(p − move) ÷ s; d = d₀(q) × s
The point is moved back, multiplied by the transpose of the 3 × 3 and divided by the scale; the distance found is multiplied by the scale. The transpose is the inverse only if the 3 × 3 is a rotation, which is not checked.
field/field.js
lines 258 to 268
PartCraft 0.8.1
case 'xf': {
const A = compile(spec.a), fa = A.f, s = spec.scale, R = spec.rot, m = spec.move;
const inv = (x, y, z) => { const px = x - m[0], py = y - m[1], pz = z - m[2]; return [(R[0] * px + R[3] * py + R[6] * pz) / s, (R[1] * px + R[4] * py + R[7] * pz) / s, (R[2] * px + R[5] * py + R[8] * pz) / s]; };
const fwd = (x, y, z) => [s * (R[0] * x + R[1] * y + R[2] * z) + m[0], s * (R[3] * x + R[4] * y + R[5] * z) + m[1], s * (R[6] * x + R[7] * y + R[8] * z) + m[2]];
const b = A.bounds; let nb;
if (b.every(Number.isFinite)) {
nb = [INF, INF, INF, -INF, -INF, -INF];
for (let i = 0; i < 8; i++) { const p = fwd(b[i & 1 ? 3 : 0], b[i & 2 ? 4 : 1], b[i & 4 ? 5 : 2]); for (let a = 0; a < 3; a++) { if (p[a] < nb[a]) nb[a] = p[a]; if (p[a] > nb[a + 3]) nb[a + 3] = p[a]; } }
} else nb = [-INF, -INF, -INF, INF, INF, INF];
return {
f: (x, y, z) => { const p = inv(x, y, z); return fa(p[0], p[1], p[2]) * s; }, bounds: nb,
Rotation matrix
64-bit
The 3 × 3 matrix of a turn about an axis by an angle.
R = c·I + s·[u]ₓ + (1 − c)·u uᵀ, c = cos θ, s = sin θ
The axis is divided by its length with no zero check. Cosine and sine come from detmath.js dcos and dsin, so the matrix has the same bits in every engine.
field/field.js
lines 72 to 76
PartCraft 0.8.1
export function rotationMatrix(axis, angle) {
let [x, y, z] = axis; const n = Math.sqrt(x * x + y * y + z * z); x /= n; y /= n; z /= n;
const c = dcos(angle), s = dsin(angle), t = 1 - c;
return [t * x * x + c, t * x * y - s * z, t * x * z + s * y, t * x * y + s * z, t * y * y + c, t * y * z - s * x, t * x * z - s * y, t * y * z + s * x, t * z * z + c];
}
Beam or round cone
64-bit
Signed distance to a beam whose radius changes from one end to the other.
t = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); d = |p − a − t(b − a)| − (r₀ + (r₁ − r₀)t)
The code's own comment: exact for a capsule, a close estimate for a cone. A zero-length beam becomes a sphere of the larger radius, so the division by |b − a|² is guarded.
field/field.js
lines 282 to 294
PartCraft 0.8.1
function beamNode(spec) {
const [ax, ay, az] = spec.a, [bx, by, bz] = spec.b, r0 = spec.r0, r1 = spec.r1;
const dx = bx - ax, dy = by - ay, dz = bz - az, L2 = dx * dx + dy * dy + dz * dz;
const f = L2 === 0 ? (x, y, z) => Math.sqrt((x - ax) * (x - ax) + (y - ay) * (y - ay) + (z - az) * (z - az)) - Math.max(r0, r1)
: (x, y, z) => {
const px = x - ax, py = y - ay, pz = z - az;
let t = (px * dx + py * dy + pz * dz) / L2; t = t < 0 ? 0 : t > 1 ? 1 : t;
const qx = px - t * dx, qy = py - t * dy, qz = pz - t * dz;
return Math.sqrt(qx * qx + qy * qy + qz * qz) - (r0 + (r1 - r0) * t); // exact for a capsule, a close estimate for a cone
};
const R = Math.max(r0, r1);
return leaf(f, 1.0 + Math.abs(r1 - r0) / Math.sqrt(L2 || 1), [Math.min(ax, bx) - R, Math.min(ay, by) - R, Math.min(az, bz) - R, Math.max(ax, bx) + R, Math.max(ay, by) + R, Math.max(az, bz) + R]);
}
Lattice of beams
64-bit
The least distance to many beams.
Beams are entered in every bin they come within reach of; the bin size is twice the larger of the mean beam length and the largest diameter, and a sample reads its own bin. Where no listed beam is nearer than the bin size the function returns the bin size instead of the distance.
field/field.js
lines 296 to 332
PartCraft 0.8.1
/** Many beams, binned on a coarse lattice so each sample only looks at the nearby ones. */
function beamsNode(spec) {
const list = spec.list, n = list.length;
if (!n) return { f: () => INF, bounds: [INF, INF, INF, -INF, -INF, -INF], cls: () => OUT };
const bb = [INF, INF, INF, -INF, -INF, -INF];
let maxR = 0;
for (const s of list) { const R = Math.max(s[6], s[7]); maxR = Math.max(maxR, R); for (let a = 0; a < 3; a++) { bb[a] = Math.min(bb[a], s[a], s[a + 3]); bb[a + 3] = Math.max(bb[a + 3], s[a], s[a + 3]); } }
let meanLen = 0; for (const s of list) meanLen += Math.sqrt((s[3] - s[0]) ** 2 + (s[4] - s[1]) ** 2 + (s[5] - s[2]) ** 2);
const cell = Math.max(meanLen / n, maxR * 2, 1e-9) * 2; // bin size ≈ two typical beam lengths
const reach = cell; // a sample looks at the 27 bins around it; beams are entered in every bin they touch ± reach
const bins = new Map();
const bkey = (i, j, k) => (i + 4096) * 16777216 + (j + 4096) * 4096 + (k + 4096);
list.forEach((s, idx) => {
const lo = [0, 1, 2].map((a) => Math.floor((Math.min(s[a], s[a + 3]) - Math.max(s[6], s[7]) - reach) / cell));
const hi = [0, 1, 2].map((a) => Math.floor((Math.max(s[a], s[a + 3]) + Math.max(s[6], s[7]) + reach) / cell));
for (let i = lo[0]; i <= hi[0]; i++) for (let j = lo[1]; j <= hi[1]; j++) for (let k = lo[2]; k <= hi[2]; k++) { const q = bkey(i, j, k); let l = bins.get(q); if (!l) bins.set(q, (l = [])); l.push(idx); }
});
const dist = (s, x, y, z) => {
const ax = s[0], ay = s[1], az = s[2], dx = s[3] - ax, dy = s[4] - ay, dz = s[5] - az, L2 = dx * dx + dy * dy + dz * dz;
const px = x - ax, py = y - ay, pz = z - az;
let t = L2 ? (px * dx + py * dy + pz * dz) / L2 : 0; t = t < 0 ? 0 : t > 1 ? 1 : t;
const qx = px - t * dx, qy = py - t * dy, qz = pz - t * dz;
return Math.sqrt(qx * qx + qy * qy + qz * qz) - (s[6] + (s[7] - s[6]) * t);
};
const far = reach; // beyond this from every beam: report +far (a valid lower bound inside the band checks below)
const f = (x, y, z) => {
const l = bins.get(bkey(Math.floor(x / cell), Math.floor(y / cell), Math.floor(z / cell)));
if (!l) return far;
let m = far;
for (let i = 0; i < l.length; i++) { const v = dist(list[l[i]], x, y, z); if (v < m) m = v; }
return m;
};
return {
f, bounds: [bb[0] - maxR, bb[1] - maxR, bb[2] - maxR, bb[3] + maxR, bb[4] + maxR, bb[5] + maxR],
cls(cx, cy, cz, r, band) { return band + r < reach && !bins.get(bkey(Math.floor(cx / cell), Math.floor(cy / cell), Math.floor(cz / cell))) ? OUT : MIX; },
};
}
Ramped cell size and wall
64-bit
A number that changes linearly along an axis or with radius.
v = v₀ + (v₁ − v₀) × clamp((p − a) ÷ (b − a), 0, 1)
One division by the ramp's length, not checked for zero, then a clamp and a linear blend.
field/field.js
lines 91 to 92
PartCraft 0.8.1
if (s.t === 'ramp') { const ax = s.axis, a = s.a, b = s.b, v0 = s.v0, v1 = s.v1; return (x, y, z) => { const p = ax === 0 ? x : ax === 1 ? y : z; const u = clamp01((p - a) / (b - a)); return v0 + (v1 - v0) * u; }; }
if (s.t === 'radialramp') { const [cx, cy, cz] = s.c, r0 = s.r0, r1 = s.r1, v0 = s.v0, v1 = s.v1; return (x, y, z) => { const d = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)); const u = clamp01((d - r0) / (r1 - r0)); return v0 + (v1 - v0) * u; }; }
Phase of a graded lattice
64-bit
Maps a point to the lattice's own period coordinates when the cell size changes.
fixed cell: X = 2π·p ÷ cell; ramp: X = ∫k dp with k linear in p
2π is typed as 2 × 3.141592653589793. For a ramp the frequency is interpolated linearly, so the integral is a closed-form quadratic. Any other map scales by the local frequency, which the comment calls crude where it changes fast.
field/field.js
lines 334 to 372
PartCraft 0.8.1
/**
* The phase map of a graded TPMS: world point → the point (X, Y, Z) in the surface's own period-2π coordinates. A plain number
* is X = 2π·p/cell. A ramp (cell size changing along one axis) integrates the local frequency along that axis and scales the
* other two by it; a radial ramp does the same along the radius. The cell size is interpolated in 1/cell (so the frequency is linear
* and the integral needs no logarithm): the end values are exact, the sizes between are harmonic. Anything else (a sampled
* map) falls back to scaling by the local frequency, which is crude where it changes fast.
*/
function phaseMap(cell) {
const TWO_PI = 2 * 3.141592653589793;
if (typeof cell === 'number') return { fixed: true, cellAt: () => cell, psi(x, y, z, o) { const k = TWO_PI / cell; o[0] = x * k; o[1] = y * k; o[2] = z * k; } };
if (cell.t === 'ramp') {
const ax = cell.axis, a = cell.a, b = cell.b, k0 = TWO_PI / cell.v0, k1 = TWO_PI / cell.v1, L = b - a;
return {
fixed: false,
cellAt(x, y, z) { const p = ax === 0 ? x : ax === 1 ? y : z, u = clamp01((p - a) / L); return TWO_PI / (k0 + (k1 - k0) * u); },
psi(x, y, z, o) {
const p = ax === 0 ? x : ax === 1 ? y : z, u = clamp01((p - a) / L), k = k0 + (k1 - k0) * u;
// phase along the axis: ∫k dp, with k constant outside the ramp
const q = p < a ? k0 * (p - a) : p > b ? 0.5 * (k0 + k1) * L + k1 * (p - b) : k0 * (p - a) + 0.5 * (k1 - k0) * (p - a) * (p - a) / L;
o[ax] = q; o[(ax + 1) % 3] = (ax === 0 ? y : ax === 1 ? z : x) * k; o[(ax + 2) % 3] = (ax === 0 ? z : ax === 1 ? x : y) * k;
},
};
}
if (cell.t === 'radialramp') {
const [cx, cy, cz] = cell.c, r0 = cell.r0, r1 = cell.r1, k0 = TWO_PI / cell.v0, k1 = TWO_PI / cell.v1, L = r1 - r0;
return {
fixed: false,
cellAt(x, y, z) { const r = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)), u = clamp01((r - r0) / L); return TWO_PI / (k0 + (k1 - k0) * u); },
psi(x, y, z, o) {
const dx = x - cx, dy = y - cy, dz = z - cz, r = Math.sqrt(dx * dx + dy * dy + dz * dz);
const q = r < r0 ? k0 * r : r > r1 ? k0 * r0 + 0.5 * (k0 + k1) * L + k1 * (r - r1) : k0 * r0 + k0 * (r - r0) + 0.5 * (k1 - k0) * (r - r0) * (r - r0) / L;
const s = r > 1e-12 ? q / r : k0;
o[0] = dx * s; o[1] = dy * s; o[2] = dz * s;
},
};
}
const cf = scalar(cell);
return { fixed: false, cellAt: cf, psi(x, y, z, o) { const k = TWO_PI / cf(x, y, z); o[0] = x * k; o[1] = y * k; o[2] = z * k; } };
}
Gyroid, Schwarz P and diamond
64-bit
Signed distance to a triply periodic minimal surface, as a sheet or a network.
d ≈ g ÷ |∇g|; near the surface: two projections q ← q − g∇g ÷ |∇g|², d = ±|p − q|
Sines and cosines come from detmath.js. The gradient size is floored at 1e-3 × 2π ÷ cell so the division cannot blow up. Within 0.3 of a cell the distance is replaced by the length to a point found by exactly two projection steps (not repeated to a tolerance), blended in between 0.3 and 0.2 of a cell.
field/field.js
lines 374 to 425
PartCraft 0.8.1
function tpmsNode(spec) {
const kind = spec.kind, wallF = scalar(spec.wall), mode = spec.mode, iso = spec.iso;
if (!['gyroid', 'schwarzp', 'diamond'].includes(kind)) throw new Error('field: unknown TPMS ' + kind);
const pm = phaseMap(spec.cell), O = [0, 0, 0], O1 = [0, 0, 0], O2 = [0, 0, 0];
const G = [0, 0, 0, 0]; // g − iso and its gradient in period coordinates, written by gev()
const gev = (X, Y, Z) => {
const sx = dsin(X), cx = dcos(X), sy = dsin(Y), cy = dcos(Y), sz = dsin(Z), cz = dcos(Z);
if (kind === 'gyroid') { G[0] = sx * cy + sy * cz + sz * cx; G[1] = cx * cy - sz * sx; G[2] = -sx * sy + cy * cz; G[3] = -sy * sz + cz * cx; }
else if (kind === 'schwarzp') { G[0] = cx + cy + cz; G[1] = -sx; G[2] = -sy; G[3] = -sz; }
else {
G[0] = sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz;
G[1] = cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz;
G[2] = sx * cy * sz - sx * sy * cz + cx * cy * cz - cx * sy * sz;
G[3] = sx * sy * cz - sx * cy * sz - cx * sy * sz + cx * cy * cz;
}
G[0] -= iso;
};
const W = [0, 0, 0, 0]; // g − iso and the gradient in world units, written by ev()
const ev = (x, y, z) => {
pm.psi(x, y, z, O); gev(O[0], O[1], O[2]);
W[0] = G[0];
if (pm.fixed) { const kk = 2 * 3.141592653589793 / pm.cellAt(); W[1] = G[1] * kk; W[2] = G[2] * kk; W[3] = G[3] * kk; return; }
// chain rule through the phase map: ∇g(world) = Jᵀ ∇g(period), J by central differences
const e = 0.01 * pm.cellAt(x, y, z), J = [0, 0, 0, 0, 0, 0, 0, 0, 0];
pm.psi(x + e, y, z, O1); pm.psi(x - e, y, z, O2); for (let r = 0; r < 3; r++) J[r * 3] = (O1[r] - O2[r]) / (2 * e);
pm.psi(x, y + e, z, O1); pm.psi(x, y - e, z, O2); for (let r = 0; r < 3; r++) J[r * 3 + 1] = (O1[r] - O2[r]) / (2 * e);
pm.psi(x, y, z + e, O1); pm.psi(x, y, z - e, O2); for (let r = 0; r < 3; r++) J[r * 3 + 2] = (O1[r] - O2[r]) / (2 * e);
W[1] = J[0] * G[1] + J[3] * G[2] + J[6] * G[3]; W[2] = J[1] * G[1] + J[4] * G[2] + J[7] * G[3]; W[3] = J[2] * G[1] + J[5] * G[2] + J[8] * G[3];
};
const f = (x, y, z) => {
const cell = pm.cellAt(x, y, z);
ev(x, y, z);
const g0 = W[0], gx = W[1], gy = W[2], gz = W[3], floor = 1e-3 * 2 * 3.141592653589793 / cell;
let gm = Math.sqrt(gx * gx + gy * gy + gz * gz); if (gm < floor) gm = floor;
let d = g0 / gm;
const ad = d < 0 ? -d : d, lim = 0.3 * cell;
if (ad < lim) {
// Near a wall the first-order estimate g/|∇g| is off by a few percent (thin walls come out thin). Two projections onto the
// surface (Newton steps along the gradient) give the distance to the nearest point of it; blended back to the first-order value
// between 0.2 and 0.3 of a cell so the field stays continuous.
let qx = x - g0 * gx / (gm * gm), qy = y - g0 * gy / (gm * gm), qz = z - g0 * gz / (gm * gm);
ev(qx, qy, qz);
let m2 = W[1] * W[1] + W[2] * W[2] + W[3] * W[3]; if (m2 < floor * floor) m2 = floor * floor;
qx -= W[0] * W[1] / m2; qy -= W[0] * W[2] / m2; qz -= W[0] * W[3] / m2;
const dx = x - qx, dy = y - qy, dz = z - qz, d2 = Math.sqrt(dx * dx + dy * dy + dz * dz) * (g0 < 0 ? -1 : 1);
const s0 = (lim - ad) / (0.1 * cell), s = s0 > 1 ? 1 : s0;
d += (d2 - d) * s;
}
return mode === 'sheet' ? Math.abs(d) - wallF(x, y, z) / 2 : d;
};
return { f, bounds: [-INF, -INF, -INF, INF, INF, INF], cls: () => MIX };
}
Brick classification
64-bit
Decides whether a ball of space is wholly outside, wholly inside or near the surface.
outside when f − L·r > band; inside when f + L·r < −band
f is the shape's value at the ball's center and L its Lipschitz bound (1 for the true distances, 1 + |r₁ − r₀| ÷ length for a tapered beam). The comparisons are in 64-bit with no tolerance.
field/field.js
lines 112 to 116
PartCraft 0.8.1
// ---- compile --------------------------------------------------------------------------------------------------------------
const leaf = (f, lip, bounds) => ({
f, bounds,
cls(cx, cy, cz, r, band) { const v = f(cx, cy, cz), d = lip * r; return v - d > band ? OUT : v + d < -band ? IN : MIX; },
});
In detail
Method
Textbook distance formulas; min and max for booleans; the polynomial smooth minimum for blends (the file credits Quilez); gyroid, Schwarz P and diamond level sets with a first-order distance and two Newton projections.
Number types
64-bit numbers. A density map is kept in a Float32Array; it is recorded under the distance grid policy.
Tolerances
TPMS: gradient size floored at 1e-3 × 2π ÷ cell; projections used within 0.3 of a cell and blended out between 0.2 and 0.3; the Jacobian of a graded cell by central differences with step 0.01 × cell. Radial ramp: below r = 1e-12 the base frequency is used. Beam bins: size at least 2e-9.
Limits
Plane, cylinder and rotation divide by a length without a zero check. A transform uses the transpose of its 3 × 3 as the inverse, which is right only for a rotation. Out of reach of every beam the beams field returns the bin size, not the distance.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.