Decides which wires and symbol pins of a schematic are connected, places wire numbers and junction dots, and positions symbols, pins and wires on the sheet.
How the precision is kept
Pin positions come from one 2 × 3 matrix per symbol in 64-bit numbers. Whether two things connect is a distance test against an absolute tolerance of 0.05 drawing units, computed as squared distances with no square root. Points placed on sheets snap to a 2.5 mm grid, which is exact in binary. Pin offsets written into a block's EL attribute are rounded to 3 decimals.
Calculations under this policy
15 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.
How far a point is from a wire segment, and where along the segment the nearest point lies.
u = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); q = a + u (b − a); d² = |p − q|²
The squared distance is returned, so no square root is taken. A zero-length segment is guarded and gives u = 0.
addons/electrical/lib/wires.js
lines 28 to 35
CadCraft 0.8.2
/** Distance² from p to segment ab, and where along it (0…1). */
export function segDist2(p, a, b) {
const dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy;
let u = l2 ? ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2 : 0;
u = Math.max(0, Math.min(1, u));
const q = { x: a.x + u * dx, y: a.y + u * dy };
return { d2: d2(p, q), u, q };
}
Points within a radius
64-bit
Finds the stored points within a distance of a point.
match when (Δx² + Δy²) ≤ r²
Grid cells are found with Math.floor of coordinate ÷ cell size; the match itself compares squared distances in 64-bit, with equality counting as a match.
addons/electrical/lib/wires.js
lines 43 to 47
CadCraft 0.8.2
near(p, r) {
const out = [], c = this.cell, x0 = Math.floor((p.x - r) / c), x1 = Math.floor((p.x + r) / c), y0 = Math.floor((p.y - r) / c), y1 = Math.floor((p.y + r) / c);
for (let i = x0; i <= x1; i++) for (let j = y0; j <= y1; j++) for (const [q, v] of this.m.get(i + ',' + j) || []) if (d2(p, q) <= r * r) out.push(v);
return out;
}
What a wire end connects to
64-bit
Joins a wire end to other wire ends, to pins and to the middle of other wires that it touches.
connected when distance² ≤ tol², tol = 0.05
Ends and pins within tol are joined; an end on another wire's middle is joined when its squared distance to the segment is at most tol² and it is not that segment's own free end. Each test is absolute and independent, so connection is not transitive across a chain of near misses.
addons/electrical/lib/wires.js
lines 105 to 117
CadCraft 0.8.2
for (const o of ends.near(p, tol)) if (o.w !== w || o.i !== i) { dsu.union('w:' + w.uid, 'w:' + o.w.uid); joined = true; }
for (const { d, p: pin } of pins.near(p, tol)) { dsu.union('w:' + w.uid, pinKey(d, pin)); joined = true; m.pin = true; }
// on another wire's middle (or this wire's own, for a loop); an inner corner of a polyline is its middle too
const seen = new Set();
for (const { o, k, a, b, last } of sg.at(p)) {
if (seen.has(o)) continue;
if (segDist2(p, a, b).d2 > tol2) continue;
if ((k === 0 && d2(p, a) <= tol2) || (k === last && d2(p, b) <= tol2)) continue;
if (o === w && ((i === 0 && k === 0) || (i === 1 && k === last))) continue;
seen.add(o);
dsu.union('w:' + w.uid, 'w:' + o.uid); joined = true;
m.n += 2;
}
Where a wire number goes
64-bit
The middle and direction of the longest segment of a net, for its wire number.
x = (aₓ + bₓ) ÷ 2; y = (a_y + b_y) ÷ 2; rot = atan2(Δy, Δx) turned into (−π ÷ 2, π ÷ 2]
A segment replaces the current longest only when it is more than 1e-9 longer, so the first of equal segments is kept. The angle is flipped by π with a 1e-9 margin so the text reads left to right or bottom to top.
addons/electrical/lib/wires.js
lines 168 to 183
CadCraft 0.8.2
/** Where a wire number goes on a net: the middle of its longest segment on its first sheet. → {x, y, rot, wire}. */
export function numberSpot(net) {
let best = null;
const sheet = net.start.sheet;
for (const w of net.wires) {
if (w.sheet !== sheet && net.wires.some((x) => x.sheet === sheet)) continue;
for (const [a, b] of segs(w.pts)) {
const len = Math.sqrt(d2(a, b));
if (!best || len > best.len + 1e-9) best = { len, a, b, w };
}
}
if (!best) return null;
let ang = Math.atan2(best.b.y - best.a.y, best.b.x - best.a.x);
if (ang > Math.PI / 2 + 1e-9) ang -= Math.PI; else if (ang <= -Math.PI / 2 + 1e-9) ang += Math.PI; // text reads left to right or bottom to top
return { x: (best.a.x + best.b.x) / 2, y: (best.a.y + best.b.y) / 2, rot: ang, wire: best.w };
}
Which wire a wire number belongs to
64-bit
Assigns each wire number to the nearest wire within reach.
nearest segment with d² ≤ 3²
Compares squared distances; the association distance is 3 drawing units, absolute.
addons/electrical/lib/wires.js
lines 185 to 197
CadCraft 0.8.2
/** The net each wire number sits on (nearest segment within ASSOC). → Map wireno uid → net | null. */
export function wirenoNets(model, nl, assoc = ASSOC) {
const out = new Map();
for (const wn of model.wirenos) {
let best = null;
for (const w of model.wires) {
if (w.space.key !== wn.space.key) continue;
for (const [a, b] of segs(w.pts)) { const r = segDist2(wn.at, a, b); if (r.d2 <= assoc * assoc && (!best || r.d2 < best.d2)) best = { d2: r.d2, w }; }
}
out.set(wn.uid, best ? nl.netOfWire.get(best.w.uid) : null);
}
return out;
}
Junction dots
64-bit
Places a dot at each junction and removes dots that no longer mark one.
a dot marks a junction when d² ≤ max(tol, 0.2)²
A new dot is placed at the junction's stored point, which meetAt in netlist (same file) sets to the first wire end that reached it, not an average of the ends that meet there.
addons/electrical/lib/wires.js
lines 270 to 288
CadCraft 0.8.2
/** Put dots where junctions are and take away the ones that no longer mark one (inside a change). → {added, removed}. */
export function updateDots(doc, model, nl, { tol = TOL } = {}) {
const want = nl.junctions.map((j) => ({ ...j, have: false }));
let added = 0, removed = 0;
const t2 = Math.max(tol, 0.2) ** 2;
for (const dot of model.dots) {
const hit = want.find((j) => !j.have && j.space?.key === dot.space.key && d2(j, dot.at) <= t2);
if (hit) hit.have = true;
else { doc.remove(dot.id, dot.space.map); removed++; }
}
const missing = want.filter((j) => !j.have);
if (missing.length) ensureBlock(doc, helperBlock('dot'));
for (const j of missing) {
const w = model.wires.find((x) => x.space.key === j.space?.key && x.pts.some((p, i) => i + 1 < x.pts.length && segDist2(j, p, x.pts[i + 1]).d2 <= t2));
doc.add({ type: 'insert', block: 'EL_DOT', x: j.x, y: j.y, sx: 1, sy: 1, rot: 0, layer: w?.layer || 'EL-WIRE' }, j.space.map);
added++;
}
return { added, removed };
}
The matrix that takes a symbol's own coordinates 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 cosine and sine are computed once per symbol. At a rotation of π ÷ 2 Math.cos returns about 6e-17, not 0, so a pin 5 units out lands about 3e-16 off its axis.
addons/electrical/lib/symbols.js
lines 361 to 367
CadCraft 0.8.2
/** World matrix of an insert: [a, b, c, d, e, f] (x' = a x + c y + e, y' = b x + d y + f). */
export function insertM(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)];
}
A point through the transform
64-bit
Applies a symbol's matrix to a point.
x′ = a x + c y + e; y′ = b x + d y + f
Two products and two additions per coordinate; nothing is rounded.
addons/electrical/lib/symbols.js
line 368
CadCraft 0.8.2
export const apply = (m, x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
Pin positions of a placed symbol
64-bit
The drawing positions of a symbol's connection points.
Each pin offset read from the block's EL attribute goes through the symbol's matrix. The offsets themselves were written with 3 decimals (see the next entry).
addons/electrical/lib/symbols.js
lines 389 to 394
CadCraft 0.8.2
/** The world positions of an electrical insert's pins: [{tag, label, x, y}] (label = the pin's attribute value). */
export function pinsOf(e, blk, meta = readMeta(blk)) {
if (!meta) return [];
const m = insertM(e, blk);
return meta.pins.map((p) => ({ tag: p.tag, label: String(e.attrs?.[p.tag] ?? ''), ...apply(m, p.x, p.y) }));
}
Pin offsets written to the block
64-bit
Formats a pin offset for the block's EL attribute text.
text = round(n × 1000) ÷ 1000
Pin offsets are stored as text rounded to 3 decimals. parseMeta reads them back with a pattern that needs a digit before the point and no exponent, which the written form always satisfies.
addons/electrical/lib/symbols.js
line 215
CadCraft 0.8.2
const num = (n) => String(Math.round(n * 1000) / 1000);
Attribute text kept upright
64-bit
Decides which attribute texts of a rotated symbol would read upside down and turns them half round.
turn when cos(rot + text rot) ≤ −1e-9
The rotation is reduced modulo 2π first. The 1e-9 margin keeps text at exactly 90 degrees, where the cosine is a tiny non-zero number, from being turned.
addons/electrical/lib/symbols.js
lines 370 to 387
CadCraft 0.8.2
/**
* Attribute text that would read upside down on a reference turned past 90° (a socket facing a north wall) is given its
* own place (`attloc`) turned half round, anchored on the other corner, so it covers the same spot and reads upright.
* → the attloc object for the insert, or null when nothing needs it.
*/
export function uprightAttloc(blk, rot = 0) {
const r = (((rot % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI));
const out = {};
for (const d of blk?.attdefs || []) {
if (d.invisible || d.constant) continue;
const w = r + (d.rot || 0);
if (Math.cos(w) > -1e-9) continue;
const ha = { left: 'right', right: 'left' }[d.ha || 'left'] || 'center';
const va = { top: 'bottom', bottom: 'top', baseline: 'top' }[d.va || 'baseline'] || 'middle';
out[d.tag] = { x: d.x, y: d.y, h: d.h, rot: (d.rot || 0) + Math.PI, ha, va, wf: d.wf || 1, layer: d.layer ?? '0', color: d.color ?? null, invisible: false };
}
return Object.keys(out).length ? out : null;
}
Moves a picked point on a sheet to the nearest point of the 2.5 mm grid.
x = round(x ÷ 2.5) × 2.5
2.5 is exact in binary, so every snapped coordinate is a whole number times 2.5 with no rounding error. Points in model space are returned unchanged.
addons/electrical/main.js
lines 815 to 820
CadCraft 0.8.2
/** Points snap to the 2.5 mm half grid on sheets (symbols and wires line up). */
export function snapToGrid(doc, p) {
if (doc.inModel) return { x: p.x, y: p.y };
const g = 2.5;
return { x: Math.round(p.x / g) * g, y: Math.round(p.y / g) * g };
}
A wire point landing on a pin or wire
64-bit
Moves a picked wire point onto a pin, a wire end or a wire when it is within 2 units of one.
candidate when d² ≤ 2²; a wire's middle is ranked with d² + 0.25
The stored point is the pin's or end's own coordinates, or the projection onto the wire segment. A pin wins over another candidate that is no more than 1e-9 nearer in squared distance.
addons/electrical/main.js
lines 168 to 181
CadCraft 0.8.2
/** A click near a pin, a wire end or a wire lands on it (`on`: it connects there). */
function snapWire(doc, p) {
let best = null;
const consider = (q, d2, kind) => { if (d2 <= SNAP * SNAP && (!best || d2 < best.d2 - 1e-12 || (kind === 'pin' && best.kind !== 'pin' && d2 <= best.d2 + 1e-9))) best = { x: q.x, y: q.y, d2, kind }; };
for (const e of doc.entities.values()) {
if (e.type === 'insert') { const blk = doc.blocks.get(e.block); if (readMeta(blk)) for (const q of pinsOf(e, blk)) consider(q, (q.x - p.x) ** 2 + (q.y - p.y) ** 2, 'pin'); }
else if ((e.type === 'line' || e.type === 'poly') && isWireLayer(e.layer)) {
const pts = e.type === 'line' ? [{ x: e.x1, y: e.y1 }, { x: e.x2, y: e.y2 }] : e.pts;
for (const q of [pts[0], pts[pts.length - 1]]) consider(q, (q.x - p.x) ** 2 + (q.y - p.y) ** 2, 'end');
for (let i = 0; i + 1 < pts.length; i++) { const r = segDist2(p, pts[i], pts[i + 1]); consider(r.q, r.d2 + 0.25, 'wire'); } // ends and pins win over the middle
}
}
return best ? { x: best.x, y: best.y, on: true } : { x: p.x, y: p.y };
}
A wire cut under a new symbol
64-bit
Cuts a wire between two pins of a symbol placed on it.
pin on the wire when d² < 1e-4 and 1e-6 < u < 1 − 1e-6
A pin counts as on the wire within 0.01 drawing units. The new wire ends are the projections of the pins onto the segment, not the pins' own coordinates, so an end can sit up to 0.01 from its pin, inside the 0.05 connection tolerance.
addons/electrical/main.js
lines 119 to 139
CadCraft 0.8.2
/** A wire running straight through two pins of a new symbol is cut between them (the symbol now sits in it). */
function breakWiresUnder(doc, ins, blk) {
const pins = pinsOf(ins, blk);
if (pins.length < 2) return;
for (const e of [...doc.entities.values()]) {
if (!(e.type === 'line' || e.type === 'poly') || !isWireLayer(e.layer) || e.closed) continue;
const pts = e.type === 'line' ? [{ x: e.x1, y: e.y1 }, { x: e.x2, y: e.y2 }] : e.pts.map((q) => ({ x: q.x, y: q.y }));
for (let i = 0; i + 1 < pts.length; i++) {
const hits = pins.map((q) => ({ q, r: segDist2(q, pts[i], pts[i + 1]) })).filter((h) => h.r.d2 < 1e-4 && h.r.u > 1e-6 && h.r.u < 1 - 1e-6);
if (hits.length < 2) continue;
hits.sort((a, b) => a.r.u - b.r.u);
const a = hits[0].r.q, b = hits[hits.length - 1].r.q;
const first = [...pts.slice(0, i + 1), a], second = [b, ...pts.slice(i + 1)];
const mk = (list) => (list.length === 2 ? { type: 'line', layer: e.layer, color: e.color, x1: list[0].x, y1: list[0].y, x2: list[1].x, y2: list[1].y } : { type: 'poly', layer: e.layer, color: e.color, closed: false, pts: list.map((q) => ({ x: q.x, y: q.y, b: 0 })) });
doc.remove(e.id);
doc.add(mk(first));
doc.add(mk(second));
break;
}
}
}
Positions the symbols and wires of a solar one-line diagram from the column and row of each node.
x = x₀ + col × pitch x; y = y₀ − row × pitch y; wire bend at (sx + ex) ÷ 2
Columns and rows are whole numbers, so with the default origin and pitches every position is a whole number of millimeters or a half. The current on a wire label is rounded to 1 decimal; the fuse or breaker rating is written as given.
addons/electrical/lib/pvoneline.js
lines 50 to 81
CadCraft 0.8.2
export function layoutPvOneline(plan, { x0 = 45, y0 = 200, pitchX = 50, pitchY = 60 } = {}) {
const at = new Map(), symbols = [], wires = [], counters = {};
const LETTER = { 'pv.array': 'G', 'pv.inverter': 'T', 'pv.combiner': 'X', disconnector: 'Q' };
for (const n of plan.nodes) {
const key = SYMBOL_OF[n.symbol];
const x = x0 + n.col * pitchX, y = y0 - n.row * pitchY;
at.set(n.id, { x, y, key });
const L = LETTER[key];
const tag = L ? `-${L}${(counters[L] = (counters[L] || 0) + 1)}` : '';
const rate = Object.entries(n.rating).slice(0, 4).map(([k, v]) => `${k} ${v}`).join(', ');
symbols.push({ id: n.id, key, x, y, rot: Math.PI / 2, attrs: { TAG: tag, DESC: n.label }, caption: rate });
}
for (const e of plan.edges) {
const a = at.get(e.from), b = at.get(e.to);
const sx = a.x + 5, sy = a.y, ex = b.x - 5, ey = b.y;
const midx = ex > sx ? (sx + ex) / 2 : sx + 8;
const pts = sy === ey && ex > sx ? [[sx, sy], [ex, ey]] : [[sx, sy], [midx, sy], [midx, ey], [ex, ey]];
const parts = [e.label, e.current != null ? `${r1(e.current)} A` : '', e.wire || '', e.ocpd != null ? `${e.ocpd} A fuse/breaker` : ''].filter(Boolean);
const w = { pts, text: parts, tx: (sx + Math.max(ex, midx)) / 2, ty: Math.max(sy, ey) + 3, circuit: e.circuit };
// a fuse / breaker on the wire (EG.15, owner-approved): the breaker symbol sits in the middle of the first horizontal run, which is cut
// there (placePvOneline); the "20 A fuse/breaker" text stays beside it. A run shorter than the symbol (10 mm + room) keeps the text only.
const run = pts[1][0] - pts[0][0];
if (e.ocpd != null && run >= 14) {
const bx = (pts[0][0] + pts[1][0]) / 2, by = pts[0][1];
w.breaker = { x: bx, y: by };
symbols.push({ id: (e.id || e.from + '>' + e.to) + '-ocpd', key: 'breaker', x: bx, y: by, rot: Math.PI / 2, attrs: { TAG: `-F${(counters.F = (counters.F || 0) + 1)}`, DESC: `${e.ocpd} A` }, caption: '' });
}
wires.push(w);
}
const notes = [...(plan.codeLabel ? [plan.codeLabel] : []), ...plan.notes];
return { symbols, wires, notes };
}
In detail
Method
Point-to-segment squared distance by clamped projection; a union-find over wires and pins joined within the tolerance; grid buckets to find near points; insert transform x′ = a x + c y + e, y′ = b x + d y + f.
Number types
64-bit numbers.
Tolerances
Connection tolerance TOL = 0.05 drawing units; a wire number belongs to a wire within 3 units; junction dots match within max(tol, 0.2); a click lands on a pin, wire end or wire within 2 units; a wire is cut under a new symbol when two pins lie within 0.01 of it. All absolute.
Limits
The connection decision is not exact: two ends 0.05 apart connect and ends slightly further do not. The tolerance does not scale with the drawing's units.
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.