Resources Numerical accuracy Calculation policy

Spoken lengths, directions and extents

Works out the lengths, distances, directions and extents that CadCraft says when the drawing is described in words.

How the precision is kept

Coordinates are read from the drawing as 64-bit numbers and each length is one Math.hypot or one product. Lengths are then written with the drawing's own length format; angles and plain numbers are rounded to 2 decimals for speech. A polyline's length is the sum of its straight chords, so its arc segments are counted short.

Open item. Add each arc segment's own length in lengthOf for polylines, as geom.js primLength does for the same segments.

Calculations under this policy

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

access.js CadCraft 0.8.2 64-bit 9 calculations Reviewed

Tests bin/test-access.mjs

Rounding of spoken numbers and angles

64-bit

Turns a number or an angle into the short text that is spoken.

num(v) = round(v × 100) ÷ 100; deg(θ) = num(round(normAngle(θ) × (180 ÷ π) × 100) ÷ 100)

A whole number is said as it is; any other value is rounded to 2 decimals with Math.round(v × 100) ÷ 100, an absolute step of 0.01. The factor 180 ÷ Math.PI is computed once in 64-bit. An angle passes through the same 2-decimal rounding twice.

  • access.js
  • lines 16 to 18
  • CadCraft 0.8.2
const R2D = 180 / Math.PI;
const num = (v) => (Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
const deg = (rad) => num(Math.round(normAngle(rad) * R2D * 100) / 100);

Direction as one of eight words

64-bit

Which of the eight directions (right, up-right, up and so on) leads from one point to another.

index = round(atan2(Δy, Δx) ÷ (π ÷ 4)) mod 8

One atan2 of coordinate differences, normalized to 0…2π, divided by π ÷ 4 and rounded to the nearest whole number, so a direction within 22.5° of an axis or a diagonal takes its name. No tolerance is used.

  • access.js
  • lines 34 to 38
  • CadCraft 0.8.2
/** The eight directions, as words: from a to b, in the drawing's own axes (right = +x, up = +y). Index 0..7 counter-clockwise from right. */
export function directionOf(a, b) {
  const ang = normAngle(Math.atan2(b.y - a.y, b.x - a.x));
  return Math.round(ang / (Math.PI / 4)) % 8;
}

A move in words

64-bit

The distance, angle and the two components of a move from one point to another.

d = √(Δx² + Δy²); angle = atan2(Δy, Δx)

The distance is one Math.hypot of the coordinate differences. A move shorter than 1e-9 drawing units (absolute, not scaled to the drawing) is reported as no move. The distance and the two components go through the drawing's length format; the angle is rounded to 2 decimals.

  • access.js
  • lines 44 to 50
  • CadCraft 0.8.2
/** How far and which way: "2400 to the right and 300 up" for a move; a plain distance and direction when both parts are real. */
export function sayMove(a, b, c) {
  const { fmt, t } = ctxOf(c);
  const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
  if (d < 1e-9) return t('access.move.none');
  return t('access.move', { d: fmt(d), dir: sayDirection(a, b, c), angle: deg(Math.atan2(dy, dx)), dx: fmt(Math.abs(dx)), dy: fmt(Math.abs(dy)), sx: t(dx >= 0 ? 'access.right' : 'access.left'), sy: t(dy >= 0 ? 'access.up' : 'access.down') });
}

Length along an object

64-bit

The length of a line, circle, arc or polyline that is read out with the object.

line: √(Δx² + Δy²); circle: 2πr; arc: r × ((a₁ − a₀) mod 2π); polyline: Σ |pᵢ₊₁ − pᵢ|

An arc whose normalized sweep is exactly 0 is taken as a full circle. The polyline sum adds the straight distances between vertices in order and ignores each vertex's bulge, so a polyline with arc segments is reported shorter than it is.

  • access.js
  • lines 52 to 65
  • CadCraft 0.8.2
/** The length along an object, when it has one. */
export function lengthOf(e) {
  switch (e.type) {
    case 'line': return Math.hypot(e.x2 - e.x1, e.y2 - e.y1);
    case 'circle': return 2 * Math.PI * e.r;
    case 'arc': { let s = normAngle(e.a1 - e.a0); if (s === 0) s = 2 * Math.PI; return s * e.r; }
    case 'poly': {
      let L = 0; const n = e.pts.length, m = e.closed ? n : n - 1;
      for (let i = 0; i < m; i++) { const p = e.pts[i], q = e.pts[(i + 1) % n]; L += Math.hypot(q.x - p.x, q.y - p.y); }   // bulges are counted straight
      return L;
    }
    default: return null;
  }
}

Extents of the visible objects

64-bit

The width, height and corners of the box round everything visible, said with the object counts.

w = maxX − minX; h = maxY − minY

The corners are the minimum and maximum of the boxes from geom.js entityBounds, taken with Math.min and Math.max, so the only arithmetic here is the two subtractions. A box with a corner that is not finite is left out. The results are written with the drawing's length format.

  • access.js
  • lines 88 to 107
  • CadCraft 0.8.2
/** The whole drawing: how much, of what, how big, what is chosen. */
export function describeDrawing(cx, { selection = null } = {}) {
  const c = ctxOf(cx), { doc, fmt, t } = c;
  const list = doc.all().filter((e) => doc.visible(e));
  if (!list.length) return t('access.drawing.empty');
  const byType = new Map();
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const e of list) {
    byType.set(e.type, (byType.get(e.type) || 0) + 1);
    const b = entityBounds(e, doc.blocks);
    if (b && [b.minX, b.minY, b.maxX, b.maxY].every(Number.isFinite)) { minX = Math.min(minX, b.minX); minY = Math.min(minY, b.minY); maxX = Math.max(maxX, b.maxX); maxY = Math.max(maxY, b.maxY); }
  }
  const kinds = [...byType].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 6)
    .map(([type, n]) => t('access.count', { n, kind: kindName({ type }, c) })).join(', ');
  const layers = new Set(list.map((e) => e.layer)).size;
  const parts = [t('access.drawing', { n: list.length, kinds, layers })];
  if (Number.isFinite(minX)) parts.push(t('access.extents', { w: fmt(maxX - minX), h: fmt(maxY - minY), x0: fmt(minX), y0: fmt(minY), x1: fmt(maxX), y1: fmt(maxY) }));
  if (selection && selection.size) parts.push(t('access.selected', { n: selection.size }));
  return parts.join(' ');
}

