diff --git a/src/App.vue b/src/App.vue index e4d64dc..8fa58ff 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import { computed, onMounted, reactive, ref, watchEffect } from 'vue' import AIAssistant from './components/AIAssistant.vue' import { elements, type CircuitElement, type Preset } from './elements' +import { formatValue } from './utils' // 1) 左侧面板元件清单(显式导入) type PaletteItem = CircuitElement @@ -105,8 +106,6 @@ function zoomAt(clientX: number, clientY: number, deltaY: number) { const DND_MIME = 'application/x-circuit-element' function onPaletteDragStart(e: DragEvent, item: PaletteItem) { - console.log(e); - if (!e.dataTransfer) return e.dataTransfer.setData(DND_MIME, item.key) e.dataTransfer.setData('text/plain', item.key) @@ -125,13 +124,10 @@ function onCanvasDrop(e: DragEvent) { const item = palette.find(p => p.key === key) if (!item) return const { x, y } = clientToWorld(e.clientX, e.clientY) - // 吸附到网格(可选) - const gx = Math.round(x / state.gridSize) * state.gridSize - const gy = Math.round(y / state.gridSize) * state.gridSize const defSize = item.defaultSize ?? 64 const props: Record = {} ; (item.propertySchemas || []).forEach((p) => { props[p.key] = p.default }) - const inst: Instance = { id: String(++idSeq), key: item.key, url: item.url, x: gx, y: gy, size: defSize, rotation: 0, props } + const inst: Instance = { id: String(++idSeq), key: item.key, url: item.url, x: x, y: y, size: defSize, rotation: 0, props } if (item.stateImages) inst.state = 'off' instances.push(inst) } @@ -507,7 +503,7 @@ function solveMNA() { }) // 对于未分类(断开)器件,保持 null - + // 根据计算电流,自动切换需要按电流显示状态的元件(如灯泡) const CURRENT_ON_THRESHOLD = 0.1 // A,即 100 mA for (const inst of instances) { @@ -564,25 +560,7 @@ function gaussianSolve(A: number[][], b: number[]): number[] | null { // 绑定求解到响应式数据 watchEffect(() => { solveMNA() }) -function formatValue(v: number | null, unit: string) { - if (v == null || !Number.isFinite(v)) return '—' - // 简易工程计数法 - const absv = Math.abs(v) - const units = [ - { s: 1e-9, u: 'n' }, - { s: 1e-6, u: 'µ' }, - { s: 1e-3, u: 'm' }, - { s: 1, u: '' }, - { s: 1e3, u: 'k' }, - { s: 1e6, u: 'M' }, - ] - let u = '' - let val = v - for (let i = units.length - 1; i >= 0; i--) { - if (absv >= units[i].s) { u = units[i].u; val = v / units[i].s; break } - } - return `${val.toFixed(3)} ${u}${unit}` -} + function meterText(inst: Instance) { const live = instLive[inst.id] @@ -593,7 +571,7 @@ function meterText(inst: Instance) { try { // 允许两种形态:"(i)=>..." 或 "(i,v,props)=>..." // 使用字符串拼接避免模板字符串与用户代码中的反引号冲突 - const fn = new Function('i','r','props', 'return (' + code + ')(i, r, props)') + const fn = new Function('i', 'r', 'props', 'return (' + code + ')(i, r, props)') const out = fn(i, r, inst.props) return String(out) } catch { @@ -747,7 +725,6 @@ function loadFromJSONText(text: string) { // 兼容校验 const world = obj.world || {} const rawInstances = Array.isArray(obj.instances) ? obj.instances : [] - const rawWires = Array.isArray(obj.wires) ? obj.wires : [] // 过滤仅保留存在于库里的元件 const knownKeys = new Set(elements.map(e => e.key)) @@ -770,64 +747,46 @@ function loadFromJSONText(text: string) { props: rawProps, state: r.state === 'on' || r.state === 'off' ? r.state : undefined, } - // 暂存连接信息在实例对象上(不入 props)以便后续重建导线 - ;(inst as any).__savedConnections = savedConnections + // 暂存连接信息在实例对象上(不入 props)以便后续重建导线 + ; (inst as any).__savedConnections = savedConnections validInstances.push(inst) } // 基于实例与元件接线点数量校验导线 const instMap = new Map(validInstances.map(i => [i.id, i])) const validWires: Wire[] = [] - if (rawWires.length > 0) { - // 旧格式:直接读取 wires - for (const w of rawWires) { - if (!w || typeof w !== 'object') continue - const a = w.a, b = w.b - if (!a || !b) continue - const ia = instMap.get(String(a.instId)) - const ib = instMap.get(String(b.instId)) - if (!ia || !ib) continue - const elemA = elements.find(e => e.key === ia.key) - const elemB = elements.find(e => e.key === ib.key) - const cpsA = elemA?.connectionPoints?.length ?? 0 - const cpsB = elemB?.connectionPoints?.length ?? 0 - const cpa = safeNumber(a.cpIndex) - const cpb = safeNumber(b.cpIndex) - if (cpa < 0 || cpa >= cpsA || cpb < 0 || cpb >= cpsB) continue - validWires.push({ id: String(w.id ?? (++wireSeq)), a: { instId: String(a.instId), cpIndex: cpa }, b: { instId: String(b.instId), cpIndex: cpb } }) - } - } else { - // 新格式:从每个实例 props.__connections 重建导线(去重) - const dedup = new Set() - const getKey = (a: EndpointRef, b: EndpointRef) => { - const k1 = `${a.instId}:${a.cpIndex}` - const k2 = `${b.instId}:${b.cpIndex}` - return k1 < k2 ? `${k1}|${k2}` : `${k2}|${k1}` - } - for (const inst of validInstances) { - const conns: SaveConnections | undefined = (inst as any).__savedConnections - if (!conns) continue - const elem = elements.find(e => e.key === inst.key) - const cps = elem?.connectionPoints?.length ?? 0 - for (const [cpStr, arr] of Object.entries(conns)) { - const cpi = safeNumber(cpStr) - if (cpi < 0 || cpi >= cps) continue - for (const ref of arr || []) { - const tgt = instMap.get(String(ref.instId)) - if (!tgt) continue - const elemB = elements.find(e => e.key === tgt.key) - const cpsB = elemB?.connectionPoints?.length ?? 0 - const cpb = safeNumber(ref.cpIndex) - if (cpb < 0 || cpb >= cpsB) continue - const a: EndpointRef = { instId: inst.id, cpIndex: cpi } - const b: EndpointRef = { instId: String(ref.instId), cpIndex: cpb } - const key = getKey(a, b) - if (dedup.has(key)) continue - dedup.add(key) - validWires.push({ id: String(++wireSeq), a, b }) - } + + // 从每个实例 props.__connections 重建导线(去重) + const dedup = new Set() + const getKey = (a: EndpointRef, b: EndpointRef) => { + const k1 = `${a.instId}:${a.cpIndex}` + const k2 = `${b.instId}:${b.cpIndex}` + return k1 < k2 ? `${k1}|${k2}` : `${k2}|${k1}` + } + for (const inst of validInstances) { + const conns: SaveConnections | undefined = (inst as any).__savedConnections + if (!conns) continue + const elem = elements.find(e => e.key === inst.key) + const cps = elem?.connectionPoints?.length ?? 0 + for (const [cpStr, arr] of Object.entries(conns)) { + const cpi = safeNumber(cpStr) + if (cpi < 0 || cpi >= cps) continue + for (const ref of arr || []) { + const tgt = instMap.get(String(ref.instId)) + if (!tgt) continue + const elemB = elements.find(e => e.key === tgt.key) + const cpsB = elemB?.connectionPoints?.length ?? 0 + const cpb = safeNumber(ref.cpIndex) + if (cpb < 0 || cpb >= cpsB) continue + const a: EndpointRef = { instId: inst.id, cpIndex: cpi } + const b: EndpointRef = { instId: String(ref.instId), cpIndex: cpb } + const key = getKey(a, b) + if (dedup.has(key)) continue + dedup.add(key) + validWires.push({ id: String(++wireSeq), a, b }) } } + } // 写回世界与状态 @@ -904,7 +863,7 @@ function deleteSelected() {
- + {{ item.name }}
@@ -913,11 +872,11 @@ function deleteSelected() { + @change="onImportFileChange" /> - +
- - + + - + + - + :stroke="'url(#wireGrad-' + w.id + ')'" stroke-linecap="round" vector-effect="non-scaling-stroke" /> @@ -974,8 +928,9 @@ function deleteSelected() {
+ :style="{ left: inst.x + 'px', top: inst.y + 'px', transform: 'translate(-50%, -50%) rotate(' + (inst.rotation || 0) + 'deg)' }" + @mousedown="(e) => onInstanceMouseDown(e, inst)" @click.stop="() => selectInstance(inst.id)" + :title="inst.key">
@@ -1011,12 +966,12 @@ function deleteSelected() { hover: isNearest(inst.id, idx), connected: hoveredEndpoint && isConnectedEndpoint(inst.id, idx) }" :style="{ - left: getConnectionPointWorldPos(inst, idx).x + 'px', - top: getConnectionPointWorldPos(inst, idx).y + 'px' - }" @click.stop="(e) => onEndpointClick(inst.id, idx, e)" - @mouseenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }" - @mouseleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }" - :title="cp.name || ('P' + (idx + 1))">
+ left: getConnectionPointWorldPos(inst, idx).x + 'px', + top: getConnectionPointWorldPos(inst, idx).y + 'px' + }" @click.stop="(e) => onEndpointClick(inst.id, idx, e)" + @mouseenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }" + @mouseleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }" + :title="cp.name || ('P' + (idx + 1))">
@@ -1029,18 +984,15 @@ function deleteSelected() { -