cca03cd2f3
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>
189 lines
7.0 KiB
JavaScript
189 lines
7.0 KiB
JavaScript
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Theme — light / dark plus user overrides for the colours that make wires hard
|
|
// to read: canvas background, grid dots, device fill and device outline.
|
|
//
|
|
// Loaded FIRST, before canvas.js, so the canvas can ask for its colours as it
|
|
// draws. App chrome is themed by CSS custom properties on <html data-theme>;
|
|
// Konva cannot read CSS variables, so device colours come from here instead.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
const THEME_KEY = "wiredraw.theme";
|
|
|
|
const THEME_PRESETS = {
|
|
dark: {
|
|
canvasBg: "#131320",
|
|
gridDot: "#2e2e50",
|
|
deviceStroke: "#5a5a8a",
|
|
deviceText: "#dde0f5",
|
|
deviceSubtext: "#8890b8",
|
|
deviceRef: "#99aaee", // reference designator on the device body
|
|
pinFill: "#0a0f1a",
|
|
pinStroke: "#5566aa",
|
|
notch: "#333355", // connector key notch
|
|
rowBg: "#0e0e1a", // cable conductor rows
|
|
wireLabel: "#aabbcc", // wire tag, drawn on the canvas background
|
|
harnessBg: "#12122a",
|
|
harnessEdge: "#4466cc",
|
|
harnessText: "#99bbff",
|
|
loomShadow: "#0a0a18",
|
|
// Device fills are tinted per type so you can tell a relay from a fuse at a
|
|
// glance. Light mode derives pastel equivalents from these same hues rather
|
|
// than keeping a second hand-tuned table in sync.
|
|
lightness: null,
|
|
},
|
|
light: {
|
|
canvasBg: "#f7f8fa",
|
|
gridDot: "#c3c9d8",
|
|
deviceStroke: "#8089a8",
|
|
deviceText: "#1b2030",
|
|
deviceSubtext: "#5d657e",
|
|
deviceRef: "#2c4b96",
|
|
pinFill: "#ffffff",
|
|
pinStroke: "#5a6ba8",
|
|
notch: "#b9bed4",
|
|
rowBg: "#eceef5",
|
|
wireLabel: "#41506b",
|
|
harnessBg: "#dfe4f3",
|
|
harnessEdge: "#4466cc",
|
|
harnessText: "#26418f",
|
|
loomShadow: "#b9c0d6",
|
|
lightness: 0.88,
|
|
},
|
|
};
|
|
|
|
const DEVICE_HUES = {
|
|
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",
|
|
pdm: "#2a1408",
|
|
group: "rgba(40,40,80,0.35)",
|
|
};
|
|
|
|
// ── colour maths ─────────────────────────────────────────────────────────────
|
|
function _hexToRgb(hex) {
|
|
const h = hex.replace("#", "");
|
|
const s = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
return {
|
|
r: parseInt(s.slice(0, 2), 16),
|
|
g: parseInt(s.slice(2, 4), 16),
|
|
b: parseInt(s.slice(4, 6), 16),
|
|
};
|
|
}
|
|
|
|
function _rgbToHsl({ r, g, b }) {
|
|
r /= 255; g /= 255; b /= 255;
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
const l = (max + min) / 2;
|
|
let h = 0, s = 0;
|
|
if (max !== min) {
|
|
const d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
else h = ((r - g) / d + 4) / 6;
|
|
}
|
|
return { h, s, l };
|
|
}
|
|
|
|
// Re-light a colour to a target lightness, keeping its hue so the per-type
|
|
// distinction survives the theme switch.
|
|
function _relight(hex, targetL, satScale = 0.55) {
|
|
if (!hex || hex.startsWith("rgba")) return hex;
|
|
const { h, s } = _rgbToHsl(_hexToRgb(hex));
|
|
return `hsl(${Math.round(h * 360)}, ${Math.round(Math.min(1, s * satScale) * 100)}%, ${Math.round(targetL * 100)}%)`;
|
|
}
|
|
|
|
// ── state ────────────────────────────────────────────────────────────────────
|
|
const Theme = {
|
|
_state: {
|
|
mode: "dark",
|
|
canvasBg: null, // null = follow the mode preset
|
|
gridDot: null,
|
|
deviceStroke: null,
|
|
deviceFill: null, // null = tint per device type
|
|
},
|
|
_listeners: [],
|
|
|
|
load() {
|
|
try {
|
|
const raw = localStorage.getItem(THEME_KEY);
|
|
if (raw) Object.assign(this._state, JSON.parse(raw));
|
|
} catch { /* corrupt or unavailable storage — fall back to defaults */ }
|
|
if (!THEME_PRESETS[this._state.mode]) this._state.mode = "dark";
|
|
return this._state;
|
|
},
|
|
|
|
save() {
|
|
try { localStorage.setItem(THEME_KEY, JSON.stringify(this._state)); }
|
|
catch { /* private mode — theme just will not persist */ }
|
|
},
|
|
|
|
get() { return { ...this._state }; },
|
|
preset() { return THEME_PRESETS[this._state.mode]; },
|
|
|
|
set(patch) {
|
|
Object.assign(this._state, patch);
|
|
this.save();
|
|
this.apply();
|
|
this._listeners.forEach((fn) => fn(this._state));
|
|
},
|
|
|
|
reset() {
|
|
this._state = { mode: this._state.mode, canvasBg: null, gridDot: null, deviceStroke: null, deviceFill: null };
|
|
this.save();
|
|
this.apply();
|
|
this._listeners.forEach((fn) => fn(this._state));
|
|
},
|
|
|
|
onChange(fn) { this._listeners.push(fn); },
|
|
|
|
// Push the theme into CSS. The app chrome reads data-theme; the canvas
|
|
// background and grid are plain custom properties so a user override is a
|
|
// one-line change with no repaint logic.
|
|
apply() {
|
|
const p = this.preset();
|
|
const root = document.documentElement;
|
|
root.setAttribute("data-theme", this._state.mode);
|
|
root.style.setProperty("--bg-canvas", this._state.canvasBg || p.canvasBg);
|
|
root.style.setProperty("--grid-dot", this._state.gridDot || p.gridDot);
|
|
},
|
|
|
|
// ── canvas colours (Konva cannot read CSS variables) ──
|
|
deviceFill(type) {
|
|
if (this._state.deviceFill) return this._state.deviceFill;
|
|
const base = DEVICE_HUES[type] || "#1e1e2e";
|
|
const p = this.preset();
|
|
return p.lightness == null ? base : _relight(base, p.lightness);
|
|
},
|
|
deviceStroke() { return this._state.deviceStroke || this.preset().deviceStroke; },
|
|
deviceText() { return this.preset().deviceText; },
|
|
deviceSubtext() { return this.preset().deviceSubtext; },
|
|
deviceRef() { return this.preset().deviceRef; },
|
|
pinFill() { return this.preset().pinFill; },
|
|
pinStroke() { return this.preset().pinStroke; },
|
|
notch() { return this.preset().notch; },
|
|
rowBg() { return this.preset().rowBg; },
|
|
wireLabel() { return this.preset().wireLabel; },
|
|
harnessBg() { return this.preset().harnessBg; },
|
|
harnessEdge() { return this.preset().harnessEdge; },
|
|
harnessText() { return this.preset().harnessText; },
|
|
loomShadow() { return this.preset().loomShadow; },
|
|
canvasBg() { return this._state.canvasBg || this.preset().canvasBg; },
|
|
};
|
|
|
|
Theme.load();
|
|
Theme.apply();
|