12、电子电路设计与PCB布局组件 (概念) - /设计与仿真组件/pcb-layout-tool
76个工业组件库示例汇总
电子电路设计与 PCB 布局组件 (概念演示)
概述
这是一个交互式的 Web 组件,用于演示电子电路原理图设计和 PCB 布局的基本概念。用户可以从元件库中选择元件,在原理图和 PCB 画布上放置、移动,进行原理图连线,并触发模拟的 PCB 自动布线和高亮联动效果。请注意,这是一个高度简化的概念演示,并非功能完善的 EDA 工具。
主要功能
- 双视图编辑: 同时提供原理图 (Schematic) 和 PCB 布局 (Layout) 两个画布区域。
- 元件库与放置:
- 提供包含常用元件(电阻、电容、IC、连接器、LED)的元件库。
- 支持从库中选择元件并点击放置到任一画布(另一画布会同步生成默认位置的对应元件)。
- 基本交互:
- 选择/移动: 可选中元件并在原理图或 PCB 画布上拖动。
- 原理图连线: 在原理图画布上,可通过点击引脚来创建连接线 (Wire),并自动分配网络 (Net)。
- 概念性 PCB 功能:
- 模拟自动布线: 点击按钮后,根据原理图的网络连接,在 PCB 画布上逐步绘制简单的 L 形走线 (Trace) 连接对应引脚(视觉效果)。
- 视图联动:
- 高亮显示: 可切换高亮联动模式。选中原理图/PCB 上的元件、引脚或导线时,另一视图中对应的元素以及属于同一网络的所有元素都会高亮显示。
- 界面与风格:
- 采用苹果科技风格,界面简洁。
- 三栏响应式布局(控制 | 原理图 | PCB),适应不同屏幕尺寸。
如何使用
- 打开页面: 在浏览器中打开
index.html
。 - 选择工具: 在左侧"工具"栏选择 “选择/移动” 或 “连接 (原理图)”。
- 放置元件:
- 点击左侧"常用元件"列表中的元件类型。
- 此时工具会自动切换到"放置"模式 (光标变为 copy 状)。
- 在原理图或 PCB 画布上点击想要放置的位置。
- 放置后工具会自动切换回 “选择/移动”。
- 移动元件:
- 确保处于 “选择/移动” 工具模式。
- 在任一画布上点击并拖动元件主体进行移动。
- 原理图连线:
- 切换到 “连接 (原理图)” 工具模式。
- 在 原理图画布 上,依次点击两个需要连接的元件引脚。
- 连线会自动生成,并分配网络号。点击空白处或同一引脚可取消连线操作。
- PCB 模拟布线:
- 完成原理图连线后,点击左侧操作区的 “模拟布线 (PCB)” 按钮。
- 观察 PCB 画布上逐步绘制出连接走线的动画(仅为视觉效果)。
- 高亮联动:
- 点击 “启用/禁用高亮联动” 按钮切换模式。
- 启用后,在 “选择/移动” 模式下,点击原理图或 PCB 上的元件、引脚、导线,查看关联元素的高亮效果。
- 清空: 点击 “清空画布” 按钮将移除所有元件和连线。
文件结构
pcb-layout-tool/
├── index.html # HTML 页面结构
├── styles.css # CSS 样式定义
├── script.js # JavaScript 交互与绘图逻辑
└── README.md # 本说明文件
技术栈
- HTML5 / CSS3 (Flexbox, CSS Variables)
- JavaScript (ES6+)
- HTML Canvas 2D API
重要提示
- 概念性演示: 功能高度简化,仅用于展示基本概念和交互流程。
- 绘图基础: 使用基础的 Canvas 2D API 绘制,未进行深度优化。
- 元件符号/封装: 仅为简单的矩形或圆形示意,非标准库。
- 原理图连线: 目前仅支持直线连接,无复杂的布线规则。
- PCB 自动布线: 仅为视觉动画模拟,使用极简的 L 形路径连接引脚,不执行任何实际的布线算法、DRC (设计规则检查) 或优化。
- 数据持久化: 不支持保存或加载设计。
- 性能: 放置大量元件或执行复杂操作可能导致性能下降。
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>电子电路设计与PCB布局 - 工业控制器</title><link rel="stylesheet" href="styles.css">
</head>
<body><div class="pcb-design-container"><header class="app-header"><h1>电子电路设计 & PCB 布局</h1><p>应用于工业控制器电路板开发 (概念演示)</p></header><div class="main-layout-area"><!-- 左侧: 元件库与工具箱 --><aside class="controls-and-library-panel"><h2>元件库 & 工具</h2><div class="tool-section"><h3>工具</h3><button id="selectTool" class="tool-button active">选择/移动</button><button id="wireTool" class="tool-button">连接 (原理图)</button><!-- <button id="routeTool" class="tool-button">布线 (PCB)</button> --></div><div class="library-section"><h3>常用元件</h3><ul id="componentList"><li data-type="resistor">电阻 (R)</li><li data-type="capacitor">电容 (C)</li><li data-type="ic_dip8">IC (DIP8)</li><li data-type="ic_qfp32">IC (QFP32)</li><li data-type="connector">连接器</li><li data-type="led">LED</li></ul><small>点击选择元件,然后在画布上放置。</small></div><div class="action-section"><h3>操作</h3><button id="autoRouteButton">模拟布线 (PCB)</button><button id="clearButton">清空画布</button><button id="toggleHighlightButton">切换高亮联动</button></div><div class="status-display"><h3>状态</h3><p id="statusText">准备就绪。</p></div></aside><!-- 中间: 原理图绘制区 --><section class="schematic-view-area view-panel"><div class="view-header">原理图 (Schematic)</div><canvas id="schematicCanvas"></canvas></section><!-- 右侧: PCB 布局区 --><section class="pcb-view-area view-panel"><div class="view-header">PCB 布局 (Layout)</div><canvas id="pcbCanvas"></canvas></section></div><footer class="app-footer"><p>概念性电子电路设计与 PCB 布局组件</p></footer></div><script src="script.js"></script>
</body>
</html>
styles.css
/* styles.css - PCB Layout Tool Component */:root {--primary-bg: #ffffff;--secondary-bg: #f5f5f7;--controls-bg: #e8e8ed;--canvas-bg: #ffffff; /* White canvas background */--text-primary: #1d1d1f;--text-secondary: #515154;--accent-blue: #007aff;--accent-blue-hover: #005ec4;--border-color: #d2d2d7;--shadow-color: rgba(0, 0, 0, 0.08);--highlight-color: rgba(255, 215, 0, 0.5); /* Gold highlight */--schematic-wire-color: #333333;--pcb-trace-color: #00aa00; /* Green for traces */--component-fill: #f0f0f0;--component-stroke: #888888;--apple-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}body {font-family: var(--apple-font);margin: 0;background-color: var(--secondary-bg);color: var(--text-primary);font-size: 14px;line-height: 1.5;overflow: hidden; /* Prevent body scroll */
}.pcb-design-container {width: 100%;height: 100vh; /* Full viewport height */display: flex;flex-direction: column;background-color: var(--primary-bg);box-sizing: border-box;
}.app-header {flex-shrink: 0; /* Prevent header shrinking */background-color: var(--primary-bg);text-align: center;padding: 10px 20px; /* Reduced padding */border-bottom: 1px solid var(--border-color);
}.app-header h1 {margin: 0 0 2px 0;font-size: 1.5em; /* Slightly smaller */font-weight: 600;
}.app-header p {margin: 0;color: var(--text-secondary);font-size: 0.85em;
}.main-layout-area {flex-grow: 1; /* Fill remaining vertical space */display: flex;width: 100%;overflow: hidden; /* Prevent layout area scroll */
}.controls-and-library-panel {width: 280px; /* Fixed width for controls/library */flex-shrink: 0;background-color: var(--controls-bg);padding: 15px;border-right: 1px solid var(--border-color);overflow-y: auto; /* Allow scrolling for controls */box-sizing: border-box;display: flex;flex-direction: column;
}.controls-and-library-panel h2 {margin-top: 0;margin-bottom: 20px;font-size: 1.2em;font-weight: 600;color: var(--text-primary);border-bottom: 1px solid #c8c8cc;padding-bottom: 8px;
}.tool-section,
.library-section,
.action-section,
.status-display {margin-bottom: 20px;
}.tool-section h3,
.library-section h3,
.action-section h3,
.status-display h3 {margin-top: 0;margin-bottom: 10px;font-size: 0.95em;font-weight: 600;color: var(--text-secondary);
}.tool-button {display: block;width: 100%;padding: 8px 12px;margin-bottom: 8px;font-size: 0.9em;text-align: left;background-color: #fff;border: 1px solid var(--border-color);border-radius: 5px;cursor: pointer;transition: background-color 0.2s ease, border-color 0.2s ease;
}.tool-button:hover {background-color: #f8f8fa;border-color: #b8b8bd;
}.tool-button.active {background-color: var(--accent-blue);color: white;border-color: var(--accent-blue);
}#componentList {list-style: none;padding: 0;margin: 0;
}#componentList li {padding: 6px 10px;margin-bottom: 5px;background-color: #fff;border: 1px solid transparent; /* Placeholder for selected state */border-radius: 4px;cursor: pointer;transition: background-color 0.2s ease, border-color 0.2s ease;
}#componentList li:hover {background-color: #f8f8fa;
}#componentList li.selected {border-color: var(--accent-blue);background-color: #e0efff; /* Light blue background */
}.library-section small {font-size: 0.8em;color: var(--text-secondary);
}.action-section button {display: block;width: 100%;padding: 8px 12px;margin-bottom: 10px;font-size: 0.9em;font-weight: 500;color: #fff;background-color: #5856d6; /* Purple for actions */border: none;border-radius: 5px;cursor: pointer;transition: background-color 0.2s ease;
}.action-section button:hover {background-color: #4341a0;
}#clearButton {background-color: #dc3545; /* Red for clear */
}
#clearButton:hover {background-color: #c82333;
}.status-display {margin-top: auto; /* Push status to bottom */padding-top: 15px;border-top: 1px solid #c8c8cc;
}#statusText {font-size: 0.85em;color: var(--text-primary);min-height: 2.5em;
}.view-panel {flex-grow: 1; /* Allow panels to grow */position: relative; /* For positioning header/canvas */border-left: 1px solid var(--border-color);display: flex;flex-direction: column;overflow: hidden; /* Important: Prevent canvas overflow */
}.view-header {flex-shrink: 0;padding: 8px 15px;background-color: var(--secondary-bg);border-bottom: 1px solid var(--border-color);font-weight: 500;color: var(--text-secondary);font-size: 0.9em;
}canvas {display: block; /* Remove extra space below canvas */width: 100%;height: 100%; /* Fill the available space within the parent */background-color: var(--canvas-bg);cursor: crosshair; /* Default cursor for drawing areas */
}.tool-button.active[id="selectTool"] + .schematic-view-area canvas,
.tool-button.active[id="selectTool"] + .pcb-view-area canvas {cursor: default; /* Or 'grab'/'move' depending on interaction */
}.app-footer {flex-shrink: 0;text-align: center;padding: 8px 20px;border-top: 1px solid var(--border-color);background-color: var(--primary-bg);color: var(--text-secondary);font-size: 0.8em;
}/* Responsive adjustments */
@media (max-width: 900px) { /* Stack schematic and PCB */.main-layout-area {flex-direction: column; /* Stack all sections */}.controls-and-library-panel {width: 100%;max-height: 40vh; /* Limit height */border-right: none;border-bottom: 1px solid var(--border-color);order: 1; /* Show controls first */}.view-panel {border-left: none;flex-grow: 1;height: 30vh; /* Equal height distribution (approx) */min-height: 200px; /* Ensure minimum drawing space */order: 2; /* Show views after controls */}
}@media (max-width: 480px) { /* Further adjustments for very small screens */.controls-and-library-panel {max-height: 50vh; /* Allow more control space */}.view-panel {height: 25vh;min-height: 150px;}.app-header h1 {font-size: 1.3em;}
}
script.js
// script.js - PCB Layout Tool Componentdocument.addEventListener('DOMContentLoaded', () => {// --- DOM Elements ---const schematicCanvas = document.getElementById('schematicCanvas');const pcbCanvas = document.getElementById('pcbCanvas');const componentList = document.getElementById('componentList');const selectToolBtn = document.getElementById('selectTool');const wireToolBtn = document.getElementById('wireTool');const autoRouteButton = document.getElementById('autoRouteButton');const clearButton = document.getElementById('clearButton');const toggleHighlightButton = document.getElementById('toggleHighlightButton');const statusText = document.getElementById('statusText');// --- Canvas Contexts ---const schCtx = schematicCanvas.getContext('2d');const pcbCtx = pcbCanvas.getContext('2d');// --- Default Style Values (Fallbacks for CSS Variables) ---const styles = {highlightColor: 'rgba(255, 215, 0, 0.5)', // Gold highlightschematicWireColor: '#333333',pcbTraceColor: '#00aa00', // Green for tracescomponentFill: '#f0f0f0',componentStroke: '#888888',textPrimary: '#1d1d1f',accentBlue: '#007aff',padColor: '#b87333', // Copper colorsilkscreenColor: 'rgba(180, 180, 180, 0.5)'};// --- State Variables ---let components = []; // {id, type, schX, schY, pcbX, pcbY, width, height, pins: [{id, def:{x,y}, schX, schY, pcbX, pcbY, net}, ...]}let wires = []; // {id, startCompId, startPinId, endCompId, endPinId, net}let traces = []; // {id, net, path: [{x,y}, ...]}let nextCompId = 1;let nextWireId = 1;let nextTraceId = 1;let currentTool = 'select'; // 'select', 'wire', 'place'let selectedComponentType = null; // Type of component selected from library for placinglet selectedItem = null; // Generic selected object {type: 'component'/'pin'/'wire'/'trace', id, ...}let draggingItem = null; // {type, id, isSchematic, startX, startY, offsetX, offsetY}let wiringState = { startPin: null }; // { componentId, pinId, x, y }let highlightLinked = false;let nextNet = 1;let isAnimating = false; // Flag to prevent multiple animations// --- Update Status Function ---function updateStatus(message) {if (statusText) {statusText.textContent = message;} else {console.warn("Status text element not found.");}}// --- Component Definitions (Simplified) ---const componentDefs = {resistor: { schWidth: 40, schHeight: 15, pcbWidth: 10, pcbHeight: 4, pins: [{ id: 1, x: -20, y: 0 }, { id: 2, x: 20, y: 0 }] },capacitor: { schWidth: 20, schHeight: 15, pcbWidth: 6, pcbHeight: 6, pins: [{ id: 1, x: -10, y: 0 }, { id: 2, x: 10, y: 0 }] },ic_dip8: { schWidth: 40, schHeight: 60, pcbWidth: 15, pcbHeight: 25, pins: Array.from({ length: 8 }, (_, i) => ({ id: i + 1, x: i < 4 ? -20 : 20, y: (i % 4) * 15 - 22.5 })) },ic_qfp32: { schWidth: 70, schHeight: 70, pcbWidth: 20, pcbHeight: 20, pins: Array.from({ length: 32 }, (_, i) => {const side = Math.floor(i / 8);const posOnSide = i % 8;const spacing = 2.5;const halfSidePins = 7 * spacing / 2;const edgeOffset = 10;if (side === 0) return { id: i + 1, x: -edgeOffset, y: posOnSide * spacing - halfSidePins }; // Leftif (side === 1) return { id: i + 1, x: posOnSide * spacing - halfSidePins, y: edgeOffset }; // Top (relative to center)if (side === 2) return { id: i + 1, x: edgeOffset, y: -(posOnSide * spacing - halfSidePins) }; // Rightreturn { id: i + 1, x: -(posOnSide * spacing - halfSidePins), y: -edgeOffset }; // Bottom}) },connector: { schWidth: 15, schHeight: 50, pcbWidth: 8, pcbHeight: 18, pins: Array.from({ length: 5 }, (_, i) => ({ id: i + 1, x: 0, y: i * 10 - 20 })) },led: { schWidth: 25, schHeight: 25, pcbWidth: 5, pcbHeight: 5, pins: [{ id: 1, x: -12.5, y: 0 }, { id: 2, x: 12.5, y: 0 }] }};// --- Canvas Setup & Drawing ---function resizeCanvases() {[schematicCanvas, pcbCanvas].forEach(canvas => {const container = canvas.parentElement;// Check if container has valid dimensionsif (container && container.clientWidth > 0 && container.clientHeight > 0) {// Subtract padding/border if needed, or use offsetWidth/Heightconst width = container.clientWidth; const height = container.clientHeight; canvas.width = width;canvas.height = height;} else {// Fallback or wait for layoutconsole.warn("Canvas container not ready or has zero dimensions during resize.");}});drawAll(); // Redraw after resize}function clearCanvas(ctx) {ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);}function drawAll() {drawSchematic();drawPCB();}// --- Schematic Drawing ---function drawSchematic() {clearCanvas(schCtx);wires.forEach(wire => drawSchematicWire(wire));components.forEach(comp => drawSchematicComponent(comp));if (currentTool === 'wire' && wiringState.startPin) {// Draw preview wire - handled in mousemove}}function drawSchematicComponent(comp) {const def = componentDefs[comp.type];if (!def) { console.error(`Definition not found for type: ${comp.type}`); return; }schCtx.save();schCtx.translate(comp.schX, comp.schY);schCtx.fillStyle = styles.componentFill;schCtx.strokeStyle = styles.componentStroke;schCtx.lineWidth = 1;schCtx.fillRect(-def.schWidth / 2, -def.schHeight / 2, def.schWidth, def.schHeight);schCtx.strokeRect(-def.schWidth / 2, -def.schHeight / 2, def.schWidth, def.schHeight);schCtx.fillStyle = styles.textPrimary;schCtx.font = '10px sans-serif';schCtx.textAlign = 'center';schCtx.fillText(`${comp.type.toUpperCase()}${comp.id}`, 0, -def.schHeight / 2 - 5);comp.pins.forEach(pin => {const relX = pin.def ? pin.def.x : 0;const relY = pin.def ? pin.def.y : 0;schCtx.beginPath();schCtx.moveTo(relX, relY);if (Math.abs(relX) === def.schWidth / 2) { // Horizontal pinsschCtx.lineTo(relX + (relX > 0 ? 5 : -5), relY);} else { // Vertical pinsschCtx.lineTo(relX, relY + (relY > 0 ? 5 : -5));}schCtx.strokeStyle = styles.schematicWireColor;schCtx.stroke();// Highlight pin calculation needs absolute pin coords stored in pin objectconst absPinX = pin.schX; // Use stored absolute coordsconst absPinY = pin.schY;if ( (selectedItem && selectedItem.type === 'pin' && selectedItem.componentId === comp.id && selectedItem.pinId === pin.id) ||(highlightLinked && selectedItem && selectedItem.type === 'component' && pin.net && isNetSelected(pin.net)) ) {schCtx.fillStyle = styles.highlightColor;schCtx.beginPath();schCtx.arc(relX, relY, 4, 0, Math.PI * 2); // Highlight around relative pin posschCtx.fill();}});if (selectedItem && selectedItem.type === 'component' && selectedItem.id === comp.id) {schCtx.strokeStyle = styles.accentBlue;schCtx.lineWidth = 1.5;schCtx.strokeRect(-def.schWidth / 2 - 2, -def.schHeight / 2 - 2, def.schWidth + 4, def.schHeight + 4);}schCtx.restore();}function drawSchematicWire(wire) {const startPin = findPin(wire.startCompId, wire.startPinId);const endPin = findPin(wire.endCompId, wire.endPinId);if (!startPin || !endPin) return;schCtx.beginPath();schCtx.moveTo(startPin.schX, startPin.schY);schCtx.lineTo(endPin.schX, endPin.schY);schCtx.strokeStyle = styles.schematicWireColor;schCtx.lineWidth = 1;if ((selectedItem && selectedItem.type === 'wire' && selectedItem.id === wire.id) ||(highlightLinked && wire.net && isNetSelected(wire.net)) ) {schCtx.strokeStyle = styles.accentBlue;schCtx.lineWidth = 2.5;}schCtx.stroke();}// --- PCB Drawing ---function drawPCB() {clearCanvas(pcbCtx);traces.forEach(trace => drawPCBTrace(trace));components.forEach(comp => drawPCBFootprint(comp));}function drawPCBFootprint(comp) {const def = componentDefs[comp.type];if (!def) return;pcbCtx.save();pcbCtx.translate(comp.pcbX, comp.pcbY);pcbCtx.fillStyle = styles.silkscreenColor;pcbCtx.fillRect(-def.pcbWidth / 2, -def.pcbHeight / 2, def.pcbWidth, def.pcbHeight);comp.pins.forEach(pin => {const padSize = comp.type.includes('ic') ? 1.5 : 2.5;const relX = pin.def ? pin.def.x : 0; // Use relative def for drawing pad posconst relY = pin.def ? pin.def.y : 0;pcbCtx.fillStyle = styles.padColor;pcbCtx.beginPath();// Draw square pads for ICs, round for others (example)if (comp.type.includes('ic') || comp.type.includes('conn')) {pcbCtx.fillRect(relX - padSize, relY - padSize, padSize * 2, padSize * 2);} else {pcbCtx.arc(relX, relY, padSize, 0, Math.PI * 2);pcbCtx.fill();}if ( (selectedItem && selectedItem.type === 'pin' && selectedItem.componentId === comp.id && selectedItem.pinId === pin.id) ||(highlightLinked && selectedItem && selectedItem.type === 'component' && pin.net && isNetSelected(pin.net)) ) {pcbCtx.fillStyle = styles.highlightColor;pcbCtx.beginPath();pcbCtx.arc(relX, relY, padSize + 2, 0, Math.PI * 2);pcbCtx.fill();}});if (selectedItem && selectedItem.type === 'component' && selectedItem.id === comp.id) {pcbCtx.strokeStyle = styles.accentBlue;pcbCtx.lineWidth = 1.5;pcbCtx.strokeRect(-def.pcbWidth / 2 - 2, -def.pcbHeight / 2 - 2, def.pcbWidth + 4, def.pcbHeight + 4);}pcbCtx.restore();}function drawPCBTrace(trace) {if (!trace.path || trace.path.length < 2) return;pcbCtx.beginPath();pcbCtx.moveTo(trace.path[0].x, trace.path[0].y);for (let i = 1; i < trace.path.length; i++) {pcbCtx.lineTo(trace.path[i].x, trace.path[i].y);}pcbCtx.strokeStyle = styles.pcbTraceColor;pcbCtx.lineWidth = 1.5;if ((selectedItem && selectedItem.type === 'trace' && selectedItem.id === trace.id) ||(highlightLinked && trace.net && isNetSelected(trace.net)) ) {pcbCtx.strokeStyle = styles.accentBlue;pcbCtx.lineWidth = 3;}pcbCtx.stroke();}// --- Interaction Logic ---function setupEventListeners() {selectToolBtn.addEventListener('click', () => setTool('select'));wireToolBtn.addEventListener('click', () => setTool('wire'));componentList.addEventListener('click', (e) => {if (e.target.tagName === 'LI') {selectComponentFromLibrary(e.target);}});[schematicCanvas, pcbCanvas].forEach(canvas => {canvas.addEventListener('mousedown', handleMouseDown);canvas.addEventListener('mousemove', handleMouseMove);canvas.addEventListener('mouseup', handleMouseUp);canvas.addEventListener('click', handleCanvasClick);});autoRouteButton.addEventListener('click', simulateAutoRouting);clearButton.addEventListener('click', clearDesign);toggleHighlightButton.addEventListener('click', toggleHighlight);window.addEventListener('resize', resizeCanvases);}function setTool(tool) {currentTool = tool;wiringState.startPin = null;document.querySelectorAll('.tool-button').forEach(btn => btn.classList.remove('active'));const activeBtn = document.getElementById(tool + 'Tool');if (activeBtn) activeBtn.classList.add('active');deselectComponentFromLibrary(); // Deselect library item when changing toolselectedItem = null; // Deselect any canvas itemupdateStatus(`工具已切换: ${tool}`);[schematicCanvas, pcbCanvas].forEach(canvas => {if (tool === 'wire') canvas.style.cursor = 'crosshair';else if (tool === 'place') canvas.style.cursor = 'copy';else canvas.style.cursor = 'default';});drawAll(); // Redraw to remove selection highlights if any}function selectComponentFromLibrary(listItem) {// No explicit 'place' tool, selection implies placement modecurrentTool = 'place';document.querySelectorAll('#componentList li').forEach(li => li.classList.remove('selected'));listItem.classList.add('selected');selectedComponentType = listItem.dataset.type;updateStatus(`已选择元件: ${selectedComponentType},请在画布上点击放置。`);document.querySelectorAll('.tool-button').forEach(btn => btn.classList.remove('active')); // Deactivate tool buttons[schematicCanvas, pcbCanvas].forEach(canvas => canvas.style.cursor = 'copy');}function deselectComponentFromLibrary() {document.querySelectorAll('#componentList li').forEach(li => li.classList.remove('selected'));selectedComponentType = null;// If tool was 'place', revert to 'select'if (currentTool === 'place') {setTool('select');}}function getMousePos(canvas, event) {const rect = canvas.getBoundingClientRect();return {x: event.clientX - rect.left,y: event.clientY - rect.top};}function handleCanvasClick(event) {const pos = getMousePos(event.target, event);const isSchematic = event.target === schematicCanvas;if (currentTool === 'place' && selectedComponentType) {placeComponent(pos.x, pos.y, isSchematic);} else if (currentTool === 'select') {selectItemAt(pos.x, pos.y, isSchematic);} else if (currentTool === 'wire' && isSchematic) {handleWireClick(pos.x, pos.y);}}function handleMouseDown(event) {if (currentTool !== 'select') return;const pos = getMousePos(event.target, event);const item = getItemAt(pos.x, pos.y, event.target === schematicCanvas);if (item && item.type === 'component') {// Check if we clicked the same component again to prevent losing selectionif (!selectedItem || selectedItem.type !== 'component' || selectedItem.id !== item.component.id) {selectItemAt(pos.x, pos.y, event.target === schematicCanvas); // Select before dragging}draggingItem = {type: 'component',id: item.component.id,isSchematic: event.target === schematicCanvas,startX: pos.x,startY: pos.y,offsetX: pos.x - (event.target === schematicCanvas ? item.component.schX : item.component.pcbX),offsetY: pos.y - (event.target === schematicCanvas ? item.component.schY : item.component.pcbY)};[schematicCanvas, pcbCanvas].forEach(canvas => canvas.style.cursor = 'grabbing');} else {// Clear selection if clicking empty spaceif (selectedItem) {selectItemAt(pos.x, pos.y, event.target === schematicCanvas); // This will clear if no item found}draggingItem = null; // Ensure no dragging starts}}function handleMouseMove(event) {const pos = getMousePos(event.target, event);const isSchematic = event.target === schematicCanvas;if (draggingItem) {const targetX = pos.x - draggingItem.offsetX;const targetY = pos.y - draggingItem.offsetY;moveComponent(draggingItem.id, targetX, targetY, draggingItem.isSchematic);} else if (currentTool === 'wire' && wiringState.startPin && isSchematic) {drawAll(); // Redraw baseschCtx.beginPath();schCtx.moveTo(wiringState.startPin.x, wiringState.startPin.y);schCtx.lineTo(pos.x, pos.y);schCtx.strokeStyle = 'rgba(0, 0, 255, 0.5)';schCtx.setLineDash([5, 3]);schCtx.stroke();schCtx.setLineDash([]);}}function handleMouseUp(event) {if (draggingItem) {updateStatus(`元件 ${draggingItem.id} 已移动。`);draggingItem = null;[schematicCanvas, pcbCanvas].forEach(canvas => canvas.style.cursor = (currentTool === 'wire' ? 'crosshair' : 'default'));// No need to redraw here, mousemove already did}}function placeComponent(x, y, isSchematic) {if (!selectedComponentType) return;const def = componentDefs[selectedComponentType];const compId = nextCompId++;const newComp = {id: compId,type: selectedComponentType,// Place on both canvases simultaneously, using default pos for the non-clicked oneschX: isSchematic ? x : 10 + (compId % 5) * 60,schY: isSchematic ? y : 50 + Math.floor(compId / 5) * 80,pcbX: !isSchematic ? x : 50 + (compId % 8) * 40,pcbY: !isSchematic ? y : 50 + Math.floor(compId / 8) * 40,pins: [] // Initialize pins array};// Calculate absolute pin positionsnewComp.pins = def.pins.map(pDef => ({id: pDef.id,def: pDef, // Store relative definitionschX: newComp.schX + pDef.x,schY: newComp.schY + pDef.y,pcbX: newComp.pcbX + pDef.x, // Use relative pin def for PCB toopcbY: newComp.pcbY + pDef.y,net: null}));components.push(newComp);updateStatus(`放置元件: ${newComp.type}${newComp.id}`);deselectComponentFromLibrary(); // This will switch tool back to 'select'drawAll();}function moveComponent(id, x, y, isSchematic) {const comp = components.find(c => c.id === id);if (!comp) return;if (isSchematic) {comp.schX = x;comp.schY = y;comp.pins.forEach(pin => {pin.schX = x + pin.def.x;pin.schY = y + pin.def.y;});// If PCB view exists, maybe snap PCB pos to grid or keep relative?// For now, moving one doesn't auto-move the other precisely.} else {comp.pcbX = x;comp.pcbY = y;comp.pins.forEach(pin => {pin.pcbX = x + pin.def.x;pin.pcbY = y + pin.def.y;});}drawAll();}function getItemAt(x, y, isSchematic, tolerance = 5) {// Check pins firstfor (const comp of components) {for (const pin of comp.pins) {const pinX = isSchematic ? pin.schX : pin.pcbX;const pinY = isSchematic ? pin.schY : pin.pcbY;if (Math.abs(x - pinX) < tolerance && Math.abs(y - pinY) < tolerance) {return { type: 'pin', componentId: comp.id, pinId: pin.id, x: pinX, y: pinY, pin: pin };}}}// Check component bodiesfor (const comp of components) {const compX = isSchematic ? comp.schX : comp.pcbX;const compY = isSchematic ? comp.schY : comp.pcbY;const def = componentDefs[comp.type];const width = isSchematic ? def.schWidth : def.pcbWidth;const height = isSchematic ? def.schHeight : def.pcbHeight;if (x > compX - width / 2 - tolerance && x < compX + width / 2 + tolerance &&y > compY - height / 2 - tolerance && y < compY + height / 2 + tolerance) {return { type: 'component', component: comp }; // Return the component object}}// Check wires (Schematic only)if (isSchematic) {for (const wire of wires) {const startPin = findPin(wire.startCompId, wire.startPinId);const endPin = findPin(wire.endCompId, wire.endPinId);if (!startPin || !endPin) continue;// Basic line collision check (distance from point to line segment)// Simplified: check distance to endpoints for nowif ((Math.abs(x - startPin.schX) < tolerance && Math.abs(y - startPin.schY) < tolerance) ||(Math.abs(x - endPin.schX) < tolerance && Math.abs(y - endPin.schY) < tolerance)) {// This might select the pin instead, needs better line hit test}// TODO: Implement point-to-line segment distance check}}// Check traces (PCB only) - TODOreturn null;}function selectItemAt(x, y, isSchematic) {const itemInfo = getItemAt(x, y, isSchematic);if (itemInfo) {// Reconstruct selectedItem structure for consistencyif (itemInfo.type === 'component') {selectedItem = { type: 'component', id: itemInfo.component.id };} else if (itemInfo.type === 'pin') {selectedItem = { type: 'pin', componentId: itemInfo.componentId, pinId: itemInfo.pinId };}// TODO: Handle wire/trace selectionupdateStatus(`选中: ${selectedItem.type} ${selectedItem.id !== undefined ? selectedItem.id : selectedItem.pinId}`);} else {selectedItem = null;updateStatus("选择已清除。");}drawAll(); // Redraw to show selection}function findPin(componentId, pinId) {const comp = components.find(c => c.id === componentId);return comp ? comp.pins.find(p => p.id === pinId) : null;}function handleWireClick(x, y) {const clickedPinInfo = getItemAt(x, y, true, 8); // Schematic only, larger toleranceif (clickedPinInfo && clickedPinInfo.type === 'pin') {if (!wiringState.startPin) {wiringState.startPin = clickedPinInfo; // Store { type, componentId, pinId, x, y, pin }updateStatus(`开始连接,起点: 元件 ${clickedPinInfo.componentId}, 引脚 ${clickedPinInfo.pinId}。点击终点引脚。`);} else {if (wiringState.startPin.componentId === clickedPinInfo.componentId &&wiringState.startPin.pinId === clickedPinInfo.pinId) {wiringState.startPin = null;updateStatus("连接已取消。");} else {const startPinObj = wiringState.startPin.pin; // Actual pin objectconst endPinObj = clickedPinInfo.pin; // Actual pin objectif (startPinObj && endPinObj) {let assignedNet = startPinObj.net || endPinObj.net;if (!assignedNet) {assignedNet = `N${nextNet++}`;}// Assign net to all connected pins/wires implicitlypropagateNet(startPinObj, assignedNet);propagateNet(endPinObj, assignedNet);const newWire = {id: nextWireId++,startCompId: wiringState.startPin.componentId,startPinId: wiringState.startPin.pinId,endCompId: clickedPinInfo.componentId,endPinId: clickedPinInfo.pinId,net: assignedNet};wires.push(newWire);updateStatus(`连接完成: (${newWire.startCompId}:${newWire.startPinId} <-> ${newWire.endCompId}:${newWire.endPinId}) Net ${assignedNet}`);wiringState.startPin = null;} else {updateStatus("错误:无法找到引脚对象。");wiringState.startPin = null;}}}} else {if (wiringState.startPin) {wiringState.startPin = null;updateStatus("连接已取消。");}}drawAll();}// Propagate net assignment through connected wiresfunction propagateNet(startPin, net) {if (!startPin || startPin.net === net) return; // Already assigned or no pinstartPin.net = net;// Find wires connected to this pin and propagate to the other endwires.forEach(wire => {let otherPin = null;if (wire.startCompId === startPin.componentId && wire.startPinId === startPin.id) {otherPin = findPin(wire.endCompId, wire.endPinId);wire.net = net; // Assign net to wire itself} else if (wire.endCompId === startPin.componentId && wire.endPinId === startPin.id) {otherPin = findPin(wire.startCompId, wire.startPinId);wire.net = net; // Assign net to wire itself}if (otherPin) {propagateNet(otherPin, net);}});// Also update corresponding pins on other components if needed (e.g., through pre-existing wires)components.forEach(comp => {comp.pins.forEach(p => {if(p !== startPin && p.net === null) { // Check other pins// See if this pin is connected to the startPin via a wireconst connectedWire = wires.find(w => (w.startCompId === startPin.componentId && w.startPinId === startPin.id && w.endCompId === p.componentId && w.endPinId === p.id) ||(w.endCompId === startPin.componentId && w.endPinId === startPin.id && w.startCompId === p.componentId && w.startPinId === p.id));if(connectedWire) {propagateNet(p, net);}}});});}function isNetSelected(net) {if (!selectedItem || !net) return false;if (selectedItem.type === 'component') {const comp = components.find(c => c.id === selectedItem.id);return comp && comp.pins.some(p => p.net === net);} else if (selectedItem.type === 'pin') {const pin = findPin(selectedItem.componentId, selectedItem.pinId);return pin && pin.net === net;} else if (selectedItem.type === 'wire') {const wire = wires.find(w => w.id === selectedItem.id);return wire && wire.net === net;} else if (selectedItem.type === 'trace') {const trace = traces.find(t => t.id === selectedItem.id);return trace && trace.net === net;}return false;}// --- Action Button Logics (Simplified/Conceptual) ---function simulateAutoRouting() {if (isAnimating) return; // Prevent re-entryisAnimating = true;autoRouteButton.disabled = true;updateStatus("开始模拟 PCB 自动布线 (概念性)... ");traces = [];nextTraceId = 1;let currentNetIndex = 1;const maxNetIndex = nextNet -1;let traceDelay = 50; // Faster delayfunction routeNextNet() {const netName = `N${currentNetIndex}`;if (currentNetIndex > maxNetIndex) {updateStatus("模拟布线完成。");isAnimating = false;autoRouteButton.disabled = false;drawPCB();return;}let pinsInNet = [];components.forEach(comp => {comp.pins.forEach(pin => {if (pin.net === netName) {pinsInNet.push({ x: pin.pcbX, y: pin.pcbY });}});});if (pinsInNet.length < 2) {currentNetIndex++;setTimeout(routeNextNet, 5); // Skip quicklyreturn;}// Simple routing: connect pins sequentially with L-shapesconst newTrace = { id: nextTraceId++, net: netName, path: [] };let currentPathPoint = { x: pinsInNet[0].x, y: pinsInNet[0].y };newTrace.path.push(currentPathPoint);let pinIndex = 1;function routeToNextPin() {if (pinIndex >= pinsInNet.length) {traces.push(newTrace); // Add completed tracecurrentNetIndex++;setTimeout(routeNextNet, traceDelay * 2); // Pause between netsreturn;}const targetPin = pinsInNet[pinIndex];// Simple L-route: X then Yconst midPoint = { x: targetPin.x, y: currentPathPoint.y };// Animate drawing segmentsanimateTraceSegment(currentPathPoint, midPoint, () => {animateTraceSegment(midPoint, targetPin, () => {currentPathPoint = targetPin;pinIndex++;routeToNextPin();});});}routeToNextPin(); // Start routing for the current net}// Helper for animating trace drawingfunction animateTraceSegment(start, end, callback) {const dx = end.x - start.x;const dy = end.y - start.y;const distance = Math.sqrt(dx*dx + dy*dy);const segments = Math.max(1, Math.ceil(distance / 5)); // Draw in 5px segmentslet currentSegment = 0;function drawSegment() {if (currentSegment > segments) {// Ensure the final point is exactly addedif (newTrace.path[newTrace.path.length - 1].x !== end.x || newTrace.path[newTrace.path.length - 1].y !== end.y) {newTrace.path.push({ x: end.x, y: end.y });}drawPCB();if (callback) setTimeout(callback, traceDelay / 2);return;}const t = currentSegment / segments;const currentX = start.x + dx * t;const currentY = start.y + dy * t;newTrace.path.push({ x: currentX, y: currentY });currentSegment++;drawPCB();setTimeout(drawSegment, traceDelay / segments);}drawSegment();}routeNextNet(); // Start the process}function clearDesign() {components = [];wires = [];traces = [];selectedItem = null;draggingItem = null;wiringState.startPin = null;nextCompId = 1;nextWireId = 1;nextTraceId = 1;nextNet = 1;if (highlightLinked) toggleHighlight();updateStatus("画布已清空。");drawAll();}function toggleHighlight() {highlightLinked = !highlightLinked;toggleHighlightButton.textContent = highlightLinked ? "禁用高亮联动" : "启用高亮联动";updateStatus(highlightLinked ? "高亮联动已启用。选择查看关联。" : "高亮联动已禁用。");if (selectedItem) drawAll();}// --- Initialization Call ---// Use setTimeout to ensure layout is complete before getting canvas sizesetTimeout(() => {resizeCanvases();setupEventListeners();setTool('select');updateStatus("电子电路设计组件初始化成功。");}, 100); // Delay slightly});