Resources Numerical accuracy Calculation policy

Automatic dimensions

Chooses what to dimension from the level and plumb edges, openings, circles and grid lines of a drawing and places chains, overall dimensions and diameters.

How the precision is kept

Edge positions are read from the drawing in 64-bit and grouped with a tolerance scaled to the drawing: tol = max(10⁻⁵ × diagonal, 1e-9). Positions within 20 × tol are merged into one, keeping the lowest, and intervals shorter than max(0.003 × diagonal, 10 × tol) are skipped. A dimension's definition points are stored coordinates; this file does not compute the measured value.

Open item. Dimension merged positions from their own coordinates, or report the merge, so a chain cannot hide an offset of up to 0.02 % of the drawing's diagonal.

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.

autodim.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests bin/test-autodim.mjs

Merging near-equal positions

64-bit

Sorts edge positions and merges those that are within a tolerance of each other.

A value within the tolerance of the last kept one is merged into it: the kept position is the lowest of the cluster and only its extents grow. The comparison is always with the kept value, so a cluster cannot grow wider than the tolerance.

  • autodim.js
  • lines 24 to 28
  • CadCraft 0.8.2
const sortedUnique = (vals, tol) => {
  const v = vals.slice().sort((a, b) => a.v - b.v), out = [];
  for (const x of v) { if (out.length && x.v - out[out.length - 1].v <= tol) { out[out.length - 1].lo = Math.min(out[out.length - 1].lo, x.lo); out[out.length - 1].hi = Math.max(out[out.length - 1].hi, x.hi); } else out.push({ ...x }); }
  return out;
};

Level and plumb edges, circles and points

64-bit

Collects the positions to dimension from the chosen objects and the size of the box round them.

diagonal = √(w² + h²); plumb when |Δx| ≤ tol; level when |Δy| ≤ tol

The diagonal is Math.hypot of the selection's box and the tolerance is supplied by planDimensions as max(10⁻⁵ × diagonal, 1e-9). For a plumb segment the position recorded for the chain is its first end's x, while the opening search uses the mean of both ends; the two can differ by up to tol. Circles and points contribute their own coordinates unchanged.

  • autodim.js
  • lines 30 to 57
  • CadCraft 0.8.2
/** Features of the chosen objects: ortho edges' ends, circle centres, block points → { xs, ys, segs, circles, box }. */
function featuresOf(doc, ids, tolFn) {
  const xs = [], ys = [], hsegs = [], vsegs = [], circles = [];
  const box = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
  const take = (e) => { const b = entityBounds(e, doc.blocks); if (validBounds(b)) { box.minX = Math.min(box.minX, b.minX); box.minY = Math.min(box.minY, b.minY); box.maxX = Math.max(box.maxX, b.maxX); box.maxY = Math.max(box.maxY, b.maxY); } };
  const list = [];
  if (ids && ids.size) for (const id of ids) { const e = doc.get(id); if (e) list.push(e); }
  else { let n = 0; for (const e of doc.entities.values()) { if (++n > LOOK_MAX) break; if (doc.visible(e)) list.push(e); } }
  const segs = [];
  for (const e of list) {
    if (e.type === 'dim') continue;
    take(e);
    if (e.type === 'line') segs.push({ a: { x: e.x1, y: e.y1 }, b: { x: e.x2, y: e.y2 }, layer: e.layer });
    else if (e.type === 'poly') { const n = e.pts.length, m = e.closed ? n : n - 1; for (let i = 0; i < m; i++) if (!(Math.abs(e.pts[i].b || 0) > 1e-12)) segs.push({ a: e.pts[i], b: e.pts[(i + 1) % n], layer: e.layer }); }
    else if (e.type === 'circle') circles.push(e);
    else if (e.type === 'insert' || e.type === 'point') { xs.push({ v: e.x, lo: e.y, hi: e.y }); ys.push({ v: e.y, lo: e.x, hi: e.x }); }
  }
  const diag = Math.hypot(box.maxX - box.minX, box.maxY - box.minY);
  const tol = tolFn(diag);
  for (const s of segs) {
    const dx = s.b.x - s.a.x, dy = s.b.y - s.a.y;
    if (Math.hypot(dx, dy) < tol) continue;
    if (Math.abs(dx) <= tol) { vsegs.push({ x: (s.a.x + s.b.x) / 2, lo: Math.min(s.a.y, s.b.y), hi: Math.max(s.a.y, s.b.y), layer: s.layer }); xs.push({ v: s.a.x, lo: Math.min(s.a.y, s.b.y), hi: Math.max(s.a.y, s.b.y) }); ys.push({ v: s.a.y, lo: s.a.x, hi: s.a.x }, { v: s.b.y, lo: s.b.x, hi: s.b.x }); }
    else if (Math.abs(dy) <= tol) { hsegs.push({ y: (s.a.y + s.b.y) / 2, lo: Math.min(s.a.x, s.b.x), hi: Math.max(s.a.x, s.b.x), layer: s.layer }); ys.push({ v: s.a.y, lo: Math.min(s.a.x, s.b.x), hi: Math.max(s.a.x, s.b.x) }); xs.push({ v: s.a.x, lo: s.a.y, hi: s.a.y }, { v: s.b.x, lo: s.b.y, hi: s.b.y }); }
  }
  for (const c of circles) { xs.push({ v: c.cx, lo: c.cy, hi: c.cy }); ys.push({ v: c.cy, lo: c.cx, hi: c.cx }); }
  return { xs, ys, hsegs, vsegs, circles, box, diag, tol };
}

Is the dimension already there

64-bit

A key that is equal for two dimensions of one kind with the same end points.

key = kind : round(x₁ ÷ g), round(y₁ ÷ g), round(x₂ ÷ g), round(y₂ ÷ g), g = 20 × tol

Two dimensions are the same when all four end coordinates fall in the same cells of a grid 20 × tol wide. Points on either side of a cell boundary are not matched even when they are closer than the grid.

  • autodim.js
  • line 59
  • CadCraft 0.8.2
const key = (k, p1, p2, tol) => k + ':' + [p1.x, p1.y, p2.x, p2.y].map((v) => Math.round(v / tol)).join(',');

Chains and overall dimensions

64-bit

The dimensions strung between neighboring edge positions along each chosen side, and one overall dimension outside them.

minLen = max(0.003 × diagonal, 10 × tol); chain line at box edge ∓ 6 × text height; overall line 5 × text height further out

After merging, a position closer than minLen to the last kept one is dropped, so short intervals get no dimension. Each dimension's two definition points are the kept positions with the edge's own near extent; the location point is the mean of the two positions on the dimension line. The text height is the style's txt × scale.

  • autodim.js
  • lines 69 to 108
  • CadCraft 0.8.2
const st = { ...doc.current.dim }, th = (st.txt ?? 2.5) * (st.scale ?? 1);
const off1 = o.offset ?? th * 6, off2 = off1 + th * 5, minLen = Math.max(F.diag * o.minFrac, F.tol * 10);
const dims = [], counts = { chain: 0, overall: 0, openings: 0, holes: 0, grid: 0, skipped: 0 };
const have = new Set();
for (const e of doc.entities.values()) if (e.type === 'dim' && e.p1 && (e.p2 || e.c)) { const q = e.p2 || e.c; have.add(key(e.kind, e.p1, q, F.tol * 20)).add(key(e.kind, q, e.p1, F.tol * 20)); }
const make = (kind, p1, p2, loc, rot, group) => {
  const k = key(kind, p1, p2, F.tol * 20);
  if (have.has(k)) { counts.skipped++; return; }
  have.add(k);
  dims.push({ type: 'dim', kind, layer: o.layer, color: null, p1, p2, loc, ...(kind === 'linear' ? { rot } : {}), text: '', st: { ...st }, ...(doc.current.dimstyle ? { dimstyle: doc.current.dimstyle } : {}) });
  counts[group]++;
};
const b = F.box;
const chainPositions = (list) => { const u = sortedUnique(list, F.tol * 20), kept = []; for (const p of u) if (!kept.length || p.v - kept[kept.length - 1].v >= minLen) kept.push(p); return kept; };
const px = chainPositions(F.xs), py = chainPositions(F.ys);
const side = (name) => o.sides.includes(name);
// chains and overall
const along = (pos, horizontal, near) => {   // near: the dimension line sits on this side of the drawing
  if (pos.length < 2) return;
  const lineAt = horizontal ? (near === 'bottom' ? b.minY - off1 : b.maxY + off1) : (near === 'left' ? b.minX - off1 : b.maxX + off1);
  const lineOver = horizontal ? (near === 'bottom' ? b.minY - off2 : b.maxY + off2) : (near === 'left' ? b.minX - off2 : b.maxX + off2);
  if (o.chain && pos.length > 2) {
    for (let i = 0; i + 1 < pos.length; i++) {
      const A = pos[i], B = pos[i + 1];
      const p1 = horizontal ? { x: A.v, y: near === 'bottom' ? A.lo : A.hi } : { x: near === 'left' ? A.lo : A.hi, y: A.v };
      const p2 = horizontal ? { x: B.v, y: near === 'bottom' ? B.lo : B.hi } : { x: near === 'left' ? B.lo : B.hi, y: B.v };
      make('linear', p1, p2, horizontal ? { x: (A.v + B.v) / 2, y: lineAt } : { x: lineAt, y: (A.v + B.v) / 2 }, horizontal ? 0 : Math.PI / 2, 'chain');
    }
  }
  if (o.overall) {
    const A = pos[0], B = pos[pos.length - 1];
    const p1 = horizontal ? { x: A.v, y: near === 'bottom' ? A.lo : A.hi } : { x: near === 'left' ? A.lo : A.hi, y: A.v };
    const p2 = horizontal ? { x: B.v, y: near === 'bottom' ? B.lo : B.hi } : { x: near === 'left' ? B.lo : B.hi, y: B.v };
    make('linear', p1, p2, horizontal ? { x: (A.v + B.v) / 2, y: pos.length > 2 && o.chain ? lineOver : lineAt } : { x: pos.length > 2 && o.chain ? lineOver : lineAt, y: (A.v + B.v) / 2 }, horizontal ? 0 : Math.PI / 2, 'overall');
  }
};
if (side('bottom')) along(px, true, 'bottom');
if (side('top')) along(px, true, 'top');
if (side('left')) along(py, false, 'left');
if (side('right')) along(py, false, 'right');

