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:
+61
-53
@@ -173,7 +173,7 @@ class DiagramCanvas {
|
||||
// Loom background
|
||||
this.harnessLayer.add(new Konva.Line({
|
||||
points: [cx, cy, ex, ey],
|
||||
stroke: '#0a0a18', strokeWidth: trunkW + 4,
|
||||
stroke: Theme.loomShadow(), strokeWidth: trunkW + 4,
|
||||
lineCap: 'round', listening: false,
|
||||
}));
|
||||
// Colored wire stripes
|
||||
@@ -192,7 +192,7 @@ class DiagramCanvas {
|
||||
// Split-off circle at loom end
|
||||
this.harnessLayer.add(new Konva.Circle({
|
||||
x: ex, y: ey, radius: trunkW / 2 + 4,
|
||||
fill: '#12122a', stroke: '#4466cc', strokeWidth: 2, listening: false,
|
||||
fill: Theme.harnessBg(), stroke: Theme.harnessEdge(), strokeWidth: 2, listening: false,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -238,7 +238,7 @@ class DiagramCanvas {
|
||||
// Trunk background
|
||||
this.harnessLayer.add(new Konva.Line({
|
||||
points: [ax, ay, bx, by],
|
||||
stroke: '#0a0a18', strokeWidth: trunkW + 4,
|
||||
stroke: Theme.loomShadow(), strokeWidth: trunkW + 4,
|
||||
lineCap: 'round', lineJoin: 'round', listening: false,
|
||||
}));
|
||||
// Colored wire stripes
|
||||
@@ -260,12 +260,12 @@ class DiagramCanvas {
|
||||
this.harnessLayer.add(new Konva.Rect({
|
||||
x: mx - 22, y: my - 9,
|
||||
width: 44, height: 18, cornerRadius: 4,
|
||||
fill: '#12122a', stroke: '#4466cc', strokeWidth: 1.5, listening: false,
|
||||
fill: Theme.harnessBg(), stroke: Theme.harnessEdge(), strokeWidth: 1.5, listening: false,
|
||||
}));
|
||||
this.harnessLayer.add(new Konva.Text({
|
||||
x: mx - 22, y: my - 6,
|
||||
text: badgeText, width: 44, align: 'center',
|
||||
fontSize: 9, fontFamily: 'monospace', fill: '#99bbff', listening: false,
|
||||
fontSize: 9, fontFamily: 'monospace', fill: Theme.harnessText(), listening: false,
|
||||
}));
|
||||
}
|
||||
});
|
||||
@@ -619,7 +619,7 @@ class DiagramCanvas {
|
||||
if (!g) return;
|
||||
const d = this.deviceData.get(id);
|
||||
const isGroup = d?.device_type === "group";
|
||||
const offColor = isGroup ? (d.properties?.fillColor || "#2828a0") : "#5a5a8a";
|
||||
const offColor = isGroup ? (d.properties?.fillColor || "#2828a0") : Theme.deviceStroke();
|
||||
g.findOne("Rect").stroke(on ? "#4db8ff" : offColor);
|
||||
(isGroup ? this.groupLayer : this.deviceLayer).batchDraw();
|
||||
}
|
||||
@@ -638,6 +638,13 @@ class DiagramCanvas {
|
||||
this.wireLayer.batchDraw();
|
||||
}
|
||||
|
||||
// Re-render every device. Konva bakes colours in at draw time rather than
|
||||
// reading CSS, so a theme change needs an explicit repaint.
|
||||
repaintAll() {
|
||||
[...this.deviceData.values()].forEach((d) => this.updateDevice(d));
|
||||
this.stage.batchDraw();
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this._netHighlightIds.forEach(id => this._wireHighlight(id, false));
|
||||
this._netHighlightIds.clear();
|
||||
@@ -1026,26 +1033,10 @@ class DiagramCanvas {
|
||||
|
||||
// ── Device rendering ──────────────────────────────────────────────────────────
|
||||
|
||||
// Fills, outline and label colours come from the theme so light mode can
|
||||
// re-tint them; see theme.js. Konva cannot read CSS custom properties.
|
||||
_deviceFill(type) {
|
||||
return {
|
||||
connector: "#12253a",
|
||||
terminal_block: "#122a1a",
|
||||
component: "#1e1230",
|
||||
splice: "#2a1e10",
|
||||
label: "#22220e",
|
||||
fuse: "#2a1c08",
|
||||
relay: "#0a1628",
|
||||
switch: "#0a2218",
|
||||
bulb: "#24220a",
|
||||
motor: "#1a0a28",
|
||||
diode: "#28081a",
|
||||
resistor: "#1a1a08",
|
||||
capacitor: "#081a1a",
|
||||
ground: "#0e140e",
|
||||
power: "#1a0808",
|
||||
cable: "#1a1a1a",
|
||||
group: "rgba(40,40,80,0.35)",
|
||||
}[type] || "#1e1e2e";
|
||||
return Theme.deviceFill(type);
|
||||
}
|
||||
|
||||
_renderDevice(device) {
|
||||
@@ -1072,7 +1063,7 @@ class DiagramCanvas {
|
||||
group.add(new Konva.Text({
|
||||
name: "device-label",
|
||||
x: 8, y: 4, width: device.width - 16,
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#ffffff",
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||
fontStyle: "bold", listening: false,
|
||||
}));
|
||||
} else {
|
||||
@@ -1080,7 +1071,7 @@ class DiagramCanvas {
|
||||
const rect = new Konva.Rect({
|
||||
name: "device-rect",
|
||||
width: device.width, height: device.height,
|
||||
fill: this._deviceFill(device.device_type), stroke: "#5a5a8a", strokeWidth: 2, cornerRadius: 4,
|
||||
fill: this._deviceFill(device.device_type), stroke: Theme.deviceStroke(), strokeWidth: 2, cornerRadius: 4,
|
||||
});
|
||||
group.add(rect);
|
||||
|
||||
@@ -1089,7 +1080,7 @@ class DiagramCanvas {
|
||||
group.add(new Konva.Text({
|
||||
name: "device-label",
|
||||
x: 8, y: 8, width: device.width - 16, height: device.height - 16,
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#dde0f5",
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||
align: "left", verticalAlign: "top", wrap: "word",
|
||||
}));
|
||||
} else if (device.device_type === "cable") {
|
||||
@@ -1160,25 +1151,25 @@ class DiagramCanvas {
|
||||
// Header jacket bar
|
||||
group.add(new Konva.Rect({ x: 2, y: 2, width: device.width - 4, height: 22, fill: jacket, cornerRadius: [3,3,0,0] }));
|
||||
if (device.reference) {
|
||||
group.add(new Konva.Text({ x: 6, y: 6, text: device.reference, fontSize: 9, fontFamily: "monospace", fill: "#99aaee", fontStyle: "bold" }));
|
||||
group.add(new Konva.Text({ x: 6, y: 6, text: device.reference, fontSize: 9, fontFamily: "monospace", fill: Theme.deviceRef(), fontStyle: "bold" }));
|
||||
}
|
||||
group.add(new Konva.Text({
|
||||
name: "device-label",
|
||||
x: 4, y: 6, width: device.width - 8, align: "center",
|
||||
text: device.label, fontSize: 10, fontFamily: "monospace", fill: "#dde0f5",
|
||||
text: device.label, fontSize: 10, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||
}));
|
||||
// Conductor rows
|
||||
conductors.forEach((cond, i) => {
|
||||
const rowY = 26 + i * 24;
|
||||
const color = cond.color || "#888888";
|
||||
group.add(new Konva.Rect({ x: 1, y: rowY, width: device.width - 2, height: 24, fill: "#0e0e1a" }));
|
||||
group.add(new Konva.Rect({ x: 1, y: rowY, width: device.width - 2, height: 24, fill: Theme.rowBg() }));
|
||||
group.add(new Konva.Line({
|
||||
points: [14, rowY + 12, device.width - 14, rowY + 12],
|
||||
stroke: color, strokeWidth: 5, lineCap: "round", listening: false,
|
||||
}));
|
||||
group.add(new Konva.Text({
|
||||
x: 16, y: rowY + 4, text: cond.name || String(i + 1),
|
||||
fontSize: 9, fontFamily: "monospace", fill: "#c8ccee", listening: false,
|
||||
fontSize: 9, fontFamily: "monospace", fill: Theme.deviceText(), listening: false,
|
||||
}));
|
||||
});
|
||||
// Footer jacket bar
|
||||
@@ -1186,7 +1177,7 @@ class DiagramCanvas {
|
||||
group.add(new Konva.Text({
|
||||
name: "device-type",
|
||||
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
||||
text: "cable", fontSize: 8, fontFamily: "monospace", fill: "#888888",
|
||||
text: "cable", fontSize: 8, fontFamily: "monospace", fill: Theme.deviceSubtext(),
|
||||
}));
|
||||
} else if (device.device_type === "connector" && device.properties?.shape === "circular") {
|
||||
rect.visible(false);
|
||||
@@ -1197,32 +1188,32 @@ class DiagramCanvas {
|
||||
group.add(new Konva.Circle({
|
||||
name: "device-body",
|
||||
x: cx, y: cy, radius: r,
|
||||
fill: this._deviceFill("connector"), stroke: "#5a5a8a", strokeWidth: 2,
|
||||
fill: this._deviceFill("connector"), stroke: Theme.deviceStroke(), strokeWidth: 2,
|
||||
}));
|
||||
// Key notch anchored to body top edge
|
||||
group.add(new Konva.Rect({
|
||||
x: cx - 5, y: cy - r - 3, width: 10, height: 7, cornerRadius: 2,
|
||||
fill: "#333355", stroke: "#5a5a8a", strokeWidth: 1, listening: false,
|
||||
fill: Theme.notch(), stroke: Theme.deviceStroke(), strokeWidth: 1, listening: false,
|
||||
}));
|
||||
group.add(new Konva.Text({
|
||||
name: "device-label",
|
||||
x: 4, y: cy - 6, width: device.width - 8,
|
||||
text: device.label, fontSize: Math.min(10, fontSize),
|
||||
fontFamily: "monospace", fill: "#dde0f5", align: "center",
|
||||
fontFamily: "monospace", fill: Theme.deviceText(), align: "center",
|
||||
}));
|
||||
} else {
|
||||
if (device.reference) {
|
||||
group.add(new Konva.Text({ x: 6, y: 5, text: device.reference, fontSize: 10, fontFamily: "monospace", fill: "#99aaee", fontStyle: "bold" }));
|
||||
group.add(new Konva.Text({ x: 6, y: 5, text: device.reference, fontSize: 10, fontFamily: "monospace", fill: Theme.deviceRef(), fontStyle: "bold" }));
|
||||
}
|
||||
group.add(new Konva.Text({
|
||||
name: "device-label",
|
||||
x: 4, y: device.height / 2 - fontSize / 2 - 2, width: device.width - 8, align: "center",
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#dde0f5", wrap: "word",
|
||||
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(), wrap: "word",
|
||||
}));
|
||||
group.add(new Konva.Text({
|
||||
name: "device-type",
|
||||
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
||||
text: device.device_type, fontSize: 8, fontFamily: "monospace", fill: "#444466",
|
||||
text: device.device_type, fontSize: 8, fontFamily: "monospace", fill: Theme.deviceSubtext(),
|
||||
}));
|
||||
}
|
||||
} // end non-group else
|
||||
@@ -1491,7 +1482,7 @@ class DiagramCanvas {
|
||||
_addPin(group, device, pin) {
|
||||
const circle = new Konva.Circle({
|
||||
x: pin.x_offset, y: pin.y_offset, radius: 5,
|
||||
fill: "#0a0f1a", stroke: "#5566aa", strokeWidth: 1.5,
|
||||
fill: Theme.pinFill(), stroke: Theme.pinStroke(), strokeWidth: 1.5,
|
||||
});
|
||||
const hit = new Konva.Circle({
|
||||
x: pin.x_offset, y: pin.y_offset, radius: 14,
|
||||
@@ -1499,29 +1490,46 @@ class DiagramCanvas {
|
||||
});
|
||||
hit._pinMeta = { deviceId: device.id, pinId: pin.id, side: pin.side };
|
||||
|
||||
// Position label inside the device body, clear of the pin circle (r=5, gap=3 → offset 8)
|
||||
// Pin labels scale with the device font rather than sitting at a fixed 8px,
|
||||
// so raising the font size makes the whole device readable, not just its
|
||||
// title. The ratios below reproduce the previous look exactly at the
|
||||
// default size of 12.
|
||||
const baseFont = device.properties?.fontSize || 12;
|
||||
const pinFont = Math.max(6, Math.round(baseFont * 0.7));
|
||||
const GAP = 8;
|
||||
let lx, ly, lw, la;
|
||||
const lw = Math.round(pinFont * 2.5);
|
||||
const vc = Math.round(pinFont * 0.62); // vertical centring on the pin
|
||||
let lx, ly, la;
|
||||
switch (pin.side) {
|
||||
case "right":
|
||||
lx = pin.x_offset - 28; ly = pin.y_offset - 5; lw = 20; la = "right"; break;
|
||||
lx = pin.x_offset - (GAP + lw); ly = pin.y_offset - vc; la = "right"; break;
|
||||
case "top":
|
||||
lx = pin.x_offset - 10; ly = pin.y_offset + GAP; lw = 20; la = "center"; break;
|
||||
lx = pin.x_offset - lw / 2; ly = pin.y_offset + GAP; la = "center"; break;
|
||||
case "bottom":
|
||||
lx = pin.x_offset - 10; ly = pin.y_offset - 13; lw = 20; la = "center"; break;
|
||||
lx = pin.x_offset - lw / 2; ly = pin.y_offset - GAP - vc; la = "center"; break;
|
||||
default: // left
|
||||
lx = pin.x_offset + GAP; ly = pin.y_offset - 5; lw = 20; la = "left";
|
||||
lx = pin.x_offset + GAP; ly = pin.y_offset - vc; la = "left";
|
||||
}
|
||||
// Pass-through devices (bulkheads) carry the same signal on both faces, so
|
||||
// one pin of the pair draws the name centred between them and the other
|
||||
// draws nothing — one label per circuit rather than two.
|
||||
if (!pin.hide_label) {
|
||||
const centred = !!pin.center_label;
|
||||
group.add(new Konva.Text({
|
||||
x: centred ? 0 : lx,
|
||||
y: centred ? pin.y_offset - vc : ly,
|
||||
width: centred ? device.width : lw,
|
||||
align: centred ? "center" : la,
|
||||
text: pin.name, fontSize: pinFont, fontFamily: "monospace",
|
||||
fill: Theme.deviceSubtext(), listening: false,
|
||||
}));
|
||||
}
|
||||
group.add(new Konva.Text({
|
||||
x: lx, y: ly, width: lw, align: la,
|
||||
text: pin.name, fontSize: 8, fontFamily: "monospace", fill: "#556688",
|
||||
}));
|
||||
group.add(circle);
|
||||
group.add(hit);
|
||||
|
||||
const highlightPin = (active) => {
|
||||
circle.fill(active ? "#003300" : "#0a0f1a");
|
||||
circle.stroke(active ? "#00dd00" : "#5566aa");
|
||||
circle.fill(active ? "#003300" : Theme.pinFill());
|
||||
circle.stroke(active ? "#00dd00" : Theme.pinStroke());
|
||||
circle.radius(active ? 7 : 5);
|
||||
this.deviceLayer.batchDraw();
|
||||
};
|
||||
@@ -1685,7 +1693,7 @@ class DiagramCanvas {
|
||||
lbl = new Konva.Text({
|
||||
x: mx + 3, y: my - 10,
|
||||
text: labelText,
|
||||
fontSize: 9, fontFamily: "monospace", fill: "#aabbcc", listening: false,
|
||||
fontSize: 9, fontFamily: "monospace", fill: Theme.wireLabel(), listening: false,
|
||||
shadowColor: "#000", shadowBlur: 3, shadowOpacity: 0.8,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user