<script setup lang="ts">
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
const palette: PaletteItem[] = [...elements]

// 2) 画布世界状态（平移/缩放）
const viewportRef = ref<HTMLDivElement | null>(null)
const worldRef = ref<HTMLDivElement | null>(null)

// 右侧 AI 面板显示开关
const showAI = ref(true)

const state = reactive({
  scale: 1,
  minScale: 0.2,
  maxScale: 3,
  translateX: 0,
  translateY: 0,
  worldSize: 5000, // 世界为正方形 5000x5000，中心在 2500,2500
  gridSize: 40, // 网格间距（未缩放前）
})

const worldStyle = computed(() => ({
  width: state.worldSize + 'px',
  height: state.worldSize + 'px',
  transform: `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`,
  transformOrigin: '0 0',
  // 提供给 CSS 的网格尺寸变量
  '--grid': state.gridSize + 'px',
  '--grid5': state.gridSize * 5 + 'px',
}))

// 3) 已放置的元件
type Instance = {
  id: string
  key: string // 文件名键
  url: string
  x: number
  y: number
  size: number
  rotation: number // 角度，单位：deg
  props: Record<string, number | string>
  state?: 'on' | 'off'
}

let idSeq = 0
const instances = reactive<Instance[]>([])

// 连接点与导线
type EndpointRef = { instId: string; cpIndex: number }
type Wire = { id: string; a: EndpointRef; b: EndpointRef }
let wireSeq = 0
const wires = reactive<Wire[]>([])
let pendingEndpoint: EndpointRef | null = null

// 选中与属性编辑
const selectedId = ref<string | null>(null)
const selectedInst = computed(() => instances.find(i => i.id === selectedId.value) || null)
function selectInstance(id: string) { selectedId.value = id }
function clearSelection() { selectedId.value = null }

// 选中元件的元数据（用于读取预设清单）
const selectedMeta = computed(() => {
  const inst = selectedInst.value
  if (!inst) return null
  return elements.find(e => e.key === inst.key) || null
})

// 应用预设：将预设中的属性值写到实例 props
function applyPreset(p: Preset) {
  const inst = selectedInst.value
  if (!inst) return
  const values = p?.propertyValues || {}
  for (const [k, v] of Object.entries(values)) {
    inst.props[k] = v as any
  }
}

// 4) 视口 <-> 世界 坐标转换
function clientToWorld(clientX: number, clientY: number) {
  const viewport = viewportRef.value!
  const rect = viewport.getBoundingClientRect()
  const x = (clientX - rect.left - state.translateX) / state.scale
  const y = (clientY - rect.top - state.translateY) / state.scale
  return { x, y }
}

function zoomAt(clientX: number, clientY: number, deltaY: number) {
  const { x: wx, y: wy } = clientToWorld(clientX, clientY)
  const zoomFactor = Math.exp(-deltaY * 0.0015) // 平滑缩放
  const next = Math.min(state.maxScale, Math.max(state.minScale, state.scale * zoomFactor))
  const prev = state.scale
  if (next === prev) return
  // 调整平移以保持光标下的世界点不变
  state.translateX = clientX - viewportRef.value!.getBoundingClientRect().left - wx * next
  state.translateY = clientY - viewportRef.value!.getBoundingClientRect().top - wy * next
  state.scale = next
}

// 5) 拖拽放置（来自左侧面板）
const DND_MIME = 'application/x-circuit-element'

function onPaletteDragStart(e: DragEvent, item: PaletteItem) {
  if (!e.dataTransfer) return
  e.dataTransfer.setData(DND_MIME, item.key)
  e.dataTransfer.setData('text/plain', item.key)
  e.dataTransfer.effectAllowed = 'copy'
}

function onCanvasDragOver(e: DragEvent) {
  e.preventDefault()
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}

function onCanvasDrop(e: DragEvent) {
  e.preventDefault()
  const key = e.dataTransfer?.getData(DND_MIME) || e.dataTransfer?.getData('text/plain')
  if (!key) return
  const item = palette.find(p => p.key === key)
  if (!item) return
  const { x, y } = clientToWorld(e.clientX, e.clientY)
  const defSize = item.defaultSize ?? 64
  const props: Record<string, number | string> = {}
    ; (item.propertySchemas || []).forEach((p) => { props[p.key] = p.default })
  // 通用：为所有元件注入“隐藏属性”控制，默认关闭
  if (props['hideProps'] === undefined) props['hideProps'] = 'off'
  const inst: Instance = { id: String(++idSeq), key: item.key, url: item.url, x: x, y: y, size: defSize, rotation: 0, props }
  // 仍保留 state 字段用于灯泡等展示，但对于开关用 props.state 作为真值
  if (item.stateImages) inst.state = String(props['state'] ?? 'off') as any
  instances.push(inst)
}

// 6) 背景平移
let isPanning = false
let panStart = { x: 0, y: 0 }
let panOrigin = { x: 0, y: 0 }

function onViewportMouseDown(e: MouseEvent) {
  // 只在点到空白区域时平移（元素自身会阻止冒泡）
  if (e.button !== 0) return
  isPanning = true
  panStart = { x: e.clientX, y: e.clientY }
  panOrigin = { x: state.translateX, y: state.translateY }
}

function onViewportMouseMove(e: MouseEvent) {
  if (!isPanning) return
  const dx = e.clientX - panStart.x
  const dy = e.clientY - panStart.y
  state.translateX = panOrigin.x + dx
  state.translateY = panOrigin.y + dy
}

function onViewportMouseUp() {
  isPanning = false
}

function onWheel(e: WheelEvent) {
  // 触控板垂直滚轮缩放，按住 Ctrl 也缩放
  if (e.ctrlKey || Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
    e.preventDefault()
    zoomAt(e.clientX, e.clientY, e.deltaY)
  }
}

// 7) 画布内元素拖动
let draggingId: string | null = null
let dragOffset = { x: 0, y: 0 } // 元素内相对偏移（世界坐标）
const mouseWorld = reactive({ x: 0, y: 0 })
// 滑动变阻器滑块拖动状态
let sliderDraggingId: string | null = null

function onInstanceMouseDown(e: MouseEvent, inst: Instance) {
  if (e.button !== 0) return
  e.stopPropagation()
  draggingId = inst.id
  const { x: wx, y: wy } = clientToWorld(e.clientX, e.clientY)
  dragOffset = { x: wx - inst.x, y: wy - inst.y }
}

function onViewportMouseMoveDrag(e: MouseEvent) {
  if (!draggingId) return
  const inst = instances.find(i => i.id === draggingId)
  if (!inst) return
  const { x: wx, y: wy } = clientToWorld(e.clientX, e.clientY)
  inst.x = wx - dragOffset.x
  inst.y = wy - dragOffset.y
}

function onViewportMouseUpDrag() {
  draggingId = null
}

// 将世界坐标转换为实例局部坐标（考虑旋转）
function worldToLocal(inst: Instance, wx: number, wy: number) {
  const dx = wx - inst.x
  const dy = wy - inst.y
  const rad = (inst.rotation || 0) * Math.PI / 180
  const cos = Math.cos(rad)
  const sin = Math.sin(rad)
  // 逆旋转
  const lx = dx * cos + dy * sin
  const ly = -dx * sin + dy * cos
  return { lx, ly }
}

