Finds the faces that overhang past an angle, places simple column or tree supports under them, and estimates the support material.
How the precision is kept
All arithmetic is 64-bit, but the result is an estimate by design: support points are sampled on a plan grid, and the volume is a sum over those points with an assumed fill density or branch radius. The code's header calls it a preview, not a slicer. The grid widens when the overhang would need more than maxPoints points.
Calculations under this policy
5 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.
Which triangles need support, and their total area.
overhang if n_z < −cos((90 − angle)·π ÷ 180) and not on the bed; area = Σ triangle areas
The comparison uses the unit normal's z from triMesh with no tolerance on the angle. The bed tolerance zTol is scaled to the model's size with a floor of 0.001 mm.
print-supports.js
lines 34 to 45
ModelCraft 0.8.1
const cosLimit = Math.cos((90 - angle) * Math.PI / 180);
const zTol = Math.max(1e-3, 1e-6 * Math.max(...T.size, 1));
const over = [];
let overhangArea = 0;
for (let t = 0; t < nt; t++) {
const nz = N[t * 3 + 2];
if (!(nz < -cosLimit) || !A[t]) continue;
const a = I[t * 3] * 3, b = I[t * 3 + 1] * 3, c = I[t * 3 + 2] * 3;
if (Math.abs(P[a + 2] - zBed) <= zTol && Math.abs(P[b + 2] - zBed) <= zTol && Math.abs(P[c + 2] - zBed) <= zTol) continue; // on the bed
over.push(t);
overhangArea += A[t];
}
Support points under the overhangs
64-bit
Grid points in plan that fall inside an overhanging triangle, with their height on it.
s = max(spacing, √(overhang area ÷ maxPoints)); (x, y) = ((i + ½)·s, (j + ½)·s); z = λ₁z_a + λ₂z_b + λ₃z_c
The grid is offset by about a millionth of a cell so a point does not fall exactly on a shared edge. Barycentric weights are divided by the plan determinant; triangles with |det| < 1e-12 (absolute, mm²) are skipped. The weights are tested against 0 with no tolerance.
print-supports.js
lines 48 to 67
ModelCraft 0.8.1
const OX = 0.5 + 1.234567e-6, OY = 0.5 + 2.345671e-6;
let pts = new Float64Array(3 * 256), np = 0;
// a laptop's limit (rule 19): about maxPoints points; a bigger overhang gets a wider grid (the result says which)
const s = Math.max(spacing, Math.sqrt(overhangArea / maxPoints));
for (const t of over) {
const a = I[t * 3] * 3, b = I[t * 3 + 1] * 3, c = I[t * 3 + 2] * 3;
const ax = P[a], ay = P[a + 1], bx = P[b], by = P[b + 1], cx = P[c], cy = P[c + 1];
const den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy);
if (Math.abs(den) < 1e-12) continue; // seen edge-on from above
const i0 = Math.ceil(Math.min(ax, bx, cx) / s - OX), i1 = Math.floor(Math.max(ax, bx, cx) / s - OX);
const j0 = Math.ceil(Math.min(ay, by, cy) / s - OY), j1 = Math.floor(Math.max(ay, by, cy) / s - OY);
for (let i = i0; i <= i1; i++) for (let j = j0; j <= j1; j++) {
const x = (i + OX) * s, y = (j + OY) * s;
const l1 = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den, l2 = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den, l3 = 1 - l1 - l2;
if (l1 < 0 || l2 < 0 || l3 < 0) continue;
if (np * 3 + 3 > pts.length) { const g = new Float64Array(pts.length * 2); g.set(pts); pts = g; }
pts[np * 3] = x; pts[np * 3 + 1] = y; pts[np * 3 + 2] = l1 * P[a + 2] + l2 * P[b + 2] + l3 * P[c + 2];
np++;
}
}
Where a support lands
64-bit
The height below each support point where a straight line down meets the model, or the bed.
bottom = z − eps − distance down to the first face, else bed z
The ray starts eps below the point so it does not hit the face it hangs from, and is limited to the distance to the bed.
print-supports.js
lines 69 to 75
ModelCraft 0.8.1
const bottom = new Float64Array(np), onModel = new Uint8Array(np), hit = { tri: -1, back: 0 };
const eps = Math.max(1e-4, zTol);
for (let k = 0; k < np; k++) {
const x = pts[k * 3], y = pts[k * 3 + 1], z = pts[k * 3 + 2];
const d = rayNearest(T, x, y, z - eps, 0, 0, -1, 0, Math.max(0, z - eps - zBed), -1, hit);
if (d >= 0) { bottom[k] = z - eps - d; onModel[k] = 1; } else bottom[k] = zBed;
}
Column volume
64-bit
The material one column adds to the estimate.
columns: V = s² × density × h; tree style: V = π × tip² × h; drawn radius = s × √(density ÷ π)
The volume is the grid cell's area times an assumed fill share times the height. Columns shorter than eps are left out. The sum is accumulated in point order.
print-supports.js
lines 78 to 86
ModelCraft 0.8.1
const column = (k) => {
const h = pts[k * 3 + 2] - bottom[k];
if (h <= eps) return;
segs.push(pts[k * 3], pts[k * 3 + 1], pts[k * 3 + 2], pts[k * 3], pts[k * 3 + 1], bottom[k]);
radii.push(style === 'tree' ? tip : s * Math.sqrt(density / Math.PI));
volume += style === 'tree' ? Math.PI * tip * tip * h : s * s * density * h;
columns++;
if (onModel[k]) toModel++;
};
Tree supports
64-bit
Joins nearby support points to one trunk and adds the branches' and trunk's volume.
joint z = min(zᵢ − horizontal distanceᵢ ÷ tan(branch)); trunk radius = tip × √k; V = Σ π·tip²·lengthᵢ + π·r²·(joint z − base z)
Points in the same plan cell of 4 × spacing share a trunk under the mean of their positions. A branch that would pass through the model, or a trunk that would start inside it, falls back to a straight column for that point.
print-supports.js
lines 97 to 134
ModelCraft 0.8.1
const tanB = Math.tan(branch * Math.PI / 180);
for (const g of groups.values()) {
if (g.length < 2) { column(g[0]); continue; }
let mx = 0, my = 0;
for (const k of g) { mx += pts[k * 3]; my += pts[k * 3 + 1]; }
mx /= g.length; my /= g.length;
// the joint: low enough that every branch is no flatter than `branch` from vertical
let jz = Infinity;
for (const k of g) jz = Math.min(jz, pts[k * 3 + 2] - Math.hypot(pts[k * 3] - mx, pts[k * 3 + 1] - my) / tanB);
// the trunk lands where a line down from the joint does; it must start outside the model
const dj = rayNearest(T, mx, my, jz, 0, 0, -1, 0, Math.max(0, jz - zBed), -1, hit);
const trunkInside = dj >= 0 && hit.back;
const jb = dj >= 0 ? jz - dj : zBed;
const joined = [];
for (const k of g) {
const px = pts[k * 3], py = pts[k * 3 + 1], pz = pts[k * 3 + 2];
if (trunkInside || jz <= bottom[k] + eps || jz <= zBed + eps) { column(k); continue; }
// a branch through the model is no branch: that point gets its own column
const dx = mx - px, dy = my - py, dz = jz - (pz - eps), L = Math.hypot(dx, dy, dz);
if (L > eps && rayNearest(T, px, py, pz - eps, dx / L, dy / L, dz / L, 0, L, -1, null) >= 0) { column(k); continue; }
joined.push(k);
}
if (joined.length < 2) { for (const k of joined) column(k); continue; }
for (const k of joined) {
const px = pts[k * 3], py = pts[k * 3 + 1], pz = pts[k * 3 + 2];
segs.push(px, py, pz, mx, my, jz);
radii.push(tip);
volume += Math.PI * tip * tip * Math.hypot(mx - px, my - py, jz - pz);
branches++;
}
const r = tip * Math.sqrt(joined.length);
segs.push(mx, my, jz, mx, my, jb);
radii.push(r);
volume += Math.PI * r * r * (jz - jb);
columns++;
if (dj >= 0) toModel++;
}
}
In detail
Method
Overhang by the face normal's z against cos(90° − angle); points on a plan grid inside each overhanging triangle; a ray down from each point to the bed or the model.
Number types
Float64Array for points, segments and radii; millimeters and mm³.
Tolerances
A triangle counts as lying on the bed within zTol = max(1e-3, 1e-6 × largest size) mm. A triangle with plan determinant under 1e-12 mm² is skipped as edge-on. Rays start eps = max(1e-4, zTol) below the point.
Limits
The volume depends on the grid spacing and the assumed density (0.15) or tip radius (0.6 mm); it is not what a slicer will use.
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.