Resources Numerical accuracy Calculation policy

Stock simulation and machining time

Simulates the stock being cut by every tool move, reports material removed, stock left and gouges, and estimates the machining time.

How the precision is kept

The stock is a grid of heights in a Float64Array, one height per cell, so results are resolved to the cell size (0.1 to 2 mm unless set) and to the 0.01 mm height steps used for sloping moves. The part it is compared with is the mesh height from cam.js. Volumes are cell counts times cell area times height. The colored view of the stock uses a separate Float32Array copy that is only drawn.

Calculations under this policy

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

cam-sim.js PartCraft 0.8.1 64-bit 8 calculations Reviewed

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

Simulation grid

64-bit

Sets the cell size of the stock grid and the tolerances used to call a gouge.

cell = min(2, max(0.1, max(w, h) ÷ 500)); lateral tolerance = 2e-4 × diagonal + 0.01

The lateral tolerance is tied in a comment to the display mesh's chord tolerance of 2e-4 × diagonal. Cell centers are x₀ + (i + ½) × cell.

  • cam-sim.js
  • lines 30 to 55
  • PartCraft 0.8.1
constructor(model, { cell = 0, tolV = 0.02, tolLat = null } = {}) {
  this.model = model;
  const s = model.stock;
  const w = s.max[0] - s.min[0], h = s.max[1] - s.min[1];
  this.cell = cell > 0 ? cell : Math.min(2, Math.max(0.1, Math.max(w, h) / 500));
  this.nx = Math.max(1, Math.ceil(w / this.cell));
  this.ny = Math.max(1, Math.ceil(h / this.cell));
  if (this.nx * this.ny > 4e6) throw new Error(t('cam.sim.grid_too_fine'));
  this.x0 = s.min[0]; this.y0 = s.min[1];
  this.top = s.max[2]; this.bottom = s.min[2];
  const n = this.nx * this.ny;
  this.z = new Float64Array(n);
  this.inStock = new Uint8Array(n);
  for (let j = 0; j < this.ny; j++) for (let i = 0; i < this.nx; i++) {
    const [x, y] = this.centre(i, j);
    const ins = s.kind === 'cylinder' ? Math.hypot(x - s.c[0], y - s.c[1]) <= s.r : x <= s.max[0] && y <= s.max[1];
    this.inStock[j * this.nx + i] = ins ? 1 : 0;
    this.z[j * this.nx + i] = ins ? this.top : -Infinity;
  }
  const diag = Math.hypot(...model.box.max.map((v, k) => v - model.box.min[k]));
  this.tolV = tolV;
  this.tolLat = tolLat ?? 2e-4 * diag + 0.01;   // the exact kernel's display mesh: linear deflection 2e-4 × diagonal
  this.hp = null;
  this.gouge = new Uint8Array(n);
  this.log = [];
}

Cutting along a level move

64-bit

Lowers the stock cells under one level move of the tool.

z_cell = min(z_cell, z + h(distance from cell center to the track))

The distance from each cell center to the line or arc is exact (cam-offset.js), and the profile height is evaluated at that distance. The result is sampled at cell centers.

  • cam-sim.js
  • lines 82 to 95
  • PartCraft 0.8.1
/** Lower the cells under one flat XY track (a prim) at height z. → cells lowered by more than tolV */
stamp(prim, z, prof) {
  const R = prof.R, b = O.boxOf(prim);
  const i0 = Math.max(0, Math.floor((b[0] - R - this.x0) / this.cell)), i1 = Math.min(this.nx - 1, Math.floor((b[2] + R - this.x0) / this.cell));
  const j0 = Math.max(0, Math.floor((b[1] - R - this.y0) / this.cell)), j1 = Math.min(this.ny - 1, Math.floor((b[3] + R - this.y0) / this.cell));
  let hit = 0;
  for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) {
    const k = j * this.nx + i;
    if (!this.inStock[k]) continue;
    const zc = z + prof.h(O.distToPrim(this.centre(i, j), prim));
    if (zc < this.z[k]) { if (this.z[k] - zc > this.tolV) hit++; this.z[k] = zc; }
  }
  return hit;
}

Cutting along a sloping move

64-bit

Cuts a move that changes height as a series of short level moves.