function onViewportMouseMoveSlider(e: MouseEvent) {
  if (!sliderDraggingId) return
  const inst = instances.find(i => i.id === sliderDraggingId)
  if (!inst) return
  if (inst.key !== 'sliding_rheostat') return
  const { x: wx, y: wy } = clientToWorld(e.clientX, e.clientY)
  const { lx } = worldToLocal(inst, wx, wy)
  // 滑道范围（与 getRheostatPinOffset 对齐）
  const leftFrac = 0.25, rightFrac = 0.75
  const t = (lx / inst.size + 0.5 - leftFrac) / (rightFrac - leftFrac)
  const pos = Math.max(0, Math.min(1, t))
  inst.props['position'] = pos
}

function onViewportMouseUpSlider() {
  sliderDraggingId = null
}

// 连接点点击逻辑
function onEndpointClick(instId: string, cpIndex: number, e: MouseEvent) {
  e.stopPropagation()
  const ep = { instId, cpIndex }
  if (!pendingEndpoint) {
    pendingEndpoint = ep
    return
  }
  // 不允许同一端点两次
  if (pendingEndpoint.instId === instId && pendingEndpoint.cpIndex === cpIndex) {
    pendingEndpoint = null
    return
  }
  // 创建导线
  wires.push({ id: String(++wireSeq), a: pendingEndpoint, b: ep })
  pendingEndpoint = null
}

function removeWire(id: string, e?: MouseEvent) {
  if (e) e.stopPropagation()
  const idx = wires.findIndex(w => w.id === id)
  if (idx >= 0) wires.splice(idx, 1)
}

// 工具函数：获取实例、连接点坐标（世界坐标）
function getInstance(instId: string) {
  return instances.find(i => i.id === instId)
}

function getConnectionPointWorldPos(inst: Instance, cpIndex: number) {
  const elem = elements.find(e => e.key === inst.key)
  const cp = elem?.connectionPoints[cpIndex]
  if (!cp) return { x: inst.x, y: inst.y }
  const offsetX = (cp.x - 0.5) * inst.size
  const offsetY = (cp.y - 0.5) * inst.size
  // 旋转偏移
  const rad = (inst.rotation || 0) * Math.PI / 180
  const cos = Math.cos(rad)
  const sin = Math.sin(rad)
  const rx = offsetX * cos - offsetY * sin
  const ry = offsetX * sin + offsetY * cos
  return { x: inst.x + rx, y: inst.y + ry }
}

function onViewportMouseMoveTrack(e: MouseEvent) {
  const { x, y } = clientToWorld(e.clientX, e.clientY)
  mouseWorld.x = x
  mouseWorld.y = y
}

// 命中测试：鼠标是否靠近某连接点（返回最近的端点）
const SNAP_RADIUS = 12
function findClosestEndpoint() {
  let best: { instId: string; cpIndex: number; dist: number } | null = null
  for (const inst of instances) {
    const elem = elements.find(e => e.key === inst.key)
    const cps = elem?.connectionPoints || []
    cps.forEach((_, idx) => {
      const p = getConnectionPointWorldPos(inst, idx)
      const dx = p.x - mouseWorld.x
      const dy = p.y - mouseWorld.y
      const d = Math.hypot(dx, dy)
      if (d <= SNAP_RADIUS && (!best || d < best.dist)) best = { instId: inst.id, cpIndex: idx, dist: d }
    })
  }
  return best
}

type Nearest = { instId: string; cpIndex: number; dist: number } | null
const nearestEndpoint = computed<Nearest>(() => findClosestEndpoint())
const previewTarget = computed(() => {
  const n = nearestEndpoint.value
  // 若靠近端点，且不是与 pendingEndpoint 相同的端点，则吸附到该端点；否则跟随鼠标
  if (n && !(pendingEndpoint && n.instId === pendingEndpoint.instId && n.cpIndex === pendingEndpoint.cpIndex)) {
    const inst = getInstance(n.instId)!
    return getConnectionPointWorldPos(inst, n.cpIndex)
  }
  return { x: mouseWorld.x, y: mouseWorld.y }
})

function isNearest(instId: string, cpIndex: number) {
  const n = nearestEndpoint.value
  return !!n && n.instId === instId && n.cpIndex === cpIndex
}

// 悬停端点与其连通集高亮
const hoveredEndpoint = ref<EndpointRef | null>(null)
const connectedEndpointSet = computed<Set<string>>(() => {
  const set = new Set<string>()
  const start = hoveredEndpoint.value
  if (!start) return set
  // 构建无向邻接表（仅基于导线）
  const key = (ep: EndpointRef) => `${ep.instId}:${ep.cpIndex}`
  const adj = new Map<string, string[]>()
  const add = (a: EndpointRef, b: EndpointRef) => {
    const ka = key(a), kb = key(b)
    if (!adj.has(ka)) adj.set(ka, [])
    if (!adj.has(kb)) adj.set(kb, [])
    adj.get(ka)!.push(kb)
    adj.get(kb)!.push(ka)
  }
  for (const w of wires) add(w.a, w.b)
  // BFS/DFS 查找连通端点
  const startKey = key(start)
  const stack = [startKey]
  set.add(startKey)
  while (stack.length) {
    const cur = stack.pop()!
    for (const nxt of adj.get(cur) || []) {
      if (!set.has(nxt)) { set.add(nxt); stack.push(nxt) }
    }
  }
  return set
})
function isConnectedEndpoint(instId: string, cpIndex: number) {
  return connectedEndpointSet.value.has(`${instId}:${cpIndex}`)
}

// 7.5) 直流电路求解（MNA）
// - 支持：电压源(battery)、电阻(resistor/light_bulb/meter)、开关(ON=0V源，OFF=断开)、电感(DC=0V源)、电容(DC=断开)
// - 结果：每个实例的端口电压差与电流（方向：从连接点0 -> 连接点1 为正）

type InstLive = {
  v: number | null // 端口电压 v = V(p0) - V(p1) 或电压源设定值
  i: number | null // 支路电流，正方向 p0->p1
  node0?: string
  node1?: string
}

const instLive = reactive<Record<string, InstLive>>({})

function getElemMeta(inst: Instance) {
  return elements.find(e => e.key === inst.key)
}

// 针对滑动变阻器：计算滑块的屏幕内偏移（未考虑旋转，先期简化为只支持0°/180°朝向的水平滑动）
function getRheostatPinOffset(inst: Instance) {
  const pos = Math.max(0, Math.min(1, Number(inst.props?.['position'] ?? 0.5)))

  const leftFrac = 0.25, rightFrac = 0.75
  const cx = (leftFrac + (rightFrac - leftFrac) * pos) * inst.size
  return cx
}

// 并查集用于将端点连成节点
class DSU {
  parent = new Map<string, string>()
  find(x: string): string {
    let p = this.parent.get(x) || x
    if (p !== x) {
      p = this.find(p)
      this.parent.set(x, p)
    } else {
      this.parent.set(x, x)
    }
    return p
  }
  union(a: string, b: string) {
    const ra = this.find(a)
    const rb = this.find(b)
    if (ra !== rb) this.parent.set(ra, rb)
  }
}

