Resources Numerical accuracy Calculation policy

Distance, area, angle and point inquiries

Works out what DIST, AREA, MEASUREGEOM, ID and LIST print: distances, Δx and Δy, angles, areas, perimeters, running area totals and coordinates; and the unit factor EXPORTPOINTS applies to the points it writes.

How the precision is kept

Each result is computed in 64-bit from the picked points or the stored object: Math.hypot for distances, atan2 for angles, geom.js polygonMeasure and entityArea for areas. The numbers are rounded only when they are written, by the drawing's length and area formats or, for plain angles and ratios, to 4 decimals.

Calculations under this policy

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

commands.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-cadcraft.mjs

Distance, Δx, Δy and angle between two points

64-bit

What DIST prints for two picked points.

d = √(Δx² + Δy²); angle = atan2(Δy, Δx), normalized to 0…360°

One Math.hypot, two subtractions and one atan2 on the picked points. Δx and Δy are along the world axes. The angle is handed to units.js tAngle with the drawing's angle format.

  • commands.js
  • lines 3707 to 3711
  • CadCraft 0.8.2
def('DIST', ['DI'], async (c) => {
  const a = await c.point(t('cmd.mea.p1'));
  c.preview((cur) => [{ type: 'line', x1: a.x, y1: a.y, x2: cur.x, y2: cur.y }]);
  const b = await c.point(t('cmd.mea.p2'), { base: a });
  c.print(tAngle('cmd.mea.dist', { d: fmtLen(c, dist(a, b)), dx: fmtLen(c, b.x - a.x), dy: fmtLen(c, b.y - a.y) }, c.doc.meta, deg(normAngle(angleOf(a, b)))));

Area and perimeter of picked points

64-bit

The area and perimeter of the outline through points picked one by one.

area = |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)| ÷ 2; perimeter = Σ |pᵢ₊₁ − pᵢ|

The sums are geom.js polygonMeasure on the picked points as stored. It multiplies absolute coordinates, so far from the origin the products are large and nearly cancel and the area keeps fewer digits than the coordinates. The absolute value is taken, so the picking direction does not matter; a point within 1e-12 of the previous one is skipped.

  • commands.js
  • lines 3720 to 3735
  • CadCraft 0.8.2
/** Area and perimeter of points picked one by one (Enter closes the outline). → { area, perimeter } | null */
async function areaByPoints(c, first) {
  const pts = [first];
  for (;;) {
    c.preview((cur) => [{ type: 'poly', closed: true, pts: [...pts, cur].map((p) => ({ x: p.x, y: p.y, b: 0 })) }]);
    const p = await c.point(t('cmd.area.next'), { base: pts[pts.length - 1], none: true, options: pts.length > 1 ? [opt('U', t('opt.undo'))] : [] });
    if (!p) break;
    if (p.option === 'U') { pts.pop(); continue; }
    if (dist(p, pts[pts.length - 1]) > 1e-12) pts.push({ x: p.x, y: p.y });
  }
  c.preview(null);
  if (pts.length < 3) { c.print(t('cmd.area.few')); return null; }
  const m = polygonMeasure(pts);
  c.print(t('cmd.area.result', { area: fmtArea(c, Math.abs(m.area)), perimeter: fmtLen(c, m.perimeter) }));
  return { area: Math.abs(m.area), perimeter: m.perimeter };
}

Area and perimeter of an object

64-bit

What AREA prints for a picked circle, polyline, ellipse or hatch.

The area and perimeter are geom.js entityArea for the object, written with the drawing's area and length formats. An object with no area prints a message and adds nothing to a running total.

  • commands.js
  • lines 3737 to 3744
  • CadCraft 0.8.2
/** One object's area, printed. → number | null */
function areaOfObject(c, e) {
  const m = entityArea(e);
  if (!m) { c.print(t('cmd.area.noarea')); return null; }
  const key = m.kind === 'circle' ? 'cmd.area.result.circle' : m.kind === 'open' ? 'cmd.area.result.open' : 'cmd.area.result';
  c.print(t(key, { area: fmtArea(c, m.area), perimeter: fmtLen(c, m.perimeter) }));
  return m.area;
}

Running area total

64-bit

The total AREA keeps while areas are added and subtracted.

total = Σ ± areaᵢ

A plain running sum in 64-bit: each area is added or subtracted in the order picked. Subtracting an area nearly equal to the total leaves a small difference with fewer correct digits. The total is written with the drawing's area format.

  • commands.js
  • lines 3747 to 3748
  • CadCraft 0.8.2
let mode = null, total = 0;   // null, 'add' or 'sub' (a running total)
const tally = (a) => { if (a === null || !mode) return; total += mode === 'add' ? a : -a; c.print(t('cmd.area.total', { area: fmtArea(c, total) })); };

Angle at a vertex, of an arc, or between two lines

64-bit

The angle MEASUREGEOM prints.

angle = |atan2(b − v) − atan2(a − v)|, folded to 0…180°; arc: its sweep, with 0 read as 360°

The angle is the difference of two atan2 results, so a very small angle carries the absolute rounding of both, a few 10⁻¹⁶ radians. For two lines the vertex is their intersection from geom.js intersect and each leg points to the line's end farther from it. The result is printed in degrees with at most 4 decimals.

  • commands.js
  • lines 3799 to 3814
  • CadCraft 0.8.2
  ang = Math.abs(normAngle(angleOf(v, b) - angleOf(v, a))); if (ang > Math.PI) ang = TAU - ang;
} else {
  const e = c.doc.get(pick.id);
  if (e.type === 'arc') ang = normAngle(e.a1 - e.a0) || TAU;
  else if (e.type === 'line') {
    const q = await c.entity(t('cmd.mea.line2'));
    const f = c.doc.get(q.id);
    if (f.type !== 'line') { c.print(t('cmd.mea.line2.only')); continue; }
    // between the two lines, each toward its end farther from where they meet
    const x = intersect({ k: 'L', ...e }, { k: 'L', ...f }, true, true)[0];
    if (!x) { c.print(t('cmd.chamfer.err.parallel')); continue; }
    const far = (l) => (dist(x, { x: l.x1, y: l.y1 }) > dist(x, { x: l.x2, y: l.y2 }) ? { x: l.x1, y: l.y1 } : { x: l.x2, y: l.y2 });
    ang = Math.abs(normAngle(angleOf(x, far(f)) - angleOf(x, far(e)))); if (ang > Math.PI) ang = TAU - ang;
  } else { c.print(t('cmd.mea.angle.only')); continue; }
}
c.print(t('cmd.mea.angle', { a: fmt(deg(ang)) }));

