<script setup lang="ts">
import { ref, computed, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
import DeterminationMachine from './components/DeterminationMachine.vue';
//@ts-ignore
import SilverKnob from './components/SilverKnob.vue';
import Oscilloscope from './components/Oscilloscope.vue';
import SignalGen from './components/SignalGen.vue';
import Notebook from './components/Notebook.vue';
import ea from './assets/sounds';
import guidePng from './assets/入门引导.webp';

const distance = ref(20);
// Signal generator frequency (kHz)
const signalFrequencyKHz = ref(35.702);

// =========================
// 埋点（POST /api/tracker）
// =========================

const trackerSessionId = (() => {
  try {
    const key = 'sound-speed:tracker-session';
    const existed = window.localStorage.getItem(key);
    if (existed) return existed;
    const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
    window.localStorage.setItem(key, id);
    return id;
  } catch {
    return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
  }
})();

const postTracker = (event: string, data?: unknown) => {
  const body = JSON.stringify({
    event,
    data,
    ts: Date.now(),
    sessionId: trackerSessionId,
    step: currentStepTitle.value,
  });

  try {
    if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
      const blob = new Blob([body], { type: 'application/json' });
      // @ts-ignore
      navigator.sendBeacon('/api/tracker', blob);
      return;
    }
  } catch {
    // ignore
  }

  fetch('/api/tracker', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body,
    keepalive: true,
  }).catch(() => { /* ignore */ });
};

const makeThrottle = <T extends any[]>(fn: (...args: T) => void, ms: number) => {
  let last = 0;
  let timer: number | null = null;
  let pending: T | null = null;
  return (...args: T) => {
    const now = Date.now();
    const remain = ms - (now - last);
    if (remain <= 0) {
      last = now;
      fn(...args);
      return;
    }
    pending = args;
    if (timer !== null) return;
    timer = window.setTimeout(() => {
      timer = null;
      last = Date.now();
      if (pending) fn(...pending);
      pending = null;
    }, remain);
  };
};

const trackDistanceChange = makeThrottle((v: number) => {
  postTracker('transducer_distance_change', { distanceMm: v });
}, 300);

const onDistanceChange = (v: number) => {
  trackDistanceChange(v);
  scheduleUpdateDmRightCablePaths();
};

const trackFrequencyChange = makeThrottle((v: number) => {
  postTracker('signal_frequency_change', { frequencyKHz: v });
}, 300);

watch(signalFrequencyKHz, (v, prev) => {
  // 初始化时避免打一条“变更”
  if (prev === undefined) return;
  trackFrequencyChange(v);
});

// 仪器位置状态
interface InstrumentState {
  left: number;  // 百分比
  bottom: number; // 百分比
  baseScale: number; // 基础缩放
  zIndex: number; // 层级
}

const machine = ref<InstrumentState>({ left: 35, bottom: 40, baseScale: 0.8, zIndex: 0 });
const oscilloscope = ref<InstrumentState>({ left: 10, bottom: 23, baseScale: 1.2, zIndex: 0 });
const signalGen = ref<InstrumentState>({ left: 65, bottom: 17, baseScale: 0.95, zIndex: 0 });
const notebook = ref<InstrumentState>({ left: 12, bottom: 40, baseScale: 0.9, zIndex: 0 });

// 基于bottom值自动计算zIndex：bottom越小（越靠下），zIndex越大
const getAutoZIndex = (instrument: InstrumentState) => {
  // 将bottom值反转，使得bottom=0时zIndex最大
  return Math.round(1000 - instrument.bottom * 10);
};

// 透视计算：bottom越小（越靠下），scale应该越大（近大远小）
// bottom范围: 0-100%, 映射到scale: 1.5-0.5倍
const getPerspectiveScale = (instrument: InstrumentState) => {
  // bottom越小越靠下，scale越大
  const perspectiveFactor = 1.5 - (instrument.bottom / 100) * 1.0; // bottom=0时1.5, bottom=100时0.5
  return instrument.baseScale * perspectiveFactor;
};

const machineStyle = computed(() => ({
  left: `${machine.value.left}%`,
  bottom: `${machine.value.bottom}%`,
  transform: `scale(${getPerspectiveScale(machine.value)})`,
  zIndex: getAutoZIndex(machine.value)
}));

const oscilloscopeStyle = computed(() => ({
  left: `${oscilloscope.value.left}%`,
  bottom: `${oscilloscope.value.bottom}%`,
  transform: `scale(${getPerspectiveScale(oscilloscope.value)})`,
  zIndex: getAutoZIndex(oscilloscope.value)
}));

const signalGenStyle = computed(() => ({
  left: `${signalGen.value.left}%`,
  bottom: `${signalGen.value.bottom}%`,
  transform: `scale(${getPerspectiveScale(signalGen.value)})`,
  zIndex: getAutoZIndex(signalGen.value)
}));

const notebookStyle = computed(() => ({
  left: `${notebook.value.left}%`,
  bottom: `${notebook.value.bottom}%`,
  transform: `scale(${getPerspectiveScale(notebook.value)})`,
  zIndex: getAutoZIndex(notebook.value)
}));

// 拖动功能
interface DragState {
  isDragging: boolean;
  startX: number;
  startY: number;
  startLeft: number;
  startBottom: number;
}

const dragState = ref<DragState>({
  isDragging: false,
  startX: 0,
  startY: 0,
  startLeft: 0,
  startBottom: 0
});

const currentInstrument = ref<InstrumentState | null>(null);

const startDrag = (instrument: InstrumentState, event: PointerEvent) => {
  hideTooltip();
  dragState.value = {
    isDragging: true,
    startX: event.clientX,
    startY: event.clientY,
    startLeft: instrument.left,
    startBottom: instrument.bottom
  };
  currentInstrument.value = instrument;

  // 防止文本选择和默认触摸行为
  event.preventDefault();
  event.stopPropagation();
};

const onPointerMove = (event: PointerEvent) => {
  if (tooltip.visible && tooltipTargetEl.value) {
    updateTooltipPosition(tooltipTargetEl.value);
  }
  if (!dragState.value.isDragging || !currentInstrument.value) return;

  const tableWidth = 1600;
  const tableHeight = 900;

  // 计算移动距离（像素）
  const deltaX = event.clientX - dragState.value.startX;
  const deltaY = event.clientY - dragState.value.startY;

  // 转换为百分比
  const deltaLeftPercent = (deltaX / tableWidth) * 100;
  const deltaBottomPercent = -(deltaY / tableHeight) * 100; // Y轴向下为正，bottom向上为正

  // 更新位置
  currentInstrument.value.left = Math.max(0, Math.min(100, dragState.value.startLeft + deltaLeftPercent));
  currentInstrument.value.bottom = Math.max(0, Math.min(55, dragState.value.startBottom + deltaBottomPercent));

  scheduleUpdateCablePaths();
};

const onPointerUp = () => {
  dragState.value.isDragging = false;
  currentInstrument.value = null;
};

// =========================
// 入门引导（全屏图片）
// =========================

const showGuide = ref(true);
const guideImgRef = ref<HTMLImageElement | null>(null);

const closeGuide = () => {
  showGuide.value = false;
};

const onGuideClick = (e: MouseEvent) => {
  const img = guideImgRef.value;
  if (!img) return;
  const rect = img.getBoundingClientRect();

  // 仅当点击在图片范围内，且位于图片底部 1/8 区域时关闭
  const x = e.clientX;
  const y = e.clientY;
  const inImg = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
  if (!inImg) return;

  const yIn = y - rect.top;
  if (yIn >= rect.height * 7 / 8) {
    closeGuide();
  }
};

// =========================
// 任务追踪（示波器/李萨如）
// =========================

const oscStatus = reactive({
  vdch1: 320,
  vdch2: 320,
  currentModeIndex: 0,
  xyMode: false,
  power: false,
});

const onOscSettings = (payload: { vdch1: number; vdch2: number; currentModeIndex: number; xyMode: boolean; power: boolean }) => {
  oscStatus.vdch1 = payload.vdch1;
  oscStatus.vdch2 = payload.vdch2;
  oscStatus.currentModeIndex = payload.currentModeIndex;
  oscStatus.xyMode = payload.xyMode;
  oscStatus.power = payload.power;
};

const trackOscSettings = makeThrottle((p: { vdch1: number; vdch2: number; currentModeIndex: number; xyMode: boolean; power: boolean }) => {
  postTracker('osc_settings_change', p);
}, 300);

watch(() => ({
  vdch1: oscStatus.vdch1,
  vdch2: oscStatus.vdch2,
  currentModeIndex: oscStatus.currentModeIndex,
  xyMode: oscStatus.xyMode,
  power: oscStatus.power,
}), (p) => {
  trackOscSettings(p);
}, { deep: true });

const lissajousLineStreak = ref(0);
const lastLineK = ref<number | null>(null);

const onLissajousLine = (payload: { k: number; phaseRadTotal: number }) => {
  // 仅在 X-Y 模式下计数
  if (!oscStatus.xyMode) return;

  if (lastLineK.value === null) {
    lissajousLineStreak.value = 1;
    lastLineK.value = payload.k;
    return;
  }

  // 连续：k 必须递增 1；否则从 1 重新计数
  if (payload.k === lastLineK.value + 1) {
    lissajousLineStreak.value += 1;
  } else {
    lissajousLineStreak.value = 1;
  }
  lastLineK.value = payload.k;
};

watch(lissajousLineStreak, (v, prev) => {
  if (v !== prev) postTracker('lissajous_line_streak', { streak: v });
});


// =========================
// 接线仿真（pin 点击连线）
// =========================

type PinId =
  | 'osc:ch1'
  | 'osc:ch2'
  | 'sg:tx-wave'
  | 'sg:tx-transducer'
  | 'sg:rx-wave'
  | 'sg:rx-transducer'
  | 'dm:left'
  | 'dm:right';

type WireKey =
  | 'txWave_to_ch1'
  | 'txTransducer_to_left'
  | 'rxWave_to_ch2'
  | 'rxTransducer_to_right';

// App.vue 中的 4 条线是否接上的追踪变量
const wiringConnected = reactive<Record<WireKey, boolean>>({
  txWave_to_ch1: false,
  txTransducer_to_left: false,
  rxWave_to_ch2: false,
  rxTransducer_to_right: false,
});

// =========================
// 步骤标题（document.title）
// =========================

const inRange = (v: number, a: number, b: number) => v >= a && v <= b;

const stage1Done = computed(() => {
  return wiringConnected.txWave_to_ch1
    && wiringConnected.txTransducer_to_left
    && wiringConnected.rxWave_to_ch2
    && wiringConnected.rxTransducer_to_right;
});

const stage2Done = computed(() => {
  return oscStatus.power
    && inRange(oscStatus.vdch1, 140, 220)
    && inRange(oscStatus.vdch2, 140, 220)
    && oscStatus.currentModeIndex === 2;
});

const stage3Done = computed(() => inRange(signalFrequencyKHz.value, 35.98, 36.02));

const stage4Done = computed(() => {
  return oscStatus.xyMode && lissajousLineStreak.value >= 4;
});

const currentStepTitle = computed(() => {
  if (!stage1Done.value) return '步骤一：仪器接线';
  if (!stage2Done.value) return '步骤二：调整示波器设置';
  if (!stage3Done.value) return '步骤三：找到固有频率';
  if (!stage4Done.value) return '步骤四：相位比较法测声速';
  return '全部完成';
});

watch(currentStepTitle, (t) => {
  document.title = `声速测量实验 - ${t}`;
}, { immediate: true });
const tableRef = ref<HTMLDivElement | null>(null);

// =========================
// 悬停引导（Tooltip）
// =========================

type TooltipSide = 'right' | 'left';

const tooltip = reactive({
  visible: false,
  text: '',
  x: 0,
  y: 0,
  side: 'right' as TooltipSide,
});

const tooltipTargetEl = ref<HTMLElement | null>(null);

const hideTooltip = () => {
  tooltip.visible = false;
  tooltipTargetEl.value = null;
};

const updateTooltipPosition = (el: HTMLElement) => {
  const table = tableRef.value;
  if (!table) return;

  const tableRect = table.getBoundingClientRect();
  const r = el.getBoundingClientRect();

  const margin = 10;
  const approxBubbleW = 280; // 用于判断是否需要翻转到左侧

  // 锚点：元素右上角（必要时翻转到左上角）
  const anchorY = r.top - tableRect.top - margin;
  const rightX = r.right - tableRect.left + margin;
  const leftX = r.left - tableRect.left - margin;

  const hasRoomOnRight = rightX + approxBubbleW <= tableRect.width;
  tooltip.side = hasRoomOnRight ? 'right' : 'left';
  tooltip.x = hasRoomOnRight ? rightX : leftX;

  // y 以上方为主，避免贴边
  tooltip.y = Math.max(10, Math.min(tableRect.height - 10, anchorY));
};

const showTooltipForEl = (el: HTMLElement) => {
  const raw = (el.dataset.tooltip ?? '').trim();
  // 模板里为了可读性可能写成 "\\n"，这里统一转换成真实换行
  const text = raw.replace(/\\n/g, '\n');
  if (!text) return;
  tooltipTargetEl.value = el;
  tooltip.text = text;
  tooltip.visible = true;
  updateTooltipPosition(el);
};

const findTooltipEl = (target: EventTarget | null): HTMLElement | null => {
  if (!target) return null;
  const t = target as HTMLElement;
  if (!t?.closest) return null;
  const el = t.closest<HTMLElement>('[data-tooltip]');
  if (!el) return null;
  const table = tableRef.value;
  if (table && !table.contains(el)) return null;
  return el;
};

const onTooltipPointerOver = (e: PointerEvent) => {
  if (showGuide.value) return;
  const el = findTooltipEl(e.target);
  if (!el) return;
  if (tooltipTargetEl.value === el && tooltip.visible) return;
  showTooltipForEl(el);
};

const onTooltipPointerOut = (e: PointerEvent) => {
  const current = tooltipTargetEl.value;
  if (!current) return;

  const to = e.relatedTarget as HTMLElement | null;
  if (to && (current === to || current.contains(to))) return;

  const nextEl = findTooltipEl(to);
  if (nextEl) {
    showTooltipForEl(nextEl);
    return;
  }
  hideTooltip();
};

const tooltipStyle = computed(() => {
  const base: Record<string, string> = {
    left: `${tooltip.x}px`,
    top: `${tooltip.y}px`,
  };
  base.transform = tooltip.side === 'right' ? 'translate(0, -50%)' : 'translate(-100%, -50%)';
  return base;
});

const pinEls = new Map<PinId, HTMLElement>();
const selectedPin = ref<PinId | null>(null);

const syncPinConnectingClass = () => {
  const selected = selectedPin.value;
  for (const [id, el] of pinEls) {
    el.classList.toggle('pin-connecting', selected === id);
  }
};

type Cable = {
  key: WireKey;
  a: PinId;
  b: PinId;
  d: string;
};

const cables = ref<Cable[]>([]);
let cablesRaf: number | null = null;

let pendingCableUpdateAll = false;
let pendingCableUpdatePins: Set<PinId> | null = null;

const flushCablePathUpdate = () => {
  const doAll = pendingCableUpdateAll;
  const pins = pendingCableUpdatePins;
  pendingCableUpdateAll = false;
  pendingCableUpdatePins = null;

  if (doAll || !pins) {
    updateCablePaths();
  } else {
    updateCablePaths(pins);
  }
};

const scheduleUpdateCablePaths = () => {
  pendingCableUpdateAll = true;
  pendingCableUpdatePins = null;
  if (cablesRaf !== null) return;
  cablesRaf = requestAnimationFrame(() => {
    cablesRaf = null;
    flushCablePathUpdate();
  });
};

const scheduleUpdateCablePathsForPins = (pins: Iterable<PinId>) => {
  if (pendingCableUpdateAll) return;
  if (!pendingCableUpdatePins) pendingCableUpdatePins = new Set<PinId>();
  for (const p of pins) pendingCableUpdatePins.add(p);
  if (cablesRaf !== null) return;
  cablesRaf = requestAnimationFrame(() => {
    cablesRaf = null;
    flushCablePathUpdate();
  });
};

let dmRightCableLast = 0;
let dmRightCableTimer: number | null = null;
const scheduleUpdateDmRightCablePaths = () => {
  const now = Date.now();
  const remain = 1000 - (now - dmRightCableLast);
  if (remain <= 0) {
    dmRightCableLast = now;
    scheduleUpdateCablePathsForPins(['dm:right']);
    return;
  }
  if (dmRightCableTimer !== null) return;
  dmRightCableTimer = window.setTimeout(() => {
    dmRightCableTimer = null;
    dmRightCableLast = Date.now();
    scheduleUpdateCablePathsForPins(['dm:right']);
  }, remain);
};

const normalizePair = (a: PinId, b: PinId) => {
  return a < b ? `${a}|${b}` : `${b}|${a}`;
};

const REQUIRED: Array<{ a: PinId; b: PinId; key: WireKey }> = [
  { a: 'sg:tx-wave', b: 'osc:ch1', key: 'txWave_to_ch1' },
  { a: 'sg:tx-transducer', b: 'dm:left', key: 'txTransducer_to_left' },
  { a: 'sg:rx-wave', b: 'osc:ch2', key: 'rxWave_to_ch2' },
  { a: 'sg:rx-transducer', b: 'dm:right', key: 'rxTransducer_to_right' },
];

const requiredByPair = new Map<string, WireKey>();
for (const c of REQUIRED) {
  requiredByPair.set(normalizePair(c.a, c.b), c.key);
}

const registerPin = (payload: { id: PinId; el: HTMLElement }) => {
  pinEls.set(payload.id, payload.el);
  // 新注册的 pin 也要立即同步“正在连接”样式
  payload.el.classList.toggle('pin-connecting', selectedPin.value === payload.id);
  if (payload.id === 'dm:right') scheduleUpdateDmRightCablePaths();
  else scheduleUpdateCablePaths();
};

const unregisterPin = (id: PinId) => {
  pinEls.delete(id);
  if (id === 'dm:right') scheduleUpdateDmRightCablePaths();
  else scheduleUpdateCablePaths();
};

const addCableIfNeeded = (key: WireKey, a: PinId, b: PinId) => {
  if (cables.value.some((c) => c.key === key)) return;
  cables.value.push({ key, a, b, d: '' });
  scheduleUpdateCablePaths();
};

const alertWiringError = () => {
  window.alert('接线错误：请按接线示意连接对应端口。');
};

const onPinClick = (id: PinId) => {
  ea.play('线缆插入')

  postTracker('pin_click', { pinId: id, hasSelected: selectedPin.value !== null });

  if (selectedPin.value === null) {
    selectedPin.value = id;
    syncPinConnectingClass();
    return;
  }

  const first = selectedPin.value;
  selectedPin.value = null;
  syncPinConnectingClass();

  if (first === id) return;

  const key = requiredByPair.get(normalizePair(first, id));
  if (!key) {
    postTracker('wire_error', { a: first, b: id });
    alertWiringError();
    return;
  }

  // 已经连过就不重复画线，但依然视为已接
  const was = wiringConnected[key];
  wiringConnected[key] = true;
  addCableIfNeeded(key, first, id);

  // 已存在的线也刷新一次，避免 DOM/布局变动导致的偏差
  scheduleUpdateCablePaths();

  if (!was) {
    postTracker('wire_connected', { key, a: first, b: id });
  }
};

const getPinCenterInTable = (pin: HTMLElement) => {
  const table = tableRef.value;
  if (!table) return null;
  const tableRect = table.getBoundingClientRect();
  const r = pin.getBoundingClientRect();
  const x = r.left + r.width / 2 - tableRect.left;
  const y = r.top + r.height / 2 - tableRect.top;
  return { x, y };
};

const buildSagCablePath = (p1: { x: number; y: number }, p2: { x: number; y: number }) => {
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  const dist = Math.hypot(dx, dy);
  const sag = Math.min(220, Math.max(50, dist * 0.18));

  const c1x = p1.x + dx * 0.25;
  const c1y = p1.y + sag;
  const c2x = p1.x + dx * 0.75;
  const c2y = p2.y + sag;

  return `M ${p1.x.toFixed(2)} ${p1.y.toFixed(2)} C ${c1x.toFixed(2)} ${c1y.toFixed(2)} ${c2x.toFixed(2)} ${c2y.toFixed(2)} ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;
};

const updateCablePaths = (onlyPins?: Set<PinId>) => {
  for (const cable of cables.value) {
    if (onlyPins && !onlyPins.has(cable.a) && !onlyPins.has(cable.b)) continue;
    const aEl = pinEls.get(cable.a);
    const bEl = pinEls.get(cable.b);
    if (!aEl || !bEl) {
      cable.d = '';
      continue;
    }
    const p1 = getPinCenterInTable(aEl);
    const p2 = getPinCenterInTable(bEl);
    if (!p1 || !p2) {
      cable.d = '';
      continue;
    }
    cable.d = buildSagCablePath(p1, p2);
  }
};

onMounted(() => {
  scheduleUpdateCablePaths();
  window.addEventListener('resize', scheduleUpdateCablePaths);
});

onBeforeUnmount(() => {
  if (cablesRaf !== null) cancelAnimationFrame(cablesRaf);
  window.removeEventListener('resize', scheduleUpdateCablePaths);
  if (dmRightCableTimer !== null) window.clearTimeout(dmRightCableTimer);
});

</script>

<template>
  <div ref="tableRef" class="table" @pointermove="onPointerMove"
    @pointerover="onTooltipPointerOver" @pointerout="onTooltipPointerOut"
    @pointerup="onPointerUp" @pointerleave="onPointerUp(); hideTooltip()">
    <Transition name="guide">
      <div v-if="showGuide" class="guide-overlay" @click="onGuideClick">
        <img ref="guideImgRef" class="guide-img" :src="guidePng" alt="入门引导" draggable="false" />
      </div>
    </Transition>

    <div v-if="tooltip.visible" class="tooltip" :class="`tooltip--${tooltip.side}`" :style="tooltipStyle">
      <div class="tooltip-inner">{{ tooltip.text }}</div>
    </div>

    <svg class="cables" :width="1600" :height="900" viewBox="0 0 1600 900"
      aria-hidden="true">
      <defs>
        <filter id="cableShadow" x="-50%" y="-50%" width="200%" height="200%">
          <feDropShadow dx="0" dy="2" stdDeviation="2"
            flood-color="rgba(0,0,0,0.55)" />
        </filter>
      </defs>
      <g filter="url(#cableShadow)">
        <g v-for="c in cables" :key="c.key">
          <path v-if="c.d" :d="c.d" class="cable-base" />
          <path v-if="c.d" :d="c.d" class="cable-highlight" />
        </g>
      </g>
    </svg>

    <div class="instrument-wrapper" :style="machineStyle">
      <div class="drag-handle" data-tooltip="拖动把手可移动仪器位置" @pointerdown="startDrag(machine, $event)">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
          viewBox="0 0 24 24" fill="none" stroke="currentColor"
          stroke-width="2">
          <circle cx="12" cy="5" r="1" />
          <circle cx="12" cy="12" r="1" />
          <circle cx="12" cy="19" r="1" />
          <circle cx="8" cy="5" r="1" />
          <circle cx="8" cy="12" r="1" />
          <circle cx="8" cy="19" r="1" />
          <circle cx="16" cy="5" r="1" />
          <circle cx="16" cy="12" r="1" />
          <circle cx="16" cy="19" r="1" />
        </svg>
      </div>
      <DeterminationMachine v-model="distance" class="machine"
        @register-pin="registerPin" @unregister-pin="unregisterPin"
        @pin-click="onPinClick" @change="onDistanceChange" />
    </div>

    <div class="instrument-wrapper" :style="notebookStyle">
      <div class="drag-handle" data-tooltip="拖动把手可移动仪器位置" @pointerdown="startDrag(notebook, $event)">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
          viewBox="0 0 24 24" fill="none" stroke="currentColor"
          stroke-width="2">
          <circle cx="12" cy="5" r="1" />
          <circle cx="12" cy="12" r="1" />
          <circle cx="12" cy="19" r="1" />
          <circle cx="8" cy="5" r="1" />
          <circle cx="8" cy="12" r="1" />
          <circle cx="8" cy="19" r="1" />
          <circle cx="16" cy="5" r="1" />
          <circle cx="16" cy="12" r="1" />
          <circle cx="16" cy="19" r="1" />
        </svg>
      </div>
      <Notebook class="machine" :wiring="wiringConnected"
        :osc="{ vdch1: oscStatus.vdch1, vdch2: oscStatus.vdch2, currentModeIndex: oscStatus.currentModeIndex, xyMode: oscStatus.xyMode, power: oscStatus.power }"
        :signal-frequency-k-hz="signalFrequencyKHz"
        :lissajous-line-streak="lissajousLineStreak" />
    </div>

    <div class="instrument-wrapper" :style="oscilloscopeStyle">
      <div class="drag-handle" data-tooltip="拖动把手可移动仪器位置" @pointerdown="startDrag(oscilloscope, $event)">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
          viewBox="0 0 24 24" fill="none" stroke="currentColor"
          stroke-width="2">
          <circle cx="12" cy="5" r="1" />
          <circle cx="12" cy="12" r="1" />
          <circle cx="12" cy="19" r="1" />
          <circle cx="8" cy="5" r="1" />
          <circle cx="8" cy="12" r="1" />
          <circle cx="8" cy="19" r="1" />
          <circle cx="16" cy="5" r="1" />
          <circle cx="16" cy="12" r="1" />
          <circle cx="16" cy="19" r="1" />
        </svg>
      </div>
      <Oscilloscope :distance="distance" :frequency="signalFrequencyKHz * 1000"
        class="machine" @register-pin="registerPin"
        @unregister-pin="unregisterPin" @pin-click="onPinClick"
        @settings="onOscSettings" @lissajous-line="onLissajousLine" />
    </div>

    <div class="instrument-wrapper" :style="signalGenStyle">
      <div class="drag-handle" data-tooltip="拖动把手可移动仪器位置" @pointerdown="startDrag(signalGen, $event)">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
          viewBox="0 0 24 24" fill="none" stroke="currentColor"
          stroke-width="2">
          <circle cx="12" cy="5" r="1" />
          <circle cx="12" cy="12" r="1" />
          <circle cx="12" cy="19" r="1" />
          <circle cx="8" cy="5" r="1" />
          <circle cx="8" cy="12" r="1" />
          <circle cx="8" cy="19" r="1" />
          <circle cx="16" cy="5" r="1" />
          <circle cx="16" cy="12" r="1" />
          <circle cx="16" cy="19" r="1" />
        </svg>
      </div>
      <SignalGen v-model="signalFrequencyKHz" class="machine"
        @register-pin="registerPin" @unregister-pin="unregisterPin"
        @pin-click="onPinClick" />
    </div>
  </div>
</template>

<style scoped>
.table {
  width: 1700px;
  height: 900px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  background-image: url(./assets/桌面.webp);
  background-size: cover;
  background-position: center;
  user-select: none;
}

.guide-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
}

.guide-img {
  width: 100vw;
  height: 100vh;
  object-fit: contain;
  user-select: none;
}

.guide-enter-active,
.guide-leave-active {
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.guide-enter-from,
.guide-leave-to {
  opacity: 0;
  transform: translateY(10px);
}

.cables {
  position: absolute;
  inset: 0;
  z-index: 850;
  pointer-events: none;
}

.cable-base {
  fill: none;
  stroke: rgba(35, 35, 35, 0.95);
  stroke-width: 10;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cable-highlight {
  fill: none;
  stroke: rgba(255, 255, 255, 0.16);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.instrument-wrapper {
  position: absolute;
  filter: drop-shadow(0 10px 10px black);
  transition: filter 0.2s;
}

.machine {
  pointer-events: auto;
}

.drag-handle {
  touch-action: none;
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  width: 40px;
  height: 24px;
  background: linear-gradient(135deg, rgba(100, 100, 100, 0.9), rgba(60, 60, 60, 0.95));
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  z-index: 1000;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.2);
  opacity: 0.7;
  transition: all 0.2s;
  border: 1px solid rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(4px);
}

.drag-handle:hover {
  opacity: 1;
  background: linear-gradient(135deg, rgba(120, 120, 120, 0.95), rgba(80, 80, 80, 1));
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5), inset 0 1px 2px rgba(255, 255, 255, 0.3);
  transform: translateX(-50%) scale(1.1);
}

.drag-handle:active {
  cursor: grabbing;
  background: linear-gradient(135deg, rgba(80, 80, 80, 1), rgba(50, 50, 50, 1));
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6), inset 0 1px 2px rgba(255, 255, 255, 0.2);
}

.drag-handle svg {
  width: 20px;
  height: 20px;
  color: rgba(255, 255, 255, 0.8);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}

.instrument-wrapper:hover .drag-handle {
  opacity: 0.9;
}

.tooltip {
  position: absolute;
  z-index: 9000;
  pointer-events: none;
  max-width: 280px;
}

.tooltip-inner {
  background:
    linear-gradient(rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.96)),
    repeating-linear-gradient(
      0deg,
      rgba(0, 0, 0, 0.035) 0px,
      rgba(0, 0, 0, 0.035) 1px,
      rgba(0, 0, 0, 0.0) 10px,
      rgba(0, 0, 0, 0.0) 18px
    );
  color: rgba(20, 20, 20, 0.95);
  border: 2px solid rgba(20, 20, 20, 0.70);
  padding: 10px 12px;
  border-radius: 12px;
  font-size: 14px;
  line-height: 1.3;
  box-shadow: 4px 5px 0 rgba(0, 0, 0, 0.18), 0 12px 18px rgba(0, 0, 0, 0.18);
  white-space: pre-line;
  transform: rotate(-0.6deg);
}

.tooltip::before {
  content: '';
  position: absolute;
  z-index: 1;
  bottom: 10px;
  width: 10px;
  height: 10px;
  background: rgba(255, 255, 255, 0.97);
  border: 2px solid rgba(20, 20, 20, 0.70);
  transform: rotate(45deg);
}

.tooltip--right::before {
  left: -5px;
  border-right: 0;
  border-top: 0;
}

.tooltip--left::before {
  right: -5px;
  border-left: 0;
  border-bottom: 0;
}
</style>
