Works out the arc markup's circle from its three points, its length, and the angle between two legs shown by the angle measurement.
How the precision is kept
An arc markup stores its three picked points; the center, radius, start angle and sweep are rebuilt from them in 64-bit each time. The length is r × |sweep|. The angle is the acos of a clamped, normalized dot product. The drawn arc is a chain of cubic Béziers, which approximates the circle; the length does not use it.
Open item. Compute the circle center from coordinates relative to the first point instead of from absolute x² + y².
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.
The center, radius, start angle and signed sweep of the arc from the first point through the second to the third.
d = 2·(aₓ(m_y − b_y) + mₓ(b_y − a_y) + bₓ(a_y − m_y)); cₓ = Σ (x² + y²)·Δy ÷ d; r = hypot(a − c)
The center comes from sums of (x² + y²) × a coordinate difference using absolute page coordinates, so for three close points far from the origin the terms are large and nearly cancel. The straight-line test |d| < 1e-9 × span² is scaled to the points' spread; the radius cap of 1e7 points is absolute. The radius is measured from the first point only.
The arc as cubic Bézier segments of at most 90° each, for drawing and for the saved appearance.
n = ⌈|sweep| ÷ (π ÷ 2) − 1e-9⌉; k = (4 ÷ 3)·tan(step ÷ 4)
The end points of each piece are computed from the center, radius and angle, so they lie on the circle to 64-bit rounding; between them a cubic Bézier approximates the arc and does not equal it. The 1e-9 keeps an exact multiple of 90° from gaining an extra piece. Used for the picture only.
arcs.js
lines 32 to 44
MarkupCraft 0.8.1
export function arcBeziers({ c, r, a0, sweep }) {
const n = Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 2) - 1e-9));
const step = sweep / n;
const k = (4 / 3) * Math.tan(step / 4);
const out = [];
for (let i = 0; i < n; i++) {
const t0 = a0 + i * step, t1 = t0 + step;
const p0 = [c[0] + r * Math.cos(t0), c[1] + r * Math.sin(t0)];
const p3 = [c[0] + r * Math.cos(t1), c[1] + r * Math.sin(t1)];
out.push([p0, [p0[0] - k * r * Math.sin(t0), p0[1] + k * r * Math.cos(t0)], [p3[0] + k * r * Math.sin(t1), p3[1] - k * r * Math.cos(t1)], p3]);
}
return out;
}
Arc length
64-bit
The length of an arc markup in points.
length = r × |sweep|
One 64-bit multiplication of the radius and sweep from arcThrough; it inherits their precision. Returns 0 when the three points are in a line and there is no arc.
The angle at a vertex between the directions to two other points, from 0° to 180°.
θ = acos((u·w) ÷ (|u|·|w|)) × 180 ÷ π
The cosine is clamped to [−1, 1] before acos, so rounding cannot produce NaN. Near 0° and 180° the cosine changes very little with the angle, so acos returns fewer good digits there than an atan2 of cross and dot products would. A zero-length leg gives 0.
arcs.js
lines 48 to 54
MarkupCraft 0.8.1
export function angleAt(a, v, b) {
const u = [a[0] - v[0], a[1] - v[1]], w = [b[0] - v[0], b[1] - v[1]];
const lu = Math.hypot(...u), lw = Math.hypot(...w);
if (!lu || !lw) return 0;
const cos = Math.max(-1, Math.min(1, (u[0] * w[0] + u[1] * w[1]) / (lu * lw)));
return (Math.acos(cos) * 180) / Math.PI;
}
Angle as text
64-bit
Writes the angle with a degree sign.
round(deg × 10ᵖ) ÷ 10ᵖ, p = 1 by default
Rounds to the given number of places, 1 when none is given, then prints with toFixed. This is the text on the sheet and in the Properties panel; the three points are stored unrounded.
Circumcenter from the determinant form with x² + y²; start, middle and end angles by atan2; the sweep runs counterclockwise when the middle point comes before the end point going counterclockwise. Bézier pieces of at most 90° with control length (4 ÷ 3)·tan(step ÷ 4). Angle = acos(u·w ÷ (|u|·|w|)).
Number types
64-bit numbers throughout. Angles in radians inside, degrees for the angle label.
Tolerances
Three points count as a straight line when |d| < 1e-9 × span², where span is the longer of the two legs from the first point (at least 1e-9); also when the radius is 1e7 points or more (absolute). The piece count subtracts 1e-9 before rounding up. The angle label rounds to 1 decimal place.
Limits
The circumcenter uses absolute page coordinates squared, so three close points far from the page origin lose digits. acos loses digits for angles very near 0° and 180°.
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.