Opening widths

64-bit

The width of a gap between collinear lines on one layer, taken as a door or window opening.

w = next.lo − this.hi; kept when minLen ≤ w ≤ 0.35 × diagonal and w ≤ 0.8 × the longer neighbor

Collinear segments are grouped by layer and by Math.round(position ÷ (20 × tol)), and the gap is one subtraction of stored extents. The ratios 0.35 and 0.8 are fixed in the code. Segments on either side of a grid boundary fall in different groups and give no opening.

  • autodim.js
  • lines 111 to 127
  • CadCraft 0.8.2
const gap = (list, key2, horizontal) => {
  const groups = new Map();
  for (const s of list) { const k = s.layer + '|' + Math.round(s[key2] / (F.tol * 20)); (groups.get(k) || groups.set(k, []).get(k)).push(s); }
  for (const g of groups.values()) {
    g.sort((a, c) => a.lo - c.lo);
    for (let i = 0; i + 1 < g.length; i++) {
      const w = g[i + 1].lo - g[i].hi;
      if (w < minLen || w > F.diag * 0.35 || w > 0.8 * Math.max(g[i].hi - g[i].lo, g[i + 1].hi - g[i + 1].lo)) continue;
      const at = g[i][key2], cx = (b.minX + b.maxX) / 2, cy = (b.minY + b.maxY) / 2;
      const out = horizontal ? (at <= cy ? -1 : 1) : (at <= cx ? -1 : 1);
      const lineAt = at + out * th * 3;
      const p1 = horizontal ? { x: g[i].hi, y: at } : { x: at, y: g[i].hi }, p2 = horizontal ? { x: g[i + 1].lo, y: at } : { x: at, y: g[i + 1].lo };
      make('linear', p1, p2, horizontal ? { x: (p1.x + p2.x) / 2, y: lineAt } : { x: lineAt, y: (p1.y + p2.y) / 2 }, horizontal ? 0 : Math.PI / 2, 'openings');
    }
  }
};
gap(F.hsegs, 'y', true); gap(F.vsegs, 'x', false);

Hole diameters

64-bit

One diameter dimension for each size of circle.

p₁ = center + r·(cos 45°, sin 45°); text at center + (r + 3 × text height)·(cos 45°, sin 45°)

Circles with a radius over 0.2 × the diagonal are skipped and one dimension is made per radius cell of 20 × tol, so two radii in the same cell share one dimension. The dimension stores the circle's center and a point on it at 45°, which is on the circle to within the rounding of the sine and cosine of π ÷ 4.

  • autodim.js
  • lines 131 to 139
  • CadCraft 0.8.2
const seen = new Set();
for (const c of F.circles) {
  if (c.r > F.diag * 0.2) continue;
  const k = Math.round(c.r / (F.tol * 20));
  if (seen.has(k)) continue; seen.add(k);
  const p1 = polar({ x: c.cx, y: c.cy }, Math.PI / 4, c.r), loc = polar({ x: c.cx, y: c.cy }, Math.PI / 4, c.r + th * 3);
  make('diameter', p1, { x: c.cx, y: c.cy }, loc, 0, 'holes');
  const d = dims[dims.length - 1];
  if (d && d.kind === 'diameter') { d.c = { x: c.cx, y: c.cy }; d.p1 = p1; delete d.p2; }

In detail

Method
Rules only: sort the positions of level and plumb edges, merge near-equal ones, and string dimensions between neighbors along the chosen sides.
Number types
64-bit numbers.
Tolerances
tol = max(10⁻⁵ × the diagonal of the selection's box, 1e-9). An edge is level or plumb when its rise or run is ≤ tol; segments shorter than tol are ignored; positions merge within 20 × tol; existing dimensions, collinear segments and circle sizes are matched on a grid of 20 × tol; polyline arc segments (|bulge| > 1e-12) are left out.
Limits
Positions closer than 20 × tol (0.02 % of the diagonal) are dimensioned as one position at the lowest of them, so a chain can differ from the true spacing by up to that amount. An edge up to tol out of level or plumb is treated as level or plumb and is placed at its first end's coordinate.

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