优化示波器与连线对应,优化接线错误提示
This commit is contained in:
parent
8f673e0095
commit
a23facccba
92
src/App.vue
92
src/App.vue
@ -336,6 +336,23 @@ const wiringConnected = reactive<Record<WireKey, boolean>>({
|
|||||||
rxTransducer_to_right: false,
|
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)
|
// 步骤标题(document.title)
|
||||||
// =========================
|
// =========================
|
||||||
@ -389,6 +406,17 @@ const tooltip = reactive({
|
|||||||
side: 'right' as TooltipSide,
|
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<HTMLElement | null>(null);
|
const tooltipTargetEl = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const hideTooltip = () => {
|
const hideTooltip = () => {
|
||||||
@ -396,7 +424,7 @@ const hideTooltip = () => {
|
|||||||
tooltipTargetEl.value = null;
|
tooltipTargetEl.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTooltipPosition = (el: HTMLElement) => {
|
const calcBubblePosition = (el: HTMLElement) => {
|
||||||
const table = tableRef.value;
|
const table = tableRef.value;
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
|
||||||
@ -412,11 +440,21 @@ const updateTooltipPosition = (el: HTMLElement) => {
|
|||||||
const leftX = r.left - tableRect.left - margin;
|
const leftX = r.left - tableRect.left - margin;
|
||||||
|
|
||||||
const hasRoomOnRight = rightX + approxBubbleW <= tableRect.width;
|
const hasRoomOnRight = rightX + approxBubbleW <= tableRect.width;
|
||||||
tooltip.side = hasRoomOnRight ? 'right' : 'left';
|
const side = hasRoomOnRight ? 'right' : 'left';
|
||||||
tooltip.x = hasRoomOnRight ? rightX : leftX;
|
const x = hasRoomOnRight ? rightX : leftX;
|
||||||
|
|
||||||
// y 以上方为主,避免贴边
|
// 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) => {
|
const showTooltipForEl = (el: HTMLElement) => {
|
||||||
@ -430,6 +468,27 @@ const showTooltipForEl = (el: HTMLElement) => {
|
|||||||
updateTooltipPosition(el);
|
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 => {
|
const findTooltipEl = (target: EventTarget | null): HTMLElement | null => {
|
||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
const t = target as HTMLElement;
|
const t = target as HTMLElement;
|
||||||
@ -473,6 +532,15 @@ const tooltipStyle = computed(() => {
|
|||||||
return base;
|
return base;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const wiringErrorToastStyle = computed(() => {
|
||||||
|
const base: Record<string, string> = {
|
||||||
|
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<PinId, HTMLElement>();
|
const pinEls = new Map<PinId, HTMLElement>();
|
||||||
const selectedPin = ref<PinId | null>(null);
|
const selectedPin = ref<PinId | null>(null);
|
||||||
|
|
||||||
@ -584,13 +652,16 @@ const addCableIfNeeded = (key: WireKey, a: PinId, b: PinId) => {
|
|||||||
scheduleUpdateCablePaths();
|
scheduleUpdateCablePaths();
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertWiringError = () => {
|
const alertWiringError = (anchorEl?: HTMLElement) => {
|
||||||
window.alert('接线错误:请按接线示意连接对应端口。');
|
showWiringErrorToast('接线错误:请按接线示意连接对应端口。', anchorEl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPinClick = (id: PinId) => {
|
const onPinClick = (id: PinId) => {
|
||||||
ea.play('线缆插入')
|
ea.play('线缆插入')
|
||||||
|
|
||||||
|
// 有任何点击就把“错误提示”收起,避免遮挡
|
||||||
|
if (wiringErrorToast.visible) wiringErrorToast.visible = false;
|
||||||
|
|
||||||
postTracker('pin_click', { pinId: id, hasSelected: selectedPin.value !== null });
|
postTracker('pin_click', { pinId: id, hasSelected: selectedPin.value !== null });
|
||||||
|
|
||||||
if (selectedPin.value === null) {
|
if (selectedPin.value === null) {
|
||||||
@ -608,7 +679,7 @@ const onPinClick = (id: PinId) => {
|
|||||||
const key = requiredByPair.get(normalizePair(first, id));
|
const key = requiredByPair.get(normalizePair(first, id));
|
||||||
if (!key) {
|
if (!key) {
|
||||||
postTracker('wire_error', { a: first, b: id });
|
postTracker('wire_error', { a: first, b: id });
|
||||||
alertWiringError();
|
alertWiringError(pinEls.get(id) ?? pinEls.get(first));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,6 +748,7 @@ onBeforeUnmount(() => {
|
|||||||
if (cablesRaf !== null) cancelAnimationFrame(cablesRaf);
|
if (cablesRaf !== null) cancelAnimationFrame(cablesRaf);
|
||||||
window.removeEventListener('resize', scheduleUpdateCablePaths);
|
window.removeEventListener('resize', scheduleUpdateCablePaths);
|
||||||
if (dmRightCableTimer !== null) window.clearTimeout(dmRightCableTimer);
|
if (dmRightCableTimer !== null) window.clearTimeout(dmRightCableTimer);
|
||||||
|
if (wiringErrorToastTimer !== null) window.clearTimeout(wiringErrorToastTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -695,6 +767,11 @@ onBeforeUnmount(() => {
|
|||||||
<div class="tooltip-inner">{{ tooltip.text }}</div>
|
<div class="tooltip-inner">{{ tooltip.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wiringErrorToast.visible" class="tooltip" :class="`tooltip--${wiringErrorToast.side}`"
|
||||||
|
:style="wiringErrorToastStyle">
|
||||||
|
<div class="tooltip-inner">{{ wiringErrorToast.text }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<svg class="cables" :width="1600" :height="900" viewBox="0 0 1600 900"
|
<svg class="cables" :width="1600" :height="900" viewBox="0 0 1600 900"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<defs>
|
<defs>
|
||||||
@ -771,6 +848,7 @@ onBeforeUnmount(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<Oscilloscope :distance="distance" :frequency="signalFrequencyKHz * 1000"
|
<Oscilloscope :distance="distance" :frequency="signalFrequencyKHz * 1000"
|
||||||
|
:ch1-has-signal="oscCh1HasSignal" :ch2-has-signal="oscCh2HasSignal"
|
||||||
class="machine" @register-pin="registerPin"
|
class="machine" @register-pin="registerPin"
|
||||||
@unregister-pin="unregisterPin" @pin-click="onPinClick"
|
@unregister-pin="unregisterPin" @pin-click="onPinClick"
|
||||||
@settings="onOscSettings" @lissajous-line="onLissajousLine" />
|
@settings="onOscSettings" @lissajous-line="onLissajousLine" />
|
||||||
|
|||||||
@ -31,6 +31,15 @@ const props = defineProps({
|
|||||||
// 声速,单位m/s
|
// 声速,单位m/s
|
||||||
soundSpeed: { type: Number, default: 340 },
|
soundSpeed: { type: Number, default: 340 },
|
||||||
|
|
||||||
|
// 接线状态:控制通道是否真正有信号
|
||||||
|
// CH1:仅当 [信号发生器 发射端 波形] -> [示波器 CH1] 接入时为 true
|
||||||
|
ch1HasSignal: { type: Boolean, default: false },
|
||||||
|
// CH2:仅当三条线都接入时为 true
|
||||||
|
// [信号发生器 发射端 换能器] -> [换能器 左侧输入]
|
||||||
|
// [信号发生器 接收端 波形] -> [示波器 CH2]
|
||||||
|
// [信号发生器 接收端 换能器] -> [换能器 右侧输出]
|
||||||
|
ch2HasSignal: { type: Boolean, default: false },
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -147,8 +156,8 @@ function drawOscilloscope() {
|
|||||||
const gainCH1 = getGain(vdch1.value);
|
const gainCH1 = getGain(vdch1.value);
|
||||||
const gainCH2 = getGain(vdch2.value);
|
const gainCH2 = getGain(vdch2.value);
|
||||||
|
|
||||||
// X-Y 模式下检测“李萨如图形出现直线”的进入事件
|
// X-Y 模式下检测“李萨如图形出现直线”的进入事件(需两路都有信号)
|
||||||
if (power.value && isXYMode.value) {
|
if (power.value && isXYMode.value && props.ch1HasSignal && props.ch2HasSignal) {
|
||||||
const phase = phy.phaseRadTotal;
|
const phase = phy.phaseRadTotal;
|
||||||
const isLineNow = Math.abs(Math.sin(phase)) < 0.10 && phy.resonanceFactor > 0.05;
|
const isLineNow = Math.abs(Math.sin(phase)) < 0.10 && phy.resonanceFactor > 0.05;
|
||||||
if (isLineNow && !lastWasLine.value) {
|
if (isLineNow && !lastWasLine.value) {
|
||||||
@ -171,7 +180,8 @@ function drawOscilloscope() {
|
|||||||
|
|
||||||
if (isXYMode.value) {
|
if (isXYMode.value) {
|
||||||
// ================= X-Y Mode (李萨如图形) =================
|
// ================= 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})`;
|
ctx.strokeStyle = `rgba(0, 255, 0, ${opacity})`;
|
||||||
|
|
||||||
// 强辉光效果 - 多层叠加
|
// 强辉光效果 - 多层叠加
|
||||||
@ -180,8 +190,8 @@ function drawOscilloscope() {
|
|||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const phase = phy.phaseRadTotal;
|
const phase = phy.phaseRadTotal;
|
||||||
const ampX = gainCH1;
|
const ampX = props.ch1HasSignal ? gainCH1 : 0;
|
||||||
const ampY = phy.resonanceFactor * gainCH2;
|
const ampY = props.ch2HasSignal ? (phy.resonanceFactor * gainCH2) : 0;
|
||||||
|
|
||||||
for (let t = 0; t <= 2 * Math.PI; t += 0.03) {
|
for (let t = 0; t <= 2 * Math.PI; t += 0.03) {
|
||||||
const x = scale * ampX * Math.cos(t);
|
const x = scale * ampX * Math.cos(t);
|
||||||
@ -197,6 +207,12 @@ function drawOscilloscope() {
|
|||||||
|
|
||||||
ctx.shadowBlur = 0;
|
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.fillStyle = 'rgba(0, 255, 0, 0.3)';
|
||||||
ctx.fillRect(cx - 1, cy - 5, 2, 10);
|
ctx.fillRect(cx - 1, cy - 5, 2, 10);
|
||||||
@ -218,19 +234,53 @@ function drawOscilloscope() {
|
|||||||
|
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
|
|
||||||
// CH1 (Source) - 黄色参考信号(在CH1、双踪、叠加模式下显示)
|
// 叠加模式:直接绘制 CH1 + CH2 的合成波形(形状更复杂)
|
||||||
if (currentModeIndex.value === 0 || currentModeIndex.value === 2 || currentModeIndex.value === 3) {
|
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.beginPath();
|
||||||
ctx.strokeStyle = 'rgba(255, 230, 0, 0.9)';
|
ctx.strokeStyle = 'rgba(255, 230, 0, 0.9)';
|
||||||
ctx.shadowBlur = 12;
|
ctx.shadowBlur = 12;
|
||||||
ctx.shadowColor = '#ffff00';
|
ctx.shadowColor = '#ffff00';
|
||||||
|
|
||||||
|
if (props.ch1HasSignal) {
|
||||||
for (let x = 0; x < w; x += 2) {
|
for (let x = 0; x < w; x += 2) {
|
||||||
const t = (x / pixPerCycle) * 2 * Math.PI;
|
const t = (x / pixPerCycle) * 2 * Math.PI;
|
||||||
const yVal = scale * gainCH1 * 0.8 * Math.sin(t + time);
|
const yVal = scale * gainCH1 * 0.8 * Math.sin(t + time);
|
||||||
if (x === 0) ctx.moveTo(x, cy - yVal);
|
if (x === 0) ctx.moveTo(x, cy - yVal);
|
||||||
else ctx.lineTo(x, cy - yVal);
|
else ctx.lineTo(x, cy - yVal);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 未接线:显示中心直线
|
||||||
|
ctx.moveTo(0, cy);
|
||||||
|
ctx.lineTo(w, cy);
|
||||||
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 第二层辉光
|
// 第二层辉光
|
||||||
@ -238,8 +288,8 @@ function drawOscilloscope() {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CH2 (Receiver) - 绿色接收信号(在CH2、双踪、叠加模式下显示)
|
// CH2 (Receiver) - 绿色接收信号(在CH2、双踪模式下显示)
|
||||||
if (currentModeIndex.value === 1 || currentModeIndex.value === 2 || currentModeIndex.value === 3) {
|
if (currentModeIndex.value === 1 || currentModeIndex.value === 2) {
|
||||||
const opacity = 0.3 + 0.7 * phy.resonanceFactor;
|
const opacity = 0.3 + 0.7 * phy.resonanceFactor;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = `rgba(0, 255, 0, ${opacity})`;
|
ctx.strokeStyle = `rgba(0, 255, 0, ${opacity})`;
|
||||||
@ -249,12 +299,19 @@ function drawOscilloscope() {
|
|||||||
const phaseShift = phy.phaseRadTotal;
|
const phaseShift = phy.phaseRadTotal;
|
||||||
const ampY = phy.resonanceFactor * gainCH2 * 0.8;
|
const ampY = phy.resonanceFactor * gainCH2 * 0.8;
|
||||||
|
|
||||||
|
if (props.ch2HasSignal) {
|
||||||
for (let x = 0; x < w; x += 2) {
|
for (let x = 0; x < w; x += 2) {
|
||||||
const t = (x / pixPerCycle) * 2 * Math.PI;
|
const t = (x / pixPerCycle) * 2 * Math.PI;
|
||||||
const yVal = scale * ampY * Math.sin(t - phaseShift + time);
|
const yVal = scale * ampY * Math.sin(t - phaseShift + time);
|
||||||
if (x === 0) ctx.moveTo(x, cy - yVal);
|
if (x === 0) ctx.moveTo(x, cy - yVal);
|
||||||
else ctx.lineTo(x, cy - yVal);
|
else ctx.lineTo(x, cy - yVal);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 未接线:显示中心直线
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
ctx.moveTo(0, cy);
|
||||||
|
ctx.lineTo(w, cy);
|
||||||
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 第二层辉光
|
// 第二层辉光
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user