Builds what each markup draws, on screen and in the saved PDF: its outline as lines and Bézier curves, and the label text of a length, area, angle or count.
How the precision is kept
One display list per markup is computed in 64-bit from the stored points and serves both the screen and the PDF appearance. Label values are computed from the stored points and the sheet's calibration each time the list is built. Curved outlines (ellipses, clouds, arcs) are cubic Béziers, which approximate the true curve; no measurement is taken from them.
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.
The dimension line, its end ticks and the length text of a measure markup.
len = hypot(bₓ − aₓ, b_y − a_y); label = len × mmPerPt, formatted
The length is one Math.hypot of the two stored end points, recomputed whenever the list is built; the label is not stored with the markup. On a sheet with no calibration the label is the text kept with the markup, or a no-scale notice.
shapes.js
lines 366 to 376
MarkupCraft 0.8.1
case 'measure': {
if (pts.length < 2) break;
const [a, b] = pts;
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
const ux = len ? (b[0] - a[0]) / len : 1, uy = len ? (b[1] - a[1]) / len : 0;
const tk = Math.max(4, s.width * 3 + 3);
const d = [['M', a[0], a[1]], ['L', b[0], b[1]]];
for (const p of [a, b]) d.push(['M', p[0] - uy * tk, p[1] + ux * tk], ['L', p[0] + uy * tk, p[1] - ux * tk]);
for (const p of [a, b]) { const dx = (ux - uy) * tk * 0.7, dy = (uy + ux) * tk * 0.7; d.push(['M', p[0] - dx, p[1] - dy], ['L', p[0] + dx, p[1] + dy]); }
out.push(stroke(d, s, { cap: 'butt' }));
const label = env.cal ? measure(len, env.cal) : (m.text || t('status.scale.none'));
Area measurement label
64-bit
The area and perimeter text of an area markup, placed at the outline's centroid.
The two values come from measureArea in units.js with the markup's cut-outs. The label is anchored at the centroid of the outer outline only; cut-outs do not move it.
The angle text of an angle markup and where it sits.
label = angle at the vertex, 1 decimal; position = vertex + (r + 1.2 × font size) along the bisector
The text is formatAngle(angleAt(...)) with the default of 1 decimal place. The position uses cos and sin of the indicator arc's middle angle and affects placement only.
Shoelace sums in absolute page coordinates. When |Σ k| < 1e-9 (absolute, in points²) the vertex average is returned instead. The result positions a label; it is not shown as a number.
shapes.js
lines 404 to 414
MarkupCraft 0.8.1
/** Area centroid of a closed outline (falls back to the vertex average for degenerate ones). */
export function centroid(pts) {
let a = 0, cx = 0, cy = 0;
for (let i = 0; i < pts.length; i++) {
const p = pts[i], q = pts[(i + 1) % pts.length];
const k = p[0] * q[1] - q[0] * p[1];
a += k; cx += (p[0] + q[0]) * k; cy += (p[1] + q[1]) * k;
}
if (Math.abs(a) < 1e-9) return [pts.reduce((t, p) => t + p[0], 0) / pts.length, pts.reduce((t, p) => t + p[1], 0) / pts.length];
return [cx / (3 * a), cy / (3 * a)];
}
Circle-to-Bézier constant
64-bit
The control-point factor for drawing a quarter circle as one cubic Bézier.
K = 4·(√2 − 1) ÷ 3 ≈ 0.5522847498
Written as a ten-digit literal; the 64-bit value of 4·(√2 − 1) ÷ 3 is 0.5522847498307936. It shapes the drawn curve only, and the Bézier it defines is itself an approximation of the circle.
shapes.js
line 21
MarkupCraft 0.8.1
const K = 0.5522847498; // circle → cubic Bézier
Ellipse outline
64-bit
An ellipse markup's outline as four cubic Bézier curves inside its box.
c = (p₀ + p₁) ÷ 2; r = (p₁ − p₀) ÷ 2; control offset = r × K
The four end points sit at the box's edge midpoints, computed as center ± radius. The curve between them is the standard Bézier approximation; the markup stores its box, not this curve.
Each edge is divided into n equal scallops, so they always end exactly at the corners. The sign of the outline's shoelace area decides which side is outward. Points closer than 1e-6 points (absolute) to the one before are dropped first. The factors 0.6667 and 0.1 set the scallop's look.
shapes.js
lines 86 to 113
MarkupCraft 0.8.1
/**
* A revision cloud round a closed outline: each edge split into scallops of about `arc` points,
* each a bump bulging outward (one cubic Bézier, close to a half circle).
*/
export function cloudPath(pts, arc) {
const ring = pts.filter((p, i) => i === 0 || Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]) > 1e-6);
if (ring.length > 2 && Math.hypot(ring[0][0] - ring.at(-1)[0], ring[0][1] - ring.at(-1)[1]) < 1e-6) ring.pop();
if (ring.length < 2) return [];
const ccw = area(ring) >= 0;
const d = [['M', ring[0][0], ring[0][1]]];
for (let i = 0; i < ring.length; i++) {
const a = ring[i], b = ring[(i + 1) % ring.length];
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
if (len < 1e-6) continue;
const n = Math.max(1, Math.round(len / arc));
const ux = (b[0] - a[0]) / len, uy = (b[1] - a[1]) / len;
// outward normal: right of the edge for a CCW ring, left for CW
const nx = ccw ? uy : -uy, ny = ccw ? -ux : ux;
const step = len / n, h = step * 0.6667;
for (let j = 0; j < n; j++) {
const sx = a[0] + ux * step * j, sy = a[1] + uy * step * j;
const ex = sx + ux * step, ey = sy + uy * step;
d.push(['C', sx + nx * h - ux * step * 0.1, sy + ny * h - uy * step * 0.1, ex + nx * h + ux * step * 0.1, ey + ny * h + uy * step * 0.1, ex, ey]);
}
}
d.push(['Z']);
return d;
}
Signed area of an outline
64-bit
The signed shoelace area used to tell which way an outline runs.
A = Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ) ÷ 2
Only the sign is used (cloudPath tests A ≥ 0). The sum uses absolute coordinates; for an outline with almost no area the sign can be decided by rounding.
shapes.js
lines 79 to 84
MarkupCraft 0.8.1
/** Signed area (y up: > 0 = counter-clockwise). */
export function area(pts) {
let a = 0;
for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; a += p[0] * q[1] - q[0] * p[1]; }
return a / 2;
}
In detail
Method
Ellipse: four cubic Béziers with control factor K. Cloud: each edge split into equal scallops, each one cubic Bézier bulging outward. Label anchor of an area: the area centroid by shoelace sums. Length label: hypot of the two end points × mmPerPt.
Number types
64-bit numbers throughout. The display list holds plain arrays of numbers.
Tolerances
Cloud outline: points closer than 1e-6 points to the one before are dropped. Centroid: falls back to the vertex average when |Σ cross products| < 1e-9 points². Both are absolute.
Limits
K is written as 0.5522847498, ten digits of 4·(√2 − 1) ÷ 3. A cubic Bézier is not an exact circle or ellipse.
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.