Resources Numerical accuracy Calculation policy

Field builtins for scripts

Lets a script build implicit shapes, sample them, turn them into a body, measure its volume and check it for manufacturing.

How the precision is kept

The builtins pass 64-bit script values to the field library. Two steps narrow to 32 bits: the contour mesh that to_body and field_volume use comes back from field/contour.js as a Float32Array, and a density map given to field_density is stored in a Float32Array. Bodies are contoured from a grid of spacing h, which also limits the accuracy of the shape, its volume and its area.

Open item. Keep the contour positions and the density map in 64-bit: field/contour.js builds its positions in a Float64Array and converts them to Float32Array when it returns.

Calculations under this policy

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

engineer/cs-lib-field.js PartCraft 0.8.1 32-bit in part 6 calculations Reviewed

Tests None found by the review

Body, volume and area from a field

32-bit in part

The mesh that to_body returns, with its volume, area and triangle count.

FD.contour returns its positions as a Float32Array (field/contour.js). Float64Array.from here widens those 32-bit values without restoring digits, and the volume and area are computed from the same 32-bit positions. The body's nodes and the reported volume and area therefore carry about 7 significant digits.

  • engineer/cs-lib-field.js
  • lines 55 to 60
  • PartCraft 0.8.1
function bodyFromGrid(g) {
  const m = FD.contour(g), top = FD.meshTopology(m);
  const nodes = Float64Array.from(m.positions), elems = Int32Array.from(m.indices);
  const info = struct({ volume: FD.meshVolume(m), area: FD.meshArea(m), triangles: m.indices.length / 3, vertices: m.positions.length / 3, watertight: top.closed && top.vertexManifold, components: top.components, bricks: g.bricks.size, h: g.h, hash: FD.fingerprint(m.positions, m.indices) });
  return [meshValue('tri', nodes, elems), info];
}

Volume of a field

32-bit in part

The number field_volume returns to the script.

The field is baked on a grid of spacing h, contoured, and the contour's volume is returned. The contour positions are 32-bit, and the grid spacing sets how closely the contour follows the shape.

  • engineer/cs-lib-field.js
  • line 154
  • PartCraft 0.8.1
field_volume: [(a, ctx) => { const o = optsOf(a[2]), g = bakeChecked(fieldArg(a[0]), bakeOpts(a[1], o), checker(ctx)); return FD.meshVolume(FD.contour(g)); }, 'V = field_volume(f, h, opts)'],

Density map for a graded size

32-bit in part

Turns a 0 to 1 density array into a profile that drives a cell size or wall thickness.

d = clamp(D, 0, 1), stored in 32 bits; size = v₀ + (v₁ − v₀) × d

Each density is clamped to 0…1 and written into a Float32Array, so it keeps about 7 significant digits. FD.density in field/field.js keeps it as 32-bit as well. Rows are flipped so that row 1 of the array is the top.

  • engineer/cs-lib-field.js
  • lines 90 to 101
  • PartCraft 0.8.1
field_density: [wrap((a) => {
  const D = asTensor(a[0], 'density'), shape = D.shape.length === 3 ? D.shape : [D.shape[0], D.shape[1], 1];
  const [ny, nx, nz] = shape;                                            // rows are y from the top (image order), columns x, pages z
  const box = Array.from(asTensor(a[1], 'box').data);
  if (box.length !== 6) throw err(t('script.field.bbox6'));
  const v0 = N(a[2], 'v0'), v1 = N(a[3], 'v1'), data = new Float32Array(nx * ny * nz);
  for (let k = 0; k < nz; k++) for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
    const dv = D.data[j + ny * (i + nx * k)];                              // column-major tensor: (row j, column i, page k)
    data[i + nx * ((ny - 1 - j) + ny * k)] = dv < 0 ? 0 : dv > 1 ? 1 : dv;   // row 1 is the top: y runs up, so flip
  }
  return { t: 'sdf', kind: 'dens', scalar: true, spec: FD.density(data, [nx, ny, nz], box, v0, v1) };
}), 'p = field_density(D, box, v0, v1)'],

