Resources Numerical accuracy Calculation policy

Picked points, object snaps and viewport views

Turns a pointer position into drawing coordinates, finds the object snap under it, and works out the center, scale and twist of a sheet viewport after it was panned or zoomed.

How the precision is kept

The view is four 64-bit numbers: center x and y, zoom in CSS pixels per drawing unit, and a turn in radians. A pointer position is divided by the zoom and added to the center. An object snap returns the candidate point's own coordinates, not the pointer's. A viewport's view is read back from a 2 × 3 matrix product and compared with the stored one using a tolerance scaled to the values.

Calculations under this policy

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

view.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests bin/test-cadcraft.mjs

Pointer position to drawing coordinates

64-bit

The drawing point under a screen position, in the active pane.

u = (sx − w ÷ 2) ÷ zoom; v = (h ÷ 2 − sy) ÷ zoom; x = cx + cos(tw)·u − sin(tw)·v; y = cy + sin(tw)·u + cos(tw)·v

64-bit numbers; one division by the zoom and one addition to the view center. With no turn the sine and cosine are skipped, so the result is exactly cx + u and cy + v. The measured input is the pointer position in CSS pixels.

  • view.js
  • lines 400 to 405
  • CadCraft 0.8.2
toWorld(sx, sy) {
  const u = (sx - this.w / 2) / this.zoom, v = (this.h / 2 - sy) / this.zoom;
  if (!this.tw) return { x: u + this.cx, y: v + this.cy };
  const c = Math.cos(this.tw), s = Math.sin(this.tw);
  return { x: this.cx + c * u - s * v, y: this.cy + s * u + c * v };
}

Object snap under the pointer

64-bit

The best snap point (endpoint, midpoint, center, intersection and so on) within the aperture of the pointer.

r = px ÷ zoom; keep candidates with |c − p| ≤ r; lowest priority number wins, then the smallest distance

The returned x and y are the candidate's own 64-bit coordinates from snapPoints and intersect in geom.js; the pointer position only selects among them. The aperture r is absolute in drawing units and shrinks as the view zooms in. Collection stops after about 400 nearby objects and intersections are tried among the first 60.

  • view.js
  • lines 577 to 607
  • CadCraft 0.8.2
/** The best object snap within the aperture (px) of p. */
snap(p, { kinds, base = null, px = 10 }) {
  if (!kinds || !kinds.size) return null;
  const r = px / this.zoom;
  const bounds = this.ensureCache().bounds;
  const nearby = [], frz = this.enteredFrozen();
  for (const e of this.doc.entities.values()) {
    if (!this.doc.visible(e) || this.hiddenHere(e, frz)) continue;
    const b = bounds.get(e.id);
    if (!b || (UNBOUNDED.has(e.type) ? distToEntity(e, p, null) > r : (p.x < b.minX - r || p.x > b.maxX + r || p.y < b.minY - r || p.y > b.maxY + r))) continue;
    nearby.push(e);
    if (nearby.length > 400) break;
  }
  let best = null;
  const consider = (c) => {
    const d = Math.hypot(c.x - p.x, c.y - p.y);
    if (d > r) return;
    const pr = c.pr ?? SNAP_PRIORITY[c.kind] ?? 5;
    if (!best || pr < best.pr || (pr === best.pr && d < best.d)) best = { ...c, d, pr };
  };
  for (const e of nearby) for (const c of snapPoints(e, p, r, kinds, this.doc.blocks, base)) consider(c);
  if (kinds.has('int') && nearby.length > 1) {
    const ps = nearby.slice(0, 60).map((e) => prims(e, this.doc.blocks).filter((q) => !q.box && nearPrim(q, p, r)));
    for (let i = 0; i < ps.length; i++) for (let j = i + 1; j < ps.length; j++) {
      for (const a of ps[i]) for (const b of ps[j]) for (const x of intersect(a, b)) consider({ kind: 'int', x: x.x, y: x.y });
    }
  }
  if (this.snapUnder) for (const c of this.snapUnder(p, r, kinds) || []) consider(c);   // a PDF underlay's own lines (0.7 D7.24)
  if (kinds.has('node') && this.snapExtra) { const c = this.snapExtra(p, r); if (c) consider(c); }   // a point cloud's points (SG.1)
  return best;
}

Viewport view after pan or zoom inside it

64-bit

The center, scale and twist an entered sheet viewport shows on screen, returned for the caller to store in the viewport when they differ from the stored ones.

M = P⁻¹ · T; s = √(M₀² + M₁²); tw = atan2(M₁, M₀); center = M⁻¹ · (vp.x, vp.y)

Scale and twist come from the first column of a 2 × 3 matrix product and the center from the matrix inverse. The comparison with the stored view uses 1e-9 × max(1, |a|, |b|), so it scales with the size of the values. A twist is added to the result only when it or the stored twist is not zero.

  • view.js
  • lines 347 to 356
  • CadCraft 0.8.2
