feat: standard parts library, fuse box configurator, and theming

Standard parts (partsLibrary.js)
- ACDC TXL palette: 15 solids plus the 16 stocked stripe combinations
- 29 wire functions fix colour, stripe and gauge per signal, so a wire
  drawn from a given pin comes out identical on every build
- 16 pre-labelled parts: Skudak VCU (GRAY/BLACK), openinverter LDU
  23-pin, Dilong OBC/DCDC, BMW pedal, Honeywell CSSV1500, Bender
  ISO175, Bosch iBooster, Prius EPS, Volvo PS pump, cluster, DNR
  switch, 32-way enclosure bulkhead, contactors, VW MEB BMS
- OEM colours override the in-house standard where we splice into a
  factory loom (VW MEB LV connector, recovered from the VW bms diagram)
- Brown is ground, matching the MEB loom and European practice; orange
  is reserved for AC mains and OEM high-voltage runs

Fuse box configurator (pdmLibrary.js)
- GEP FRH-A12/A24 and the 48-way PDM as 280-footprint cavity grids
- Place, drag and rotate fuses, relays, diodes, breakers and bus bars
- Bus bars occupy their own collision layer and feed the blades they
  cross, suppressing those inputs so one wire supplies the run
- Cavity capacity is tracked; Apply is blocked while a placement
  overlaps or overhangs
- Naming a circuit renames its pins: "VCU" gives "VCU in" / "VCU out"

Theming (theme.js)
- Light and dark modes; canvas background, grid dots, part fill and
  part outline are configurable and persisted
- CSS converted to custom properties with property-aware light-mode
  pairs, since a colour used as text and as a surface must flip
  differently; no rule falls below 3:1 contrast in either theme
- Canvas devices, pins, labels, cable rows and the harness view are
  themed too, since Konva bakes colours in at draw time and cannot
  read CSS variables

Also
- Pass-through bulkheads: one device covering both connector faces,
  with a single centred label per circuit
- Pin labels scale with the device font instead of a fixed 8px
- Keyboard handling is modal-scoped, so Delete no longer removes the
  canvas device while the fuse box editor is open

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:54:49 -04:00
parent 43c0ea46c6
commit cca03cd2f3
8 changed files with 2410 additions and 200 deletions
+73 -10
View File
@@ -213,16 +213,77 @@ function connectorToDevice(connId, diagramId) {
};
}
// Standard rectangular connector
const w = 120;
const h = Math.max(60, pinCount * 18 + 20);
const pins = Array.from({ length: pinCount }, (_, i) => ({
id: `pin_${i + 1}`,
name: conn.pinLabels ? (conn.pinLabels[i] || String(i + 1)) : String(i + 1),
side: "right",
x_offset: w,
y_offset: ((i + 1) / (pinCount + 1)) * h,
}));
// Standard rectangular connector.
// Parts carrying pinSpecs (see partsLibrary.js) get their standard wire
// function stamped onto each pin, so wires drawn off them always come out the
// same colour, stripe and gauge.
const specs = conn.pinSpecs || null;
// Pass-through parts (bulkheads) present both faces of the same connector as
// one device: every circuit gets an IN pin on the left and an OUT pin on the
// right at the same height, sharing a single centred label. Wiring both sides
// of a bulkhead then needs one device, not a mated pair.
if (conn.passThrough) {
const w = 240;
const h = Math.max(60, pinCount * 18 + 24);
const pins = [];
(specs || Array.from({ length: pinCount })).forEach((spec, i) => {
const y = ((i + 1) / (pinCount + 1)) * h;
const base = {
name: spec ? spec.name : String(i + 1),
pin_number: spec ? spec.pin : String(i + 1),
wire_fn: spec ? spec.fn : null,
wire_oem: spec && spec.oem ? spec.oem : null,
note: spec ? spec.note : "",
};
pins.push({ ...base, id: `pin_${i + 1}_in`, side: "left", x_offset: 0, y_offset: y, center_label: true });
pins.push({ ...base, id: `pin_${i + 1}_out`, side: "right", x_offset: w, y_offset: y, hide_label: true });
});
return {
diagram_id: diagramId,
device_type: "connector",
label: conn.name,
reference: "",
x: 200, y: 200,
width: w, height: h,
properties: {
pinCount,
orientation: "right",
passThrough: true,
partNumber: conn.partNumber || "",
manufacturer: conn.manufacturer || "",
connectorLibraryId: connId,
standardPart: !!specs,
verifyPinout: !!conn.verify,
},
pins,
};
}
// Wide parts get two columns so a 20- or 32-way body stays readable.
const split = pinCount > 12;
const perSide = split ? Math.ceil(pinCount / 2) : pinCount;
const w = split ? 200 : 120;
const h = Math.max(60, perSide * 18 + 20);
const pins = Array.from({ length: pinCount }, (_, i) => {
const spec = specs ? specs[i] : null;
const onRight = !split || i >= perSide;
const idx = onRight && split ? i - perSide : i;
const count = onRight && split ? pinCount - perSide : perSide;
return {
id: `pin_${i + 1}`,
name: spec ? spec.name : (conn.pinLabels ? (conn.pinLabels[i] || String(i + 1)) : String(i + 1)),
side: onRight ? "right" : "left",
x_offset: onRight ? w : 0,
y_offset: ((idx + 1) / (count + 1)) * h,
// Standard-parts metadata — consumed by createWire() and the props panel.
pin_number: spec ? spec.pin : String(i + 1),
wire_fn: spec ? spec.fn : null,
wire_oem: spec && spec.oem ? spec.oem : null,
note: spec ? spec.note : "",
};
});
return {
diagram_id: diagramId,
@@ -237,6 +298,8 @@ function connectorToDevice(connId, diagramId) {
partNumber: conn.partNumber || "",
manufacturer: conn.manufacturer || "",
connectorLibraryId: connId,
standardPart: !!specs,
verifyPinout: !!conn.verify,
},
pins,
};