Resources Numerical accuracy Calculation policy

Supports preview and material estimate

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.

print-supports.js ModelCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/modelcraft-test/print07.mjs

Overhanging faces and their area

64-bit

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.

Every policy that takes this approach