function safeNum(v: any, def = 0) {
  const n = typeof v === 'number' ? v : Number(v)
  return Number.isFinite(n) ? n : def
}

type BuiltNet = {
  nodeIds: string[] // 所有节点根ID
  ground: string | null
  // 器件分类
  resistors: { inst: Instance; nPlus: string; nMinus: string; R: number }[]
  vsrcs: { inst: Instance; nPlus: string; nMinus: string; V: number }[]
}

function buildNet(): BuiltNet {
  const dsu = new DSU()
  const termId = (instId: string, cp: number) => `${instId}:${cp}`

  // 先把显式导线合并
  for (const w of wires) {
    dsu.union(termId(w.a.instId, w.a.cpIndex), termId(w.b.instId, w.b.cpIndex))
  }

  // 将“短路类器件”作为0V电压源（开关ON、DC电感）
  // OFF开关/电容视为断开，不合并

  // 收集所有端点以便形成节点清单
  const allTerms: string[] = []
  for (const inst of instances) {
    const meta = getElemMeta(inst)
    const cps = meta?.connectionPoints?.length || 0
    for (let i = 0; i < cps; i++) allTerms.push(termId(inst.id, i))
  }

  // 形成节点集合
  const roots = new Set<string>()
  for (const t of allTerms) roots.add(dsu.find(t))
  // 仅基于真实端点选择参考地
  const nodeIds = Array.from(roots)
  const ground = nodeIds.length ? nodeIds[0] : null // 任取一个参考地

  // 为后续引入的“内部节点”（例如串联内阻的中间点）保留集合，避免重复
  const nodeSet = new Set(nodeIds)
  const addNode = (nid: string) => {
    if (!nodeSet.has(nid)) { nodeSet.add(nid); nodeIds.push(nid) }
  }

  // 分类器件
  const resistors: BuiltNet['resistors'] = []
  const vsrcs: BuiltNet['vsrcs'] = []

  const getNode = (inst: Instance, cp: number) => dsu.find(termId(inst.id, cp))

  for (const inst of instances) {
    const meta = getElemMeta(inst)
    if (!meta || meta.connectionPoints.length < 2) continue
    const n0 = getNode(inst, 0)
    const n1 = getNode(inst, 1)

    const key = meta.key
    if (key === 'battery') {
      const V = safeNum(inst.props?.['voltage'], 0)
      const Rint = safeNum(inst.props?.['internalResistance'], 0.1)
      // 用内部节点将电压源与内阻串联：n0 --[V]--> nInt --[Rint]--> n1
      const nInt = `${inst.id}:int`
      addNode(nInt)
      vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V })
      resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) })
    } else if (key === 'power_supply') {
      const V = safeNum(inst.props?.['voltage'], 0)
      const Rint = safeNum(inst.props?.['internalResistance'], 0.1)
      // 用内部节点将电压源与内阻串联：n0 --[V]--> nInt --[Rint]--> n1
      const nInt = `${inst.id}:int`
      addNode(nInt)
      vsrcs.push({ inst, nPlus: n0, nMinus: nInt, V })
      resistors.push({ inst, nPlus: nInt, nMinus: n1, R: Math.max(1e-9, Rint) })
    } else if (key === 'switch') {
      const sw = (inst.props?.['state'] as any) === 'on' || inst.state === 'on'
      if (sw) {
        vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 })
      }
      // off = open, 忽略
    } else if (key === 'inductor') {
      // DC 等效 0V 源
      vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 })
    } else if (key === 'capacitor') {
      // DC 断开
      // 忽略
    } else if (key === 'sliding_rheostat') {
      // 四脚：A(0) B(1) C(2) D(3)
      const n2 = getNode(inst, 2)
      const n3 = getNode(inst, 3)
      // AB 直通：用 0V 电压源建模
      vsrcs.push({ inst, nPlus: n0, nMinus: n1, V: 0 })
      // 变量电阻：AB->C、AB->D 与滑块位置有关
      const Rmax = Math.max(1e-6, safeNum(inst.props?.['maxResistance'], 100))
      const pos = Math.max(0, Math.min(1, safeNum(inst.props?.['position'], 0.5)))
      const Rac = Math.max(1e-6, Rmax * pos)
      const Rad = Math.max(1e-6, Rmax * (1 - pos))
      // 将 AB 汇成一个等效节点（任选 A 侧 n0）到 C/D
      resistors.push({ inst, nPlus: n0, nMinus: n2, R: Rac })
      resistors.push({ inst, nPlus: n0, nMinus: n3, R: Rad })
      // CD 固定为最大电阻
      resistors.push({ inst, nPlus: n2, nMinus: n3, R: Rmax })
    } else if (key === 'resistor' || key === 'light_bulb' || key === 'meter' || key === 'resistance_box') {
      const R = Math.max(1e-6, safeNum(inst.props?.['resistance'], 0))
      resistors.push({ inst, nPlus: n0, nMinus: n1, R })
    } else {
      // 其他未知器件，按开路处理
    }
  }

  return { nodeIds, ground, resistors, vsrcs }
}

function solveMNA() {
  const net = buildNet()
  // 清空上一次结果
  for (const inst of instances) instLive[inst.id] = { v: null, i: null }

  if (!net.ground) return { ok: false }

  // 建索引（去掉参考地）
  const nodeIndex = new Map<string, number>()
  let idx = 0
  for (const nid of net.nodeIds) {
    if (nid === net.ground) continue
    nodeIndex.set(nid, idx++)
  }
  const n = nodeIndex.size
  const m = net.vsrcs.length
  const N = n + m
  if (N === 0) return { ok: true }

  // 构造 A x = z
  const A: number[][] = Array.from({ length: N }, () => Array(N).fill(0))
  const z: number[] = Array(N).fill(0)
  const gmin = 1e-12

  const mapNi = (nid: string) => (nid === net.ground ? -1 : (nodeIndex.get(nid) ?? -1))

  // 电导矩阵 G（顶部 n×n）
  for (const r of net.resistors) {
    const a = mapNi(r.nPlus)
    const b = mapNi(r.nMinus)
    const g = 1 / r.R
    if (a >= 0) A[a][a] += g + gmin
    if (b >= 0) A[b][b] += g + gmin
    if (a >= 0 && b >= 0) {
      A[a][b] -= g
      A[b][a] -= g
    }
  }
  // 稳定性：对所有节点加 gmin 到地
  for (let i = 0; i < n; i++) A[i][i] += gmin

  // 电压源耦合矩阵 B、C
  // B: (n×m)，位于 A 的右上；C = B^T（m×n），位于 A 的左下
  net.vsrcs.forEach((vs, k) => {
    const p = mapNi(vs.nPlus)
    const q = mapNi(vs.nMinus)
    const row = n + k // 对应电流未知量的位置
    z[row] = vs.V
    if (p >= 0) { A[p][n + k] += 1; A[row][p] += 1 }
    if (q >= 0) { A[q][n + k] -= 1; A[row][q] -= 1 }
    // D 为 0，已初始化
  })

  // 求解线性方程
  const x = gaussianSolve(A, z)
  if (!x) return { ok: false }

  // 组装节点电压
  const Vn = (nid: string) => {
    const i = mapNi(nid)
    return i >= 0 ? x[i] : 0 // 地节点 0V
  }

  // 写回每个器件的 v / i
  for (const r of net.resistors) {
    const vp = Vn(r.nPlus)
    const vn = Vn(r.nMinus)
    const v = vp - vn
    const i = v / r.R
    instLive[r.inst.id] = { v, i, node0: r.nPlus, node1: r.nMinus }
  }
  net.vsrcs.forEach((vs, k) => {
    const vp = Vn(vs.nPlus)
    const vn = Vn(vs.nMinus)
    const v = vp - vn // 由解出的节点电压
    const i = x[n + k] // 电压源支路电流，方向 nPlus -> nMinus
    instLive[vs.inst.id] = { v, i, node0: vs.nPlus, node1: vs.nMinus }
  })

  // 对于未分类（断开）器件，保持 null

  // 根据计算电流，自动切换需要按电流显示状态的元件（如灯泡）
  const CURRENT_ON_THRESHOLD = 0.1 // A，即 100 mA
  for (const inst of instances) {
    if (inst.key === 'light_bulb') {
      const live = instLive[inst.id]
      const iabs = live && typeof live.i === 'number' ? Math.abs(live.i) : 0
      if (iabs > CURRENT_ON_THRESHOLD) inst.state = 'on'
      else inst.state = 'off'
    }
  }

  return { ok: true }
}

