Resources Numerical accuracy Calculation policy

Drawing compare

Decides which objects are unchanged, moved, changed, added or removed between two versions of a drawing.

How the precision is kept

Every number of an object is rounded to a grid of 0.0001 drawing units before two objects are compared, so the comparison is a stated tolerance and not an exact test. What is left is paired by the distance between bounding-box centers, in 64-bit. The compared numbers are only used to decide; the drawing's own values are not changed.

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.

drawing-compare.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-changes.mjs, bin/test-cadcraft.mjs

Number rounded for comparison

64-bit

The text a number is compared by: its value on a grid of 0.0001.

Math.round(v ÷ 0.0001) × 0.0001, written with at most 4 decimals

Values are equal when they round to the same grid step. Two values up to 0.0001 apart can be equal, and two values closer than that on either side of a half step differ. Digits after the fourth decimal in the product's text are cut off, which removes the binary noise of n × 0.0001.

  • drawing-compare.js
  • line 28
  • CadCraft 0.8.2
const num = (v) => (Number.isFinite(v) ? String(Math.round(v / STEP) * STEP).replace(/(\.\d{4})\d+$/, '$1').replace(/^-0$/, '0') : String(v));

Center and size of an object

64-bit

The middle of an object's bounding box and the length of its diagonal, used to pair objects.

cx = (minX + maxX) ÷ 2; cy = (minY + maxY) ÷ 2; d = √((maxX − minX)² + (maxY − minY)²)

The diagonal uses Math.hypot. An object without valid bounds is placed at its own x, y (or center) with size 0.

  • drawing-compare.js
  • lines 77 to 82
  • CadCraft 0.8.2
function boxOf(e, blocks) {
  let b;
  try { b = e.type === 'text' ? textBox(e) : entityBounds(e, blocks); } catch { b = emptyBounds(); }
  if (!validBounds(b)) { const x = Number.isFinite(e.x) ? e.x : Number.isFinite(e.cx) ? e.cx : 0, y = Number.isFinite(e.y) ? e.y : Number.isFinite(e.cy) ? e.cy : 0; return { cx: x, cy: y, d: 0 }; }
  return { cx: (b.minX + b.maxX) / 2, cy: (b.minY + b.maxY) / 2, d: Math.hypot(b.maxX - b.minX, b.maxY - b.minY) };
}

Pairing by distance

64-bit

Pairs each leftover object with the nearest object of the other version that is close enough.

d = |center_a − center_b|; candidate when d ≤ max(size_a, size_b) + 1e-9; nearest first

Candidates are looked up in a grid whose cell is the largest object size (at least 1), in the cell and its 8 neighbors. The 1e-9 slack is absolute. Each object is used in at most one pair.

  • drawing-compare.js
  • lines 131 to 151
  • CadCraft 0.8.2
const pair = (listA, listB, ok) => {   // nearest first, on a grid of the larger sizes
  if (!listA.length || !listB.length) return [listA, listB];
  for (const x of listA) x.box ||= boxOf(x.e, ba);
  for (const y of listB) y.box ||= boxOf(y.e, bb);
  const cell = Math.max(1, ...listA.map((x) => x.box.d), ...listB.map((y) => y.box.d)) || 1;
  const grid = new Map(), key = (cx, cy) => Math.floor(cx / cell) + ',' + Math.floor(cy / cell);
  for (const x of listA) { const k = key(x.box.cx, x.box.cy); (grid.get(k) || grid.set(k, []).get(k)).push(x); }
  const cand = [];
  for (const y of listB) {
    const gx = Math.floor(y.box.cx / cell), gy = Math.floor(y.box.cy / cell);
    for (let i = -1; i <= 1; i++) for (let j = -1; j <= 1; j++) for (const x of grid.get((gx + i) + ',' + (gy + j)) || []) {
      if (!ok(x, y)) continue;
      const d = Math.hypot(x.box.cx - y.box.cx, x.box.cy - y.box.cy);
      if (d <= Math.max(x.box.d, y.box.d) + 1e-9) cand.push([d, x, y]);
    }
  }
  cand.sort((p, q) => p[0] - q[0]);
  const usedA = new Set(), usedB = new Set();
  for (const [, x, y] of cand) { if (usedA.has(x) || usedB.has(y)) continue; usedA.add(x); usedB.add(y); changed.push({ a: x.e, b: y.e, what: what(x.e, y.e) }); }
  return [listA.filter((x) => !usedA.has(x)), listB.filter((y) => !usedB.has(y))];
};

Moved or reshaped

64-bit

Says whether a changed object only moved: every x shifted by one amount and every y by another.

|Δxᵢ − Δx₀| ≤ 1e-3 and |Δyᵢ − Δy₀| ≤ 1e-3 for every coordinate; other numbers equal on the 0.0001 grid

The shifts are compared with an absolute tolerance of 1e-3 drawing units, ten times the equality grid. Radii, heights and angles must be equal after rounding to the grid.

  • drawing-compare.js
  • lines 103 to 117
  • CadCraft 0.8.2
/** Same size and shape, only shifted: every number differs by one shift (x with x, y with y). */
function pureMove(a, b) {
  const pts = (e) => { const out = []; const walk = (v) => { if (v && typeof v === 'object') for (const k of Object.keys(v)) { if (k === 'x' || k === 'x1' || k === 'x2' || k === 'cx') out.push(['x', v[k]]); else if (k === 'y' || k === 'y1' || k === 'y2' || k === 'cy') out.push(['y', v[k]]); else if (!k.startsWith('_') && !SKIP.has(k)) walk(v[k]); } }; walk(e); return out; };
  const pa = pts(a), pb = pts(b);
  if (!pa.length || pa.length !== pb.length) return false;
  let dx = null, dy = null;
  for (let i = 0; i < pa.length; i++) {
    if (pa[i][0] !== pb[i][0] || !Number.isFinite(pa[i][1]) || !Number.isFinite(pb[i][1])) return false;
    const d = pb[i][1] - pa[i][1];
    if (pa[i][0] === 'x') { if (dx === null) dx = d; else if (Math.abs(d - dx) > 1e-3) return false; } else if (dy === null) dy = d; else if (Math.abs(d - dy) > 1e-3) return false;
  }
  // the numbers other than positions (radius, height, angles) must be equal
  const strip = (e) => canon(e, true).replace(/(?:^|[,{])(?:x|y|x1|y1|x2|y2|cx|cy):[^,}]*/g, '');
  return strip(a) === strip(b);
}

In detail

Method
Canonical text of each object with numbers on a 0.0001 grid, hashed; then nearest-first pairing on a grid of cells the size of the largest object.
Number types
64-bit numbers; signatures are 53-bit string hashes that are compared, never decoded.
Tolerances
0.0001 drawing units (absolute grid) for equality; a move is pure when every coordinate shift agrees within 1e-3 (absolute); two objects can pair when their centers are within the larger object's diagonal plus 1e-9.
Limits
The grid is absolute: it does not scale with the drawing's units or size. Two values less than 0.0001 apart can fall on different grid steps and count as changed.

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