Distance at points

64-bit

The signed distance of a field at each row of a points matrix.

Calls the compiled field function once per point with 64-bit coordinates and stores the results in a Float64Array.

  • engineer/cs-lib-field.js
  • lines 138 to 144
  • PartCraft 0.8.1
sample: [(a) => {
  const f = FD.compile(fieldArg(a[0])).f, P = asTensor(a[1], 'points'), [n, c] = P.shape;
  if (c !== 3) throw err(t('script.field.points_shape'));
  const d = new Float64Array(n);
  for (let i = 0; i < n; i++) d[i] = f(P.data[i], P.data[i + n], P.data[i + 2 * n]);
  return mat(n, 1, d);
}, 'd = sample(f, points)'],

Inside or outside on a box grid

64-bit

Marks each cell of a box as inside (−1) or outside (0) the field.

nᵢ = round((maxᵢ − minᵢ) ÷ h); sample at min + (i + ½) × h

The cell counts are rounded to whole numbers, so the grid covers the box exactly only when its sides are multiples of h. Each cell center is tested with the 64-bit field function. At most 2.5 × 10⁷ cells.

  • engineer/cs-lib-field.js
  • lines 156 to 164
  • PartCraft 0.8.1
field_kind: [wrap((a, ctx) => {
  const f = FD.compile(fieldArg(a[0])).f, hh = posArg(a[1], 'h'), b = Array.from(asTensor(a[2], 'box').data);
  if (b.length !== 6) throw err(t('script.field.bbox6'));
  const n = [0, 1, 2].map((i) => Math.round((b[i + 3] - b[i]) / hh));
  if (n.some((x) => x < 1) || n[0] * n[1] * n[2] > 2.5e7) throw err(t('script.field.too_big', { n: n[0] * n[1] * n[2] }));
  const d = new Float64Array(n[0] * n[1] * n[2]), check = checker(ctx);
  for (let k = 0; k < n[2]; k++) { check(); for (let j = 0; j < n[1]; j++) for (let i = 0; i < n[0]; i++) d[i + n[0] * (j + n[1] * k)] = f(b[0] + (i + 0.5) * hh, b[1] + (j + 0.5) * hh, b[2] + (k + 0.5) * hh) < 0 ? -1 : 0; }
  return tensor(n, d);
}), 'kind = field_kind(f, h, box)'],

Overhang reported by the manufacturing check

64-bit

The overhang problem mfg_check adds, with its area and locations.

reported when area > 4 × h²

The area is rounded to 2 decimals for the found value and to 1 decimal for the message, the share to 0.1 %, and each location to 3 decimals, before they are stored in the result the script receives. The mesh the check runs on is the 32-bit contour.

  • engineer/cs-lib-field.js
  • lines 182 to 185
  • PartCraft 0.8.1
if (prof.overhang !== null) {
  const oh = FD.mfgOverhang({ positions: mesh.positions, indices: mesh.indices }, prof.overhang);
  if (oh.area > g.h * g.h * 4) problems.push({ code: 'mfg.overhang', severity: 'warning', req: { op: '<=', value: prof.overhang, unit: 'deg' }, found: { value: +oh.area.toFixed(2), unit: 'mm2' }, vars: { area: +oh.area.toFixed(1), share: +(100 * oh.share).toFixed(1), angle: prof.overhang, process: prof.short || prof.name }, at: oh.at.map((p) => p.map((x) => +x.toFixed(3))) });
}

In detail

Method
Shapes are signed-distance fields; a body is made by baking the field on a grid of spacing h and contouring it (field/contour.js, dual contouring).
Number types
64-bit numbers and Float64Array for points and samples; Float32Array for the contour mesh positions and for the density map.
Limits
Body nodes, volume and area come from 32-bit positions. The overhang area and positions reported by mfg_check are rounded to 2 and 3 decimals before they are stored in the result.

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