// 简单高斯消元（带部分选主元）
function gaussianSolve(A: number[][], b: number[]): number[] | null {
  const n = b.length
  // 扩展矩阵
  for (let i = 0; i < n; i++) A[i].push(b[i])

  for (let col = 0, row = 0; col < n && row < n; col++, row++) {
    // 选主元
    let piv = row
    let maxAbs = Math.abs(A[row][col])
    for (let r = row + 1; r < n; r++) {
      const v = Math.abs(A[r][col])
      if (v > maxAbs) { maxAbs = v; piv = r }
    }
    if (maxAbs < 1e-14) {
      // 退化，放弃
      return null
    }
    if (piv !== row) { const tmp = A[piv]; A[piv] = A[row]; A[row] = tmp }

    // 归一化
    const div = A[row][col]
    for (let c = col; c <= n; c++) A[row][c] /= div

    // 消元
    for (let r = 0; r < n; r++) {
      if (r === row) continue
      const factor = A[r][col]
      if (factor === 0) continue
      for (let c = col; c <= n; c++) A[r][c] -= factor * A[row][c]
    }
  }

  // 回读解
  const x = new Array(n).fill(0)
  for (let i = 0; i < n; i++) x[i] = A[i][n]
  return x
}

// 绑定求解到响应式数据
watchEffect(() => { solveMNA() })



function meterText(inst: Instance) {
  const live = instLive[inst.id]
  const i = live?.i ?? 0
  const r = inst.props?.['resistance'] ? safeNum(inst.props?.['resistance'], 0) : 0
  const code = String(inst.props?.['renderFunc'] || '')
  if (!code) return `${(i).toFixed(6)} A`
  try {
    // 允许两种形态："(i)=>..." 或 "(i,v,props)=>..."
    const fn = new Function('i', 'r', 'props', 'return (' + code + ')(i, r, props)')
    const out = fn(i, r, inst.props)
    return String(out)
  } catch {
    return `ERR_RF`
  }
}

// 灯泡发光参数：随电流变化增加线条数量、长度、透明度
function clamp01(x: number) { return x < 0 ? 0 : x > 1 ? 1 : x }
function getBulbGlow(inst: Instance) {
  if (inst.key !== 'light_bulb') return { t: 0, count: 0, opacity: 0, lenFrac: 0 }
  const live = instLive[inst.id]
  const iabs = live && typeof live.i === 'number' ? Math.abs(live.i) : 0
  // 使 0.5A 达到比较饱和的光效，0A 无光
  const t = clamp01(iabs / 0.5)
  const count = 12 + Math.round(8 * t) // 12..20 根射线
  const opacity = 0.8 * t // 0..0.8
  const lenFrac = 0.18 + 0.45 * t // 以尺寸为基准的长度比例
  return { t, count, opacity, lenFrac }
}

// 电阻色环：根据实时电阻值绘制 4 环（两位有效数字 + 倍率 + 5% 容差金色）
const DIGIT_COLORS = ['#000000', '#8B4513', '#FF0000', '#FFA500', '#FFFF00', '#008000', '#0000FF', '#800080', '#808080', '#FFFFFF']
const GOLD = '#D4AF37'
const SILVER = '#C0C0C0'

function computeResistorColorBands(ohms: number): string[] {
  // 4 环：D1, D2, multiplier, tolerance(默认金色)
  if (!Number.isFinite(ohms)) ohms = 0
  const R = Math.max(0, ohms)
  // 特殊：0Ω 跳线，显示单一黑环加固定金色作为占位
  if (R === 0) return ['#000000', '#000000', '#000000', GOLD]

  // 将 R 逼近到 (10..99) × 10^k 的形式，以两位有效数字表示
  let exp = Math.floor(Math.log10(R)) - 1
  // 处理非常小的阻值（<0.01Ω）和非常大的阻值，限制到可表示范围
  if (!Number.isFinite(exp)) exp = 0
  let N = R / Math.pow(10, exp)
  // 目标区间为 [10, 99]
  let D = Math.round(N)
  if (D < 10) {
    // 提升到两位
    while (D < 10) { D *= 10; exp -= 1 }
  } else if (D > 99) {
    // 回落并进位倍率
    while (D > 99) { D = Math.round(D / 10); exp += 1 }
  }

  // 提取两位数字
  D = Math.max(10, Math.min(99, D))
  let d1 = Math.floor(D / 10)
  let d2 = D % 10

  // 倍率色环支持指数 -2..9（-1 金、-2 银；其余 0..9 用数字色）
  let mulColor: string
  if (exp <= -2) mulColor = SILVER
  else if (exp === -1) mulColor = GOLD
  else mulColor = DIGIT_COLORS[Math.max(0, Math.min(9, exp))]

  const c1 = DIGIT_COLORS[d1]
  const c2 = DIGIT_COLORS[d2]
  const tol = GOLD // 默认 5%
  return [c1, c2, mulColor, tol]
}

function getResistorBandColors(inst: Instance): string[] {
  if (inst.key !== 'resistor') return []
  const R = safeNum(inst.props?.['resistance'], 0)
  return computeResistorColorBands(R)
}

// 色环在图片中的相对位置（经验位置，适配当前素材）
const RES_BAND_X_FRACS = [0.4, 0.4666, 0.5334, 0.6]
const RES_BAND_WIDTH_FRAC = 0.03
const RES_BAND_TOP_FRAC = 0.432
const RES_BAND_HEIGHT_FRAC = 0.13

function getResBandX(inst: Instance, index: number) {
  const cx = inst.size * (RES_BAND_X_FRACS[index] || 0.3)
  const w = inst.size * RES_BAND_WIDTH_FRAC
  return cx - w / 2
}

