Resources Numerical accuracy Calculation policy

Drawing to laser paths

Turns the drawing's lines, arcs, polylines, curves and hatches into paths in millimeters for the laser panel.

How the precision is kept

Objects are taken as line and arc pieces, chained into paths and put through the block transforms and one drawing-unit-to-millimeter scale in 64-bit numbers. Arcs stay arcs (as bulges) while the transform keeps shapes; otherwise they are flattened to a tolerance of 0.01 mm by default.

Calculations under this policy

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

laser-cad.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests tools/cadcraft-test/laser-mount.mjs

Chaining pieces into paths

64-bit

Joins line and arc pieces that follow one another into open or closed paths.

arc → bulge = tan((a1 − a0) ÷ 4); full circle → two half arcs with bulge 1

A piece joins the path when it starts within 1e-6 × max(1, |coordinate|) of where the last one ended, and the path keeps the earlier point. Arc end points are computed from center, radius and angle with cos and sin. A full circle is written as two half arcs from (cx + r, cy) to (cx − r, cy).

  • laser-cad.js
  • lines 30 to 56
  • CadCraft 0.8.2
/** Primitives (geom.js L / A, in order) → paths [{ pts: [{x, y, b?}], closed }]: joined while each starts where the last ended. */
export function chainPrims(list) {
  const out = [];
  let cur = null;
  const finish = () => {
    if (!cur) return;
    const pts = cur.pts;
    if (pts.length > 2 && near(pts[0], pts[pts.length - 1])) { pts.pop(); cur.closed = true; }
    if (pts.length >= 2) out.push(cur);
    cur = null;
  };
  for (const p of list) {
    if (p.box) continue;   // text boxes, arrowheads: for picking only
    if (p.k === 'A' && (p.full || p.a1 - p.a0 >= TAU - 1e-9)) {   // a whole circle: two half arcs, closed
      finish();
      out.push({ pts: [{ x: p.cx + p.r, y: p.cy, b: 1 }, { x: p.cx - p.r, y: p.cy, b: 1 }], closed: true });
      continue;
    }
    const s = primStart(p), e = primEnd(p);
    if (!cur || !near(cur.pts[cur.pts.length - 1], s)) { finish(); cur = { pts: [{ x: s.x, y: s.y }], closed: false }; }
    const last = cur.pts[cur.pts.length - 1];
    if (p.k === 'A') last.b = Math.tan((p.a1 - p.a0) / 4);
    cur.pts.push({ x: e.x, y: e.y });
  }
  finish();
  return out;
}

A path through a block transform

64-bit

A path placed by its block reference, keeping arcs where the transform allows.

The transform keeps shapes when |a² + b² − (c² + d²)| and |a·c + b·d| are both within 1e-9 of the sum of the four squares. Then points go through the matrix and bulges are kept, negated under a mirror. Otherwise arcs are flattened first, by laser/laser-geom.js, to a tolerance of tol ÷ √|det|.

  • laser-cad.js
  • lines 58 to 65
  • CadCraft 0.8.2
/** A path through the matrix m; arcs stay arcs while m keeps shapes (a mirror turns their bulges round), else they are flattened. */
function txPath(path, m, tol) {
  const [a, b, c, d] = m, det = a * d - b * c;
  const similar = Math.abs(a * a + b * b - (c * c + d * d)) <= 1e-9 * (a * a + b * b + c * c + d * d) && Math.abs(a * c + b * d) <= 1e-9 * (a * a + b * b + c * c + d * d);
  if (similar || !path.pts.some((p) => p.b)) return { closed: path.closed, pts: path.pts.map((p) => { const q = mApply(m, p); return p.b && similar ? { x: q.x, y: q.y, b: det < 0 ? -p.b : p.b } : { x: q.x, y: q.y }; }) };
  const s = Math.sqrt(Math.abs(det)) || 1;
  return { closed: path.closed, pts: flatten(path.pts, path.closed, tol / s).map((p) => mApply(m, p)) };
}

Drawing units to millimeters

64-bit

The one scale factor that puts the drawing into millimeters.

k = millimeters per drawing unit, or 1 when the drawing has no units

The factor comes from mmPerDrawingUnit in units.js and is used as the scale of the root transform, so every coordinate is multiplied by it once.

  • laser-cad.js
  • lines 83 to 84
  • CadCraft 0.8.2
const per = mmPerDrawingUnit(doc.meta?.lunits, doc.meta?.units);
const k = per || 1;

Picture placement

64-bit

Where a picture lies on the laser bed and how large it is.

w = u_x × picture width; h = v_y × picture height, u and v the picture's axes through the transform

A picture is sent only when it lies square: its u axis along +x and its v axis along +y, with the cross components within 1e-6 of the main ones. Its position is the picture's corner through the block transform.

  • laser-cad.js
  • lines 93 to 103
  • CadCraft 0.8.2
function picture(fr, blk, m, layer) {
  if (fr.ole) { skip('picture-missing'); return; }
  const p = fr.pic, at = mApply(m, { x: p.x, y: p.y }), u = lin(m, p.ux, p.uy), v = lin(m, p.vx, p.vy);
  const square = u.x > 0 && v.y > 0 && Math.abs(u.y) <= 1e-6 * u.x && Math.abs(v.x) <= 1e-6 * v.y;
  if (!square) { skip('picture-turned'); return; }
  const rec = findPicture(doc, p.file);
  if (!rec) { skip('picture-missing'); return; }
  const g = gray ? gray(rec, p) : null;
  if (!g) { skip('picture-loading'); return; }
  pictures.push({ layer, x: at.x, y: at.y, w: u.x * p.w, h: v.y * p.h, pw: g.pw, ph: g.ph, gray: g.gray });
}

In detail

Method
Pieces from geom.js chained end to end; bulge = tan(sweep ÷ 4); 2 × 3 matrices for blocks.
Number types
64-bit numbers.
Tolerances
Ends chain within 1e-6 × max(1, |coordinate|). A transform keeps shapes when its columns agree within 1e-9 of the sum of their squared lengths. Flattening tolerance tol ÷ √|det|, tol = 0.01 by default. A picture lies square within 1e-6, relative.
Limits
Ellipses and splines arrive as the short segments geom.js makes of them. Text, dimensions, leaders, points and construction lines are not sent.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach