Reads the connections of a schematic, places modules along mounting rails, measures harness segments and records the wire count and bundle diameter of each segment.
How the precision is kept
Rail, duct and harness lengths are Math.hypot of drawing coordinates divided by k, the drawing units per millimeter, in 64-bit numbers. Connections are decided with an absolute tolerance of 0.05 drawing units on squared distances. The bundle diameter kept on a harness segment is rounded to 0.1 mm, and the segment length drawn beside it is rounded to a whole millimeter.
Open item. Store the unrounded bundle diameter on the harness segment and round it only in the label.
Calculations under this policy
17 calculations in 4 source files. 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 matrix that takes a symbol's pin offsets to the drawing.
a = cos r × sx; b = sin r × sx; c = −sin r × sy; d = cos r × sy; e = x − (a bₓ + c b_y); f = y − (b bₓ + d b_y)
The same expressions as insertM in the Electrical add-on's symbols.js, with the cosine and sine computed once.
addons/harness/lib/schem.js
lines 45 to 50
CadCraft 0.8.2
function matrix(e, blk) {
const base = blk?.base || { x: 0, y: 0 };
const sx = e.sx ?? 1, sy = e.sy ?? 1, r = e.rot || 0, co = Math.cos(r), si = Math.sin(r);
const a = co * sx, b = si * sx, c = -si * sy, d = co * sy;
return [a, b, c, d, e.x - (a * base.x + c * base.y), e.y - (b * base.x + d * base.y)];
}
Squared distance from a point to a wire segment
64-bit
How far a wire end or pin is from a wire segment.
u = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); d² = |p − (a + u (b − a))|²
Returns the squared distance; a zero-length segment is guarded.
addons/harness/lib/schem.js
lines 63 to 67
CadCraft 0.8.2
function onSeg(p, a, b) {
const dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy;
const u = l2 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2)) : 0;
return d2(p, { x: a.x + u * dx, y: a.y + u * dy });
}
Which wires and pins are connected
64-bit
Joins wire ends to other ends, to pins and to wires they touch, to find the nets.
connected when distance² ≤ tol², tol = 0.05
Every test compares a squared distance with tol² in 64-bit. The tolerance is absolute, in drawing units, and the decision is not an exact predicate.
addons/harness/lib/schem.js
lines 111 to 135
CadCraft 0.8.2
const tol2 = tol * tol;
for (const [, s] of bySpace) {
const ends = new Cells(Math.max(tol * 20, 1)), pins = new Cells(Math.max(tol * 20, 1)), segs = new Cells(5);
for (const w of s.wires) {
dsu.find('w:' + w.uid);
for (const [i, p] of [[0, w.pts[0]], [1, w.pts[w.pts.length - 1]]]) ends.add(p.x, p.y, p.x, p.y, { w, i, p });
for (let k = 0; k + 1 < w.pts.length; k++) { const a = w.pts[k], b = w.pts[k + 1]; segs.add(Math.min(a.x, b.x) - tol, Math.min(a.y, b.y) - tol, Math.max(a.x, b.x) + tol, Math.max(a.y, b.y) + tol, { w, k, a, b, last: w.pts.length - 2 }); }
}
for (const d of s.devices) for (const p of d.pins) pins.add(p.x, p.y, p.x, p.y, { d, p });
for (const w of s.wires) {
for (const [i, p] of [[0, w.pts[0]], [1, w.pts[w.pts.length - 1]]]) {
for (const o of ends.at(p, tol)) if ((o.w !== w || o.i !== i) && d2(p, o.p) <= tol2) dsu.union('w:' + w.uid, 'w:' + o.w.uid);
for (const { d, p: pin } of pins.at(p, tol)) if (d2(p, pin) <= tol2) dsu.union('w:' + w.uid, pk(d, pin));
for (const o of segs.at(p)) {
if (onSeg(p, o.a, o.b) > tol2) continue;
if ((o.k === 0 && d2(p, o.a) <= tol2) || (o.k === o.last && d2(p, o.b) <= tol2)) continue; // that is an end, handled above
if (o.w === w && ((i === 0 && o.k === 0) || (i === 1 && o.k === o.last))) continue;
dsu.union('w:' + w.uid, 'w:' + o.w.uid);
}
}
}
for (const d of s.devices) for (const pin of d.pins) for (const o of segs.at(pin)) { // a wire passing through a pin
if (onSeg(pin, o.a, o.b) <= tol2) dsu.union('w:' + o.w.uid, pk(d, pin));
}
}
The distance along a rail and off its axis of a drawing point, in millimeters.
s = (Δx cos r + Δy sin r) ÷ k; off = (−Δx sin r + Δy cos r) ÷ k
A rotation with Math.cos and Math.sin of the rail's stored angle, then a division by k. Nothing is rounded.
addons/harness/lib/cabinet.js
lines 101 to 104
CadCraft 0.8.2
export function toRail(r, p) {
const c = Math.cos(r.rot), s = Math.sin(r.rot), dx = p.x - r.x, dy = p.y - r.y;
return { s: (dx * c + dy * s) / r.k, off: (-dx * s + dy * c) / r.k };
}
A rail position back in the drawing
64-bit
The drawing point at a distance along a rail.
x = x₀ + (s cos r − off sin r) × k; y = y₀ + (s sin r + off cos r) × k
The inverse of toRail with the cosine and sine computed again; a point taken to the rail and back can differ in its last digits.
addons/harness/lib/cabinet.js
lines 105 to 108
CadCraft 0.8.2
export function fromRail(r, s, off = 0) {
const c = Math.cos(r.rot), si = Math.sin(r.rot);
return { x: r.x + (s * c - off * si) * r.k, y: r.y + (s * si + off * c) * r.k };
}
Modules on a rail
64-bit
Which modules sit on a rail, and the stretch of rail each covers.
s₀ = s − w ÷ 2; s₁ = s + w ÷ 2
A module counts as on the rail when its center is within half the rail's width plus 5 mm of the axis and within 5 mm of its ends.
addons/harness/lib/cabinet.js
lines 110 to 118
CadCraft 0.8.2
/** mods = [{ id, e, d (clean module) }] → those on the rail (centre within the rail's width + tol), with s0 / s1 in mm along it, in order. */
export function onRail(r, mods, tol = 5) {
const out = [];
for (const m of mods) {
const p = toRail(r, { x: m.e.x, y: m.e.y });
if (Math.abs(p.off) <= r.w / 2 + tol && p.s > -tol && p.s < r.len + tol) out.push({ ...m, s: p.s, s0: p.s - m.d.w / 2, s1: p.s + m.d.w / 2 });
}
return out.sort((a, b) => a.s - b.s);
}
Modules past the end or overlapping
64-bit
Flags a module that hangs past the rail or overlaps the next one.
past when s₀ < −0.01 or s₁ > len + 0.01; overlap when next s₀ < s₁ − 0.01
The 0.01 mm allowance is absolute. Modules placed flush by running sums of widths can differ by a rounding, which the allowance covers.
addons/harness/lib/cabinet.js
lines 120 to 128
CadCraft 0.8.2
export function railChecks(r, mods) {
const out = [];
for (let i = 0; i < mods.length; i++) {
const m = mods[i];
if (m.s0 < -0.01 || m.s1 > r.len + 0.01) out.push({ code: 'past', ids: [m.id], vars: { tag: m.d.tag, len: r.len } });
for (let j = i + 1; j < mods.length && mods[j].s0 < m.s1 - 0.01; j++) out.push({ code: 'overlap', ids: [m.id, mods[j].id], vars: { a: m.d.tag, b: mods[j].d.tag } });
}
return out;
}
Free length on a rail
64-bit
The rail length not taken by modules.
free = max(0, len − Σ module widths)
A plain sum of widths subtracted from the length; overlapping modules are counted in full.
addons/harness/lib/cabinet.js
line 129
CadCraft 0.8.2
export const freeLength = (r, mods) => Math.max(0, r.len - mods.reduce((s, m) => s + m.d.w, 0));
Where a new module sits
64-bit
The center of a new module on a rail: inside the rail, and flush to a neighbor when close.
c = clamp(s, w ÷ 2, len − w ÷ 2); c = neighbor edge ± w ÷ 2 when within 3 mm
The flush position is the neighbor's edge plus half the width, so a row of modules is a chain of sums and each inherits the rounding of the one before.
addons/harness/lib/cabinet.js
lines 131 to 138
CadCraft 0.8.2
/** Where a module of width w whose centre is wanted at s may sit: inside the rail, and flush to a neighbour's edge when within `snap` mm. */
export function snapOnRail(r, mods, w, s, snap = 3) {
let c = Math.max(w / 2, Math.min(r.len - w / 2, s));
for (const m of mods) {
for (const edge of [m.s1 + w / 2, m.s0 - w / 2]) if (Math.abs(c - edge) <= snap) c = edge;
}
return Math.max(w / 2, Math.min(r.len - w / 2, c));
}
25.4 is exact; the two reciprocals each round once, so k is within a rounding or two of the true ratio.
addons/harness/main.js
line 38
CadCraft 0.8.2
const unitsOf = (doc) => { const u = ruleUnitOf(doc), per = perUnit(doc, u); return { u, k: (1 / per) * (u === 'in' ? 1 / 25.4 : 1) }; }; // k = drawing units per millimetre
A harness moved, turned or scaled
64-bit
Applies a move, rotation or scale to a harness: its nodes and its scale.
k′ = k × √(m₀² + m₁²); node′ = M · node
The scale is the length of the matrix's first column from Math.hypot; a different scale along the other axis is not seen. After a rotation the hypot of a cosine and sine can differ from 1 in its last digit, which then multiplies k.
Places every remaining module along a rail, one after the other.
center = s + w ÷ 2; s ← s + w; stop when s + w > len + 1e-9
Positions are a running sum of module widths starting at the last module's end. The end-of-rail test allows 1e-9 mm.
addons/harness/main.js
lines 353 to 357
CadCraft 0.8.2
let s = Math.max(0, rail.q.s, dir.length ? dir[dir.length - 1].s1 : 0), n = 0, left = 0;
for (const part of todo()) {
const w = sizeFor(part.type).w;
if (s + w > rail.pl.len + 1e-9) { left++; continue; }
placeModule(doc, part, rail, s + w / 2, {}); have.add(part.id); s += w; n++;
The length written beside each harness segment, and its wire count and diameter.
mm = typed length, or √(Δx² + Δy²) ÷ k; label = round(mm)
A length typed for the segment is used as given; otherwise the drawn length divided by k. The label shows whole millimeters and the diameter one decimal. The label's angle is flipped by π when its cosine is below −1e-9.
addons/harness/lib/draw.js
lines 15 to 36
CadCraft 0.8.2
export function bundleEnts(h) {
const k = h.k || 1, th = 4 * k, out = [];
const node = new Map(h.nodes.map((n) => [n.id, n]));
for (const s of h.segs) {
const a = node.get(s.a), b = node.get(s.b);
if (!a || !b) continue;
out.push(ln(a.x, a.y, b.x, b.y));
const mm = Number.isFinite(s.len) && s.len > 0 ? s.len : Math.hypot(a.x - b.x, a.y - b.y) / k;
const l = Math.hypot(b.x - a.x, b.y - a.y) || 1, nx = -(b.y - a.y) / l, ny = (b.x - a.x) / l;
let ang = Math.atan2(b.y - a.y, b.x - a.x);
if (Math.cos(ang) < -1e-9) ang += Math.PI;
const c = h.counts?.[s.id];
out.push(text((a.x + b.x) / 2 + nx * th * 1.1, (a.y + b.y) / 2 + ny * th * 1.1, th, Math.round(mm), 'center', 'middle', ang));
if (c && c.n) out.push(text((a.x + b.x) / 2 - nx * th * 1.1, (a.y + b.y) / 2 - ny * th * 1.1, th * 0.8, c.n + ' × ' + (c.od ? 'Ø' + Math.round(c.od * 10) / 10 : ''), 'center', 'middle', ang));
}
for (const n of h.nodes) {
out.push(circ(n.x, n.y, 2.2 * k));
out.push(text(n.x + 3 * k, n.y + 3 * k, th, n.label || n.id, 'left', 'bottom'));
}
if (h.name && h.nodes.length) { const n = h.nodes[0]; out.push(text(n.x, n.y - 8 * k, th * 1.3, h.name, 'left', 'top')); }
return out;
}
In detail
Method
Rail frame by a rotation with the rail's angle; modules placed at running sums of their widths; connection by distance tests and a union-find, as in the Electrical add-on.
Number types
64-bit numbers.
Tolerances
Connection tolerance 0.05 drawing units; a module is on a rail within half the rail width + 5 mm; overlap and past-the-end tests allow 0.01 mm; a module snaps flush to a neighbor within 3 mm; a harness point reuses a node within settings.tol × k (default 4 mm). All absolute.
Limits
The object's scale after a transform is taken from the first column of the matrix, so a non-uniform scale is not represented.
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.