// 8) 初始居中到世界中心
function centerWorld() {
  const vp = viewportRef.value
  if (!vp) return
  const rect = vp.getBoundingClientRect()
  const center = state.worldSize / 2
  state.translateX = rect.width / 2 - center * state.scale
  state.translateY = rect.height / 2 - center * state.scale
}

onMounted(() => {
  centerWorld()
})

// 9) 导出 / 导入（JSON 序列化）
type SaveWorld = Pick<typeof state, 'scale' | 'translateX' | 'translateY' | 'worldSize' | 'gridSize'>
// 新的连接点保存结构：每个实例在 props 下包含 __connections，按连接点索引记录连接到的端点
type SaveConnectionRef = { instId: string; cpIndex: number }
type SaveConnections = Record<string /* cpIndex */, SaveConnectionRef[]>
type SaveInstance = Pick<Instance, 'id' | 'key' | 'x' | 'y' | 'size' | 'rotation' | 'state'> & {
  props: Record<string, any> & { __connections?: SaveConnections }
}
type SaveFile = {
  version: 3
  world: SaveWorld
  instances: SaveInstance[]
}

function makeSave(): SaveFile {
  const world: SaveWorld = {
    scale: state.scale,
    translateX: state.translateX,
    translateY: state.translateY,
    worldSize: state.worldSize,
    gridSize: state.gridSize,
  }

  // 基于当前 wires 构建每个实例的端点连接表
  const connMap: Record<string, Record<number, SaveConnectionRef[]>> = {}
  const addConn = (from: EndpointRef, to: EndpointRef) => {
    if (!connMap[from.instId]) connMap[from.instId] = {}
    if (!connMap[from.instId][from.cpIndex]) connMap[from.instId][from.cpIndex] = []
    connMap[from.instId][from.cpIndex].push({ instId: to.instId, cpIndex: to.cpIndex })
  }
  for (const w of wires) {
    addConn(w.a, w.b)
    addConn(w.b, w.a)
  }

  const data: SaveFile = {
    version: 3,
    world,
    instances: instances.map(i => {
      const propsCopy: Record<string, any> = { ...i.props }
      const m = connMap[i.id]
      // 始终写入 __connections（即使为空对象），确保未连接元件也被保留
      // 将 number 索引键转成字符串键，以便 JSON 键稳定
      const mapped: SaveConnections = {}
      if (m) {
        for (const [k, arr] of Object.entries(m)) mapped[String(k)] = arr.map(x => ({ instId: x.instId, cpIndex: x.cpIndex }))
      }
      propsCopy.__connections = mapped
      return {
        id: i.id,
        key: i.key,
        x: i.x,
        y: i.y,
        size: i.size,
        rotation: i.rotation,
        props: propsCopy,
        state: i.state,
      }
    }),
  }
  return data
}

function downloadJSON(filename: string, text: string) {
  const blob = new Blob([text], { type: 'application/json;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
}

function pad(n: number) { return n.toString().padStart(2, '0') }
function timestampName() {
  const d = new Date()
  const y = d.getFullYear()
  const m = pad(d.getMonth() + 1)
  const day = pad(d.getDate())
  const hh = pad(d.getHours())
  const mm = pad(d.getMinutes())
  const ss = pad(d.getSeconds())
  return `${y}${m}${day}-${hh}${mm}${ss}`
}

function onExportJSON() {
  const data = makeSave()
  const json = JSON.stringify(data, null, 2)
  downloadJSON(`circuit-${timestampName()}.json`, json)
}

const importInputRef = ref<HTMLInputElement | null>(null)
function triggerImport() { importInputRef.value?.click() }

function safeNumber(v: any, def = 0) {
  const n = typeof v === 'number' ? v : Number(v)
  return Number.isFinite(n) ? n : def
}

function loadFromJSONText(text: string) {
  let obj: any
  try {
    obj = JSON.parse(text)
  } catch (err) {
    alert('JSON 解析失败：' + (err as Error).message)
    return
  }
  if (!obj || typeof obj !== 'object') {
    alert('无效的文件内容')
    return
  }
  // 兼容校验
  const world = obj.world || {}
  const rawInstances = Array.isArray(obj.instances) ? obj.instances : []

  // 过滤仅保留存在于库里的元件
  const knownKeys = new Set(elements.map(e => e.key))
  const validInstances: Instance[] = []
  for (const r of rawInstances) {
    if (!r || typeof r !== 'object') continue
    if (!knownKeys.has(r.key)) continue
    // 提前拿到连接信息（若为新格式）
    const rawProps = (typeof r.props === 'object' && r.props) ? { ...r.props } : {}
    const savedConnections: SaveConnections | undefined = rawProps.__connections
    if ('__connections' in rawProps) delete (rawProps as any)['__connections']
    // 兼容：若缺少通用隐藏属性开关，补上默认值
    if (rawProps['hideProps'] === undefined) rawProps['hideProps'] = 'off'
    const inst: Instance = {
      id: String(r.id ?? (++idSeq)),
      key: String(r.key),
      url: elements.find(e => e.key === r.key)!.url,
      x: safeNumber(r.x),
      y: safeNumber(r.y),
      size: safeNumber(r.size, elements.find(e => e.key === r.key)?.defaultSize ?? 64),
      rotation: safeNumber(r.rotation, 0),
      props: rawProps,
      state: r.state === 'on' || r.state === 'off' ? r.state : undefined,
    }
    // 兼容老文件：若为开关且 props.state 缺失，则使用 legacy state 或默认 off
    if (inst.key === 'switch') {
      const cur = String((inst.props as any)['state'] ?? '')
      if (cur !== 'on' && cur !== 'off') {
        (inst.props as any)['state'] = (inst.state === 'on' ? 'on' : 'off')
      }
    }
    // 暂存连接信息在实例对象上（不入 props）以便后续重建导线
    ; (inst as any).__savedConnections = savedConnections
    validInstances.push(inst)
  }

  // 基于实例与元件接线点数量校验导线
  const instMap = new Map(validInstances.map(i => [i.id, i]))
  const validWires: Wire[] = []

  // 从每个实例 props.__connections 重建导线（去重）
  const dedup = new Set<string>()
  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 })
      }
    }

  }

  // 写回世界与状态
  state.scale = safeNumber(world.scale, state.scale)
  state.translateX = safeNumber(world.translateX, state.translateX)
  state.translateY = safeNumber(world.translateY, state.translateY)
  // worldSize/gridSize 通常固定，如需覆盖可解开注释
  // state.worldSize = safeNumber(world.worldSize, state.worldSize)
  // state.gridSize = safeNumber(world.gridSize, state.gridSize)

  // 覆盖当前电路
  instances.splice(0, instances.length, ...validInstances)
  wires.splice(0, wires.length, ...validWires)
  selectedId.value = null
  pendingEndpoint = null

  // 续增 id 序列
  const maxInstId = validInstances.reduce((m, i) => Math.max(m, Number(i.id) || 0), 0)
  const maxWireId = validWires.reduce((m, w) => Math.max(m, Number(w.id) || 0), 0)
  idSeq = Math.max(idSeq, maxInstId)
  wireSeq = Math.max(wireSeq, maxWireId)
}

