Resources Numerical accuracy Calculation policy

Schematic wires, pins and connections

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.

addons/electrical/lib/wires.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests None found by the review

Squared distance from a point to a segment

64-bit

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 };
}
addons/electrical/lib/symbols.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests None found by the review

Transform of a placed symbol

64-bit

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

Tests None found by the review

Sheet points on the 2.5 mm grid

Exact

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;
    }
  }
}
addons/electrical/lib/pvoneline.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests None found by the review

PV one-line layout

64-bit

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.

Every policy that takes this approach