Turns the lines, arcs, circles and splines of a sketch into closed polygon loops, tests whether a point is inside them, and gives a loop's area.
How the precision is kept
A profile here is a polygon: arcs and circles are replaced by straight segments (64 per full turn by default; kernel.js passes the part's resolution) and splines by max(4, ⌈segments ÷ 8⌉) segments per knot span. The vertices lie on the true curve in 64-bit, so the accuracy of anything built on the loop is set by the number of segments.
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.
An arc as a run of points from its start to its end.
n = max(1, ⌈sweep ÷ 2π × segFull − 1e-9⌉); pointᵢ = c + r × (cos(a₀ + sweep·i ÷ n), sin(…))
Every point is computed from the angle directly, not by repeated rotation, so errors do not build up along the arc. The last point is c + r·(cos, sin) of the end angle, which can differ in the last bits from the arc's stored end point.
sketch.js
lines 693 to 703
PartCraft 0.8.1
/** Points along an arc, start to end inclusive, `segFull` segments per full turn (at least 1). */
export function tessArc(sk, a, segFull = 64) {
const { c, r, a0, sweep } = arcAngles(sk, a);
const n = Math.max(1, Math.ceil(sweep / (2 * Math.PI) * segFull - 1e-9));
const pts = [];
for (let i = 0; i <= n; i++) {
const t = a0 + sweep * i / n;
pts.push([c[0] + r * Math.cos(t), c[1] + r * Math.sin(t)]);
}
return pts;
}
Points around a circle
64-bit
A circle as a closed polygon with its corners on the circle.
pointᵢ = c + r × (cos(2π·i ÷ n), sin(2π·i ÷ n))
The polygon is inscribed, so its area and perimeter are below the circle's by a factor that depends only on the number of segments.
sketch.js
lines 705 to 709
PartCraft 0.8.1
export function tessCircle(c, r, segFull = 64) {
const pts = [];
for (let i = 0; i < segFull; i++) { const t = 2 * Math.PI * i / segFull; pts.push([c[0] + r * Math.cos(t), c[1] + r * Math.sin(t)]); }
return pts;
}
Closed loops of a sketch
64-bit
Chains the sketch's curves into closed loops, or reports an open end or a branch.
ends are one node when closer than 10 × tol, tol = 1e-6 × max(1, extent)
Ends are matched by distance, with a tolerance that scales with the sketch. Loop points are copied from the curves' own points; nothing is averaged at a join, so the first point found at a node is the one kept.
sketch.js
lines 711 to 777
PartCraft 0.8.1
/**
* Closed loops from the sketch's (non-construction) lines, arcs and circles.
* → { loops: [[[x,y]…]…], curves: [{id, loop}] } or { error } when an end is left open or branches.
*/
export function profile(sk, segFull = 64) {
const tol = 1e-6 * Math.max(1, extent(sk));
const loops = [];
const curves = [];
const segs = [];
for (const e of sk.entities) {
if (e.construction) continue;
if (e.type === 'circle') {
if (!(e.r > tol)) return { error: tr('model.sketch.circle_no_size') };
loops.push(tessCircle(pointXY(sk, e.c), e.r, segFull));
curves.push({ id: e.id, loop: loops.length - 1 });
} else if (e.type === 'line') {
const a = pointXY(sk, e.p1), b = pointXY(sk, e.p2);
if (Math.hypot(b[0] - a[0], b[1] - a[1]) <= tol) return { error: tr('model.sketch.line_no_length') };
segs.push({ id: e.id, pts: [a, b] });
} else if (e.type === 'arc') {
segs.push({ id: e.id, pts: tessArc(sk, e, segFull) });
} else if (e.type === 'spline') {
let pts;
try { pts = tessSpline(sk, e, Math.max(4, Math.ceil(segFull / 8))); } catch (err) { return { error: err.message }; }
if (e.closed) { pts.pop(); loops.push(pts); curves.push({ id: e.id, loop: loops.length - 1 }); }
else segs.push({ id: e.id, pts });
}
}
// join ends by position
const nodes = [];
const nodeOf = (p) => {
for (let i = 0; i < nodes.length; i++) if (Math.hypot(nodes[i].p[0] - p[0], nodes[i].p[1] - p[1]) <= tol * 10) return i;
nodes.push({ p, ends: [] });
return nodes.length - 1;
};
for (const s of segs) {
s.n0 = nodeOf(s.pts[0]); s.n1 = nodeOf(s.pts[s.pts.length - 1]);
nodes[s.n0].ends.push(s); nodes[s.n1].ends.push(s);
}
for (const n of nodes) {
if (n.ends.length === 1) return { error: tr('model.sketch.profile_open', { x: fx(n.p[0]), y: fx(n.p[1]) }), at: n.p };
if (n.ends.length > 2) return { error: tr('model.sketch.profile_branches', { x: fx(n.p[0]), y: fx(n.p[1]) }), at: n.p };
}
const used = new Set();
for (const s0 of segs) {
if (used.has(s0)) continue;
const loop = [];
const start = s0.n0;
let s = s0, from = start;
const li = loops.length;
for (let guard = 0; guard <= segs.length; guard++) {
used.add(s);
curves.push({ id: s.id, loop: li });
const fwd = s.n0 === from;
const pts = fwd ? s.pts : [...s.pts].reverse();
for (let i = 0; i < pts.length - 1; i++) loop.push(pts[i]);
const to = fwd ? s.n1 : s.n0;
if (to === start) break;
const nx = nodes[to].ends.find((q) => q !== s);
if (!nx || used.has(nx)) break;
from = to; s = nx;
}
if (loop.length < 3) return { error: tr('model.sketch.loop_three_sides') };
loops.push(loop);
}
return { loops, curves };
}
Point inside loops
64-bit
Tests whether a point lies inside a set of loops by the even-odd rule.
The division is safe because an edge is only tested when its two ends are on different sides of the point's y. The crossing x is a rounded 64-bit value, so a point lying on an edge can be judged either way.
sketch.js
lines 781 to 791
PartCraft 0.8.1
/** Even-odd point-in-region test against a set of loops. */
export function insideLoops(loops, p) {
let inside = false;
for (const loop of loops) {
for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
const a = loop[i], b = loop[j];
if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]) inside = !inside;
}
}
return inside;
}
Area of a loop
64-bit
The signed area of a polygon loop.
area = ½ × Σ (xⱼ·yᵢ − xᵢ·yⱼ)
The shoelace sum, added in order with no compensation and taken about the sketch origin, so for a small loop far from the origin the products are large and mostly cancel.
sketch.js
lines 793 to 797
PartCraft 0.8.1
export function loopArea(loop) {
let s = 0;
for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) s += (loop[j][0] * loop[i][1] - loop[i][0] * loop[j][1]);
return s / 2;
}
In detail
Method
Uniform angle steps on arcs and circles; ends joined by position; even-odd ray test; shoelace area.
Number types
64-bit numbers in plain arrays.
Tolerances
tol = 1e-6 × max(1, extent): a circle's radius and a line's length must exceed it. Ends within 10 × tol are joined into one node.
Limits
A circle drawn as 64 segments has an area about 0.16% under the true circle (the factor is sin(x) ÷ x with x = 2π ÷ 64).
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.