1272 lines
33 KiB
Vue
1272 lines
33 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>
|
||
|
||
<button class="btn btn-primary text-primary-content w-full" :disabled="isApplying" @click="applyOutputWave">
|
||
<div v-if="isApplying">
|
||
<span class="loading loading-spinner"></span>
|
||
应用中...
|
||
</div>
|
||
<div v-else>应用输出波形</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</CollapsibleSection>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from "vue";
|
||
import CollapsibleSection from "../CollapsibleSection.vue";
|
||
import { DDSClient } from "@/APIClient";
|
||
import { useEquipments } from "@/stores/equipments";
|
||
import { useDialogStore } from "@/stores/dialog";
|
||
import { toInteger } from "lodash";
|
||
|
||
// Component Attributes
|
||
const props = defineProps<{
|
||
modelValue: any;
|
||
}>();
|
||
|
||
const emit = defineEmits(["update:modelValue"]);
|
||
|
||
// Global varibles
|
||
const dds = new DDSClient();
|
||
const eqps = useEquipments();
|
||
const dialog = useDialogStore();
|
||
|
||
// 波形状态
|
||
const frequency = ref<number>(props.modelValue?.frequency || 1000);
|
||
const phase = ref<number>(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"];
|
||
const isApplying = ref(false);
|
||
|
||
// 波形函数集合
|
||
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: "sawtooth", 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();
|
||
}
|
||
|
||
async function applyOutputWave() {
|
||
try {
|
||
isApplying.value = true;
|
||
{
|
||
const ret = await dds.setWaveNum(
|
||
eqps.boardAddr,
|
||
eqps.boardPort,
|
||
0,
|
||
currentWaveformIndex.value,
|
||
);
|
||
if (!ret) {
|
||
dialog.error("应用失败");
|
||
}
|
||
}
|
||
|
||
{
|
||
const ret = await dds.setFreq(
|
||
eqps.boardAddr,
|
||
eqps.boardPort,
|
||
0,
|
||
currentWaveformIndex.value,
|
||
toInteger(frequency.value * Math.pow(2, 32 - 20)),
|
||
);
|
||
if (!ret) {
|
||
dialog.error("应用失败");
|
||
}
|
||
}
|
||
|
||
{
|
||
const ret = await dds.setPhase(
|
||
eqps.boardAddr,
|
||
eqps.boardPort,
|
||
0,
|
||
currentWaveformIndex.value,
|
||
toInteger((phase.value * 4096) / 360),
|
||
);
|
||
if (ret) {
|
||
dialog.info("应用成功");
|
||
} else {
|
||
dialog.error("应用失败");
|
||
}
|
||
}
|
||
} catch (e) {
|
||
dialog.error("应用失败");
|
||
console.error(e);
|
||
} finally {
|
||
isApplying.value = false;
|
||
}
|
||
}
|
||
|
||
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>
|