function onImportFileChange(e: Event) {
  const input = e.target as HTMLInputElement
  const file = input.files && input.files[0]
  if (!file) return
  const reader = new FileReader()
  reader.onload = () => {
    const text = String(reader.result || '')
    loadFromJSONText(text)
  }
  reader.onerror = () => alert('读取文件失败')
  reader.readAsText(file, 'utf-8')
  // 重置以便可重复选择同一文件
  input.value = ''
}

// 提供给 AI 助手的实时电路状态（每次访问都会序列化最新数据）
const aiState = computed(() => makeSave())

function onAiError(e: string) {
  // 采用函数方式，避免模板类型检查对全局 alert 的报错
  alert(e)
}

// 删除当前选中的元件以及所有关联导线
function deleteSelected() {
  const inst = selectedInst.value
  if (!inst) return
  // 删除连接到该实例的所有导线
  for (let i = wires.length - 1; i >= 0; i--) {
    const w = wires[i]
    if (w.a.instId === inst.id || w.b.instId === inst.id) wires.splice(i, 1)
  }
  // 如果正在连线且端点来自该实例，取消预连
  if (pendingEndpoint && pendingEndpoint.instId === inst.id) pendingEndpoint = null
  // 如果正在拖拽该实例，停止拖拽
  if (draggingId === inst.id) draggingId = null
  // 删除实例本身
  const idx = instances.findIndex(i => i.id === inst.id)
  if (idx >= 0) instances.splice(idx, 1)
  // 清理实时状态与选中
  delete instLive[inst.id]
  selectedId.value = null
}

// 文本框编辑函数
function editTextBox(inst: Instance) {
  // 仅处理文本框
  if (inst.key !== 'text_box') return
  const cur = String(inst.props?.['text'] ?? '')
  const next = globalThis.prompt ? globalThis.prompt('编辑文本', cur) : null
  if (next !== null) inst.props['text'] = next
}
</script>