n = ceil(|Δz| ÷ 0.01); piece i cut at the lower of its two end heights

Each piece is cut at its lower end, so the simulated cut is up to 0.01 mm deeper than the real ramp, as the header says. A move with |Δz| below 1e-9 is treated as level.

  • cam-sim.js
  • lines 96 to 115
  • PartCraft 0.8.1
/** Sweep one segment ({k, a, b, c?, sw?}). */
sweep(s, prof) {
  const dz = s.b[2] - s.a[2];
  const flat = Math.abs(dz) < 1e-9;
  const xyLen = s.k === 'arc' ? Math.abs(s.sw) * Math.hypot(s.a[0] - s.c[0], s.a[1] - s.c[1]) : Math.hypot(s.b[0] - s.a[0], s.b[1] - s.a[1]);
  const primOf = (t0, t1) => {
    if (s.k === 'arc') {
      const r = Math.hypot(s.a[0] - s.c[0], s.a[1] - s.c[1]), a0 = Math.atan2(s.a[1] - s.c[1], s.a[0] - s.c[0]);
      return O.arc(s.c, r, a0 + s.sw * t0, s.sw * (t1 - t0));
    }
    const p = (t) => [s.a[0] + (s.b[0] - s.a[0]) * t, s.a[1] + (s.b[1] - s.a[1]) * t];
    return O.line(p(t0), p(t1));
  };
  if (flat) return this.stamp(primOf(0, 1), s.a[2], prof);
  if (xyLen < 1e-9) return this.stamp(O.line([s.a[0], s.a[1]], [s.a[0], s.a[1]]), Math.min(s.a[2], s.b[2]), prof);
  const n = Math.max(1, Math.ceil(Math.abs(dz) / DZ));
  let hit = 0;
  for (let i = 0; i < n; i++) hit += this.stamp(primOf(i / n, (i + 1) / n), Math.min(s.a[2] + (dz * i) / n, s.a[2] + (dz * (i + 1)) / n), prof);
  return hit;
}

Stock volume

64-bit

The volume of stock left, from which the volume removed by each operation is found.

V = cell² × Σ (z − stock bottom) over cells in the stock

A sum over up to four million cells added in order, with no compensation. The volume removed by one operation is the difference of two such sums, so for a small cut on a large stock it loses digits.

  • cam-sim.js
  • lines 133 to 138
  • PartCraft 0.8.1
/** Stock volume (above the stock bottom). */
volume() {
  let v = 0;
  for (let k = 0; k < this.z.length; k++) if (this.inStock[k] && this.z[k] > this.bottom) v += this.z[k] - this.bottom;
  return v * this.cell * this.cell;
}

Part height with lateral tolerance

64-bit

The part's height at a cell, taken as the lowest of the center and eight points a small distance round it.

min over the center and 8 points at the lateral tolerance, 45° apart

The part heights come from the mesh height query in cam.js, whose vertices have 32-bit precision. Taking the minimum over nearby points keeps a cut that grazes a wall within the mesh's chord deviation from being called a gouge.

  • cam-sim.js
  • lines 64 to 70
  • PartCraft 0.8.1
/** The part's height at a cell with the lateral tolerance (min over the centre and 8 points tolLat away). */
erodedHeight(i, j) {
  const [x, y] = this.centre(i, j);
  let m = this.partHeights()[j * this.nx + i];
  for (let k = 0; k < 8 && m > -Infinity; k++) { const a = (k * Math.PI) / 4; m = Math.min(m, this.model.heightAt(x + this.tolLat * Math.cos(a), y + this.tolLat * Math.sin(a))); }
  return m;
}

Gouge detection

64-bit

Finds cells where the stock was cut below the part's surface.

gouge when z_cell < eroded part height − 0.02

A cell is first screened against the part height at its center and then against the eroded height. Cuts into the part shallower than the vertical tolerance are not reported.

  • cam-sim.js
  • lines 139 to 153
  • PartCraft 0.8.1
