Resources Numerical accuracy Calculation policy

Wire harness and control cabinet layout

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.

addons/harness/lib/schem.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests None found by the review

Transform of a schematic symbol

64-bit

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));
  }
}
addons/harness/lib/cabinet.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests None found by the review

A point in a rail's frame

64-bit

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));
}
addons/harness/main.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests None found by the review

Drawing units per millimeter

64-bit

The factor between the drawing and millimeters.

k = (1 ÷ per) × (1 ÷ 25.4 for inch rules)

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.

  • addons/harness/main.js
  • line 58
  • CadCraft 0.8.2
transform: (data, m) => { const d = cleanBundle(data); return d ? { ...d, k: d.k * Math.hypot(m[0], m[1]) || d.k, nodes: d.nodes.map((n) => ({ ...n, x: m[0] * n.x + m[2] * n.y + m[4], y: m[1] * n.x + m[3] * n.y + m[5] })) } : data; },

Module positions in millimeters

64-bit

Each placed module's position for routing wires to it.

x mm = x ÷ k; y mm = y ÷ k

One division per coordinate by the module's own k.

  • addons/harness/main.js
  • lines 116 to 117
  • CadCraft 0.8.2
for (const m of mods(doc)) if (m.d.tag) modules.set(m.d.tag, { x: m.e.x / m.d.k, y: m.e.y / m.d.k });
return { hs, ends, modules };

Total length of a harness

64-bit

The summed length of a harness's segments for the bill of materials.

mm = Σ segment length

A plain sum in segment order of the lengths from the harness graph (route.js).

  • addons/harness/main.js
  • line 131
  • CadCraft 0.8.2
const harn = [...A.G.hs].map((h) => ({ id: h.name, mm: [...h.graph.segs.values()].reduce((s, x) => s + x.len, 0) }));

Wire count and bundle diameter per segment

64-bit

The number of wires in each harness segment and the diameter of their bundle, kept on the harness.

od = round(bundleDiameter × 10) ÷ 10

The diameter from route.js is rounded to 0.1 mm before it is stored on the harness object, so the stored value carries one decimal.

  • addons/harness/main.js
  • line 258
  • CadCraft 0.8.2
for (const [key, g] of A.R.segments) if (g.harness === h.id) counts[g.seg] = { n: g.wires.length, od: g.ods.length ? Math.round(bundleDiameter(g.ods, settings.packing) * 10) / 10 : 0 };

Rail length and angle from two points

64-bit

The length in millimeters and the angle of a rail drawn between two points.

len = √(Δx² + Δy²) ÷ k; rot = atan2(Δy, Δx)

One Math.hypot and one division; the stored length is not rounded. The length printed in the command line is rounded to a whole millimeter.

  • addons/harness/main.js
  • lines 299 to 304
  • CadCraft 0.8.2
const [p, q] = pts, len = Math.hypot(q.x - p.x, q.y - p.y) / k;
const data = cleanRail({ k, len, w: 35 });
if (!data) { c.print(t('err.size')); return; }
const e = addObject(c.doc, 'hrail', data, 'HN-RAIL', { x: p.x, y: p.y, rot: Math.atan2(q.y - p.y, q.x - p.x) });
api.select([e.id]);
c.print(t('done.rail', { len: Math.round(data.len) }));

Modules placed in a row

64-bit

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++;
addons/harness/lib/draw.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests None found by the review

Segment length and bundle labels

64-bit

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.

Every policy that takes this approach