Closed loops with their curves
64-bitCollects a sketch's curves into closed loops, keeping each curve's exact definition.
tol = 1e-6 × max(1, extent); ends within 10 × tol share a node; arc mid = c + r (cos, sin)(a₀ + sweep ÷ 2)
Each segment's ends are replaced by the shared node's coordinates, which are those of the first end point met there. The 3-decimal rounding in this function is only in error messages.
/**
* Closed loops of a sketch's non-construction lines, arcs and circles, keeping each curve:
* → { loops: [{ segs: [{ id, type, a, b, mid?, c?, r? }], poly }] } or { error }.
*/
export function profileCurves(sk) {
const tol = 1e-6 * Math.max(1, extent(sk));
const loops = [];
const segs = [];
for (const e of sk.entities) {
if (e.construction) continue;
if (e.type === 'circle') {
if (!(e.r > tol)) return { error: tr('model.brep.circle_no_size') };
const c = pointXY(sk, e.c);
loops.push({ segs: [{ id: e.id, type: 'circle', c, r: e.r }], poly: tessCircle(c, e.r, 128) });
} 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.brep.line_no_length') };
segs.push({ id: e.id, type: 'line', a, b, pts: [a, b] });
} else if (e.type === 'arc') {
const { c, r, a0, sweep } = arcAngles(sk, e);
if (!(r > tol)) return { error: tr('model.brep.arc_no_size') };
const pts = tessArc(sk, e, 128);
segs.push({ id: e.id, type: 'arc', a: pts[0], b: pts[pts.length - 1], c, r, mid: [c[0] + r * Math.cos(a0 + sweep / 2), c[1] + r * Math.sin(a0 + sweep / 2)], pts });
} else if (e.type === 'spline') {
// exact B-spline (sketch-spline.js); the polyline only decides nesting
let bs;
try { bs = splineCurve(sk, e); } catch (err) { return { error: err.message }; }
const pts = SP.tessellate(bs, 16);
if (e.closed) { pts.pop(); loops.push({ segs: [{ id: e.id, type: 'spline', bs, closed: true }], poly: pts }); }
else segs.push({ id: e.id, type: 'spline', a: pts[0], b: pts[pts.length - 1], bs, pts });
}
}
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.a); s.n1 = nodeOf(s.b);
nodes[s.n0].ends.push(s); nodes[s.n1].ends.push(s);
}
const fx = (v) => String(+v.toFixed(3));
for (const n of nodes) {
if (n.ends.length === 1) return { error: tr('model.brep.profile_open', { x: fx(n.p[0]), y: fx(n.p[1]) }), at: n.p };
if (n.ends.length > 2) return { error: tr('model.brep.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 out = [], poly = [];
const start = s0.n0;
let s = s0, from = start;
for (let guard = 0; guard <= segs.length; guard++) {
used.add(s);
const fwd = s.n0 === from;
const to = fwd ? s.n1 : s.n0;
// both ends sit exactly on the shared node, so neighbouring edges meet at one vertex
out.push({ id: s.id, type: s.type, a: nodes[from].p, b: nodes[to].p, mid: s.mid, c: s.c, r: s.r, bs: s.bs && (fwd ? s.bs : SP.reverse(s.bs)) });
const pts = fwd ? s.pts : [...s.pts].reverse();
for (let i = 0; i < pts.length - 1; i++) poly.push(pts[i]);
if (to === start) break;
const nx = nodes[to].ends.find((q) => q !== s);
if (!nx || used.has(nx)) break;
from = to; s = nx;
}
if (poly.length < 3) return { error: tr('model.brep.loop_too_few') };
loops.push({ segs: out, poly });
}
return { loops };
}