// ───────────────────────────────────────────────────────────────────────────── // GEP power distribution modules — fuse box grid model // // Loaded AFTER partsLibrary.js. Registers a `pdm` device type whose pins are // derived from what is physically placed in the box, so arranging the grid in // the fuse-box tool immediately changes what you can wire to on the canvas. // // GEP FRH / PDM modules are open-plan 280-footprint grids: the "way" count is // the number of Metri-Pack 280 cavities and you place your own mix of // components anywhere in them. GEP quote the FRH-A24 as taking up to 4 five- // prong relays, or 6 four-prong relays, or 12 mini fuses — all three come to // exactly 24 cavities on a 4x6 grid with the footprints below, which is where // the geometry comes from. // ───────────────────────────────────────────────────────────────────────────── const PDM_MODULES = { "gep-frh-a24": { id: "gep-frh-a24", name: "GEP FRH-A24", manufacturer: "GEP Power Products", partNumber: "FRH-A24", cols: 4, rows: 6, description: "Sealed 24-cavity 280-footprint fuse / relay holder, IP66/IP67. ~60 x 50 x 60 mm.", }, "gep-frh-a12": { id: "gep-frh-a12", name: "GEP FRH-A12", manufacturer: "GEP Power Products", partNumber: "FRH-A12", cols: 4, rows: 3, description: "Sealed 12-cavity 280-footprint fuse / relay holder.", }, "gep-pdm-48": { id: "gep-pdm-48", name: "GEP PDM 48-way", manufacturer: "GEP Power Products", partNumber: "PDM-R4A01", cols: 4, rows: 12, description: "Sealed stackable 48-cavity 280-footprint power distribution module.", }, }; // ── Component footprints ───────────────────────────────────────────────────── // w/h are in cavities. Terminals sit at (c,r) inside the unrotated footprint; // a 5-prong relay is a 2x3 block with one cavity unused, which is why four of // them exactly fill a 24-way. const PDM_COMPONENTS = { fuse: { key: "fuse", label: "Mini fuse", short: "FUSE", w: 1, h: 2, colour: "#4a7dd6", ratings: ["2A", "3A", "5A", "7.5A", "10A", "15A", "20A", "25A", "30A"], defaultRating: "10A", terminals: [ { t: "IN", c: 0, r: 0, fn: "KL30" }, { t: "OUT", c: 0, r: 1, fn: "LOAD_12V" }, ], }, relay_spdt: { key: "relay_spdt", label: "Micro relay, 5-prong SPDT", short: "RLY5", w: 2, h: 3, colour: "#8a5fd6", ratings: ["20A/10A", "30A/20A", "40A/30A"], defaultRating: "30A/20A", terminals: [ { t: "86", c: 0, r: 0, fn: "COIL" }, { t: "85", c: 1, r: 0, fn: "COIL" }, { t: "30", c: 0, r: 1, fn: "KL30" }, { t: "87a", c: 1, r: 1, fn: "LOAD_12V" }, { t: "87", c: 0, r: 2, fn: "LOAD_12V" }, ], }, relay_spst: { key: "relay_spst", label: "Micro relay, 4-prong SPST", short: "RLY4", w: 2, h: 2, colour: "#6f4fc0", ratings: ["20A", "30A", "40A"], defaultRating: "30A", terminals: [ { t: "86", c: 0, r: 0, fn: "COIL" }, { t: "85", c: 1, r: 0, fn: "COIL" }, { t: "30", c: 0, r: 1, fn: "KL30" }, { t: "87", c: 1, r: 1, fn: "LOAD_12V" }, ], }, diode: { key: "diode", label: "Mini diode", short: "DIODE", w: 1, h: 2, colour: "#3f8f6a", ratings: ["3A", "6A"], defaultRating: "6A", terminals: [ { t: "A", c: 0, r: 0, fn: "LOAD_12V" }, { t: "K", c: 0, r: 1, fn: "LOAD_12V" }, ], }, // Bus bars link a run of cavities into one node, so a single feed wire // supplies every component blade sitting on the bar. They therefore SHARE // cavities with those blades by design — `isBus` puts them on their own // layer, colliding only with each other, and any input terminal landing on a // bar stops needing its own wire. bus2: { key: "bus2", label: "Bus bar, 2-way", short: "BUS2", isBus: true, w: 1, h: 2, colour: "#b0563c", ratings: ["—"], defaultRating: "—", terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }], }, bus3: { key: "bus3", label: "Bus bar, 3-way", short: "BUS3", isBus: true, w: 1, h: 3, colour: "#b0563c", ratings: ["—"], defaultRating: "—", terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }], }, bus4: { key: "bus4", label: "Bus bar, 4-way", short: "BUS4", isBus: true, w: 1, h: 4, colour: "#b0563c", ratings: ["—"], defaultRating: "—", terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }], }, bus6: { key: "bus6", label: "Bus bar, 6-way", short: "BUS6", isBus: true, w: 1, h: 6, colour: "#b0563c", ratings: ["—"], defaultRating: "—", terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }], }, breaker: { key: "breaker", label: "Mini circuit breaker", short: "CB", w: 1, h: 2, colour: "#b5813a", ratings: ["5A", "10A", "15A", "20A", "25A", "30A"], defaultRating: "20A", terminals: [ { t: "IN", c: 0, r: 0, fn: "KL30" }, { t: "OUT", c: 0, r: 1, fn: "LOAD_12V" }, ], }, }; // ── Rotation ───────────────────────────────────────────────────────────────── // Clockwise. Returns the footprint size at that rotation. function pdmSize(comp, rot) { return (rot === 90 || rot === 270) ? { w: comp.h, h: comp.w } : { w: comp.w, h: comp.h }; } // Maps a terminal's cell inside the unrotated footprint to its cell inside the // rotated one. function pdmRotateCell(c, r, w, h, rot) { switch (rot) { case 90: return { c: h - 1 - r, r: c }; case 180: return { c: w - 1 - c, r: h - 1 - r }; case 270: return { c: r, r: w - 1 - c }; default: return { c, r }; } } let _pdmSeq = 0; function pdmNewCircuit(typeKey) { const comp = PDM_COMPONENTS[typeKey] || PDM_COMPONENTS.fuse; return { id: `c${Date.now().toString(36)}${(_pdmSeq++).toString(36)}`, type: comp.key, rating: comp.defaultRating, name: "", col: 0, row: 0, rot: 0, }; } // Every cavity a placement covers, in absolute grid coordinates. function pdmCells(circuit) { const comp = PDM_COMPONENTS[circuit.type]; if (!comp) return []; const { w, h } = pdmSize(comp, circuit.rot || 0); const cells = []; for (let r = 0; r < h; r++) { for (let c = 0; c < w; c++) cells.push({ c: circuit.col + c, r: circuit.row + r }); } return cells; } // Absolute cavity of each terminal, plus its 1-based cavity number. function pdmTerminals(circuit, mod) { const comp = PDM_COMPONENTS[circuit.type]; if (!comp) return []; const rot = circuit.rot || 0; return comp.terminals.map((t) => { const m = pdmRotateCell(t.c, t.r, comp.w, comp.h, rot); const c = circuit.col + m.c; const r = circuit.row + m.r; return { ...t, c, r, cavity: r * mod.cols + c + 1 }; }); } // Validation: overlaps and out-of-bounds. Both are physical impossibilities, // so the tool refuses to save them rather than warning and letting them through. function pdmIsBus(circuit) { return !!PDM_COMPONENTS[circuit?.type]?.isBus; } // Which bus bar, if any, covers this cavity. function pdmBusAt(props, c, r) { for (const ci of props.circuits || []) { if (!pdmIsBus(ci)) continue; if (pdmCells(ci).some((x) => x.c === c && x.r === r)) return ci; } return null; } function pdmValidate(props) { const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"]; const circuits = props.circuits || []; // Two layers: components collide with components, bus bars with bus bars. // A bar crossing a fuse blade is the whole point of a bar, not a clash. const layers = { part: new Map(), bus: new Map() }; const errors = []; const badIds = new Set(); const nameOf = (id) => { const x = circuits.find((y) => y.id === id); return x ? (x.name || PDM_COMPONENTS[x.type]?.short || "component") : "component"; }; circuits.forEach((ci) => { const comp = PDM_COMPONENTS[ci.type]; if (!comp) return; const { w, h } = pdmSize(comp, ci.rot || 0); if (ci.col < 0 || ci.row < 0 || ci.col + w > mod.cols || ci.row + h > mod.rows) { errors.push(`${ci.name || comp.short} hangs outside the box`); badIds.add(ci.id); return; } const occupied = layers[comp.isBus ? "bus" : "part"]; pdmCells(ci).forEach(({ c, r }) => { const key = `${c},${r}`; if (occupied.has(key)) { errors.push( `${ci.name || comp.short} overlaps ${nameOf(occupied.get(key))} at cavity ${r * mod.cols + c + 1}`); badIds.add(ci.id); badIds.add(occupied.get(key)); } else { occupied.set(key, ci.id); } }); }); return { module: mod, used: layers.part.size, total: mod.cols * mod.rows, busUsed: layers.bus.size, errors: [...new Set(errors)], badIds, ok: errors.length === 0, }; } // Can this component sit here without overlapping anything or leaving the box? function pdmFits(props, circuit, col, row, rot) { const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"]; const comp = PDM_COMPONENTS[circuit.type]; if (!comp) return false; const { w, h } = pdmSize(comp, rot); if (col < 0 || row < 0 || col + w > mod.cols || row + h > mod.rows) return false; // Only same-layer components block each other — a bus bar is meant to lie // across the blades it feeds. const bus = !!comp.isBus; const taken = new Set(); (props.circuits || []).forEach((o) => { if (o.id === circuit.id || pdmIsBus(o) !== bus) return; pdmCells(o).forEach(({ c, r }) => taken.add(`${c},${r}`)); }); const probe = { ...circuit, col, row, rot }; return pdmCells(probe).every(({ c, r }) => !taken.has(`${c},${r}`)); } // Clamp a placement so the whole footprint sits inside the grid. Overlap is // deliberately allowed: an overlapping component is a state you must be able to // drag your way out of, so the editor permits it and validation flags it in red // rather than refusing the move and trapping you. function pdmClamp(props, circuit, col, row, rot) { const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"]; const comp = PDM_COMPONENTS[circuit.type]; if (!comp) return { col: 0, row: 0 }; const { w, h } = pdmSize(comp, rot ?? circuit.rot ?? 0); return { col: Math.max(0, Math.min(col, mod.cols - w)), row: Math.max(0, Math.min(row, mod.rows - h)), }; } // Pull every component back inside the grid — used after a module change, so // switching to a smaller or differently-shaped box never strands a tile off // the edge where it cannot be clicked. function pdmClampAll(props) { (props.circuits || []).forEach((ci) => { const p = pdmClamp(props, ci, ci.col, ci.row, ci.rot || 0); ci.col = p.col; ci.row = p.row; }); } // First free slot, scanning row-major. Returns null if the box is full. function pdmAutoPlace(props, circuit) { const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"]; for (let r = 0; r < mod.rows; r++) { for (let c = 0; c < mod.cols; c++) { for (const rot of [0, 90]) { if (pdmFits(props, circuit, c, r, rot)) return { col: c, row: r, rot }; } } } return null; } // ── Canvas pins ────────────────────────────────────────────────────────────── // One pin per terminal, named by cavity, carrying the wire standard. Coils go // left, switched outputs right, so power reads across the device. // Friendly suffixes so a named circuit reads as plain English on the pin. // Relay terminals keep their standard numbers — "Main contactor 87" is what is // printed on the relay, so renaming it would help nobody. const PDM_TERM_LABEL = { IN: "in", OUT: "out", FEED: "feed" }; // The pin name a terminal ends up with. Naming a fuse "VCU" turns its pins into // "VCU in" and "VCU out"; unnamed components fall back to the cavity number so // they are still identifiable on the board. function pdmPinName(circuit, comp, t) { const suffix = PDM_TERM_LABEL[t.t] || t.t; const name = (circuit.name || "").trim(); return name ? `${name} ${suffix}` : `${t.cavity}·${t.t}`; } function pdmPins(props, w, h) { const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"]; const left = [{ id: "FEED", name: "FEED", fn: "KL30", note: "Module bus input" }]; const right = []; (props.circuits || []).forEach((ci) => { const comp = PDM_COMPONENTS[ci.type]; if (!comp) return; const tag = ci.name || `${comp.short}${comp.isBus ? "" : ` ${ci.rating}`}`; pdmTerminals(ci, mod).forEach((t) => { // A component input sitting on a bus bar is fed by the bar, so it needs // no wire of its own — the bar's single FEED pin covers the whole run. if (!comp.isBus && t.fn === "KL30") { const bar = pdmBusAt(props, t.c, t.r); if (bar) return; } const pin = { id: `${ci.id}_${t.t}`, name: pdmPinName(ci, comp, t), fn: t.fn, // The cavity stays in the note, so renaming a circuit never loses where // it physically sits in the box. note: comp.isBus ? `${tag} — cavity ${t.cavity}, feeds ${pdmCells(ci).map((x) => x.r * mod.cols + x.c + 1).join(", ")}` : `${tag} (${ci.rating}) — cavity ${t.cavity}`, }; (t.fn === "COIL" ? left : right).push(pin); }); }); const place = (arr, side) => arr.map((p, i) => ({ id: p.id, name: p.name, side, x_offset: side === "left" ? 0 : w, y_offset: ((i + 1) / (arr.length + 1)) * h, wire_fn: p.fn, wire_oem: null, note: p.note, })); return [...place(left, "left"), ...place(right, "right")]; } function pdmDefaultProps() { const m = PDM_MODULES["gep-frh-a24"]; return { moduleId: m.id, circuits: [], partNumber: m.partNumber, manufacturer: m.manufacturer, }; } if (typeof DEVICE_TYPES !== "undefined") { DEVICE_TYPES.pdm = { label: "Fuse / Relay Box", description: "GEP power distribution module — arrange the cavity grid in the fuse box tool", icon: "▦", defaultProps: pdmDefaultProps(), defaultSize: (p) => { const n = Math.max((p.circuits || []).length, 3); return { w: 190, h: Math.max(90, n * 24 + 40) }; }, getPins: (p, w, h) => pdmPins(p, w, h), }; }