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),适应不同屏幕尺寸。

如何使用

  1. 打开页面: 在浏览器中打开 index.html
  2. 选择工具: 在左侧"工具"栏选择 “选择/移动”“连接 (原理图)”
  3. 放置元件:
    • 点击左侧"常用元件"列表中的元件类型。
    • 此时工具会自动切换到"放置"模式 (光标变为 copy 状)。
    • 在原理图或 PCB 画布上点击想要放置的位置。
    • 放置后工具会自动切换回 “选择/移动”
  4. 移动元件:
    • 确保处于 “选择/移动” 工具模式。
    • 在任一画布上点击并拖动元件主体进行移动。
  5. 原理图连线:
    • 切换到 “连接 (原理图)” 工具模式。
    • 原理图画布 上,依次点击两个需要连接的元件引脚。
    • 连线会自动生成,并分配网络号。点击空白处或同一引脚可取消连线操作。
  6. PCB 模拟布线:
    • 完成原理图连线后,点击左侧操作区的 “模拟布线 (PCB)” 按钮。
    • 观察 PCB 画布上逐步绘制出连接走线的动画(仅为视觉效果)。
  7. 高亮联动:
    • 点击 “启用/禁用高亮联动” 按钮切换模式。
    • 启用后,在 “选择/移动” 模式下,点击原理图或 PCB 上的元件、引脚、导线,查看关联元素的高亮效果。
  8. 清空: 点击 “清空画布” 按钮将移除所有元件和连线。

文件结构

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 Component

document.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 highlight
        schematicWireColor: '#333333',
        pcbTraceColor: '#00aa00', // Green for traces
        componentFill: '#f0f0f0',
        componentStroke: '#888888',
        textPrimary: '#1d1d1f',
        accentBlue: '#007aff',
        padColor: '#b87333', // Copper color
        silkscreenColor: '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 placing
    let 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 }; // Left
            if (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) }; // Right
            return { 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 dimensions
            if (container && container.clientWidth > 0 && container.clientHeight > 0) {
                 // Subtract padding/border if needed, or use offsetWidth/Height
                 const width = container.clientWidth; 
                 const height = container.clientHeight; 
                 canvas.width = width;
                 canvas.height = height;
            } else {
                // Fallback or wait for layout
                console.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 pins
                 schCtx.lineTo(relX + (relX > 0 ? 5 : -5), relY);
            } else { // Vertical pins
                 schCtx.lineTo(relX, relY + (relY > 0 ? 5 : -5));
            }
            schCtx.strokeStyle = styles.schematicWireColor;
            schCtx.stroke();

            // Highlight pin calculation needs absolute pin coords stored in pin object
            const absPinX = pin.schX; // Use stored absolute coords
            const 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 pos
                    schCtx.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 pos
            const 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 tool
        selectedItem = null; // Deselect any canvas item

        updateStatus(`工具已切换: ${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 mode
        currentTool = '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 selection
             if (!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 space
             if (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 base
             schCtx.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 one
            schX: 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 positions
        newComp.pins = def.pins.map(pDef => ({
            id: pDef.id,
            def: pDef, // Store relative definition
            schX: newComp.schX + pDef.x,
            schY: newComp.schY + pDef.y,
            pcbX: newComp.pcbX + pDef.x, // Use relative pin def for PCB too
            pcbY: 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 first
        for (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 bodies
        for (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 now
                 if ((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) - TODO
        return null;
    }

    function selectItemAt(x, y, isSchematic) {
         const itemInfo = getItemAt(x, y, isSchematic);
         if (itemInfo) {
             // Reconstruct selectedItem structure for consistency
             if (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 selection
             updateStatus(`选中: ${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 tolerance

         if (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 object
                     const endPinObj = clickedPinInfo.pin;     // Actual pin object

                     if (startPinObj && endPinObj) {
                         let assignedNet = startPinObj.net || endPinObj.net;
                         if (!assignedNet) {
                             assignedNet = `N${nextNet++}`;
                         }
                         // Assign net to all connected pins/wires implicitly
                         propagateNet(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 wires
    function propagateNet(startPin, net) {
        if (!startPin || startPin.net === net) return; // Already assigned or no pin
        startPin.net = net;

        // Find wires connected to this pin and propagate to the other end
        wires.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 wire
                    const 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-entry
        isAnimating = true;
        autoRouteButton.disabled = true;
        updateStatus("开始模拟 PCB 自动布线 (概念性)... ");
        traces = [];
        nextTraceId = 1;

        let currentNetIndex = 1;
        const maxNetIndex = nextNet -1;
        let traceDelay = 50; // Faster delay

        function 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 quickly
                return;
            }

            // Simple routing: connect pins sequentially with L-shapes
            const 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 trace
                    currentNetIndex++;
                    setTimeout(routeNextNet, traceDelay * 2); // Pause between nets
                    return;
                 }

                 const targetPin = pinsInNet[pinIndex];
                 // Simple L-route: X then Y
                 const midPoint = { x: targetPin.x, y: currentPathPoint.y };
                 // Animate drawing segments
                 animateTraceSegment(currentPathPoint, midPoint, () => {
                     animateTraceSegment(midPoint, targetPin, () => {
                         currentPathPoint = targetPin;
                         pinIndex++;
                         routeToNextPin();
                     });
                 });
            }
            routeToNextPin(); // Start routing for the current net
        }

        // Helper for animating trace drawing
        function 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 segments
            let currentSegment = 0;

            function drawSegment() {
                 if (currentSegment > segments) {
                      // Ensure the final point is exactly added
                     if (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 size
    setTimeout(() => {
        resizeCanvases();
        setupEventListeners();
        setTool('select');
        updateStatus("电子电路设计组件初始化成功。");
    }, 100); // Delay slightly

}); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

地上一の鹅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值