f32203b630
- FastAPI + SQLite backend with routers for diagrams, devices, wires, bundles, connectors, export, and Octopart/Nexar search - Konva.js canvas: ortho/direct/curved routing, wire crossings, harness mode, snap-to-grid, multi-select, drag, resize, undo - Wire features: color/stripe, twisted pair helix, shielded glow, gauge size label, multi-core bundle grouping, length/unit tracking - Device library: connector, terminal block, cable, splice, label, group, relay, fuse, switch, component with custom connector support - Exports: BOM CSV, assembly TXT, JSON, formboard layout, PNG image - Auto-migration for schema changes via ALTER TABLE at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
273 lines
9.9 KiB
JavaScript
273 lines
9.9 KiB
JavaScript
const WIRE_COLORS = [
|
||
{ name: "Black", hex: "#000000" },
|
||
{ name: "Brown", hex: "#8B4513" },
|
||
{ name: "Red", hex: "#CC0000" },
|
||
{ name: "Orange", hex: "#FF8C00" },
|
||
{ name: "Yellow", hex: "#FFD700" },
|
||
{ name: "Green", hex: "#007700" },
|
||
{ name: "Blue", hex: "#0000CC" },
|
||
{ name: "Violet", hex: "#7B00CC" },
|
||
{ name: "Grey", hex: "#808080" },
|
||
{ name: "White", hex: "#DDDDDD" },
|
||
{ name: "Pink", hex: "#FF69B4" },
|
||
{ name: "Tan", hex: "#D2B48C" },
|
||
];
|
||
|
||
const SIDE = { LEFT: "left", RIGHT: "right", TOP: "top", BOTTOM: "bottom" };
|
||
|
||
function makePins(side, count, w, h, prefix = "") {
|
||
return Array.from({ length: count }, (_, i) => {
|
||
const t = (i + 1) / (count + 1);
|
||
const x = side === SIDE.LEFT ? 0 : side === SIDE.RIGHT ? w : t * w;
|
||
const y = side === SIDE.TOP ? 0 : side === SIDE.BOTTOM ? h : t * h;
|
||
return { id: `${prefix}pin_${i + 1}`, name: String(i + 1), side, x_offset: x, y_offset: y };
|
||
});
|
||
}
|
||
|
||
const DEVICE_TYPES = {
|
||
connector: {
|
||
label: "Connector",
|
||
description: "Multi-pin connector (inline/header)",
|
||
icon: "🔌",
|
||
defaultProps: { pinCount: 4, orientation: "right", partNumber: "", manufacturer: "" },
|
||
defaultSize: (p) => ({ w: 100, h: Math.max(60, (p.pinCount || 4) * 20 + 20) }),
|
||
getPins: (p, w, h) => makePins(p.orientation || SIDE.RIGHT, p.pinCount || 4, w, h),
|
||
},
|
||
terminal_block: {
|
||
label: "Terminal Block",
|
||
description: "Screw terminal block",
|
||
icon: "⬛",
|
||
defaultProps: { terminalCount: 4, partNumber: "", manufacturer: "" },
|
||
defaultSize: (p) => ({ w: Math.max(60, (p.terminalCount || 4) * 24), h: 60 }),
|
||
getPins: (p, w, h) => {
|
||
const count = p.terminalCount || 4;
|
||
const pins = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const x = ((i + 0.5) / count) * w;
|
||
pins.push({ id: `t${i + 1}`, name: String(i + 1), side: SIDE.TOP, x_offset: x, y_offset: 0 });
|
||
pins.push({ id: `b${i + 1}`, name: String(i + 1), side: SIDE.BOTTOM, x_offset: x, y_offset: h });
|
||
}
|
||
return pins;
|
||
},
|
||
},
|
||
component: {
|
||
label: "Component",
|
||
description: "Generic component / IC",
|
||
icon: "⬜",
|
||
defaultProps: { leftPins: 2, rightPins: 2, partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 120, h: 80 }),
|
||
getPins: (p, w, h) => {
|
||
const left = makePins(SIDE.LEFT, p.leftPins || 2, w, h, "L");
|
||
const right = makePins(SIDE.RIGHT, p.rightPins || 2, w, h, "R");
|
||
left.forEach((pin, i) => (pin.name = `L${i + 1}`));
|
||
right.forEach((pin, i) => (pin.name = `R${i + 1}`));
|
||
return [...left, ...right];
|
||
},
|
||
},
|
||
splice: {
|
||
label: "Splice / Junction",
|
||
description: "Wire splice or junction point",
|
||
icon: "✕",
|
||
defaultProps: {},
|
||
defaultSize: () => ({ w: 32, h: 32 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "left", name: "L", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "right", name: "R", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
{ id: "top", name: "T", side: SIDE.TOP, x_offset: w / 2, y_offset: 0 },
|
||
{ id: "bottom", name: "B", side: SIDE.BOTTOM, x_offset: w / 2, y_offset: h },
|
||
],
|
||
},
|
||
label: {
|
||
label: "Note / Label",
|
||
description: "Text annotation on the diagram",
|
||
icon: "📝",
|
||
defaultProps: { text: "Note" },
|
||
defaultSize: () => ({ w: 160, h: 40 }),
|
||
getPins: () => [],
|
||
},
|
||
group: {
|
||
label: "Section Box",
|
||
description: "Labeled section rectangle for visual grouping",
|
||
icon: "⬚",
|
||
defaultProps: { fillColor: "#2828a0", fillOpacity: 0.15 },
|
||
defaultSize: () => ({ w: 320, h: 220 }),
|
||
getPins: () => [],
|
||
},
|
||
|
||
// ── Electrical components ───────────────────────────────────────────────────
|
||
fuse: {
|
||
label: "Fuse",
|
||
description: "In-line fuse or fusible link",
|
||
icon: "⚡",
|
||
defaultProps: { ampRating: "10A", fuseType: "blade", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 80, h: 44 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "A", name: "A", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "B", name: "B", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
relay: {
|
||
label: "Relay",
|
||
description: "Electromagnetic relay — SPDT 5-pin (85/86/30/87/87a)",
|
||
icon: "🔁",
|
||
defaultProps: { coilVoltage: "12V", ampRating: "30A", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 110, h: 100 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "85", name: "85", side: SIDE.LEFT, x_offset: 0, y_offset: h * 0.30 },
|
||
{ id: "86", name: "86", side: SIDE.LEFT, x_offset: 0, y_offset: h * 0.70 },
|
||
{ id: "87a", name: "87a", side: SIDE.RIGHT, x_offset: w, y_offset: h * 0.20 },
|
||
{ id: "30", name: "30", side: SIDE.RIGHT, x_offset: w, y_offset: h * 0.50 },
|
||
{ id: "87", name: "87", side: SIDE.RIGHT, x_offset: w, y_offset: h * 0.80 },
|
||
],
|
||
},
|
||
switch: {
|
||
label: "Switch",
|
||
description: "SPDT switch — common, normally-open, normally-closed",
|
||
icon: "⎘",
|
||
defaultProps: { switchType: "SPDT", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 90, h: 70 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "C", name: "C", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "NO", name: "NO", side: SIDE.RIGHT, x_offset: w, y_offset: h * 0.30 },
|
||
{ id: "NC", name: "NC", side: SIDE.RIGHT, x_offset: w, y_offset: h * 0.70 },
|
||
],
|
||
},
|
||
bulb: {
|
||
label: "Bulb / Light",
|
||
description: "Indicator lamp or lighting load",
|
||
icon: "💡",
|
||
defaultProps: { voltage: "12V", wattage: "", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 80, h: 50 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "+", name: "+", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "-", name: "−", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
motor: {
|
||
label: "Motor",
|
||
description: "DC motor or actuator",
|
||
icon: "⚙",
|
||
defaultProps: { voltage: "12V", ampRating: "", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 90, h: 60 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "+", name: "+", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "-", name: "−", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
diode: {
|
||
label: "Diode",
|
||
description: "Diode, Schottky, or TVS — anode / cathode",
|
||
icon: "⊳",
|
||
defaultProps: { diodeType: "rectifier", voltage: "", current: "", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 80, h: 44 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "A", name: "A", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "K", name: "K", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
resistor: {
|
||
label: "Resistor",
|
||
description: "Fixed resistor or load",
|
||
icon: "Ω",
|
||
defaultProps: { resistance: "", wattage: "", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 80, h: 44 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "1", name: "1", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "2", name: "2", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
capacitor: {
|
||
label: "Capacitor",
|
||
description: "Electrolytic or film capacitor",
|
||
icon: "⊏",
|
||
defaultProps: { capacitance: "", voltage: "", partNumber: "", manufacturer: "" },
|
||
defaultSize: () => ({ w: 80, h: 44 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "+", name: "+", side: SIDE.LEFT, x_offset: 0, y_offset: h / 2 },
|
||
{ id: "-", name: "−", side: SIDE.RIGHT, x_offset: w, y_offset: h / 2 },
|
||
],
|
||
},
|
||
ground: {
|
||
label: "Ground",
|
||
description: "Chassis or signal ground reference",
|
||
icon: "⏚",
|
||
defaultProps: { groundType: "chassis", reference: "" },
|
||
defaultSize: () => ({ w: 60, h: 50 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "GND", name: "GND", side: SIDE.TOP, x_offset: w / 2, y_offset: 0 },
|
||
],
|
||
},
|
||
power: {
|
||
label: "Power Rail",
|
||
description: "Battery positive, ignition, or supply rail",
|
||
icon: "⊕",
|
||
defaultProps: { voltage: "12V", sourceType: "battery", reference: "" },
|
||
defaultSize: () => ({ w: 70, h: 50 }),
|
||
getPins: (p, w, h) => [
|
||
{ id: "PWR", name: "+", side: SIDE.BOTTOM, x_offset: w / 2, y_offset: h },
|
||
],
|
||
},
|
||
};
|
||
|
||
const CABLE_DEFAULT_COLORS = [
|
||
"#dddddd", "#cc0000", "#111111", "#007700",
|
||
"#0000cc", "#ff8c00", "#8b4513", "#7b00cc",
|
||
];
|
||
|
||
const DEVICE_TYPES_EXTRA = {
|
||
cable: {
|
||
label: "Cable",
|
||
description: "Multi-conductor cable under one jacket",
|
||
icon: "🔗",
|
||
defaultProps: {
|
||
conductorCount: 4,
|
||
partNumber: "",
|
||
manufacturer: "",
|
||
jacketColor: "#2a2a2a",
|
||
sleeveLength: 60,
|
||
conductors: [
|
||
{ name: "1", color: "#dddddd" },
|
||
{ name: "2", color: "#cc0000" },
|
||
{ name: "3", color: "#111111" },
|
||
{ name: "4", color: "#007700" },
|
||
],
|
||
},
|
||
defaultSize: (p) => {
|
||
const n = p.conductorCount || p.conductors?.length || 4;
|
||
return { w: 200, h: n * 24 + 40 };
|
||
},
|
||
getPins: (p, w, h) => {
|
||
const conductors = p.conductors || Array.from({ length: p.conductorCount || 4 }, (_, i) => ({ name: String(i + 1) }));
|
||
return conductors.flatMap((c, i) => {
|
||
const yOff = 26 + i * 24 + 12;
|
||
return [
|
||
{ id: `L${i + 1}`, name: c.name || String(i + 1), side: "left", x_offset: 0, y_offset: yOff },
|
||
{ id: `R${i + 1}`, name: c.name || String(i + 1), side: "right", x_offset: w, y_offset: yOff },
|
||
];
|
||
});
|
||
},
|
||
},
|
||
};
|
||
|
||
Object.assign(DEVICE_TYPES, DEVICE_TYPES_EXTRA);
|
||
|
||
function buildDefaultDevice(typeKey, diagramId) {
|
||
const type = DEVICE_TYPES[typeKey];
|
||
if (!type) return null;
|
||
const props = { ...type.defaultProps };
|
||
const { w, h } = type.defaultSize(props);
|
||
const pins = type.getPins(props, w, h);
|
||
return {
|
||
diagram_id: diagramId,
|
||
device_type: typeKey,
|
||
label: type.label,
|
||
reference: "",
|
||
x: 200,
|
||
y: 200,
|
||
width: w,
|
||
height: h,
|
||
properties: props,
|
||
pins,
|
||
};
|
||
}
|