<template>
  <div class="app" :style="{ '--ai-width': showAI ? '360px' : '0px' }">
    <!-- 左侧元件面板 -->
    <aside class="sidebar">
      <h2>元件</h2>
      <div class="palette">
        <div v-for="item in palette" :key="item.key" class="palette-item"
          draggable="true" @dragstart="(e) => onPaletteDragStart(e, item)"
          :title="item.name">
          <img :src="item.url" :alt="item.name" draggable="false" />
          <span class="label">{{ item.name }}</span>
        </div>
      </div>

      <div class="file-actions">
        <button class="btn" @click="onExportJSON">导出 JSON</button>
        <button class="btn" @click="triggerImport">导入 JSON</button>
        <input ref="importInputRef" type="file" accept="application/json,.json"
          class="hidden-file" @change="onImportFileChange" />
      </div>
    </aside>

    <!-- 画布视口（可平移、缩放、接收拖拽） -->
    <main class="viewport" ref="viewportRef" @pointerdown="onViewportMouseDown"
      @pointermove="(e) => { onViewportMouseMove(e); onViewportMouseMoveDrag(e); onViewportMouseMoveTrack(e); onViewportMouseMoveSlider(e) }"
      @pointerup="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
      @pointerleave="() => { onViewportMouseUp(); onViewportMouseUpDrag(); onViewportMouseUpSlider() }"
      @wheel="onWheel" @dragover="onCanvasDragOver" @drop="onCanvasDrop"
      @click="clearSelection">
      <!-- 世界平面：网格背景，应用 transform -->
      <div class="world" :style="worldStyle" ref="worldRef">
        <!-- 导线（SVG 覆盖层） -->
        <svg class="wires" :width="state.worldSize" :height="state.worldSize"
          @mousemove="onViewportMouseMoveTrack">
          <g>
            <g v-for="w in wires" :key="w.id" class="wire-group"
              @click.stop="(e) => removeWire(w.id, e)">
              <line class="wire-base"
                :x1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).x"
                :y1="getConnectionPointWorldPos(getInstance(w.a.instId)!, w.a.cpIndex).y"
                :x2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).x"
                :y2="getConnectionPointWorldPos(getInstance(w.b.instId)!, w.b.cpIndex).y"
                stroke-linecap="round" vector-effect="non-scaling-stroke" />
            </g>

            <!-- 预览线：当选中一个端点时，显示到鼠标或吸附端点的线 -->
            <line v-if="pendingEndpoint"
              :x1="getConnectionPointWorldPos(getInstance(pendingEndpoint.instId)!, pendingEndpoint.cpIndex).x"
              :y1="getConnectionPointWorldPos(getInstance(pendingEndpoint.instId)!, pendingEndpoint.cpIndex).y"
              :x2="previewTarget.x" :y2="previewTarget.y"
              class="wire preview" />
          </g>
        </svg>

        <!-- 已放置的元件（可拖动） -->
        <div v-for="inst in instances" :key="inst.id" class="inst"
          :class="{ selected: selectedId === inst.id }"
          :style="{ left: inst.x + 'px', top: inst.y + 'px', transform: 'translate(-50%, -50%) rotate(' + (inst.rotation || 0) + 'deg)' }"
          @pointerdown="(e) => onInstanceMouseDown(e, inst)"
          @click.stop="() => selectInstance(inst.id)" :title="(elements.find(e => e.key === inst.key)?.name || inst.key)">
          <div class="inst-box"
            :style="{ width: inst.size > 0 ? inst.size + 'px' : 'auto', height: inst.size > 0 ? inst.size + 'px' : 'auto' }">
            <img v-if="inst.key !== 'text_box'" :src="(elements.find(e => e.key === inst.key)?.stateImages
              ? (((inst.key === 'switch' ? (inst.props['state'] as string) : inst.state) === 'on')
                ? elements.find(e => e.key === inst.key)!.stateImages!.on
                : elements.find(e => e.key === inst.key)!.stateImages!.off)
              : inst.url)" :alt="inst.key" :draggable="false"
              @contextmenu.prevent="() => {
                const meta = elements.find(e => e.key === inst.key);
                if (!meta?.stateImages) return;
                if (inst.key === 'switch') {
                  const cur = String(inst.props['state'] ?? 'off');
                  inst.props['state'] = cur === 'on' ? 'off' : 'on'
                } else {
                  inst.state = inst.state === 'on' ? 'off' : 'on'
                }
              }" />

            <!-- 文本框渲染层：不参与电路，仅显示可编辑文本 -->
            <div v-if="inst.key === 'text_box'" class="text-box"
              :style="{
                fontSize: (Number(inst.props?.['fontSize'] ?? 24)) + 'px',
                color: String(inst.props?.['color'] ?? '#111827'),
              }"
              @dblclick.stop="editTextBox(inst)"
              title="双击编辑文本">
              {{ String(inst.props?.['text'] ?? '') }}
            </div>

            <!-- 电阻色环叠加层：根据实时电阻值渲染 4 个色带 -->
            <template v-if="inst.key === 'resistor'">
              <div v-for="(c, bi) in getResistorBandColors(inst)" :key="bi"
                class="res-band" :style="{
                  left: getResBandX(inst, bi) + 'px',
                  top: (inst.size * RES_BAND_TOP_FRAC) + 'px',
                  width: (inst.size * RES_BAND_WIDTH_FRAC) + 'px',
                  height: (inst.size * RES_BAND_HEIGHT_FRAC) + 'px',
                  backgroundColor: c
                }"></div>
            </template>

            <!-- 滑动变阻器滑块 -->
            <img v-if="inst.key === 'sliding_rheostat'" class="rheo-pin"
              :src="(elements.find(e => e.key === inst.key) as any)?.pinUrl"
              alt="pin" draggable="false" :style="{
                left: ((getRheostatPinOffset(inst) / inst.size) * 100) + '%',
              }" @pointerdown.stop="() => { sliderDraggingId = inst.id }" />

            <!-- 灯泡发散光线叠加层 -->
            <svg v-if="inst.key === 'light_bulb'" class="bulb-glow"
              :width="inst.size" :height="inst.size"
              :viewBox="'0 0 ' + inst.size + ' ' + inst.size"
              aria-hidden="true">
              <g :opacity="getBulbGlow(inst).opacity">
                <line v-for="k in getBulbGlow(inst).count" :key="k"
                  :x1="(inst.size / 2) + Math.cos(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * 0.42)"
                  :y1="(inst.size / 2) + Math.sin(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * 0.42)"
                  :x2="(inst.size / 2) + Math.cos(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * (0.42 + getBulbGlow(inst).lenFrac))"
                  :y2="(inst.size / 2) + Math.sin(2 * Math.PI * (k - 1) / getBulbGlow(inst).count) * (inst.size * (0.42 + getBulbGlow(inst).lenFrac))"
                  stroke="#f59e0b" stroke-width="2" stroke-linecap="round"
                  vector-effect="non-scaling-stroke" />
              </g>
            </svg>

            <!-- meter 显示层 -->
            <div v-if="inst.key === 'meter'" class="meter-label">
              {{ meterText(inst) }}
            </div>

            <div v-if="inst.key === 'power_supply'" class="power-supply-label">
              <span>{{ instLive[inst.id]?.v?.toFixed(2) }} V</span>
              <span>{{ (-(instLive[inst.id]?.i ?? 0)).toFixed(2) }} A</span>
            </div>
          </div>
        </div>

        <!-- 连接点（显示为虚线圆圈） -->
        <div v-for="inst in instances" :key="inst.id + '-cps'">
          <template
            v-for="(cp, idx) in (elements.find(e => e.key === inst.key)?.connectionPoints || [])"
            :key="inst.id + '-' + idx">
            <div class="endpoint" :class="{
              active: pendingEndpoint && pendingEndpoint.instId === inst.id && pendingEndpoint.cpIndex === idx,
              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)"
              @pointerenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }"
              @pointerleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }"
              :title="cp.name || ('P' + (idx + 1))"></div>
          </template>
        </div>
      </div>
    </main>

    <!-- 右侧 AI 助手面板 -->
    <aside v-if="showAI" class="ai-aside">
      <AIAssistant :state="aiState" @error="onAiError"
        @close="showAI = false" />
    </aside>
    <button v-else class="ai-reopen" title="打开 AI 助手"
      @click="showAI = true">◀</button>

    <!-- 底部属性面板：编辑选中元件的业务属性（非尺寸） -->
    <footer v-if="selectedInst" class="bottom-panel"
      :style="{ right: showAI ? '360px' : '0' }">
      <div class="panel-inner">
        <div class="prop-title">{{ selectedInst.key }} 属性</div>
          <!-- 预设按钮区域：显示当前元件的预设，点击一键应用（当未隐藏属性时） -->
          <div v-if="selectedMeta?.preset?.length && String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'" class="presets">
          <span class="preset-label">预设</span>
          <button v-for="p in selectedMeta!.preset" :key="p.name"
            class="preset-btn" @click.stop="applyPreset(p)">{{
              p.name
            }}</button>
        </div>
        <div class="props">
          <!-- 实时电学量（在隐藏属性=on时不显示） -->
          <label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
            <span class="label">电压</span>
            <span>{{ formatValue(instLive[selectedInst!.id]?.v ?? null, 'V')
              }}</span>
          </label>
          <label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
            <span class="label">电流</span>
            <span>{{ formatValue(instLive[selectedInst!.id]?.i ?? null, 'A')
              }}</span>
          </label>
          <!-- 旋转控制（在隐藏属性=on时不显示） -->
          <label class="prop-row" v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'">
            <span class="label">旋转</span>
            <input type="range" min="-180" max="180" step="1"
              v-model.number="selectedInst!.rotation" />
            <input type="number" class="deg-input" step="1"
              v-model.number="selectedInst!.rotation" />
            <span class="unit">deg</span>
            <button class="rot-btn"
              @click="selectedInst!.rotation = ((selectedInst!.rotation || 0) - 45 + 360) % 360">-45°</button>
            <button class="rot-btn"
              @click="selectedInst!.rotation = ((selectedInst!.rotation || 0) + 45) % 360">+45°</button>
          </label>
            <!-- 通用：隐藏属性开关（始终显示） -->
            <label class="prop-row">
              <span class="label">隐藏属性</span>
              <select v-model="selectedInst!.props['hideProps'] as string">
                <option value="off">off</option>
                <option value="on">on</option>
              </select>
            </label>
            <!-- 业务属性编辑：当未隐藏属性时显示 -->
            <template v-if="String(selectedInst!.props['hideProps'] ?? 'off') !== 'on'"
              v-for="schema in (elements.find(e => e.key === selectedInst!.key)?.propertySchemas || [])"
              :key="schema.key">
              <label class="prop-row" v-if="schema.key !== 'hideProps'">
                <span class="label">{{ schema.label }}</span>
                <input v-if="schema.type === 'number'" type="number" step="any"
                  v-model.number="selectedInst!.props[schema.key]" />
                <input v-else-if="schema.type === 'text'" type="text"
                  v-model="selectedInst!.props[schema.key] as string" />
                <input v-else-if="schema.type === 'color'" type="color"
                  v-model="selectedInst!.props[schema.key] as string" />
                <select v-else-if="schema.type === 'select'"
                  v-model="selectedInst!.props[schema.key] as string">
                  <option
                    v-for="opt in (Array.isArray((schema as any).options) ? (schema as any).options : [])"
                    :key="typeof opt === 'string' ? opt : (opt.value)"
                    :value="typeof opt === 'string' ? opt : (opt.value)">
                    {{ typeof opt === 'string' ? opt : (opt.label || opt.value) }}
                  </option>
                </select>
                <span class="unit"
                  v-if="schema.type === 'number' && (schema as any).unit">{{
                    (schema as any).unit }}</span>
              </label>
            </template>
        </div>
        <!-- 右侧操作区：删除按钮 -->
        <div class="panel-actions">
          <button class="danger-btn" title="删除此元件及其所有连接的导线"
            @click="deleteSelected">删除元件</button>
        </div>
      </div>
    </footer>
  </div>

</template>

<style scoped>
.app {
  display: grid;
  grid-template-columns: 140px 1fr var(--ai-width, 360px);
  /* 左栏 / 画布 / AI 辅助 */
  grid-template-rows: 100vh;
  overflow: hidden;
  touch-action: none;
  user-select: none;
}

.sidebar {
  border-right: 1px solid #e5e7eb;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: #fafafa;
}

.file-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.file-actions .btn {
  padding: 6px 10px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #fff;
  color: #111827;
  cursor: pointer;
}