Objects within a radius

64-bit

The objects within a given distance of a point, nearest first.

Each distance comes from geom.js distToEntity and is compared with the radius without a tolerance. The list is sorted by distance and then by id, and cut to the first six by default.

  • access.js
  • lines 109 to 120
  • CadCraft 0.8.2
/** What lies within `radius` of a point, nearest first: [{ id, e, d, dir }]. */
export function nearObjects(cx, p, { radius, max = 6 } = {}) {
  const { doc } = ctxOf(cx), out = [];
  for (const e of doc.all()) {
    if (!doc.visible(e)) continue;
    const d = distToEntity(e, p, doc.blocks);
    if (!(d <= radius)) continue;
    out.push({ id: e.id, e, d });
  }
  out.sort((a, b) => a.d - b.d || a.id - b.id);
  return out.slice(0, max);
}

Distance and side of each nearby object

64-bit

For each object near the cursor: whether the cursor is on it, or how far away it is and on which side.

An object closer than 1e-6 drawing units (absolute) is said to be at the cursor. Otherwise its distance is written with the drawing's length format and the side is directionOf toward the nearest point on the object.

  • access.js
  • lines 122 to 135
  • CadCraft 0.8.2
/** "Near the cursor": the cursor's place, then each object close by with its distance and the side it is on. */
export function describeNear(cx, p, opts = {}) {
  const c = ctxOf(cx), { fmt, t } = c;
  const near = nearObjects(c, p, opts);
  const head = sayPoint(p, c);
  if (!near.length) return t('access.near.none', { at: head, r: fmt(opts.radius) });
  const items = near.map((o) => {
    const k = kindName(o.e, c);
    if (o.d < 1e-6) return t('access.near.on', { kind: k, id: o.id });
    const q = closestOn(o.e, p, c);
    return t('access.near.item', { kind: k, id: o.id, d: fmt(o.d), dir: q ? sayDirection(p, q, c) : '' });
  });
  return t('access.near', { at: head, items: items.join('; ') });
}

