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:
+521
-2
@@ -54,6 +54,8 @@ class WiringApp {
|
||||
this._bindPinoutModal();
|
||||
this._bindLibrarySearch();
|
||||
this._bindFormboard();
|
||||
this._bindPdm();
|
||||
this._bindTheme();
|
||||
|
||||
this.loadDiagramList()
|
||||
.then(() => this._autoOpenLast())
|
||||
@@ -226,6 +228,20 @@ class WiringApp {
|
||||
item.addEventListener("dblclick", () => { if (!this.diagramId) return this._needDiagram(); this.addDevice(key, 200 + Math.random() * 100, 150 + Math.random() * 100); });
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
// Standard parts (partsLibrary.js) also live here, not just under the
|
||||
// Connector Library tab — they are devices, and this is where you look for
|
||||
// one. They reuse the connector add path, so a dropped part arrives with
|
||||
// its pins labelled and its standard wire colours attached.
|
||||
if (typeof SKUDAK_PARTS !== "undefined") {
|
||||
Object.entries(SKUDAK_PARTS).forEach(([category, parts]) => {
|
||||
const header = document.createElement("div");
|
||||
header.className = "lib-section-header";
|
||||
header.textContent = category;
|
||||
container.appendChild(header);
|
||||
parts.forEach(part => container.appendChild(this._makeBuiltinItem(part)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connector library ─────────────────────────────────────────────────────────
|
||||
@@ -996,9 +1012,20 @@ class WiringApp {
|
||||
document.getElementById(`btn-route-${mode}`)?.classList.add("active");
|
||||
}
|
||||
|
||||
// True while any modal overlay is up. Canvas shortcuts must stand down then,
|
||||
// otherwise Delete inside the fuse box editor deletes the device itself.
|
||||
_anyModalOpen() {
|
||||
return ["connector-modal", "pinout-modal", "drc-modal", "git-modal", "pdm-modal", "theme-modal"]
|
||||
.some((id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el && el.style.display && el.style.display !== "none";
|
||||
});
|
||||
}
|
||||
|
||||
_bindKeyboard() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName)) return;
|
||||
if (this._anyModalOpen()) return;
|
||||
if (e.key === "Delete" || e.key === "Backspace") this.deleteSelected();
|
||||
if (e.key === "Escape") { this.setMode("select"); this._closeCtx(); }
|
||||
if (e.key === "w") this.setMode("wire");
|
||||
@@ -1725,17 +1752,507 @@ class WiringApp {
|
||||
return d.properties?.conductors?.[parseInt(m[1]) - 1]?.color || null;
|
||||
}
|
||||
|
||||
// Standard wire for a pin on a standard part — see partsLibrary.js.
|
||||
// Returns null for ordinary connectors so existing behaviour is unchanged.
|
||||
_standardWireForPin(deviceId, pinId) {
|
||||
if (typeof standardWireForPin !== "function") return null;
|
||||
return standardWireForPin(this.canvas.deviceData.get(deviceId), pinId);
|
||||
}
|
||||
|
||||
// ── Appearance ────────────────────────────────────────────────────────────
|
||||
// Theme changes repaint the canvas explicitly: Konva bakes colours in when it
|
||||
// draws, so unlike the CSS-driven chrome it will not update on its own.
|
||||
|
||||
_bindTheme() {
|
||||
const modal = document.getElementById("theme-modal");
|
||||
if (!modal) return;
|
||||
|
||||
// Handy backgrounds for the case this exists to solve — a black wire on a
|
||||
// near-black canvas, or a white one on white.
|
||||
const BG_SWATCHES = ["#131320", "#1c1c1c", "#243447", "#0d1b16", "#f7f8fa", "#e8e4d9", "#ffffff"];
|
||||
const wrap = document.getElementById("theme-bg-swatches");
|
||||
BG_SWATCHES.forEach((hex) => {
|
||||
const b = document.createElement("button");
|
||||
b.className = "theme-swatch";
|
||||
b.style.background = hex;
|
||||
b.title = hex;
|
||||
b.addEventListener("click", () => Theme.set({ canvasBg: hex }));
|
||||
wrap.appendChild(b);
|
||||
});
|
||||
|
||||
const sync = () => {
|
||||
const t = Theme.get();
|
||||
const p = Theme.preset();
|
||||
document.getElementById("theme-dark").classList.toggle("active", t.mode === "dark");
|
||||
document.getElementById("theme-light").classList.toggle("active", t.mode === "light");
|
||||
document.getElementById("theme-canvas-bg").value = t.canvasBg || p.canvasBg;
|
||||
document.getElementById("theme-grid-dot").value = t.gridDot || p.gridDot;
|
||||
document.getElementById("theme-dev-stroke").value = t.deviceStroke || p.deviceStroke;
|
||||
const byType = !t.deviceFill;
|
||||
document.getElementById("theme-fill-bytype").checked = byType;
|
||||
const fill = document.getElementById("theme-dev-fill");
|
||||
fill.disabled = byType;
|
||||
fill.style.opacity = byType ? 0.4 : 1;
|
||||
if (!byType) fill.value = t.deviceFill;
|
||||
};
|
||||
|
||||
Theme.onChange(() => { sync(); this.canvas?.repaintAll(); });
|
||||
|
||||
document.getElementById("btn-theme")?.addEventListener("click", () => {
|
||||
sync();
|
||||
modal.style.display = "flex";
|
||||
});
|
||||
const close = () => { modal.style.display = "none"; };
|
||||
document.getElementById("theme-close")?.addEventListener("click", close);
|
||||
modal.addEventListener("mousedown", (e) => { if (e.target === modal) close(); });
|
||||
|
||||
document.getElementById("theme-dark")?.addEventListener("click", () => Theme.set({ mode: "dark" }));
|
||||
document.getElementById("theme-light")?.addEventListener("click", () => Theme.set({ mode: "light" }));
|
||||
|
||||
document.getElementById("theme-canvas-bg")?.addEventListener("input", (e) => Theme.set({ canvasBg: e.target.value }));
|
||||
document.getElementById("theme-grid-dot")?.addEventListener("input", (e) => Theme.set({ gridDot: e.target.value }));
|
||||
document.getElementById("theme-dev-stroke")?.addEventListener("input", (e) => Theme.set({ deviceStroke: e.target.value }));
|
||||
document.getElementById("theme-dev-fill")?.addEventListener("input", (e) => Theme.set({ deviceFill: e.target.value }));
|
||||
document.getElementById("theme-fill-bytype")?.addEventListener("change", (e) => {
|
||||
// Unticking needs a concrete starting colour, so seed from the current
|
||||
// connector tint rather than dropping the user on black.
|
||||
Theme.set({ deviceFill: e.target.checked ? null : (Theme.get().deviceFill || "#2b3550") });
|
||||
});
|
||||
|
||||
document.getElementById("theme-reset")?.addEventListener("click", () => Theme.reset());
|
||||
|
||||
sync();
|
||||
}
|
||||
|
||||
// ── Fuse box configurator ─────────────────────────────────────────────────
|
||||
// A spatial editor over the module's 280-cavity grid. Work happens on a draft
|
||||
// copy so Cancel genuinely discards; Apply rewrites the device's properties
|
||||
// and regenerates its pins, so what you place becomes wireable immediately.
|
||||
|
||||
openPdmConfigurator(device) {
|
||||
if (!device || device.device_type !== "pdm" || typeof PDM_MODULES === "undefined") return;
|
||||
this._pdmDevice = device;
|
||||
this._pdmDraft = {
|
||||
moduleId: device.properties?.moduleId || "gep-frh-a24",
|
||||
circuits: JSON.parse(JSON.stringify(device.properties?.circuits || [])),
|
||||
};
|
||||
this._pdmSel = null;
|
||||
|
||||
const sel = document.getElementById("pdm-module");
|
||||
sel.innerHTML = "";
|
||||
Object.values(PDM_MODULES).forEach((m) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = m.id;
|
||||
o.textContent = `${m.name} — ${m.cols * m.rows} cavities (${m.cols}x${m.rows})`;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.value = this._pdmDraft.moduleId;
|
||||
|
||||
document.getElementById("pdm-label").value = device.label || "";
|
||||
document.getElementById("pdm-linked-ref").textContent =
|
||||
device.reference ? `linked to ${device.reference}` : `linked to device #${device.id}`;
|
||||
|
||||
this._renderPdm();
|
||||
document.getElementById("pdm-modal").style.display = "flex";
|
||||
document.getElementById("pdm-grid").focus();
|
||||
}
|
||||
|
||||
_closePdm() {
|
||||
document.getElementById("pdm-modal").style.display = "none";
|
||||
this._pdmDevice = null;
|
||||
this._pdmDraft = null;
|
||||
this._pdmSel = null;
|
||||
}
|
||||
|
||||
_pdmCell() { return 38; }
|
||||
|
||||
_renderPdm() {
|
||||
const draft = this._pdmDraft;
|
||||
if (!draft) return;
|
||||
const v = pdmValidate(draft);
|
||||
const mod = v.module;
|
||||
const CELL = this._pdmCell();
|
||||
const PAD = 6;
|
||||
|
||||
// ── grid ──
|
||||
const grid = document.getElementById("pdm-grid");
|
||||
grid.innerHTML = "";
|
||||
grid.style.width = `${mod.cols * CELL + PAD * 2}px`;
|
||||
grid.style.height = `${mod.rows * CELL + PAD * 2}px`;
|
||||
|
||||
for (let r = 0; r < mod.rows; r++) {
|
||||
for (let c = 0; c < mod.cols; c++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "pdm-cell";
|
||||
cell.style.left = `${PAD + c * CELL}px`;
|
||||
cell.style.top = `${PAD + r * CELL}px`;
|
||||
cell.style.width = `${CELL}px`;
|
||||
cell.style.height = `${CELL}px`;
|
||||
const num = document.createElement("span");
|
||||
num.className = "pdm-cell-num";
|
||||
num.textContent = r * mod.cols + c + 1;
|
||||
cell.appendChild(num);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// Bus bars paint last so they lie visibly across the blades they feed.
|
||||
const painted = [...(draft.circuits || [])].sort(
|
||||
(a, b) => (pdmIsBus(a) ? 1 : 0) - (pdmIsBus(b) ? 1 : 0));
|
||||
painted.forEach((ci) => {
|
||||
const comp = PDM_COMPONENTS[ci.type];
|
||||
if (!comp) return;
|
||||
const { w, h } = pdmSize(comp, ci.rot || 0);
|
||||
const el = document.createElement("div");
|
||||
el.className = (comp.isBus ? "pdm-bus" : "pdm-part")
|
||||
+ (this._pdmSel === ci.id ? " sel" : "")
|
||||
+ (v.badIds.has(ci.id) ? " bad" : "");
|
||||
el.style.background = comp.colour;
|
||||
el.dataset.id = ci.id;
|
||||
|
||||
if (comp.isBus) {
|
||||
// Drawn as a narrow strip lying across the run, so the components it
|
||||
// feeds stay visible underneath it.
|
||||
const T = 15;
|
||||
const horiz = w > h;
|
||||
el.style.left = `${PAD + ci.col * CELL + (horiz ? 3 : CELL / 2 - T / 2)}px`;
|
||||
el.style.top = `${PAD + ci.row * CELL + (horiz ? CELL / 2 - T / 2 : 3)}px`;
|
||||
el.style.width = `${horiz ? w * CELL - 6 : T}px`;
|
||||
el.style.height = `${horiz ? T : h * CELL - 6}px`;
|
||||
el.title = `${ci.name || comp.label} — links ${Math.max(w, h)} cavities`;
|
||||
} else {
|
||||
el.style.left = `${PAD + ci.col * CELL + 2}px`;
|
||||
el.style.top = `${PAD + ci.row * CELL + 2}px`;
|
||||
el.style.width = `${w * CELL - 4}px`;
|
||||
el.style.height = `${h * CELL - 4}px`;
|
||||
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "pdm-part-tag";
|
||||
tag.textContent = `${comp.short} ${ci.rating}`;
|
||||
el.appendChild(tag);
|
||||
if (ci.name) {
|
||||
const nm = document.createElement("span");
|
||||
nm.className = "pdm-part-name";
|
||||
nm.textContent = ci.name;
|
||||
el.appendChild(nm);
|
||||
}
|
||||
|
||||
// terminal markers, positioned in the rotated footprint. A terminal
|
||||
// sitting on a bus bar is fed by it, so it is marked rather than wired.
|
||||
pdmTerminals(ci, mod).forEach((t) => {
|
||||
const onBus = t.fn === "KL30" && pdmBusAt(draft, t.c, t.r);
|
||||
const m = document.createElement("span");
|
||||
m.className = "pdm-term" + (onBus ? " bus-fed" : "");
|
||||
m.textContent = t.t;
|
||||
if (onBus) m.title = "Fed by bus bar";
|
||||
m.style.left = `${(t.c - ci.col) * CELL + CELL / 2 - 6.5 - 2}px`;
|
||||
m.style.top = `${(t.r - ci.row) * CELL + CELL - 15 - 2}px`;
|
||||
el.appendChild(m);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener("mousedown", (e) => this._pdmDragStart(e, ci));
|
||||
grid.appendChild(el);
|
||||
});
|
||||
|
||||
// ── capacity ──
|
||||
const pct = Math.min(100, (v.used / v.total) * 100);
|
||||
const fill = document.getElementById("pdm-bar-fill");
|
||||
fill.style.width = `${pct}%`;
|
||||
fill.className = "pdm-bar-fill" + (!v.ok ? " over" : pct > 85 ? " warn" : "");
|
||||
document.getElementById("pdm-capacity-text").textContent =
|
||||
`${v.used} / ${v.total} cavities · ${(draft.circuits || []).length} components`;
|
||||
document.getElementById("pdm-warn").textContent = v.ok ? "" : v.errors[0];
|
||||
document.getElementById("pdm-save").disabled = !v.ok;
|
||||
|
||||
this._renderPdmSel();
|
||||
this._renderPdmPinout(mod);
|
||||
}
|
||||
|
||||
_renderPdmSel() {
|
||||
const ci = (this._pdmDraft?.circuits || []).find((x) => x.id === this._pdmSel);
|
||||
document.getElementById("pdm-sel").style.display = ci ? "" : "none";
|
||||
document.getElementById("pdm-sel-empty").style.display = ci ? "none" : "";
|
||||
if (!ci) return;
|
||||
const comp = PDM_COMPONENTS[ci.type];
|
||||
|
||||
document.getElementById("pdm-sel-name").value = ci.name || "";
|
||||
|
||||
const selType = document.getElementById("pdm-sel-type");
|
||||
selType.innerHTML = "";
|
||||
Object.values(PDM_COMPONENTS).forEach((c) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.key;
|
||||
o.textContent = `${c.label} (${c.w}x${c.h})`;
|
||||
selType.appendChild(o);
|
||||
});
|
||||
selType.value = ci.type;
|
||||
|
||||
const selRating = document.getElementById("pdm-sel-rating");
|
||||
selRating.innerHTML = "";
|
||||
comp.ratings.forEach((r) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = r; o.textContent = r;
|
||||
selRating.appendChild(o);
|
||||
});
|
||||
selRating.value = ci.rating;
|
||||
}
|
||||
|
||||
_renderPdmPinout(mod) {
|
||||
const body = document.getElementById("pdm-pinout");
|
||||
body.innerHTML = "";
|
||||
const rows = [];
|
||||
(this._pdmDraft?.circuits || []).forEach((ci) => {
|
||||
const comp = PDM_COMPONENTS[ci.type];
|
||||
if (!comp) return;
|
||||
pdmTerminals(ci, mod).forEach((t) => {
|
||||
rows.push({ ci, comp, t });
|
||||
});
|
||||
});
|
||||
rows.sort((a, b) => a.t.cavity - b.t.cavity);
|
||||
|
||||
rows.forEach(({ ci, comp, t }) => {
|
||||
const tr = document.createElement("tr");
|
||||
if (this._pdmSel === ci.id) tr.style.background = "#1e1e3a";
|
||||
|
||||
const c1 = document.createElement("td");
|
||||
c1.className = "pdm-cavity";
|
||||
c1.textContent = t.cavity;
|
||||
tr.appendChild(c1);
|
||||
|
||||
const c2 = document.createElement("td");
|
||||
c2.className = "pdm-cavity";
|
||||
c2.textContent = t.t;
|
||||
tr.appendChild(c2);
|
||||
|
||||
const c3 = document.createElement("td");
|
||||
// Show the pin name the device will actually get, so naming a circuit
|
||||
// "VCU" visibly turns its terminals into "VCU in" / "VCU out".
|
||||
c3.textContent = pdmPinName(ci, comp, t);
|
||||
if (!ci.name) c3.style.color = "var(--text-muted)";
|
||||
tr.appendChild(c3);
|
||||
|
||||
tr.addEventListener("click", () => { this._pdmSel = ci.id; this._renderPdm(); });
|
||||
body.appendChild(tr);
|
||||
});
|
||||
|
||||
document.getElementById("pdm-empty").style.display = rows.length ? "none" : "";
|
||||
}
|
||||
|
||||
// ── grid drag ──
|
||||
_pdmDragStart(e, circuit) {
|
||||
e.preventDefault();
|
||||
this._pdmSel = circuit.id;
|
||||
this._renderPdm();
|
||||
|
||||
const grid = document.getElementById("pdm-grid");
|
||||
const CELL = this._pdmCell();
|
||||
const PAD = 6;
|
||||
const rect = grid.getBoundingClientRect();
|
||||
const startCol = circuit.col;
|
||||
const startRow = circuit.row;
|
||||
// where inside the component the grab happened, in cells
|
||||
const grabC = Math.floor((e.clientX - rect.left - PAD) / CELL) - startCol;
|
||||
const grabR = Math.floor((e.clientY - rect.top - PAD) / CELL) - startRow;
|
||||
|
||||
// The component follows the cursor anywhere inside the grid, overlapping or
|
||||
// not. Refusing invalid positions would mean an already-overlapping part
|
||||
// could never be dragged apart — validation marks the clash in red and
|
||||
// blocks Apply instead, which leaves you a way out.
|
||||
const move = (ev) => {
|
||||
const p = pdmClamp(
|
||||
this._pdmDraft, circuit,
|
||||
Math.floor((ev.clientX - rect.left - PAD) / CELL) - grabC,
|
||||
Math.floor((ev.clientY - rect.top - PAD) / CELL) - grabR,
|
||||
circuit.rot || 0,
|
||||
);
|
||||
if (p.col === circuit.col && p.row === circuit.row) return;
|
||||
circuit.col = p.col;
|
||||
circuit.row = p.row;
|
||||
this._renderPdm();
|
||||
};
|
||||
const up = () => {
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseup", up);
|
||||
this._renderPdm();
|
||||
};
|
||||
window.addEventListener("mousemove", move);
|
||||
window.addEventListener("mouseup", up);
|
||||
}
|
||||
|
||||
_pdmRotate() {
|
||||
const ci = (this._pdmDraft?.circuits || []).find((x) => x.id === this._pdmSel);
|
||||
if (!ci) return;
|
||||
// Always rotate; just pull the footprint back inside the grid. Overlap is
|
||||
// allowed and flagged, same as dragging.
|
||||
const next = ((ci.rot || 0) + 90) % 360;
|
||||
const p = pdmClamp(this._pdmDraft, ci, ci.col, ci.row, next);
|
||||
ci.rot = next;
|
||||
ci.col = p.col;
|
||||
ci.row = p.row;
|
||||
this._renderPdm();
|
||||
}
|
||||
|
||||
_pdmAdd(typeKey) {
|
||||
const draft = this._pdmDraft;
|
||||
if (!draft) return;
|
||||
const ci = pdmNewCircuit(typeKey);
|
||||
const spot = pdmAutoPlace(draft, ci);
|
||||
if (!spot) {
|
||||
document.getElementById("pdm-warn").textContent = "No free cavities for that component.";
|
||||
return;
|
||||
}
|
||||
Object.assign(ci, spot);
|
||||
draft.circuits.push(ci);
|
||||
this._pdmSel = ci.id;
|
||||
this._renderPdm();
|
||||
document.getElementById("pdm-sel-name").focus();
|
||||
}
|
||||
|
||||
_pdmRemove() {
|
||||
const draft = this._pdmDraft;
|
||||
if (!draft || !this._pdmSel) return;
|
||||
const i = draft.circuits.findIndex((x) => x.id === this._pdmSel);
|
||||
if (i < 0) return;
|
||||
draft.circuits.splice(i, 1);
|
||||
this._pdmSel = null;
|
||||
this._renderPdm();
|
||||
}
|
||||
|
||||
async _savePdm() {
|
||||
const device = this._pdmDevice;
|
||||
const draft = this._pdmDraft;
|
||||
if (!device || !draft) return;
|
||||
const v = pdmValidate(draft);
|
||||
if (!v.ok) {
|
||||
document.getElementById("pdm-warn").textContent = v.errors[0];
|
||||
return;
|
||||
}
|
||||
|
||||
const mod = v.module;
|
||||
const label = document.getElementById("pdm-label").value.trim() || device.label || mod.name;
|
||||
const props = {
|
||||
...device.properties,
|
||||
moduleId: draft.moduleId,
|
||||
circuits: draft.circuits,
|
||||
partNumber: mod.partNumber,
|
||||
manufacturer: mod.manufacturer,
|
||||
};
|
||||
const size = DEVICE_TYPES.pdm.defaultSize(props);
|
||||
const pins = pdmPins(props, size.w, size.h);
|
||||
|
||||
try {
|
||||
const updated = await api.devices.update(device.id, {
|
||||
label, properties: props, pins,
|
||||
width: size.w, height: size.h,
|
||||
});
|
||||
this.canvas.updateDevice(updated);
|
||||
this._showDeviceProps(updated);
|
||||
this._flashSaved();
|
||||
this._closePdm();
|
||||
} catch (e) {
|
||||
console.error("Fuse box save failed:", e);
|
||||
document.getElementById("pdm-warn").textContent = "Could not save — see console.";
|
||||
}
|
||||
}
|
||||
|
||||
_bindPdm() {
|
||||
document.getElementById("prop-pdm-btn")?.addEventListener("click", () => {
|
||||
if (this._propDevice) this.openPdmConfigurator(this._propDevice);
|
||||
});
|
||||
document.getElementById("pdm-cancel")?.addEventListener("click", () => this._closePdm());
|
||||
document.getElementById("pdm-save")?.addEventListener("click", () => this._savePdm());
|
||||
document.getElementById("pdm-rotate")?.addEventListener("click", () => this._pdmRotate());
|
||||
document.getElementById("pdm-delete")?.addEventListener("click", () => this._pdmRemove());
|
||||
|
||||
document.getElementById("pdm-module")?.addEventListener("change", (e) => {
|
||||
if (!this._pdmDraft) return;
|
||||
this._pdmDraft.moduleId = e.target.value;
|
||||
// A narrower or shorter box would otherwise strand tiles off the edge
|
||||
// where they cannot be clicked, so pull everything back in.
|
||||
pdmClampAll(this._pdmDraft);
|
||||
this._renderPdm();
|
||||
});
|
||||
|
||||
const ADD = { fuse: "fuse", spdt: "relay_spdt", spst: "relay_spst", diode: "diode", breaker: "breaker", bus: "bus4" };
|
||||
Object.entries(ADD).forEach(([kind, typeKey]) => {
|
||||
document.getElementById(`pdm-add-${kind}`)?.addEventListener("click", () => this._pdmAdd(typeKey));
|
||||
});
|
||||
|
||||
document.getElementById("pdm-sel-name")?.addEventListener("input", (e) => {
|
||||
const ci = (this._pdmDraft?.circuits || []).find((x) => x.id === this._pdmSel);
|
||||
if (!ci) return;
|
||||
ci.name = e.target.value;
|
||||
// repaint the tile label without stealing focus from the field
|
||||
const el = document.querySelector(`.pdm-part[data-id="${ci.id}"] .pdm-part-name`);
|
||||
if (el) el.textContent = ci.name;
|
||||
else this._renderPdm();
|
||||
this._renderPdmPinout(PDM_MODULES[this._pdmDraft.moduleId]);
|
||||
});
|
||||
document.getElementById("pdm-sel-rating")?.addEventListener("change", (e) => {
|
||||
const ci = (this._pdmDraft?.circuits || []).find((x) => x.id === this._pdmSel);
|
||||
if (ci) { ci.rating = e.target.value; this._renderPdm(); }
|
||||
});
|
||||
document.getElementById("pdm-sel-type")?.addEventListener("change", (e) => {
|
||||
const ci = (this._pdmDraft?.circuits || []).find((x) => x.id === this._pdmSel);
|
||||
if (!ci) return;
|
||||
const prev = { type: ci.type, rating: ci.rating, rot: ci.rot };
|
||||
ci.type = e.target.value;
|
||||
ci.rating = PDM_COMPONENTS[ci.type].defaultRating;
|
||||
if (!pdmFits(this._pdmDraft, ci, ci.col, ci.row, ci.rot || 0)) {
|
||||
const spot = pdmAutoPlace(this._pdmDraft, ci);
|
||||
if (spot) Object.assign(ci, spot);
|
||||
else {
|
||||
Object.assign(ci, prev);
|
||||
document.getElementById("pdm-warn").textContent = "No room for that component.";
|
||||
}
|
||||
}
|
||||
this._renderPdm();
|
||||
});
|
||||
|
||||
// Bound on document, not the grid: clicking a tile preventDefaults the
|
||||
// mousedown so the grid never reliably holds focus.
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const modal = document.getElementById("pdm-modal");
|
||||
if (!modal || modal.style.display === "none" || !modal.style.display) return;
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName)) return;
|
||||
if (e.key === "r" || e.key === "R") { e.preventDefault(); this._pdmRotate(); }
|
||||
if (e.key === "Delete" || e.key === "Backspace") { e.preventDefault(); this._pdmRemove(); }
|
||||
if (e.key === "Escape") { e.preventDefault(); this._closePdm(); }
|
||||
});
|
||||
document.getElementById("pdm-grid")?.addEventListener("mousedown", (e) => {
|
||||
if (e.target.id === "pdm-grid" || e.target.classList.contains("pdm-cell")) {
|
||||
this._pdmSel = null;
|
||||
this._renderPdm();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("pdm-modal")?.addEventListener("mousedown", (e) => {
|
||||
if (e.target.id === "pdm-modal") this._closePdm();
|
||||
});
|
||||
}
|
||||
|
||||
async createWire(fromDev, fromPin, toDev, toPin) {
|
||||
if (!this.diagramId) return;
|
||||
try {
|
||||
const autoColor = this._conductorColorForPin(fromDev, fromPin)
|
||||
// Standard parts win: a pin that declares a wire function fixes the
|
||||
// colour, stripe and gauge so the same connection is identical on every
|
||||
// build. Cable conductor colour is next, then the plain red fallback.
|
||||
const std = this._standardWireForPin(fromDev, fromPin)
|
||||
|| this._standardWireForPin(toDev, toPin);
|
||||
const autoColor = std?.color_primary
|
||||
|| this._conductorColorForPin(fromDev, fromPin)
|
||||
|| this._conductorColorForPin(toDev, toPin)
|
||||
|| "#CC0000";
|
||||
const wire = await api.wires.create({
|
||||
diagram_id: this.diagramId,
|
||||
from_device_id: fromDev, from_pin: fromPin,
|
||||
to_device_id: toDev, to_pin: toPin,
|
||||
color_primary: autoColor, gauge: "18 AWG",
|
||||
color_primary: autoColor,
|
||||
color_stripe: std?.color_stripe ?? null,
|
||||
gauge: std?.gauge || "18 AWG",
|
||||
twisted_pair: std?.twisted_pair || false,
|
||||
label: std?.signal || std?.label || "",
|
||||
});
|
||||
this.canvas.addWire(wire);
|
||||
this.setMode("select");
|
||||
@@ -2169,6 +2686,8 @@ class WiringApp {
|
||||
document.getElementById("pin-table-body").closest(".prop-row").style.display = isGroup ? "none" : "";
|
||||
document.getElementById("prop-pinout-btn").style.display =
|
||||
(!isGroup && (device.pins || []).length > 0) ? "" : "none";
|
||||
const pdmBtn = document.getElementById("prop-pdm-btn");
|
||||
if (pdmBtn) pdmBtn.style.display = device.device_type === "pdm" ? "" : "none";
|
||||
// Reset any open search results
|
||||
const res = document.getElementById("octopart-results");
|
||||
const msg = document.getElementById("octopart-status-msg");
|
||||
|
||||
Reference in New Issue
Block a user