/** The view the entered (not locked) viewport has on screen when it differs from the doc's: { lay, vp, cx, cy, s }, else null. */
pendingViewport() {
  const ev = this.enteredViewport();
  if (!ev || ev.vp.locked) return null;
  const M = this.enteredMatrix(), s = Math.hypot(M[0], M[1]), tw = normTurn(Math.atan2(M[1], M[0]));
  const c = ap(inverse(M), ev.vp.x, ev.vp.y);
  const same = (a, b) => Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
  if (same(c.x, ev.vp.cx) && same(c.y, ev.vp.cy) && same(s, ev.vp.s) && same(tw, normTurn(ev.vp.tw || 0))) return null;
  return { ...ev, cx: c.x, cy: c.y, s, ...(tw || ev.vp.tw ? { tw } : {}) };
}

View to matrix

64-bit

The drawing-to-pixel matrix of a view for a pane of a given size.

[z·c, z·s, z·s, −z·c, w ÷ 2 − z·(c·cx + s·cy), h ÷ 2 + z·(−s·cx + c·cy)] with c = cos(tw), s = sin(tw), z = zoom

Six 64-bit expressions. The translation terms multiply the view center by the zoom, so for a center far from the origin they are large numbers whose difference from the pane center is formed afterward.

  • view.js
  • lines 1236 to 1240
  • CadCraft 0.8.2
/** A view { cx, cy, zoom, tw } → its world → CSS px affine for a w × h pane (DR.8: turned by −tw on screen). */
export function affineOf(v, w, h) {
  const z = v.zoom, c = Math.cos(v.tw || 0), s = Math.sin(v.tw || 0);
  return [z * c, z * s, z * s, -z * c, w / 2 - z * (c * v.cx + s * v.cy), h / 2 + z * (-s * v.cx + c * v.cy)];
}

Matrix to view

64-bit

Reads the center, zoom and turn back out of a drawing-to-pixel matrix.

zoom = √(A₀² + A₁²); tw = atan2(A₁, A₀); center = A⁻¹ · (w ÷ 2, h ÷ 2)

Math.hypot for the zoom, atan2 for the turn, and the 2 × 3 inverse for the center. The turn goes through normTurn, which sets angles below 1e-12 rad to exactly 0.

  • view.js
  • lines 1241 to 1246
  • CadCraft 0.8.2
/** … and back: an affine of that shape (a similarity with y flipped) → { cx, cy, zoom, tw }. */
export function viewOf(A, w, h) {
  const zoom = Math.hypot(A[0], A[1]), tw = normTurn(Math.atan2(A[1], A[0]));
  const c = ap(inverse(A), w / 2, h / 2);
  return { cx: c.x, cy: c.y, zoom, tw };
}

Inverse of a 2 × 3 matrix

64-bit

The inverse of a drawing-to-pixel matrix.

det = m₀·m₃ − m₁·m₂; inverse = adjugate ÷ det

Closed form with six divisions by the determinant. The division is not guarded in this function; for a view matrix the determinant is −zoom², and zoomAt keeps the zoom at 1e-9 or more.

  • view.js
  • lines 1247 to 1250
  • CadCraft 0.8.2
function inverse(m) {
  const det = m[0] * m[3] - m[1] * m[2];
  return [m[3] / det, -m[1] / det, -m[2] / det, m[0] / det, (m[2] * m[5] - m[3] * m[4]) / det, (m[1] * m[4] - m[0] * m[5]) / det];
}

Angle brought into one turn

64-bit

An angle reduced to the range −π to π.

r = atan2(sin a, cos a); |r| < 1e-12 → 0

The reduction goes through sine, cosine and atan2 instead of a remainder, which can change the last bits of an angle already in range. Results below 1e-12 rad in size become exactly 0.

  • view.js
  • lines 1251 to 1252
  • CadCraft 0.8.2
/** An angle in (−π, π], with the tiny ones (float noise) as 0. */
function normTurn(a) { let r = Math.atan2(Math.sin(a), Math.cos(a)); if (Math.abs(r) < 1e-12) r = 0; return r; }

In detail

Method
Similarity transform with y flipped: screen = zoom × R(−turn) × (world − center) + pane ÷ 2, and its closed-form inverse.
Number types
64-bit numbers throughout; matrices are plain arrays of 6 numbers.
Tolerances
Snap aperture 10 px ÷ zoom (drawing units). A viewport view counts as unchanged within 1e-9 × max(1, |a|, |b|). An angle smaller than 1e-12 rad is set to 0. zoomAt keeps the zoom between 1e-9 and 1e9.
Limits
A point taken from the pointer alone is only as fine as the pointer position: one CSS pixel is 1 ÷ zoom drawing units. The snap search looks at no more than about 400 nearby objects and intersects only the first 60 of them.

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