Resources Numerical accuracy Calculation policy

G-code dry run and hole check

Reads a generated G-code file and reports its extent, cutting and rapid lengths, depths and cutting time, and checks that every drilled hole lies on its panel.

How the precision is kept

Coordinates are parsed from the G-code text into 64-bit numbers, so the report starts from the decimals the file was written with. Lengths and times are summed in file order in 64-bit and every reported value is rounded to 3 decimals (r3). The reader handles straight moves and arcs in the XY plane given by I and J; it models no acceleration.

Open item. Compare an arc's end-point radius with its start radius and report a mismatch, and read R-format arcs.

Calculations under this policy

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

addons/cabinetcraft/lib/dryrun.js CadCraft 0.8.2 64-bit 10 calculations Reviewed

Tests None found by the review

Numbers read from a G-code line

64-bit

Splits a line into its letter and number words.

Each number is the matched text passed to Number(), so it keeps the decimals in the file. The pattern is an optional minus, digits and an optional point; a word written with a leading plus sign or an exponent is not matched as written.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 30 to 31
  • CadCraft 0.8.2
const words = [...code.matchAll(/([A-Z])\s*(-?\d*\.?\d+)/g)].map((m) => [m[1], Number(m[2])]);
let target = { x, y, z }, ijk = { i: 0, j: 0 }, hasXYZ = false;

Arc move length

64-bit

The length of a G2 or G3 move in the XY plane, with any Z change.

r = |start − center|; sweep = a₁ − a₀ wrapped to the move's direction; len = √((r × |sweep|)² + Δz²)

The radius is taken from the start point only; the end point's distance from the center is not checked. The sweep is the difference of two atan2 angles, wrapped with a 1e-9 slack, and a move whose end is within 1e-6 of its start is a full circle. With no I or J the center is the start point and the radius is 0.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 50 to 55
  • CadCraft 0.8.2
const cx = x + ijk.i, cy = y + ijk.j, r0 = Math.hypot(x - cx, y - cy);
let a0 = Math.atan2(y - cy, x - cx), a1 = Math.atan2(target.y - cy, target.x - cx);
let d = a1 - a0;
if (m.cw) { while (d >= -1e-9) d -= 2 * Math.PI; } else { while (d <= 1e-9) d += 2 * Math.PI; }
if (Math.hypot(target.x - x, target.y - y) < 1e-6) d = m.cw ? -2 * Math.PI : 2 * Math.PI;   // a full circle
m.len = Math.hypot(r0 * Math.abs(d), target.z - z);

Arc move extent

64-bit

The bounding box of an arc move.

extent = end points ∪ {center + r × (cos q, sin q)} for q = 0°, 90°, 180°, 270° inside the sweep

Each axis direction is included when its angle from the start, in the move's direction, is at most |sweep| + 1e-9. The points use Math.cos and Math.sin of multiples of π ÷ 2; Math.cos(π ÷ 2) and Math.sin(π) are not exactly 0 in 64-bit, so those points are off by a tiny fraction of the radius.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 57 to 64
  • CadCraft 0.8.2
const xs = [x, target.x], ys = [y, target.y];
for (let q = 0; q < 4; q++) {
  const ang = q * Math.PI / 2;
  let t = m.cw ? a0 - ang : ang - a0;
  while (t < 0) t += 2 * Math.PI;
  if (t <= Math.abs(d) + 1e-9) { xs.push(cx + r0 * Math.cos(ang)); ys.push(cy + r0 * Math.sin(ang)); }
}
m.ext = [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];

Straight move length

64-bit

The length and extent of a straight move.

len = √(Δx² + Δy² + Δz²)

One Math.hypot of three differences. Arcs in a plane other than XY also take this branch, so they are measured as straight lines.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 66 to 67
  • CadCraft 0.8.2
m.len = Math.hypot(target.x - x, target.y - y, target.z - z);
m.ext = [Math.min(x, target.x), Math.min(y, target.y), Math.max(x, target.x), Math.max(y, target.y)];

Extent of the whole program

64-bit

The smallest and largest X, Y and Z any move reaches.

Minimum and maximum over the moves' extents and end heights, then each of the six values rounded to 3 decimals.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 87 to 90
  • CadCraft 0.8.2
const bbox = p.moves.length ? [
  Math.min(...p.moves.map((m) => Math.min(m.ext[0]))), Math.min(...p.moves.map((m) => m.ext[1])), Math.min(...p.moves.map((m) => Math.min(m.z0, m.z1))),
  Math.max(...p.moves.map((m) => m.ext[2])), Math.max(...p.moves.map((m) => m.ext[3])), Math.max(...p.moves.map((m) => Math.max(m.z0, m.z1))),
].map(r3) : null;

Total cutting and rapid length

64-bit

The summed length of the cutting moves and of the rapid moves.

cutLen = Σ len over cutting moves; rapidLen = Σ len over rapids

Plain sums in file order, with no compensation. The report rounds both to 3 decimals; the unrounded sums feed the time.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 91 to 92
  • CadCraft 0.8.2
const cutLen = cuts.reduce((s, m) => s + m.len, 0), rapidLen = rapids.reduce((s, m) => s + m.len, 0);
const feeds = cuts.map((m) => m.feed).filter((f) => f > 0);

Deepest cut and cut through the stock

64-bit

The lowest Z of any cutting move and how far it goes below the bottom of the sheet.

deepest = min z; through = max(0, −deepest − thickness)

A cut is flagged as not through when −deepest < thickness − 0.01, and as below the stock when through > 0.01. The 0.01 is in the file's unit, so it is 0.01 mm or 0.01 inch. Reported values are rounded to 3 decimals.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 101 to 104
  • CadCraft 0.8.2