/** Mark new gouge cells → {count (new), at: [[x, y, depth]] up to 50 spots} */
findGouges() {
  const hp = this.partHeights();
  let count = 0;
  const at = [];
  for (let j = 0; j < this.ny; j++) for (let i = 0; i < this.nx; i++) {
    const k = j * this.nx + i;
    if (this.gouge[k] || !(hp[k] > -Infinity) || this.z[k] >= hp[k] - this.tolV) continue;
    const he = this.erodedHeight(i, j);
    if (!(this.z[k] < he - this.tolV)) continue;
    this.gouge[k] = 1; count++;
    if (at.length < 50) { const c = this.centre(i, j); at.push([c[0], c[1], he - this.z[k]]); }
  }
  return { count, at };
}

Stock left and deepest gouge

64-bit

The volume of stock left above the part, the thickest stock left and the deepest gouge.

remaining = cell² × Σ (z − max(part height, stock bottom)) where that exceeds 0.02

Stock thinner than the vertical tolerance is not counted in the remaining volume. All figures are per-cell values, so they resolve features no smaller than a cell.

  • cam-sim.js
  • lines 154 to 168
  • PartCraft 0.8.1
/** Summary: remaining stock above the part, deepest gouge, cells, method. */
report() {
  const hp = this.partHeights();
  let remaining = 0, maxLeft = 0, gouges = 0, deepest = 0;
  for (let j = 0; j < this.ny; j++) for (let i = 0; i < this.nx; i++) {
    const k = j * this.nx + i;
    if (!this.inStock[k]) continue;
    const floor = hp[k] > -Infinity ? Math.max(hp[k], this.bottom) : this.bottom;
    const left = this.z[k] - floor;
    if (left > this.tolV) { remaining += left; maxLeft = Math.max(maxLeft, left); }
    if (this.gouge[k]) { gouges++; deepest = Math.max(deepest, this.erodedHeight(i, j) - this.z[k]); }
  }
  const a = this.cell * this.cell;
  return { remaining: remaining * a, maxLeft, gouges, deepest, cell: this.cell, nx: this.nx, ny: this.ny, tolV: this.tolV, tolLat: this.tolLat, volume: this.volume() };
}

Machining time

64-bit

The time a program takes: cutting moves at their feed, rapid moves at the machine's rapid rate, plus tool changes.

seconds = 60 × (Σ L ÷ feed + Σ L ÷ rapid) + changes × change time

Segment lengths are exact for lines and helical arcs. The quotients are added in program order. A feed of zero is not guarded against.

  • cam-sim.js
  • lines 230 to 247
  • PartCraft 0.8.1
/** Machining time of a program: feed moves at their feed, rapids at the setup's rapid rate, tool changes. */
export function cycleTime(program, setup) {
  const rapid = setup.machine.rapid, tc = setup.machine.toolChange;
  let feed = 0, rap = 0, changes = 0, prevTool = null;
  const perOp = {};
  for (const ph of program.paths) {
    if (!ph.moves || !ph.moves.length) continue;
    if (ph.tool && ph.tool.id !== prevTool) { if (prevTool !== null) changes++; prevTool = ph.tool.id; }
    let f = 0, r = 0;
    for (const s of segments([ph])) {
      const L = segLength(s);
      if (s.k === 'rapid') r += L / rapid; else f += L / s.f;
    }
    perOp[ph.op.id] = (f + r) * 60;
    feed += f; rap += r;
  }
  return { seconds: (feed + rap) * 60 + changes * tc, feed: feed * 60, rapid: rap * 60, toolChanges: changes, perOp, rapidRate: rapid, toolChangeTime: tc };
}

In detail

Method
Z-map: each move lowers the cells within the tool radius of its track to z + h(ρ), with h the tool's tip profile. Gouge test against the part's height reduced by a lateral tolerance. Time = feed lengths ÷ feed + rapid lengths ÷ rapid rate + tool changes.
Number types
Float64Array for stock and part heights; Uint8Array flags; Float32Array only for the colored display mesh.
Tolerances
Cell = min(2, max(0.1, largest stock side ÷ 500)) mm, at most 4,000,000 cells. Vertical tolerance 0.02 mm. Lateral tolerance 2e-4 × model diagonal + 0.01 mm. Sloping moves cut in steps of at most 0.01 mm of height, each at its lower end.
Limits
The header states them: one height per cell, so undercuts are not represented; removal on a ramp can be up to 0.01 mm deeper than the real cut; the time ignores acceleration and moves rapids as straight lines.

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