Resources Numerical accuracy Calculation policy

Size of a copied area

Works out how big a copied area of a sheet is on paper and in the real world, the pixel size it is rendered at, and the scale and turn that fit it to two picked points.

How the precision is kept

Sizes are pixel counts (whole numbers read from the PNG header) multiplied by one 64-bit factor: points per pixel × 25.4 ÷ 72 for the size on paper, or the stored millimeters per pixel for the real size. The two-point fit is a ratio of two Math.hypot lengths and a difference of two atan2 angles. Render sizes are rounded to whole pixels.

Calculations under this policy

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

areaclip.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests bin/test-cadcraft.mjs, bin/test-markupcraft.mjs

Size in millimeters

64-bit

The width and height of the copied area, on paper or at the drawing's real size.

paper: mm = pixels × ptPerPx × (25.4 ÷ 72); real: mm = pixels × mmPerPx

MM_PER_PT is 25.4 ÷ 72 evaluated once in 64-bit; 25.4 mm per inch and 72 points per inch are exact definitions, the quotient is rounded once. Each side is a whole pixel count times one factor. Without a sheet scale the real size is null rather than a guess.

  • areaclip.js
  • lines 97 to 101
  • CadCraft 0.8.2
/** The area's size in mm: on paper, or the drawing's own size when the sheet had a scale ('real'; null without one). */
export function sizeMm(clip, { use = 'real' } = {}) {
  const k = use === 'real' ? clip?.real?.mmPerPx : clip ? clip.ptPerPx * MM_PER_PT : 0;
  return k ? [clip.px[0] * k, clip.px[1] * k] : null;
}

Render size in pixels

64-bit

The pixel width and height for an area given in points at a resolution, lowered so neither side passes the cap.

d = dpi × min(1, maxPx ÷ (longer side × dpi ÷ 72)); w = round(wPt × d ÷ 72); h = round(hPt × d ÷ 72)

Each side is rounded to a whole pixel with Math.round and is at least 1. The lowered resolution is returned unrounded beside the rounded pixel counts.

  • areaclip.js
  • lines 134 to 140
  • CadCraft 0.8.2
/** The picture's size for an area of wPt × hPt points at `dpi`, lowered (dpi too) so neither side passes maxPx. */
export function pixelsFor(wPt, hPt, dpi, maxPx = AREA_MAX_PX) {
  let d = Math.max(1, dpi);
  const big = Math.max(wPt, hPt) * d / 72;
  if (big > maxPx) d *= maxPx / big;
  return [Math.max(1, Math.round(wPt * d / 72)), Math.max(1, Math.round(hPt * d / 72)), d];
}

Scale and turn from two points

64-bit

The scale, the angle and the point mapping that carry two points on the picture onto the two points where they belong.

scale = |b₂ − b₁| ÷ |a₂ − a₁|; angle = atan2(b₂ − b₁) − atan2(a₂ − a₁); map(p) = b₁ + scale · R(angle) · (p − a₁)

Lengths are by Math.hypot; a pair closer than 1e-9 (absolute) returns null. The map subtracts the first picture point before turning, so the rotation acts on differences and the first point lands exactly on b₁. The angle that is returned is wrapped to −π…π with atan2(sin, cos).

  • areaclip.js
  • lines 142 to 154
  • CadCraft 0.8.2
/**
 * Two points on the picture (a1, a2) and where they belong (b1, b2) → the scale and turn that carry one pair onto the
 * other, and `map` for any point. null when either pair is one point.
 */
export function fitTwoPoints(a1, a2, b1, b2) {
  const ax = a2[0] - a1[0], ay = a2[1] - a1[1], bx = b2[0] - b1[0], by = b2[1] - b1[1];
  const la = Math.hypot(ax, ay), lb = Math.hypot(bx, by);
  if (!(la > 1e-9) || !(lb > 1e-9)) return null;
  const scale = lb / la, angle = Math.atan2(by, bx) - Math.atan2(ay, ax);
  const c = Math.cos(angle) * scale, s = Math.sin(angle) * scale;
  const map = ([x, y]) => { const dx = x - a1[0], dy = y - a1[1]; return [b1[0] + c * dx - s * dy, b1[1] + s * dx + c * dy]; };
  return { scale, angle: Math.atan2(Math.sin(angle), Math.cos(angle)), map };
}

Box round a turned picture

64-bit

The width and height of the upright box round a picture turned by an angle.

w′ = w·|cos θ| + h·|sin θ|; h′ = w·|sin θ| + h·|cos θ|

One sine and one cosine, their absolute values, and two sums of terms that are never negative, so nothing cancels. For a quarter turn given as Math.PI ÷ 2 the cosine evaluates to about 6.1 × 10⁻¹⁷ rather than 0.

  • areaclip.js
  • lines 156 to 159
  • CadCraft 0.8.2
export function turnedSize(w, h, angle) {
  const c = Math.abs(Math.cos(angle)), s = Math.abs(Math.sin(angle));
  return [w * c + h * s, w * s + h * c];
}

Outline of an elliptical area

64-bit

The points of the ellipse that fits a box, used as the outline of a round copied area.

pᵢ = (cx + rx·cos(2π·i ÷ n), cy + ry·sin(2π·i ÷ n)), n = 96

The outline is a polygon of n points (96 unless the caller gives another number), each from one sine and one cosine of i ÷ n × 2π. The area it encloses is that polygon's, slightly inside the true ellipse.

  • areaclip.js
  • lines 103 to 108
  • CadCraft 0.8.2
export function ellipsePoints([x0, y0, x1, y1], n = 96) {
  const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2, rx = Math.abs(x1 - x0) / 2, ry = Math.abs(y1 - y0) / 2;
  const out = [];
  for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2; out.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]); }
  return out;
}

In detail

Method
Size = pixels × size of one pixel. Two-point fit: a similarity transform (scale and turn) about the first picked point.
Number types
64-bit numbers; pixel counts are whole numbers. overlayPixels in the same file works on 8-bit color values, only changes how the picture looks, and is not listed.
Tolerances
1e-9, absolute, in the units of the points given: fitTwoPoints refuses a pair of points closer than this.
Limits
pixelsFor rounds each side to a whole pixel, so a side can differ from points × dpi ÷ 72 by up to half a pixel. The ellipse outline is a 96-sided polygon.

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