const deepest = cuts.length ? Math.min(...cuts.map((m) => Math.min(m.z0, m.z1))) : 0;
const through = thickness > 0 ? Math.max(0, -deepest - thickness) : 0;
if (thickness > 0 && -deepest < thickness - tol && cuts.length) add('deep', 'warning', null, { deepest: r3(-deepest), t: thickness });
if (through > tol) add('outside-z', 'note', null, { over: r3(through) });

Cutting and rapid time

64-bit

The minutes the cutting moves and the rapids take at their feed rates.

minutesCut = Σ len ÷ feed; minutesRapid = rapidLen ÷ rapid rate

Each cutting move's length is divided by its own feed; moves with no feed add nothing. Acceleration and tool changes are not counted. The rapid time is null when no rapid rate was given. The report rounds to 3 decimals of a minute.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 105 to 106
  • CadCraft 0.8.2
const minutesCut = feeds.length ? cuts.reduce((s, m) => s + (m.feed > 0 ? m.len / m.feed : 0), 0) : 0;
const minutesRapid = rapid > 0 ? rapidLen / rapid : null;

Hole edge distance

64-bit

How close the rim of each face hole comes to the edge of its panel.

e = min(x − r, L − x − r, y − r, W − y − r), r = d ÷ 2

The hole must lie within the panel with 1e-6 slack, and e must not be more than 1e-6 under the rule. A hole with no size typed has r = 0, so only its center is checked. The tightest value is reported rounded to 3 decimals.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 130 to 139
  • CadCraft 0.8.2
const r = h.d > 0 ? h.d / 2 : 0;
if (h.d === '' || h.depth === '') { blanks[h.kind] = (blanks[h.kind] || 0) + 1; }
checked++;
const face = h.face || pl.face;
const inRect = h.x >= -1e-6 && h.x <= L + 1e-6 && h.y >= -1e-6 && h.y <= W + 1e-6;
if (!inRect) { issues.push({ code: 'hole.outside', severity: 'error', part: pl.id, hole: i + 1, vars: { ref, kind: h.kind, x: h.x, y: h.y, l: L, w: W } }); return; }
if (face !== 'edge') {
  const e = Math.min(h.x - r, L - h.x - r, h.y - r, W - h.y - r);
  tightest = Math.min(tightest, e);
  if (e < rule - 1e-6) issues.push({ code: 'hole.edge', severity: 'error', part: pl.id, hole: i + 1, vars: { ref, kind: h.kind, edge: r3(e), min: r3(rule) } });

Rounding of reported values

64-bit

Rounds a value of the report to 3 decimals.

r3(v) = round(v × 1000) ÷ 1000

Applied to the lengths, depths, times and edge distances the report returns. The multiplication by 1000 happens in binary, so a value exactly on a half can round either way.

  • addons/cabinetcraft/lib/dryrun.js
  • lines 2 to 19
  • CadCraft 0.8.2
/**
 * dryrun.js — a dry-run checker for the files a job hands to a machine (0.8.2 DC.117, session `cabinetcraft_addon` round 3). DOM-free, pure.
 *
 * It does NOT run anything on a machine and says so: it reads the generated G-code like a very small tool-path simulator (straight moves and arcs in the XY
 * plane, absolute coordinates, modal feed and Z) and reports what it can see, so the owner starts the first real run with fewer surprises. A machine
 * run, a boring machine reading the hole list and a cabinet maker's look at the sizes stay a person's job.
 *
 *   parseGcode(text) → { moves: [{ line, rapid, arc, x0, y0, z0, x1, y1, z1, feed, len, ext }], tools: [n…], toolChanges: [{ line, n }], units ('mm' | 'in' | null),
 *                        relative (bool), spindle (bool), problems: [{ code, line, ... }] }
 *   dryRunGcode(text, { sheet: { w, h }, thickness, unit, rapid (rate in mm/min or in/min, blank = unknown) }) → report
 *     { bbox: [x0, y0, z0, x1, y1, z1], moves, cuts, rapids, arcs, cutLen, rapidLen, minFeed, maxFeed, tools, toolChanges, deepest, throughStock (how far below the stock's bottom),
 *       minutesCut, minutesRapid (null when `rapid` is blank), minutes (null when rapids are unknown: then minutesCut is a lower bound), issues: [{ code, severity, line?, vars }] }
 *     codes: units · relative · no-tool · no-spindle · no-feed · rapid-in-stock · outside · outside-z · deep · empty
 *   checkHoles(build) → { holes, parts, checked, minEdge (the rule), tightest, blanks, issues: [{ code, severity, part, hole, vars }] }
 *     every hole of the drilling plan lies inside its panel; a face hole's rim keeps at least `cupEdge` (the smallest edge distance the drilling rules themselves use)
 *     from every edge; an edge hole lies on the panel's blank; a hole with a size no deeper than the panel is thick.
 */
const r3 = (v) => Math.round(v * 1000) / 1000;

In detail

Method
Straight move length √(Δx² + Δy² + Δz²); arc length √((r × |sweep|)² + Δz²) with the center from I and J; arc extent from the end points and the axis directions inside the sweep; cutting time Σ length ÷ feed.
Number types
64-bit numbers parsed from text with Number().
Tolerances
Sweep wrap 1e-9 rad; start and end closer than 1e-6 make a full circle; stock tests use tol = 0.01 in the file's unit; hole tests use 1e-6. All absolute.
Limits
An arc given with R instead of I and J gets radius 0. K, arcs in the G18 and G19 planes, and the arc's end radius are not used. Numbers with a leading plus sign or an exponent are not matched. Times assume the programmed feed is reached instantly.

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