1138 lines
32 KiB
Vue
1138 lines
32 KiB
Vue
<template> <div class="dds-property-editor">
|
||
<CollapsibleSection title="信号发生器" :isExpanded="true">
|
||
<div class="dds-editor-container">
|
||
<div class="dds-display">
|
||
<!-- 波形显示区域 -->
|
||
<div class="waveform-display">
|
||
<svg width="100%" height="120" viewBox="0 0 300 120">
|
||
<rect width="300" height="120" fill="#1a1f25" />
|
||
<path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" />
|
||
|
||
<!-- 频率和相位显示 -->
|
||
<text x="20" y="25" fill="#0f0" font-size="14">{{ displayFrequency }}</text>
|
||
<text x="200" y="25" fill="#0f0" font-size="14">φ: {{ phase }}°</text>
|
||
<text x="150" y="110" fill="#0f0" font-size="14" text-anchor="middle">{{ displayTimebase }}</text>
|
||
</svg>
|
||
|
||
<!-- 时基控制 -->
|
||
<div class="timebase-controls">
|
||
<button class="timebase-button" @click="decreaseTimebase">-</button>
|
||
<span class="timebase-label">时基</span>
|
||
<button class="timebase-button" @click="increaseTimebase">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 波形选择区 -->
|
||
<div class="waveform-selector">
|
||
<div
|
||
v-for="(name, index) in waveformNames"
|
||
:key="`wave-${index}`"
|
||
:class="['waveform-option', { active: currentWaveformIndex === index }]"
|
||
@click="selectWaveform(index)"
|
||
>
|
||
{{ name }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 频率和相位控制 -->
|
||
<div class="control-row">
|
||
<div class="control-group">
|
||
<span class="control-label">频率:</span>
|
||
<div class="control-buttons">
|
||
<button class="control-button" @click="decreaseFrequency">-</button>
|
||
<input
|
||
v-model="frequencyInput"
|
||
@blur="applyFrequencyInput"
|
||
@keyup.enter="applyFrequencyInput"
|
||
class="control-input"
|
||
type="text"
|
||
/>
|
||
<button class="control-button" @click="increaseFrequency">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<span class="control-label">相位:</span>
|
||
<div class="control-buttons">
|
||
<button class="control-button" @click="decreasePhase">-</button>
|
||
<input
|
||
v-model="phaseInput"
|
||
@blur="applyPhaseInput"
|
||
@keyup.enter="applyPhaseInput"
|
||
class="control-input"
|
||
type="text"
|
||
/>
|
||
<button class="control-button" @click="increasePhase">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自定义波形输入 -->
|
||
<div class="custom-waveform">
|
||
<div class="section-heading">自定义波形</div> <div class="input-group">
|
||
<label class="input-label">函数表达式:</label>
|
||
<input
|
||
v-model="customWaveformExpression"
|
||
class="function-input"
|
||
placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]"
|
||
@keyup.enter="applyCustomWaveform"
|
||
/>
|
||
<button class="apply-button" @click="applyCustomWaveform">应用</button>
|
||
</div>
|
||
|
||
<div class="example-functions">
|
||
<div class="example-label">示例函数:</div>
|
||
<div class="example-buttons">
|
||
<button
|
||
class="example-button"
|
||
@click="applyExampleFunction('sin(t)')"
|
||
>正弦波</button>
|
||
<button
|
||
class="example-button"
|
||
@click="applyExampleFunction('sin(t)^3')"
|
||
>立方正弦</button> <button
|
||
class="example-button"
|
||
@click="applyExampleFunction('((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75')"
|
||
>心形函数</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawing-area">
|
||
<div class="section-heading">波形绘制</div>
|
||
<div
|
||
class="waveform-canvas-container"
|
||
ref="canvasContainer"
|
||
>
|
||
<canvas
|
||
ref="drawingCanvas"
|
||
class="drawing-canvas"
|
||
width="280"
|
||
height="100"
|
||
@mousedown="startDrawing"
|
||
@mousemove="draw"
|
||
@mouseup="stopDrawing"
|
||
@mouseleave="stopDrawing"
|
||
></canvas>
|
||
<div class="canvas-actions">
|
||
<button class="canvas-button" @click="clearCanvas">清除</button>
|
||
<button class="canvas-button" @click="applyDrawnWaveform">应用绘制</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存的波形 -->
|
||
<div class="saved-waveforms">
|
||
<div class="section-heading">波形存储槽</div>
|
||
<div class="slot-container">
|
||
<div
|
||
v-for="(slot, index) in waveformSlots"
|
||
:key="`slot-${index}`"
|
||
:class="['waveform-slot', { empty: !slot.name }]"
|
||
@click="loadWaveformSlot(index)"
|
||
>
|
||
<span class="slot-name">{{ slot.name || `槽 ${index+1}` }}</span>
|
||
<button
|
||
class="save-button"
|
||
@click.stop="saveCurrentToSlot(index)"
|
||
>
|
||
保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CollapsibleSection>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from 'vue';
|
||
import CollapsibleSection from '../CollapsibleSection.vue';
|
||
|
||
const props = defineProps<{
|
||
modelValue: any;
|
||
}>();
|
||
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
// 波形状态
|
||
const frequency = ref(props.modelValue?.frequency || 1000);
|
||
const phase = ref(props.modelValue?.phase || 0);
|
||
const timebase = ref(props.modelValue?.timebase || 1); // 时基默认为1倍
|
||
const currentWaveformIndex = ref(0);
|
||
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波', '自定义'];
|
||
const waveforms = ['sine', 'square', 'triangle', 'sawtooth', 'custom'];
|
||
|
||
// 波形函数集合
|
||
interface WaveformFunction {
|
||
(x: number, width: number, height: number, phaseRad: number): number;
|
||
}
|
||
|
||
interface WaveformFunctions {
|
||
[key: string]: WaveformFunction;
|
||
}
|
||
|
||
const waveformFunctions: WaveformFunctions = {
|
||
// 正弦波函数: sin(2π*x + φ)
|
||
sine: (x: number, width: number, height: number, phaseRad: number): number => {
|
||
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
|
||
},
|
||
|
||
// 方波函数: 周期性的高低电平
|
||
square: (x: number, width: number, height: number, phaseRad: number): number => {
|
||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||
return normX < 0.5 ? height/4 : -height/4;
|
||
},
|
||
|
||
// 三角波函数: 线性上升和下降
|
||
triangle: (x: number, width: number, height: number, phaseRad: number): number => {
|
||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||
return height/2 - height * Math.abs(2 * normX - 1);
|
||
},
|
||
|
||
// 锯齿波函数: 线性上升,瞬间下降
|
||
sawtooth: (x: number, width: number, height: number, phaseRad: number): number => {
|
||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||
return height/2 - height/2 * (2 * normX);
|
||
},
|
||
|
||
// 自定义波形函数占位符
|
||
custom: (x: number, width: number, height: number, phaseRad: number): number => {
|
||
return 0; // 默认返回0,会在应用自定义表达式时更新
|
||
}
|
||
};
|
||
|
||
// 输入控制
|
||
const frequencyInput = ref(formatFrequency(frequency.value));
|
||
const phaseInput = ref(phase.value.toString());
|
||
const customWaveformExpression = ref('');
|
||
|
||
// 波形槽
|
||
const waveformSlots = ref<{ name: string; type: string; data: number[][] | null }[]>([
|
||
{ name: '正弦波', type: 'sine', data: null },
|
||
{ name: '方波', type: 'square', data: null },
|
||
{ name: '三角波', type: 'triangle', data: null },
|
||
{ name: '', type: '', data: null }
|
||
]);
|
||
|
||
// 绘图相关
|
||
const drawingCanvas = ref<HTMLCanvasElement | null>(null);
|
||
const canvasContainer = ref<HTMLDivElement | null>(null);
|
||
const isDrawing = ref(false);
|
||
const drawPoints = ref<number[][]>([]);
|
||
|
||
// 格式化频率显示
|
||
function formatFrequency(freq: number): string {
|
||
if (freq >= 1000000) {
|
||
return `${(freq / 1000000).toFixed(2)} MHz`;
|
||
} else if (freq >= 1000) {
|
||
return `${(freq / 1000).toFixed(2)} kHz`;
|
||
} else {
|
||
return `${freq.toFixed(2)} Hz`;
|
||
}
|
||
}
|
||
|
||
// 格式化时基显示
|
||
function formatTimebase(tb: number): string {
|
||
if (tb < 0.1) {
|
||
return `${(tb * 1000).toFixed(0)} ms/div`;
|
||
} else if (tb < 1) {
|
||
return `${(tb * 1000).toFixed(0)} ms/div`;
|
||
} else {
|
||
return `${tb.toFixed(1)} s/div`;
|
||
}
|
||
}
|
||
|
||
// 计算当前显示时基
|
||
const displayTimebase = computed(() => formatTimebase(timebase.value));
|
||
|
||
// 时基调整函数
|
||
function increaseTimebase() {
|
||
if (timebase.value < 0.1) {
|
||
timebase.value *= 2;
|
||
} else if (timebase.value < 1) {
|
||
timebase.value += 0.1;
|
||
} else {
|
||
timebase.value += 0.5;
|
||
}
|
||
timebase.value = Math.min(timebase.value, 5); // 最大5s/div
|
||
updateModelValue();
|
||
}
|
||
|
||
function decreaseTimebase() {
|
||
if (timebase.value <= 0.1) {
|
||
timebase.value /= 2;
|
||
} else if (timebase.value <= 1) {
|
||
timebase.value -= 0.1;
|
||
} else {
|
||
timebase.value -= 0.5;
|
||
}
|
||
timebase.value = Math.max(timebase.value, 0.01); // 最小10ms/div
|
||
updateModelValue();
|
||
}
|
||
|
||
// 计算当前显示频率
|
||
const displayFrequency = computed(() => formatFrequency(frequency.value));
|
||
|
||
// 生成波形路径
|
||
const currentWaveformPath = computed(() => {
|
||
const width = 300;
|
||
const height = 80;
|
||
const xOffset = 0;
|
||
const yOffset = 30;
|
||
const currentWaveform = waveforms[currentWaveformIndex.value];
|
||
const phaseRadians = phase.value * Math.PI / 180;
|
||
|
||
// 时基和频率共同影响周期数量
|
||
// 频率因素 - 频率越高,一个屏幕内显示的周期越多
|
||
// 使用对数缩放可以更好地表示广泛范围的频率变化
|
||
const freqLog = Math.log10(frequency.value) - 2; // 从100Hz开始作为基准
|
||
const frequencyFactor = Math.max(0.1, Math.min(10, freqLog)); // 限制在合理范围内
|
||
|
||
// 时基影响周期数量 - 时基越小,显示的周期越多
|
||
const timebaseFactor = 1 / timebase.value;
|
||
|
||
// 组合因素
|
||
const scaleFactor = timebaseFactor * frequencyFactor;
|
||
|
||
let path = '';
|
||
// 使用函数生成波形
|
||
if (currentWaveform === 'custom') {
|
||
// 自定义波形
|
||
if (drawPoints.value.length > 0) {
|
||
path = `M${xOffset + drawPoints.value[0][0]},${yOffset + drawPoints.value[0][1]}`;
|
||
for (let i = 1; i < drawPoints.value.length; i++) {
|
||
path += ` L${xOffset + drawPoints.value[i][0]},${yOffset + drawPoints.value[i][1]}`;
|
||
}
|
||
} else {
|
||
// 如果没有绘制点但选择了自定义波形,仍然使用函数生成
|
||
const waveFunction = waveformFunctions.custom; path = `M${xOffset},${yOffset + height/2}`;
|
||
|
||
for (let x = 0; x <= width; x++) {
|
||
const scaledX = x * scaleFactor;
|
||
const y = waveFunction(scaledX, width, height, phaseRadians);
|
||
// 注意:心形函数可能返回undefined或极端值,需要处理
|
||
if (typeof y === 'number' && isFinite(y)) {
|
||
path += ` L${x + xOffset},${yOffset + height/2 - y}`;
|
||
} else {
|
||
// 如果返回异常值,保持当前位置
|
||
path += ` L${x + xOffset},${yOffset + height/2}`;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 使用预定义的波形函数
|
||
const waveFunction = waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
|
||
|
||
// 生成路径点
|
||
path = `M${xOffset},${yOffset + height/2}`;
|
||
|
||
for (let x = 0; x <= width; x++) {
|
||
// 应用组合缩放因素 - 影响x轴的缩放
|
||
const scaledX = x * scaleFactor;
|
||
const y = waveFunction(scaledX, width, height, phaseRadians);
|
||
path += ` L${x + xOffset},${yOffset + height/2 - y}`;
|
||
}
|
||
}
|
||
|
||
return path;
|
||
});
|
||
|
||
// 波形操作函数
|
||
function selectWaveform(index: number) {
|
||
currentWaveformIndex.value = index;
|
||
updateModelValue();
|
||
}
|
||
|
||
function increaseFrequency() {
|
||
if (frequency.value < 10) {
|
||
frequency.value += 0.1;
|
||
} else if (frequency.value < 100) {
|
||
frequency.value += 1;
|
||
} else if (frequency.value < 1000) {
|
||
frequency.value += 10;
|
||
} else if (frequency.value < 10000) {
|
||
frequency.value += 100;
|
||
} else if (frequency.value < 100000) {
|
||
frequency.value += 1000;
|
||
} else {
|
||
frequency.value += 10000;
|
||
}
|
||
frequency.value = Math.min(frequency.value, 10000000); // 最大10MHz
|
||
frequency.value = parseFloat(frequency.value.toFixed(1)); // 修复浮点数精度问题
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
updateModelValue();
|
||
}
|
||
|
||
function decreaseFrequency() {
|
||
if (frequency.value <= 10) {
|
||
frequency.value -= 0.1;
|
||
} else if (frequency.value <= 100) {
|
||
frequency.value -= 1;
|
||
} else if (frequency.value <= 1000) {
|
||
frequency.value -= 10;
|
||
} else if (frequency.value <= 10000) {
|
||
frequency.value -= 100;
|
||
} else if (frequency.value <= 100000) {
|
||
frequency.value -= 1000;
|
||
} else {
|
||
frequency.value -= 10000;
|
||
}
|
||
frequency.value = Math.max(frequency.value, 0.1); // 最小0.1Hz
|
||
frequency.value = parseFloat(frequency.value.toFixed(1)); // 修复浮点数精度问题
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
updateModelValue();
|
||
}
|
||
|
||
function applyFrequencyInput() {
|
||
let value = parseFloat(frequencyInput.value);
|
||
|
||
// 处理单位
|
||
if (frequencyInput.value.includes('MHz')) {
|
||
value = parseFloat(frequencyInput.value) * 1000000;
|
||
} else if (frequencyInput.value.includes('kHz')) {
|
||
value = parseFloat(frequencyInput.value) * 1000;
|
||
} else if (frequencyInput.value.includes('Hz')) {
|
||
value = parseFloat(frequencyInput.value);
|
||
}
|
||
|
||
if (!isNaN(value)) {
|
||
frequency.value = Math.min(Math.max(value, 0.1), 10000000);
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
updateModelValue();
|
||
} else {
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
}
|
||
}
|
||
|
||
function increasePhase() {
|
||
phase.value += 15;
|
||
if (phase.value >= 360) {
|
||
phase.value -= 360;
|
||
}
|
||
phaseInput.value = phase.value.toString();
|
||
updateModelValue();
|
||
}
|
||
|
||
function decreasePhase() {
|
||
phase.value -= 15;
|
||
if (phase.value < 0) {
|
||
phase.value += 360;
|
||
}
|
||
phaseInput.value = phase.value.toString();
|
||
updateModelValue();
|
||
}
|
||
|
||
function applyPhaseInput() {
|
||
let value = parseFloat(phaseInput.value);
|
||
if (!isNaN(value)) {
|
||
// 确保相位在0-360之间
|
||
while (value >= 360) value -= 360;
|
||
while (value < 0) value += 360;
|
||
phase.value = value;
|
||
phaseInput.value = phase.value.toString();
|
||
updateModelValue();
|
||
} else {
|
||
phaseInput.value = phase.value.toString();
|
||
}
|
||
}
|
||
|
||
function applyCustomWaveform() {
|
||
if (customWaveformExpression.value) {
|
||
try {
|
||
// 创建自定义波形函数
|
||
createCustomWaveformFunction();
|
||
currentWaveformIndex.value = waveforms.indexOf('custom');
|
||
drawCustomWaveformFromExpression();
|
||
updateModelValue();
|
||
} catch (error) {
|
||
console.error('Invalid expression:', error);
|
||
// 这里可以添加一些错误提示
|
||
}
|
||
}
|
||
}
|
||
|
||
// 应用示例函数
|
||
function applyExampleFunction(expression: string) {
|
||
customWaveformExpression.value = expression;
|
||
applyCustomWaveform();
|
||
}
|
||
|
||
// 创建自定义波形函数
|
||
function createCustomWaveformFunction() {
|
||
// 使用 mathjs 解析表达式并创建函数
|
||
const expression = customWaveformExpression.value;
|
||
|
||
// 导入 mathjs
|
||
import('mathjs').then((math) => {
|
||
try {
|
||
// 预编译表达式以提高性能
|
||
const compiledExpression = math.compile(expression);
|
||
|
||
// 添加自定义函数到波形函数集合中
|
||
waveformFunctions.custom = (x: number, width: number, height: number, phaseRad: number): number => {
|
||
try {
|
||
// 相位调整 - 将相位转换为x轴的位移
|
||
const phaseShift = phaseRad / (2 * Math.PI);
|
||
|
||
// 标准化参数,使x落在 0-1 范围内,并应用相位
|
||
let normalizedX = ((x / width) + phaseShift) % 1;
|
||
|
||
// 心形函数需要x映射到[-1.5,1.5]范围,以确保完整显示心形
|
||
// 这个范围比[-1,1]稍大,确保心形两侧完整显示
|
||
const scaledX = (normalizedX * 3) - 1.5;
|
||
|
||
// 创建参数对象,包括各种变量和常量供表达式使用
|
||
const scope = {
|
||
x: scaledX, // 映射后的x变量
|
||
t: normalizedX * 2 * Math.PI, // 角度变量 (0-2π),不包括相位
|
||
phase: phaseRad, // 相位值,独立参数
|
||
PI: Math.PI, // 常量 PI
|
||
a: 7.8, // 心形函数振荡参数
|
||
};
|
||
// 计算表达式
|
||
let result = compiledExpression.evaluate(scope);
|
||
|
||
// 确保结果在合理范围内
|
||
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) {
|
||
result = 0;
|
||
}
|
||
|
||
// 对于心形函数,我们可能需要额外调整振幅因子
|
||
// 确保振幅在合适范围内
|
||
result = Math.max(-1, Math.min(1, result));
|
||
|
||
// 返回适当的振幅
|
||
return height/2 * result;
|
||
} catch (e) {
|
||
console.error('Error evaluating expression:', e);
|
||
return 0;
|
||
}
|
||
};
|
||
|
||
// 立即更新波形显示
|
||
updateModelValue();
|
||
} catch (parseError) {
|
||
console.error('Error parsing expression:', parseError);
|
||
// 解析错误时使用默认正弦波
|
||
waveformFunctions.custom = (x: number, width: number, height: number, phaseRad: number): number => {
|
||
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
|
||
};
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error loading mathjs:', error);
|
||
});
|
||
};
|
||
|
||
// 绘制自定义波形
|
||
function drawCustomWaveformFromExpression() {
|
||
const canvas = drawingCanvas.value;
|
||
if (!canvas) return;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const width = canvas.width;
|
||
const height = canvas.height;
|
||
|
||
// 清除画布
|
||
ctx.clearRect(0, 0, width, height);
|
||
ctx.strokeStyle = '#0f0';
|
||
ctx.lineWidth = 2;
|
||
|
||
// 创建自定义函数 - 先创建函数再绘制
|
||
createCustomWaveformFunction();
|
||
// 使用波形函数来绘制
|
||
const phaseRad = phase.value * Math.PI / 180;
|
||
|
||
// 频率因素,与主波形显示保持一致
|
||
// 频率越高,显示的周期数就越多
|
||
const freqLog = Math.log10(frequency.value) - 2; // 从100Hz开始作为基准
|
||
const frequencyFactor = Math.max(0.1, Math.min(10, freqLog));
|
||
|
||
// 时基因素 - 时基越小,显示周期越多
|
||
const timebaseFactor = 1 / timebase.value;
|
||
|
||
// 组合因素 - 同时应用频率和时基缩放
|
||
const scaleFactor = timebaseFactor * frequencyFactor;
|
||
|
||
// 基于表达式生成点的坐标
|
||
drawPoints.value = [];
|
||
|
||
// 等待一些时间让 mathjs 加载和编译表达式
|
||
setTimeout(() => {
|
||
ctx.beginPath();
|
||
|
||
let firstPoint = true;
|
||
let previousY = null;
|
||
const samplePoints = 300; // 增加采样点以提高平滑度
|
||
|
||
for (let i = 0; i <= samplePoints; i++) {
|
||
const x = (i / samplePoints) * width;
|
||
|
||
// 应用缩放因素计算 y 值
|
||
const scaledX = x * scaleFactor;
|
||
const y = waveformFunctions.custom(scaledX, width, height, phaseRad);
|
||
const canvasY = height / 2 - y;
|
||
|
||
// 检测是否是有效值
|
||
if (isNaN(canvasY) || !isFinite(canvasY)) {
|
||
continue; // 跳过无效点
|
||
}
|
||
|
||
// 检测是否有大跳变(可能是不连续函数)
|
||
if (previousY !== null && Math.abs(canvasY - previousY) > height / 2) {
|
||
// 在大跳变处断开路径
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
firstPoint = true;
|
||
}
|
||
|
||
if (firstPoint) {
|
||
ctx.moveTo(x, canvasY);
|
||
firstPoint = false;
|
||
} else {
|
||
ctx.lineTo(x, canvasY);
|
||
}
|
||
|
||
previousY = canvasY;
|
||
drawPoints.value.push([x, canvasY]);
|
||
}
|
||
|
||
ctx.stroke();
|
||
}, 100); // 短暂延迟以确保 mathjs 已加载
|
||
}
|
||
|
||
// 绘图功能
|
||
function startDrawing(event: MouseEvent) {
|
||
isDrawing.value = true;
|
||
const canvas = drawingCanvas.value;
|
||
if (!canvas) return;
|
||
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = event.clientX - rect.left;
|
||
const y = event.clientY - rect.top;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
// 清除画布
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 开始新的绘制
|
||
drawPoints.value = [[x, y]];
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = '#0f0';
|
||
ctx.lineWidth = 2;
|
||
ctx.moveTo(x, y);
|
||
}
|
||
|
||
function draw(event: MouseEvent) {
|
||
if (!isDrawing.value) return;
|
||
|
||
const canvas = drawingCanvas.value;
|
||
if (!canvas) return;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = event.clientX - rect.left;
|
||
const y = event.clientY - rect.top;
|
||
|
||
// 添加点
|
||
drawPoints.value.push([x, y]);
|
||
|
||
// 绘制线
|
||
ctx.lineTo(x, y);
|
||
ctx.stroke();
|
||
}
|
||
|
||
function stopDrawing() {
|
||
isDrawing.value = false;
|
||
}
|
||
|
||
function clearCanvas() {
|
||
const canvas = drawingCanvas.value;
|
||
if (!canvas) return;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
drawPoints.value = [];
|
||
}
|
||
|
||
function applyDrawnWaveform() {
|
||
if (drawPoints.value.length > 0) {
|
||
currentWaveformIndex.value = waveforms.indexOf('custom');
|
||
updateModelValue();
|
||
}
|
||
}
|
||
|
||
// 波形存储槽操作
|
||
function saveCurrentToSlot(index: number) {
|
||
waveformSlots.value[index] = {
|
||
name: waveformNames[currentWaveformIndex.value],
|
||
type: waveforms[currentWaveformIndex.value],
|
||
data: drawPoints.value.length > 0 && currentWaveformIndex.value === waveforms.indexOf('custom')
|
||
? [...drawPoints.value]
|
||
: null
|
||
};
|
||
}
|
||
|
||
function loadWaveformSlot(index: number) {
|
||
const slot = waveformSlots.value[index];
|
||
if (!slot.type) return;
|
||
|
||
const waveformIndex = waveforms.indexOf(slot.type);
|
||
if (waveformIndex !== -1) {
|
||
currentWaveformIndex.value = waveformIndex;
|
||
|
||
// 如果是自定义波形且有数据,加载数据
|
||
if (slot.type === 'custom' && slot.data !== null && slot.data.length > 0) {
|
||
drawPoints.value = [...slot.data];
|
||
|
||
// 更新画布
|
||
const canvas = drawingCanvas.value;
|
||
if (canvas) {
|
||
const ctx = canvas.getContext('2d');
|
||
if (ctx) {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = '#0f0';
|
||
ctx.lineWidth = 2;
|
||
|
||
for (let i = 0; i < slot.data.length; i++) {
|
||
const [x, y] = slot.data[i];
|
||
if (i === 0) {
|
||
ctx.moveTo(x, y);
|
||
} else {
|
||
ctx.lineTo(x, y);
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
}
|
||
|
||
updateModelValue();
|
||
}
|
||
}
|
||
|
||
// 更新模型值
|
||
function updateModelValue() {
|
||
const newValue = {
|
||
...props.modelValue,
|
||
frequency: frequency.value,
|
||
phase: phase.value,
|
||
timebase: timebase.value,
|
||
waveform: waveforms[currentWaveformIndex.value],
|
||
customWaveformPoints: currentWaveformIndex.value === waveforms.indexOf('custom') ? [...drawPoints.value] : []
|
||
};
|
||
emit('update:modelValue', newValue);
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
// 初始化波形和参数
|
||
if (props.modelValue) {
|
||
if (props.modelValue.frequency !== undefined) {
|
||
frequency.value = props.modelValue.frequency;
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
}
|
||
|
||
if (props.modelValue.phase !== undefined) {
|
||
phase.value = props.modelValue.phase;
|
||
phaseInput.value = phase.value.toString();
|
||
}
|
||
|
||
if (props.modelValue.timebase !== undefined) {
|
||
timebase.value = props.modelValue.timebase;
|
||
}
|
||
|
||
if (props.modelValue.waveform) {
|
||
const index = waveforms.indexOf(props.modelValue.waveform);
|
||
if (index !== -1) {
|
||
currentWaveformIndex.value = index;
|
||
}
|
||
}
|
||
|
||
// 加载自定义波形点
|
||
if (props.modelValue.customWaveformPoints && props.modelValue.customWaveformPoints.length > 0) {
|
||
drawPoints.value = [...props.modelValue.customWaveformPoints];
|
||
|
||
// 绘制到画布上
|
||
const canvas = drawingCanvas.value;
|
||
if (canvas && currentWaveformIndex.value === waveforms.indexOf('custom')) {
|
||
const ctx = canvas.getContext('2d');
|
||
if (ctx) {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = '#0f0';
|
||
ctx.lineWidth = 2;
|
||
|
||
for (let i = 0; i < drawPoints.value.length; i++) {
|
||
const [x, y] = drawPoints.value[i];
|
||
if (i === 0) {
|
||
ctx.moveTo(x, y);
|
||
} else {
|
||
ctx.lineTo(x, y);
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 监听model变化
|
||
watch(() => props.modelValue, (newVal) => {
|
||
if (newVal && newVal.frequency !== undefined && newVal.frequency !== frequency.value) {
|
||
frequency.value = newVal.frequency;
|
||
frequencyInput.value = formatFrequency(frequency.value);
|
||
}
|
||
|
||
if (newVal && newVal.phase !== undefined && newVal.phase !== phase.value) {
|
||
phase.value = newVal.phase;
|
||
phaseInput.value = phase.value.toString();
|
||
}
|
||
|
||
if (newVal && newVal.timebase !== undefined && newVal.timebase !== timebase.value) {
|
||
timebase.value = newVal.timebase;
|
||
}
|
||
|
||
if (newVal && newVal.waveform) {
|
||
const index = waveforms.indexOf(newVal.waveform);
|
||
if (index !== -1 && index !== currentWaveformIndex.value) {
|
||
currentWaveformIndex.value = index;
|
||
}
|
||
}
|
||
}, { deep: true });
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dds-property-editor {
|
||
width: 100%;
|
||
}
|
||
|
||
.dds-editor-container {
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
background-color: var(--base-200, #2a303c);
|
||
}
|
||
|
||
.dds-display {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.waveform-display {
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.timebase-controls {
|
||
position: absolute;
|
||
bottom: 2px;
|
||
right: 2px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px;
|
||
background-color: rgba(26, 31, 37, 0.7);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.timebase-button {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--base-300, #374151);
|
||
border: none;
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.timebase-button:hover {
|
||
background-color: var(--base-content, #a6adbb);
|
||
color: var(--base-300, #374151);
|
||
}
|
||
|
||
.timebase-label {
|
||
font-size: 0.8rem;
|
||
color: var(--base-content, #a6adbb);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.waveform-selector {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.waveform-option {
|
||
flex: 1;
|
||
padding: 8px 4px;
|
||
text-align: center;
|
||
background-color: var(--base-100, #1d232a);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.waveform-option:hover {
|
||
background-color: var(--base-300, #374151);
|
||
}
|
||
|
||
.waveform-option.active {
|
||
background-color: var(--primary, #570df8);
|
||
color: white;
|
||
}
|
||
|
||
.control-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.control-group {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.control-label {
|
||
font-size: 0.9rem;
|
||
color: var(--base-content, #a6adbb);
|
||
}
|
||
|
||
.control-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.control-button {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--base-300, #374151);
|
||
border: none;
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.control-button:hover {
|
||
background-color: var(--base-content, #a6adbb);
|
||
color: var(--base-300, #374151);
|
||
}
|
||
|
||
.control-input {
|
||
flex: 1;
|
||
height: 28px;
|
||
padding: 0 8px;
|
||
background-color: var(--base-100, #1d232a);
|
||
border: 1px solid var(--base-300, #374151);
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
text-align: center;
|
||
}
|
||
|
||
.custom-waveform {
|
||
margin-top: 16px;
|
||
padding: 12px;
|
||
background-color: var(--base-100, #1d232a);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.section-heading {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: var(--base-content, #a6adbb);
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.example-functions {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.example-label {
|
||
font-size: 0.85rem;
|
||
color: var(--base-content, #a6adbb);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.example-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.example-button {
|
||
padding: 4px 8px;
|
||
background-color: var(--base-200, #2a303c);
|
||
border: 1px solid var(--base-300, #374151);
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease, color 0.2s ease;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.example-button:hover {
|
||
background-color: var(--primary, #570df8);
|
||
color: white;
|
||
}
|
||
|
||
.input-label {
|
||
font-size: 0.9rem;
|
||
color: var(--base-content, #a6adbb);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.function-input {
|
||
flex: 1;
|
||
height: 32px;
|
||
padding: 0 8px;
|
||
background-color: var(--base-200, #2a303c);
|
||
border: 1px solid var(--base-300, #374151);
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
}
|
||
|
||
.apply-button {
|
||
height: 32px;
|
||
padding: 0 12px;
|
||
background-color: var(--primary, #570df8);
|
||
border: none;
|
||
border-radius: 4px;
|
||
color: white;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.apply-button:hover {
|
||
background-color: var(--primary-focus, #4406cb);
|
||
}
|
||
|
||
.drawing-area {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.waveform-canvas-container {
|
||
border: 1px solid var(--base-300, #374151);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.drawing-canvas {
|
||
width: 100%;
|
||
height: 100px;
|
||
background-color: var(--base-200, #2a303c);
|
||
cursor: crosshair;
|
||
}
|
||
|
||
.canvas-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding: 8px;
|
||
background-color: var(--base-200, #2a303c);
|
||
}
|
||
|
||
.canvas-button {
|
||
padding: 4px 8px;
|
||
background-color: var(--base-300, #374151);
|
||
border: none;
|
||
border-radius: 4px;
|
||
color: var(--base-content, #a6adbb);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.canvas-button:hover {
|
||
background-color: var(--base-content, #a6adbb);
|
||
color: var(--base-300, #374151);
|
||
}
|
||
|
||
.saved-waveforms {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.slot-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.waveform-slot {
|
||
position: relative;
|
||
padding: 8px;
|
||
background-color: var(--base-200, #2a303c);
|
||
border: 1px solid var(--base-300, #374151);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.waveform-slot:hover {
|
||
background-color: var(--base-300, #374151);
|
||
}
|
||
|
||
.waveform-slot.empty {
|
||
background-color: var(--base-100, #1d232a);
|
||
}
|
||
|
||
.slot-name {
|
||
display: block;
|
||
font-size: 0.9rem;
|
||
margin-right: 50px;
|
||
}
|
||
|
||
.save-button {
|
||
position: absolute;
|
||
right: 8px;
|
||
top: 6px;
|
||
padding: 2px 6px;
|
||
background-color: var(--primary, #570df8);
|
||
border: none;
|
||
border-radius: 4px;
|
||
color: white;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.save-button:hover {
|
||
background-color: var(--primary-focus, #4406cb);
|
||
}
|
||
</style>
|