Coordinates of a point

64-bit

What ID prints for a picked point.

The picked point is converted to the user coordinate system by ucs.js toUcs and written with the drawing's length format. With no user coordinate system the stored coordinates are printed as they are.

  • commands.js
  • lines 3823 to 3826
  • CadCraft 0.8.2
def('ID', [], async (c) => {
  const p = await c.point(t('cmd.id.specify_point'));
  const q = toUcs(activeUcs(c.doc), p);   // in the UCS (DR.8)
  c.print(t('cmd.id.result', { x: fmtLen(c, q.x), y: fmtLen(c, q.y) }));

One object in a line of text

64-bit

The description LIST prints and assistants read for an object: its points, length, radius and angles.

line length = √(Δx² + Δy²); angles in degrees = radians × 180 ÷ π

Lengths and coordinates are written in the drawing's length format, or with at most 4 decimals when no units are passed. Arc angles, an ellipse's ratio, a viewport's scale and a construction line's angle are written with at most 4 decimals. A polyline is described by its number of vertices only.

  • commands.js
  • lines 3847 to 3866
  • CadCraft 0.8.2
export function describe(e, units = null) {
  const L = units ? (n) => formatLength(n, units) : fmt;   // lengths in the drawing's format; angles, ratios and scales stay plain
  const P = (p) => L(p.x) + ', ' + L(p.y);
  switch (e.type) {
    case 'line': return t('cmd.desc.line', { from: P({ x: e.x1, y: e.y1 }), to: P({ x: e.x2, y: e.y2 }), length: L(Math.hypot(e.x2 - e.x1, e.y2 - e.y1)) });
    case 'circle': return t('cmd.desc.circle', { center: P({ x: e.cx, y: e.cy }), r: L(e.r) });
    case 'arc': return t('cmd.desc.arc', { center: P({ x: e.cx, y: e.cy }), r: L(e.r), a0: fmt(deg(e.a0)), a1: fmt(deg(e.a1)) });
    case 'poly': return t(e.closed ? 'cmd.desc.poly.closed' : 'cmd.desc.poly', { n: e.pts.length });
    case 'text': return t('cmd.desc.text', { s: e.s, at: P(e), h: L(e.h) });
    case 'insert': { const na = e.attrs ? Object.keys(e.attrs).length : 0; return na ? t('cmd.desc.insert.attrs', { block: e.block, at: P(e), n: na }) : t('cmd.desc.insert', { block: e.block, at: P(e) }); }
    case 'point': return P(e);
    case 'ellipse': return t('cmd.desc.ellipse', { center: P({ x: e.cx, y: e.cy }), ratio: fmt(e.ratio) });
    case 'hatch': return t('cmd.desc.hatch', { n: e.loops.length });
    case 'dim': { const kind = (DIM_KINDS[e.kind] || t('cmd.desc.dim.imported')).toLowerCase(); return !e.kind ? t('cmd.desc.dim.block', { kind, block: e.block }) : e.text ? t('cmd.desc.dim.text', { kind, value: formatMeasure(e), text: e.text }) : t('cmd.desc.dim', { kind, value: formatMeasure(e) }); }
    case 'viewport': return t('cmd.desc.viewport', { w: L(e.w), h: L(e.h), at: P(e), view: P({ x: e.cx, y: e.cy }), s: fmt(e.s) });
    case 'xline': case 'ray': return t('cmd.desc.' + e.type, { at: P(e), angle: fmt(deg(normAngle(Math.atan2(e.dy, e.dx)))) });
    case 'mleader': return t('cmd.mld.describe', { s: oneLine(e.s || ''), at: P(e.pts[0]), n: e.pts.length });
    default: return '';
  }
}

Units of exported points

64-bit

The factor between drawing units and the units of the point file EXPORTPOINTS writes.

factor = mm per unit ÷ 1000 (meters), ÷ 304.8 (feet), or 1 (drawing units)

The factor is ptexport.js UNIT_FACTOR of the drawing's millimeters per unit: one division by 1000 or by 304.8, both exact definitions. Meters and feet are offered only when the drawing's unit is known. The points and the text of the file are made by ptexport.js collectPoints and formatPoints, which are not in this file.

  • commands.js
  • lines 1196 to 1199
  • CadCraft 0.8.2
const per = mmPerDrawingUnit(doc.meta.lunits, doc.meta.units);
const unitOpts = [opt('D', 'Drawing'), ...(per ? [opt('M', 'Metres'), opt('F', 'Feet')] : [])];
const un = kw(await c.keyword(t(per ? 'cmd.exportpoints.units' : 'cmd.exportpoints.units.none'), { options: unitOpts, default: 'D' }), 'D');
const factor = UNIT_FACTOR(per, un === 'M' ? 'metres' : un === 'F' ? 'feet' : 'drawing') ?? 1;

In detail

Method
Distance by Math.hypot; angle between legs as a difference of two atan2 results; area of picked points by the shoelace sum in geom.js polygonMeasure.
Number types
64-bit numbers; printed text is rounded by the drawing's formats.
Tolerances
1e-12 drawing units, absolute: a point picked on top of the last one is not added to an area outline.
Limits
Plain angles and ratios in these messages are printed with at most 4 decimals. The shoelace sum uses absolute coordinates, so far from the origin an area keeps fewer digits than the coordinates do. AREA's running total adds and subtracts in the order picked.

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