.file-actions .btn:hover {
  background: #f3f4f6;
}

.hidden-file {
  display: none;
}

.sidebar h2 {
  margin: 0 0 8px 0;
  font-size: 16px;
  color: #111827;
}

.palette {
  display: grid;
  grid-template-columns: repeat(1, minmax(0, 1fr));
  gap: 10px;
  overflow: auto;
}

.palette-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  cursor: grab;
  user-select: none;
}

.palette-item:active {
  cursor: grabbing;
}

.palette-item img {
  width: 56px;
  height: 56px;
  object-fit: contain;
}

.palette-item .label {
  font-size: 12px;
  color: #374151;
}

.inspector {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.inspector h3 {
  margin: 0;
  font-size: 14px;
  color: #111827;
}

.inspector .row {
  display: grid;
  grid-template-columns: 60px 1fr auto;
  align-items: center;
  gap: 8px;
}

.inspector .row label {
  color: #4b5563;
  font-size: 12px;
}

.inspector .row .num {
  width: 70px;
}

.viewport {
  position: relative;
  overflow: hidden;
  background: #fff;
}

/* 右侧 AI 面板容器 */
.ai-aside {
  border-left: 1px solid #e5e7eb;
  height: 100vh;
  overflow: hidden;
}

/* 右侧重新打开箭头按钮（在隐藏时显示） */
.ai-reopen {
  position: fixed;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10;
  padding: 8px 10px;
  border: 1px solid #e5e7eb;
  border-radius: 999px 0 0 999px;
  background: #fff;
  color: #111827;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

.ai-reopen:hover {
  background: #f3f4f6;
}

.world {
  position: absolute;
  left: 0;
  top: 0;
  /* 网格背景（细线 + 粗线，每 5 格一条） */
  background-image:
    linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
    linear-gradient(to right, rgba(0, 0, 0, 0.09) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.09) 1px, transparent 1px);
  background-size:
    var(--grid) 1px,
    1px var(--grid),
    var(--grid5) 1px,
    1px var(--grid5);
  background-position: 0 0, 0 0, 0 0, 0 0;
}

.wires {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
}

/* 新增：分层导线样式 */
.wire-group {
  cursor: pointer;
}

.wire-base {
  stroke: #111827;
  stroke-width: 3;
  opacity: 0.85;
}

.wire-highlight {
  stroke-width: 2;
  mix-blend-mode: multiply;
}

/* 悬停时微弱高亮 */
.wire-group:hover .wire-base {
  opacity: 0.95;
}

.wire-group:hover .wire-highlight {
  filter: brightness(1.06);
}

/* 放大可点击区域（不可见）以提升命中，通过 pointer-events 控制组级点击 */
.wire-hit {
  stroke: transparent;
  stroke-width: 10;
}

/* 预览线保留原 class 名，便于最小改动 */
.wire {
  stroke: #111827;
  stroke-width: 2;
}

.wire.preview {
  stroke: #3b82f6;
  stroke-dasharray: 10 6;
  animation: dash-move 1.2s linear infinite;
  opacity: 0.9;
}

@keyframes dash-move {
  from {
    stroke-dashoffset: 0;
  }

  to {
    stroke-dashoffset: -32;
  }
}

.inst {
  position: absolute;
  transform: translate(-50%, -50%);
  cursor: move;
  user-select: none;
  z-index: 2;
}

.inst .inst-box {
  position: relative;
}

.inst img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: -webkit-optimize-contrast;
}

/* 电阻色环样式 */
.res-band {
  position: absolute;
  transform: translateX(-50%);
  opacity: 0.8;
  top: 0;
  left: 0;
  border-radius: 2px;
  box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.2);
  pointer-events: none;
}

.rheo-pin {
  position: absolute;
  z-index: 3;
  cursor: ew-resize;
  pointer-events: auto;
  width: 15% !important;
  height: auto !important;
  top: 25.5% !important;
  transform: translateX(-50%);
}

.bulb-glow {
  position: absolute;
  left: 0;
  top: -20%;
  pointer-events: none;
  /* 轻微发光的滤镜 */
  filter: drop-shadow(0 0 2px rgba(245, 158, 11, 0.6));
}

.inst.selected img {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
  border-radius: 6px;
}

.meter-label {
  position: absolute;
  right: 25%;
  top: 20%;
  color: #111827;
  font-family: "meter-font";
  font-size: 20px;
  white-space: nowrap;
  pointer-events: none;
}

.power-supply-label {
  position: absolute;
  right: 44%;
  height: 38%;
  top: 17%;
  color: #62D3EF;
  text-shadow: 0 0 2px #62D3EF, 0 0 6px #62d3ef44;
  font-family: "meter-font";
  font-size: 22px;
  white-space: nowrap;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-between;
}

.endpoint {
  position: absolute;
  width: 14px;
  height: 14px;
  transform: translate(-50%, -50%);
  border: 2px dashed #3b82f6;
  border-radius: 50%;
  background: #ffffffcc;
  z-index: 3;
  cursor: crosshair;
}

.endpoint.active {
  background: #dbeafe;
  border-color: #2563eb;
}

.endpoint.hover {
  transform: translate(-50%, -50%) scale(1.25);
}

/* 连通端点高亮：当某端点悬停时，其所有通过导线连通的端点以强调色显示 */
.endpoint.connected {
  border-color: #10b981;
  /* teal-500 */
  background: #ecfdf5;
  /* teal-50 */
  transform: translate(-50%, -50%) scale(1.25);
}

.bottom-panel {
  position: fixed;
  left: 140px;
  /* 与左侧栏对齐 */
  right: 360px;
  /* 默认让出 AI 侧栏宽度，实际由内联样式覆盖 */
  bottom: 0;
  background: #f9fafb;
  border-top: 1px solid #e5e7eb;
}

.bottom-panel .panel-inner {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 16px;
}

/* 右侧操作区 */
.panel-actions {
  margin-left: auto;
}

.danger-btn {
  padding: 6px 10px;
  border: 1px solid #fecaca;
  border-radius: 6px;
  background: #fee2e2;
  color: #b91c1c;
  cursor: pointer;
  width: max-content;
}

.danger-btn:hover {
  background: #fecaca;
  border-color: #ef4444;
}

/* 预设区域样式 */
.presets {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.preset-label {
  color: #4b5563;
  font-size: 12px;
}

.preset-btn {
  padding: 4px 8px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #fff;
  color: #111827;
  cursor: pointer;
}

.preset-btn:hover {
  background: #f3f4f6;
}

.bottom-panel .props {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.prop-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.prop-row .label {
  color: #374151;
  font-size: 12px;
}

.prop-row .unit {
  color: #6b7280;
  font-size: 12px;
}

/* 旋转控制样式 */
.deg-input {
  width: 70px;
}

.rot-btn {
  padding: 4px 8px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #fff;
  color: #111827;
  cursor: pointer;
}

.rot-btn:hover {
  background: #f3f4f6;
}

/* 文本框样式 */
.text-box {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1.2;
  padding: 6px;
  word-break: break-word;
  white-space: pre-wrap;
  background: transparent;
  pointer-events: auto;
  user-select: text;
}
</style>