Measure between two points

64-bit

The length, angle, Δx and Δy said back after a command takes a point.

d = √(Δx² + Δy²); angle = atan2(Δy, Δx)

One Math.hypot and one atan2 of the coordinate differences; Δx and Δy are plain subtractions. The angle is rounded to 2 decimals and the lengths use the drawing's length format.

  • access.js
  • lines 194 to 199
  • CadCraft 0.8.2
/** The measure of a move or a pair of points: said back after a command takes a point ("length 3000, 30 degrees"). */
export function sayBetween(a, b, cx) {
  const { fmt, t } = ctxOf(cx);
  const d = Math.hypot(b.x - a.x, b.y - a.y);
  return t('access.between', { d: fmt(d), angle: deg(Math.atan2(b.y - a.y, b.x - a.x)), dx: fmt(b.x - a.x), dy: fmt(b.y - a.y) });
}

Snap points near the cursor

64-bit

The snap points within a radius of a point, nearest first, for stepping from one to the next with a key.

d = √((sx − px)² + (sy − py)²)

Two snap points of the same kind are counted as one when Math.round(x × 1000) and Math.round(y × 1000) agree, an absolute grid of 0.001 drawing units. The coordinates that are kept and the distance are not rounded.

  • access.js
  • lines 168 to 185
  • CadCraft 0.8.2
/** The snap points and grips near a point, nearest first, for stepping from one to the next with a key. [{ kind, x, y, d, id }] */
export function snapTargets(cx, p, { radius, kinds, max = 40 } = {}) {
  const { doc } = ctxOf(cx), out = [], seen = new Set();
  const ks = kinds || new Set(['end', 'mid', 'cen', 'quad', 'node', 'ins']);
  for (const e of doc.all()) {
    if (!doc.visible(e)) continue;
    const b = entityBounds(e, doc.blocks);
    if (!b || p.x < b.minX - radius || p.x > b.maxX + radius || p.y < b.minY - radius || p.y > b.maxY + radius) continue;
    for (const s of snapPoints(e, p, radius, ks, doc.blocks, null)) {
      const key = s.kind + ':' + Math.round(s.x * 1000) + ':' + Math.round(s.y * 1000);
      if (seen.has(key)) continue;
      seen.add(key);
      out.push({ ...s, id: e.id, d: Math.hypot(s.x - p.x, s.y - p.y) });
    }
  }
  out.sort((a, b) => a.d - b.d || a.id - b.id || (a.kind < b.kind ? -1 : 1));
  return out.slice(0, max);
}

In detail

Method
Line length by Math.hypot, circle 2πr, arc r × sweep, polyline as the sum of vertex-to-vertex distances; direction from atan2 rounded to the nearest 45°.
Number types
64-bit numbers throughout.
Tolerances
1e-9 drawing units, absolute: a shorter move is said as no move. 1e-6 drawing units, absolute: a nearer object is said to be at the cursor. 1e-6 and 1e-3 drawing units, absolute, when naming the object a snap point belongs to. Snap targets of one kind are counted as one point when their coordinates agree on a 0.001 grid.
Limits
A polyline with arc segments is reported by its chords (the code's own comment says bulges are counted straight). Spoken angles carry 2 decimals. The tolerances do not scale with the size of the drawing.

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