Resources Numerical accuracy Calculation policy

Moving a grid line with what hangs on it

Moves a grid line sideways by a distance and carries the walls, columns, bubbles, tags and dimension points that sit on it.

How the precision is kept

The move is one vector, the line's unit normal times the distance, added once to every carried point in 64-bit numbers. What counts as sitting on the grid line is decided by a tolerance of 0.05% of the line's length, so a point slightly off the line is carried and keeps its offset.

Calculations under this policy

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

gridmove.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-gridmove.mjs

Normal of a grid line

64-bit

The unit direction a positive distance moves the grid line in: to the left of its start-to-end direction.

n = (−Δy, Δx) ÷ L

Divides by the Math.hypot length. A zero length is replaced by 1, so the result is (0, 0) and not NaN.

  • gridmove.js
  • line 23
  • CadCraft 0.8.2
export const normalOf = (l) => { const d = Math.hypot(l.x2 - l.x1, l.y2 - l.y1) || 1; return { x: -(l.y2 - l.y1) / d, y: (l.x2 - l.x1) / d }; };

What moves, and by how much

64-bit

The new geometry of every object the grid line carries with it.

tol = max(0.0005 × L, 1e-6); reach = 0.15 × L; move = n × distance

A point counts as on the grid line when it is within tol of it and no further than reach past its ends. Everything that counts is moved by the same vector, added once. Because tol is 0.05% of the line's length, a point up to that far off the line is carried and keeps its offset from it.

  • gridmove.js
  • lines 56 to 96
  • CadCraft 0.8.2
/**
 * What moving grid line `id` by `distance` (+ = to the left of a → b) would change. opts: tol (how close counts as on the line, default
 * 0.05% of its length), reach (how far past its ends a bubble may sit, default 15% of its length), skip (a Set of ids that stay).
 */
export function planGridMove(doc, id, distance, opts = {}) {
  const line = doc.get(id);
  if (!line || line.type !== 'line') return null;
  const L = Math.hypot(line.x2 - line.x1, line.y2 - line.y1);
  if (L < 1e-9 || !Number.isFinite(distance)) return null;
  const tol = opts.tol ?? Math.max(L * 5e-4, 1e-6), reach = opts.reach ?? L * 0.15;
  const ux = (line.x2 - line.x1) / L, uy = (line.y2 - line.y1) / L, nx = -uy, ny = ux;
  const dx = nx * distance, dy = ny * distance;
  const along = (p) => (p.x - line.x1) * ux + (p.y - line.y1) * uy;
  const off = (p) => Math.abs((p.x - line.x1) * nx + (p.y - line.y1) * ny);
  const inside = (p) => off(p) <= tol && along(p) >= -reach && along(p) <= L + reach;
  const updates = [], counts = { moved: 0, stretched: 0, dims: 0, notes: 0 };
  let n = 0;
  for (const e of doc.entities.values()) {
    if (++n > LOOK_MAX) break;
    if (opts.skip?.has(e.id) || !usable(doc, e)) continue;
    let out = null;
    if (e.type === 'text' || e.type === 'mtext') {
      const b = entityBounds(e, doc.blocks);
      if (!validBounds(b)) continue;
      const c = { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 }, h = Math.max(b.maxY - b.minY, 0);
      if (off(c) <= Math.max(tol, h * 0.5) && along(c) >= -reach && along(c) <= L + reach) { out = txEntity(e, mTranslate(dx, dy)); counts.notes++; }
    } else if (e.type === 'circle') {
      if (off({ x: e.cx, y: e.cy }) <= Math.max(tol, e.r * 0.05) && along({ x: e.cx, y: e.cy }) >= -reach - e.r && along({ x: e.cx, y: e.cy }) <= L + reach + e.r) { out = txEntity(e, mTranslate(dx, dy)); counts.moved++; }
    } else {
      out = stretchEntity(e, inside, dx, dy);
      if (out) {
        if (e.type === 'dim') counts.dims++;
        else if (e.type === 'line' && (Math.abs(out.x1 - e.x1) + Math.abs(out.y1 - e.y1) > 0) !== (Math.abs(out.x2 - e.x2) + Math.abs(out.y2 - e.y2) > 0)) counts.stretched++;
        else if (e.type === 'poly' && out.pts.some((p, i) => p.x === e.pts[i].x && p.y === e.pts[i].y)) counts.stretched++;
        else counts.moved++;
      }
    }
    if (out) { delete out.id; updates.push({ id: e.id, entity: out }); }
  }
  return { updates, counts, line: { id, a: { x: line.x1, y: line.y1 }, b: { x: line.x2, y: line.y2 } }, normal: { x: nx, y: ny }, distance, tol };
}

In detail

Method
Unit normal from the line's direction divided by its Math.hypot length; points tested by their distance from the line and their position along it.
Number types
64-bit numbers.
Tolerances
tol = max(0.0005 × length, 1e-6) unless given; reach past the ends = 0.15 × length; text uses max(tol, half its height); circles use max(tol, 5% of the radius). A grid line shorter than 1e-9 is ignored.
Limits
At most 200,000 objects are looked at.

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