Resources Numerical accuracy Calculation policy

Draft and curvature analysis

Works out the draft angle of each face corner against a pull direction, the areas that pass or need draft, and the curvature limit shown with the curvature colors.

How the precision is kept

The arithmetic is 64-bit, but the vertex positions come from the part's display mesh, which the kernel stores in a Float32Array. The areas and the curvature limit are therefore measured on 32-bit vertices of a faceted mesh, not on the exact faces. The draft readout in draft-ui.js shows each area as a percentage of the total with one decimal.

Open item. Measure the draft areas on the exact faces, or on a 64-bit copy of the mesh positions, instead of the 32-bit display mesh.

Calculations under this policy

3 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.

analysis.js PartCraft 0.8.1 32-bit in part 3 calculations Reviewed

Tests tools/partcraft-test/test-analysis.mjs

Draft angle

64-bit

The angle between a surface normal and the plane square to the pull direction, in degrees.

angle = asin(clamp(n · pull, −1, 1)) × 180 ÷ π

The dot product is clamped to [−1, 1] before asin. Near ±90° asin is steep, so a small error in the dot product gives a larger error in the angle there; near 0° (a vertical wall, where draft matters) it is well conditioned.

  • analysis.js
  • line 20
  • PartCraft 0.8.1
export const draftAngle = (n, pull) => Math.asin(Math.max(-1, Math.min(1, n[0] * pull[0] + n[1] * pull[1] + n[2] * pull[2]))) * 180 / Math.PI;

Areas by draft class

32-bit in part

The surface area with enough draft, with too little, and facing away from the pull.

area = |(B − A) × (C − A)| ÷ 2; each corner adds area ÷ 3 to its class

Vertex positions are read from the mesh's Float32Array; the cross product and the running sums are 64-bit and added in triangle order. The class limits have an absolute slack of 1e-9 degrees. The totals carry the precision of the 32-bit vertices.

  • analysis.js
  • lines 22 to 43
  • PartCraft 0.8.1
export function draftAnalysis(mesh, pull, minDeg, colors) {
  const P = mesh.positions, I = mesh.indices, nt = I.length / 3, CN = mesh.cornerNormals && mesh.cornerNormals.length === nt * 9 ? mesh.cornerNormals : null;
  const d = unit(pull), m = Math.abs(minDeg);
  const out = new Float32Array(nt * 9);
  const areas = { positive: 0, needs: 0, negative: 0, total: 0 };
  for (let t = 0; t < nt; t++) {
    const a = I[t * 3] * 3, b = I[t * 3 + 1] * 3, c = I[t * 3 + 2] * 3;
    const u = [P[b] - P[a], P[b + 1] - P[a + 1], P[b + 2] - P[a + 2]], v = [P[c] - P[a], P[c + 1] - P[a + 1], P[c + 2] - P[a + 2]];
    const cr = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
    const area = Math.hypot(cr[0], cr[1], cr[2]) / 2, tn = unit(cr);
    areas.total += area;
    for (let k = 0; k < 3; k++) {
      const n = CN ? unit([CN[t * 9 + k * 3], CN[t * 9 + k * 3 + 1], CN[t * 9 + k * 3 + 2]]) : tn;
      const ang = draftAngle(n, d);
      const cls = ang >= m - 1e-9 ? 'positive' : ang <= -m + 1e-9 ? 'negative' : 'needs';
      areas[cls] += area / 3;
      const col = colors[cls];
      out[t * 9 + k * 3] = col[0]; out[t * 9 + k * 3 + 1] = col[1]; out[t * 9 + k * 3 + 2] = col[2];
    }
  }
  return { colors: out, areas };
}

Curvature color limit

32-bit in part

The curvature value (1/mm) at which the colors reach full blue or red, stated in the panel with its radius.

lim = 95th percentile of |mean curvature|; color weight = clamp(mean ÷ lim, −1, 1)

The mean curvature per vertex comes from surface.js, computed on the Float32Array mesh positions. The percentile is the sorted value at index floor(0.95 × n), with no interpolation; a zero limit is replaced by 1. The colors are written to a Float32Array for the screen.

  • analysis.js
  • lines 61 to 71
  • PartCraft 0.8.1
export function curvatureColors(mesh, { concave, flat, convex }) {
  const { mean } = curvature(mesh);
  const abs = Array.from(mean, Math.abs).sort((x, y) => x - y);
  const lim = abs[Math.floor(abs.length * 0.95)] || 1;
  const n = mesh.positions.length / 3, out = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {
    const t = Math.max(-1, Math.min(1, mean[i] / lim)), to = t < 0 ? concave : convex, w = Math.abs(t);
    for (let k = 0; k < 3; k++) out[i * 3 + k] = flat[k] + (to[k] - flat[k]) * w;
  }
  return { colors: out, lim };
}

In detail

Method
Draft angle = asin(n · pull) in degrees per triangle corner, using the stored surface normal when the mesh has one, else the triangle normal. A triangle's area is split in thirds among its corners by class. Curvature colors are scaled to the 95th percentile of the absolute mean curvature from surface.js.
Number types
Positions and corner normals read from Float32Array; angles, areas and the percentile in 64-bit numbers; colors written to Float32Array for the screen.
Tolerances
A corner counts as positive when its angle is at least the minimum minus 1e-9 degrees, and negative when at most minus the minimum plus 1e-9 degrees (absolute).
Limits
Areas are facet areas of the mesh. A triangle whose corners fall in different classes is split in thirds, not along the true boundary.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach