diff --git a/src/App.vue b/src/App.vue index b6c3c8e..c83e3bb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -336,6 +336,23 @@ const wiringConnected = reactive>({ rxTransducer_to_right: false, }); +// ========================= +// 示波器信号有效性(由接线决定) +// ========================= + +// CH1:仅当 [信号发生器 发射端 波形] -> [示波器 CH1] 接入时才有波形 +const oscCh1HasSignal = computed(() => wiringConnected.txWave_to_ch1); + +// CH2:仅当这三条线都接入时才有波形 +// [信号发生器 发射端 换能器] -> [换能器 左侧输入] +// [信号发生器 接收端 波形] -> [示波器 CH2] +// [信号发生器 接收端 换能器] -> [换能器 右侧输出] +const oscCh2HasSignal = computed(() => + wiringConnected.txTransducer_to_left + && wiringConnected.rxWave_to_ch2 + && wiringConnected.rxTransducer_to_right +); + // ========================= // 步骤标题(document.title) // ========================= @@ -389,6 +406,17 @@ const tooltip = reactive({ side: 'right' as TooltipSide, }); +// 接线错误:短暂提示(不打断交互) +const wiringErrorToast = reactive({ + visible: false, + text: '', + x: 0, + y: 0, + side: 'right' as TooltipSide, +}); + +let wiringErrorToastTimer: number | null = null; + const tooltipTargetEl = ref(null); const hideTooltip = () => { @@ -396,7 +424,7 @@ const hideTooltip = () => { tooltipTargetEl.value = null; }; -const updateTooltipPosition = (el: HTMLElement) => { +const calcBubblePosition = (el: HTMLElement) => { const table = tableRef.value; if (!table) return; @@ -412,11 +440,21 @@ const updateTooltipPosition = (el: HTMLElement) => { const leftX = r.left - tableRect.left - margin; const hasRoomOnRight = rightX + approxBubbleW <= tableRect.width; - tooltip.side = hasRoomOnRight ? 'right' : 'left'; - tooltip.x = hasRoomOnRight ? rightX : leftX; + const side = hasRoomOnRight ? 'right' : 'left'; + const x = hasRoomOnRight ? rightX : leftX; // y 以上方为主,避免贴边 - tooltip.y = Math.max(10, Math.min(tableRect.height - 10, anchorY)); + const y = Math.max(10, Math.min(tableRect.height - 10, anchorY)); + + return { x, y, side } as const; +}; + +const updateTooltipPosition = (el: HTMLElement) => { + const pos = calcBubblePosition(el); + if (!pos) return; + tooltip.side = pos.side; + tooltip.x = pos.x; + tooltip.y = pos.y; }; const showTooltipForEl = (el: HTMLElement) => { @@ -430,6 +468,27 @@ const showTooltipForEl = (el: HTMLElement) => { updateTooltipPosition(el); }; +const showWiringErrorToast = (text: string, anchorEl?: HTMLElement) => { + const el = anchorEl ?? null; + if (el) { + const pos = calcBubblePosition(el); + if (pos) { + wiringErrorToast.side = pos.side; + wiringErrorToast.x = pos.x; + wiringErrorToast.y = pos.y; + } + } + + wiringErrorToast.text = text; + wiringErrorToast.visible = true; + + if (wiringErrorToastTimer !== null) window.clearTimeout(wiringErrorToastTimer); + wiringErrorToastTimer = window.setTimeout(() => { + wiringErrorToastTimer = null; + wiringErrorToast.visible = false; + }, 1600); +}; + const findTooltipEl = (target: EventTarget | null): HTMLElement | null => { if (!target) return null; const t = target as HTMLElement; @@ -473,6 +532,15 @@ const tooltipStyle = computed(() => { return base; }); +const wiringErrorToastStyle = computed(() => { + const base: Record = { + left: `${wiringErrorToast.x}px`, + top: `${wiringErrorToast.y}px`, + }; + base.transform = wiringErrorToast.side === 'right' ? 'translate(0, -50%)' : 'translate(-100%, -50%)'; + return base; +}); + const pinEls = new Map(); const selectedPin = ref(null); @@ -584,13 +652,16 @@ const addCableIfNeeded = (key: WireKey, a: PinId, b: PinId) => { scheduleUpdateCablePaths(); }; -const alertWiringError = () => { - window.alert('接线错误:请按接线示意连接对应端口。'); +const alertWiringError = (anchorEl?: HTMLElement) => { + showWiringErrorToast('接线错误:请按接线示意连接对应端口。', anchorEl); }; const onPinClick = (id: PinId) => { ea.play('线缆插入') + // 有任何点击就把“错误提示”收起,避免遮挡 + if (wiringErrorToast.visible) wiringErrorToast.visible = false; + postTracker('pin_click', { pinId: id, hasSelected: selectedPin.value !== null }); if (selectedPin.value === null) { @@ -608,7 +679,7 @@ const onPinClick = (id: PinId) => { const key = requiredByPair.get(normalizePair(first, id)); if (!key) { postTracker('wire_error', { a: first, b: id }); - alertWiringError(); + alertWiringError(pinEls.get(id) ?? pinEls.get(first)); return; } @@ -677,6 +748,7 @@ onBeforeUnmount(() => { if (cablesRaf !== null) cancelAnimationFrame(cablesRaf); window.removeEventListener('resize', scheduleUpdateCablePaths); if (dmRightCableTimer !== null) window.clearTimeout(dmRightCableTimer); + if (wiringErrorToastTimer !== null) window.clearTimeout(wiringErrorToastTimer); }); @@ -695,6 +767,11 @@ onBeforeUnmount(() => {
{{ tooltip.text }}
+
+
{{ wiringErrorToast.text }}
+
+ diff --git a/src/components/Oscilloscope.vue b/src/components/Oscilloscope.vue index 45c62fd..a25e009 100644 --- a/src/components/Oscilloscope.vue +++ b/src/components/Oscilloscope.vue @@ -31,6 +31,15 @@ const props = defineProps({ // 声速,单位m/s soundSpeed: { type: Number, default: 340 }, + // 接线状态:控制通道是否真正有信号 + // CH1:仅当 [信号发生器 发射端 波形] -> [示波器 CH1] 接入时为 true + ch1HasSignal: { type: Boolean, default: false }, + // CH2:仅当三条线都接入时为 true + // [信号发生器 发射端 换能器] -> [换能器 左侧输入] + // [信号发生器 接收端 波形] -> [示波器 CH2] + // [信号发生器 接收端 换能器] -> [换能器 右侧输出] + ch2HasSignal: { type: Boolean, default: false }, + }); const emit = defineEmits<{ @@ -147,8 +156,8 @@ function drawOscilloscope() { const gainCH1 = getGain(vdch1.value); const gainCH2 = getGain(vdch2.value); - // X-Y 模式下检测“李萨如图形出现直线”的进入事件 - if (power.value && isXYMode.value) { + // X-Y 模式下检测“李萨如图形出现直线”的进入事件(需两路都有信号) + if (power.value && isXYMode.value && props.ch1HasSignal && props.ch2HasSignal) { const phase = phy.phaseRadTotal; const isLineNow = Math.abs(Math.sin(phase)) < 0.10 && phy.resonanceFactor > 0.05; if (isLineNow && !lastWasLine.value) { @@ -171,7 +180,8 @@ function drawOscilloscope() { if (isXYMode.value) { // ================= X-Y Mode (李萨如图形) ================= - const opacity = 0.3 + 0.7 * phy.resonanceFactor; + const hasAnySignal = props.ch1HasSignal || props.ch2HasSignal; + const opacity = hasAnySignal ? (0.3 + 0.7 * phy.resonanceFactor) : 0.15; ctx.strokeStyle = `rgba(0, 255, 0, ${opacity})`; // 强辉光效果 - 多层叠加 @@ -180,8 +190,8 @@ function drawOscilloscope() { ctx.beginPath(); const phase = phy.phaseRadTotal; - const ampX = gainCH1; - const ampY = phy.resonanceFactor * gainCH2; + const ampX = props.ch1HasSignal ? gainCH1 : 0; + const ampY = props.ch2HasSignal ? (phy.resonanceFactor * gainCH2) : 0; for (let t = 0; t <= 2 * Math.PI; t += 0.03) { const x = scale * ampX * Math.cos(t); @@ -197,6 +207,12 @@ function drawOscilloscope() { ctx.shadowBlur = 0; + // 两路都没信号时,画一个中心点(避免完全不可见) + if (!props.ch1HasSignal && !props.ch2HasSignal) { + ctx.fillStyle = 'rgba(0, 255, 0, 0.25)'; + ctx.fillRect(cx - 1.5, cy - 1.5, 3, 3); + } + // 中心点装饰 ctx.fillStyle = 'rgba(0, 255, 0, 0.3)'; ctx.fillRect(cx - 1, cy - 5, 2, 10); @@ -218,18 +234,52 @@ function drawOscilloscope() { ctx.lineWidth = 3; - // CH1 (Source) - 黄色参考信号(在CH1、双踪、叠加模式下显示) - if (currentModeIndex.value === 0 || currentModeIndex.value === 2 || currentModeIndex.value === 3) { + // 叠加模式:直接绘制 CH1 + CH2 的合成波形(形状更复杂) + if (currentModeIndex.value === 3) { + const opacity = 0.45 + 0.55 * phy.resonanceFactor; + ctx.beginPath(); + // 用 CH1 的黄色线条 + CH2 的绿色辉光,便于与单通道/双踪区分 + ctx.strokeStyle = `rgba(255, 230, 0, ${opacity})`; + ctx.shadowBlur = 14 * (0.4 + 0.6 * phy.resonanceFactor); + ctx.shadowColor = '#00ff00'; + + const phaseShift = phy.phaseRadTotal; + const amp1 = gainCH1 * 0.8; + const amp2 = phy.resonanceFactor * gainCH2 * 0.8; + + for (let x = 0; x < w; x += 2) { + const t = (x / pixPerCycle) * 2 * Math.PI; + const y1 = props.ch1HasSignal ? (amp1 * Math.sin(t + time)) : 0; + const y2 = props.ch2HasSignal ? (amp2 * Math.sin(t - phaseShift + time)) : 0; + const yVal = scale * (y1 + y2); + if (x === 0) ctx.moveTo(x, cy - yVal); + else ctx.lineTo(x, cy - yVal); + } + ctx.stroke(); + + // 第二层辉光 + ctx.shadowBlur = 22 * (0.4 + 0.6 * phy.resonanceFactor); + ctx.stroke(); + } + + // CH1 (Source) - 黄色参考信号(在CH1、双踪模式下显示) + if (currentModeIndex.value === 0 || currentModeIndex.value === 2) { ctx.beginPath(); ctx.strokeStyle = 'rgba(255, 230, 0, 0.9)'; ctx.shadowBlur = 12; ctx.shadowColor = '#ffff00'; - for (let x = 0; x < w; x += 2) { - const t = (x / pixPerCycle) * 2 * Math.PI; - const yVal = scale * gainCH1 * 0.8 * Math.sin(t + time); - if (x === 0) ctx.moveTo(x, cy - yVal); - else ctx.lineTo(x, cy - yVal); + if (props.ch1HasSignal) { + for (let x = 0; x < w; x += 2) { + const t = (x / pixPerCycle) * 2 * Math.PI; + const yVal = scale * gainCH1 * 0.8 * Math.sin(t + time); + if (x === 0) ctx.moveTo(x, cy - yVal); + else ctx.lineTo(x, cy - yVal); + } + } else { + // 未接线:显示中心直线 + ctx.moveTo(0, cy); + ctx.lineTo(w, cy); } ctx.stroke(); @@ -238,8 +288,8 @@ function drawOscilloscope() { ctx.stroke(); } - // CH2 (Receiver) - 绿色接收信号(在CH2、双踪、叠加模式下显示) - if (currentModeIndex.value === 1 || currentModeIndex.value === 2 || currentModeIndex.value === 3) { + // CH2 (Receiver) - 绿色接收信号(在CH2、双踪模式下显示) + if (currentModeIndex.value === 1 || currentModeIndex.value === 2) { const opacity = 0.3 + 0.7 * phy.resonanceFactor; ctx.beginPath(); ctx.strokeStyle = `rgba(0, 255, 0, ${opacity})`; @@ -249,11 +299,18 @@ function drawOscilloscope() { const phaseShift = phy.phaseRadTotal; const ampY = phy.resonanceFactor * gainCH2 * 0.8; - for (let x = 0; x < w; x += 2) { - const t = (x / pixPerCycle) * 2 * Math.PI; - const yVal = scale * ampY * Math.sin(t - phaseShift + time); - if (x === 0) ctx.moveTo(x, cy - yVal); - else ctx.lineTo(x, cy - yVal); + if (props.ch2HasSignal) { + for (let x = 0; x < w; x += 2) { + const t = (x / pixPerCycle) * 2 * Math.PI; + const yVal = scale * ampY * Math.sin(t - phaseShift + time); + if (x === 0) ctx.moveTo(x, cy - yVal); + else ctx.lineTo(x, cy - yVal); + } + } else { + // 未接线:显示中心直线 + ctx.shadowBlur = 0; + ctx.moveTo(0, cy); + ctx.lineTo(w, cy); } ctx.stroke(); diff --git a/接线示意.md b/接线示意.md new file mode 100644 index 0000000..c7d12f3 --- /dev/null +++ b/接线示意.md @@ -0,0 +1,8 @@ +src\components\DeterminationMachine.vue -> 换能器 +src\components\Oscilloscope.vue -> 示波器 +src\components\SignalGen.vue -> 信号发生器 + +[信号发生器的 发射端 波形] 接 [示波器的 CH1 输入] +[信号发生器的 发射端 换能器] 接 [换能器 左侧输入] +[信号发生器的 接收端 波形] 接 [示波器的 CH2 输入] +[信号发生器的 接收端 换能器] 接 [换能器 右侧输出]