<script setup lang="ts">
|
import { ref, onMounted, onUnmounted } from 'vue';
|
import * as THREE from 'three';
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
import { createDataReceiver } from '../utils/dataFetcher';
|
const containerRef = ref<HTMLElement | null>(null);
|
let scene: THREE.Scene | null = null;
|
let camera: THREE.PerspectiveCamera | null = null;
|
let renderer: THREE.WebGLRenderer | null = null;
|
let controls: OrbitControls | null = null;
|
|
// 力和力矩的箭头
|
let forceArrows: THREE.ArrowHelper[] = [];
|
let torqueArcs: THREE.Line[] = [];
|
|
// 当前力和力矩数据
|
const currentData = ref<number[]>([0, 0, 0, 0, 0, 0]);
|
|
// 颜色定义
|
const colors = {
|
fx: 0xff0000, // 红色
|
fy: 0x00ff00, // 绿色
|
fz: 0x0000ff, // 蓝色
|
mx: 0xff00ff, // 紫色
|
my: 0xffff00, // 黄色
|
mz: 0x00ffff // 青色
|
};
|
|
// 初始化3D场景
|
function initScene() {
|
if (!containerRef.value) return;
|
|
// 创建场景
|
scene = new THREE.Scene();
|
scene.background = new THREE.Color(0x000000);
|
|
// 创建相机
|
const width = containerRef.value.clientWidth;
|
const height = containerRef.value.clientHeight;
|
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
camera.position.set(5, 5, 5);
|
camera.lookAt(0, 0, 0);
|
|
// 创建渲染器
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
renderer.setSize(width, height);
|
containerRef.value.appendChild(renderer.domElement);
|
|
// 添加轨道控制器
|
controls = new OrbitControls(camera, renderer.domElement);
|
controls.enableDamping = true;
|
controls.dampingFactor = 0.25;
|
|
// 添加坐标轴
|
const axesHelper = new THREE.AxesHelper(4);
|
axesHelper.setColors(
|
new THREE.Color(0x707070), // X轴颜色
|
new THREE.Color(0x707070), // Y轴颜色
|
new THREE.Color(0x707070) // Z轴颜色
|
);
|
// 设置坐标轴的渲染顺序为较低值
|
axesHelper.renderOrder = -1;
|
if (axesHelper instanceof THREE.Object3D) {
|
axesHelper.traverse((child) => {
|
if (child instanceof THREE.Line) {
|
if (child.material instanceof THREE.LineBasicMaterial) {
|
child.material.depthTest = false;
|
}
|
}
|
});
|
}
|
scene.add(axesHelper);
|
|
// 添加坐标轴标签
|
const textDistance = 4.2; // 坐标轴长度 + 一点间距
|
|
// 创建标签精灵
|
function createAxisLabel(text: string, position: THREE.Vector3) {
|
const canvas = document.createElement('canvas');
|
const context = canvas.getContext('2d');
|
if (!context) return null;
|
|
canvas.width = 64;
|
canvas.height = 64;
|
|
context.fillStyle = '#ffffff';
|
context.font = 'bold 48px Arial';
|
context.textAlign = 'center';
|
context.textBaseline = 'middle';
|
context.fillText(text, 32, 32);
|
|
const texture = new THREE.CanvasTexture(canvas);
|
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
|
const sprite = new THREE.Sprite(spriteMaterial);
|
|
sprite.position.copy(position);
|
sprite.scale.set(0.5, 0.5, 1);
|
return sprite;
|
}
|
|
// 创建并添加XYZ轴标签
|
const labelX = createAxisLabel('X', new THREE.Vector3(textDistance, 0, 0));
|
const labelY = createAxisLabel('Y', new THREE.Vector3(0, textDistance, 0));
|
const labelZ = createAxisLabel('Z', new THREE.Vector3(0, 0, textDistance));
|
|
if (labelX) scene.add(labelX);
|
if (labelY) scene.add(labelY);
|
if (labelZ) scene.add(labelZ);
|
|
// 添加网格
|
const gridHelper = new THREE.GridHelper(20, 20, 0x404040, 0x404040);
|
gridHelper.material.opacity = 0.5;
|
gridHelper.material.transparent = true;
|
scene.add(gridHelper);
|
|
// 添加环境光
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
|
scene.add(ambientLight);
|
|
// 添加方向光
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
directionalLight.position.set(10, 10, 10);
|
scene.add(directionalLight);
|
|
// 初始化力和力矩的箭头
|
initForceArrows();
|
initTorqueArcs();
|
|
// 开始动画循环
|
animate();
|
|
// 响应窗口大小变化
|
window.addEventListener('resize', handleResize);
|
}
|
|
// 初始化力的箭头
|
function initForceArrows() {
|
if (!scene) return;
|
|
// 清除现有的箭头
|
forceArrows.forEach(arrow => scene?.remove(arrow));
|
forceArrows = [];
|
|
// 创建三个力的箭头 (Fx, Fy, Fz)
|
const headLength = 0.5;
|
const headWidth = 0.3;
|
|
// 创建力的箭头 (初始长度为0)
|
const fxArrow = new THREE.ArrowHelper(
|
new THREE.Vector3(1, 0, 0),
|
new THREE.Vector3(0, 0, 0),
|
0.001,
|
colors.fx,
|
headLength,
|
headWidth
|
);
|
fxArrow.renderOrder = 1;
|
|
|
const fyArrow = new THREE.ArrowHelper(
|
new THREE.Vector3(0, 1, 0),
|
new THREE.Vector3(0, 0, 0),
|
0.001,
|
colors.fy,
|
headLength,
|
headWidth
|
);
|
fyArrow.renderOrder = 1;
|
|
|
const fzArrow = new THREE.ArrowHelper(
|
new THREE.Vector3(0, 0, 1),
|
new THREE.Vector3(0, 0, 0),
|
0.001,
|
colors.fz,
|
headLength,
|
headWidth
|
);
|
fzArrow.renderOrder = 1;
|
|
|
// 添加到场景
|
scene.add(fxArrow);
|
scene.add(fyArrow);
|
scene.add(fzArrow);
|
|
// 保存引用
|
forceArrows = [fxArrow, fyArrow, fzArrow];
|
}
|
|
// 初始化力矩的弧形表示
|
function initTorqueArcs() {
|
if (!scene) return;
|
|
// 清除现有的弧形
|
torqueArcs.forEach(arc => scene?.remove(arc));
|
torqueArcs = [];
|
|
// 创建三个力矩的弧形 (Mx, My, Mz)
|
const radius = 2;
|
const segments = 32;
|
|
// 创建X轴力矩弧形 (绕X轴旋转)
|
const mxGeometry = new THREE.BufferGeometry();
|
const mxPoints = [];
|
for (let i = 0; i <= segments; i++) {
|
const theta = (i / segments) * Math.PI;
|
mxPoints.push(new THREE.Vector3(0, radius * Math.cos(theta), radius * Math.sin(theta)));
|
}
|
mxGeometry.setFromPoints(mxPoints);
|
const mxArc = new THREE.Line(mxGeometry, new THREE.LineBasicMaterial({ color: colors.mx, linewidth: 2 }));
|
|
// 创建Y轴力矩弧形 (绕Y轴旋转)
|
const myGeometry = new THREE.BufferGeometry();
|
const myPoints = [];
|
for (let i = 0; i <= segments; i++) {
|
const theta = (i / segments) * Math.PI;
|
myPoints.push(new THREE.Vector3(radius * Math.cos(theta), 0, radius * Math.sin(theta)));
|
}
|
myGeometry.setFromPoints(myPoints);
|
const myArc = new THREE.Line(myGeometry, new THREE.LineBasicMaterial({ color: colors.my, linewidth: 2 }));
|
|
// 创建Z轴力矩弧形 (绕Z轴旋转)
|
const mzGeometry = new THREE.BufferGeometry();
|
const mzPoints = [];
|
for (let i = 0; i <= segments; i++) {
|
const theta = (i / segments) * Math.PI;
|
mzPoints.push(new THREE.Vector3(radius * Math.cos(theta), radius * Math.sin(theta), 0));
|
}
|
mzGeometry.setFromPoints(mzPoints);
|
const mzArc = new THREE.Line(mzGeometry, new THREE.LineBasicMaterial({ color: colors.mz, linewidth: 2 }));
|
|
// 添加到场景
|
scene.add(mxArc);
|
scene.add(myArc);
|
scene.add(mzArc);
|
|
// 保存引用
|
torqueArcs = [mxArc, myArc, mzArc];
|
|
// 初始状态下隐藏弧形
|
torqueArcs.forEach(arc => {
|
arc.visible = false;
|
});
|
}
|
|
// 更新力和力矩的可视化
|
function updateVisualization(forceData: number[]) {
|
if (!scene) return;
|
|
// 更新当前数据
|
currentData.value = forceData;
|
|
// 计算力的最大值,用于归一化
|
const maxForce = Math.max(
|
Math.abs(forceData[0]),
|
Math.abs(forceData[1]),
|
Math.abs(forceData[2])
|
) || 1; // 避免除以零
|
|
// 计算力矩的最大值,用于归一化
|
const maxTorque = Math.max(
|
Math.abs(forceData[3]),
|
Math.abs(forceData[4]),
|
Math.abs(forceData[5])
|
) || 1; // 避免除以零
|
|
// 更新力的箭头
|
const forceScale = 3; // 最大长度为3个单位
|
for (let i = 0; i < 3; i++) {
|
const force = forceData[i];
|
const length = Math.abs(force) / maxForce * forceScale;
|
|
// 更新箭头长度和方向
|
if (forceArrows[i]) {
|
// 如果力为零或非常小,则几乎不可见
|
if (Math.abs(force) < 0.1) {
|
forceArrows[i].setLength(0.001);
|
} else {
|
forceArrows[i].setLength(length);
|
|
// 如果力为负,则反转方向
|
if (force < 0) {
|
const dir = new THREE.Vector3();
|
if (i === 0) dir.set(-1, 0, 0);
|
if (i === 1) dir.set(0, -1, 0);
|
if (i === 2) dir.set(0, 0, -1);
|
forceArrows[i].setDirection(dir);
|
} else {
|
const dir = new THREE.Vector3();
|
if (i === 0) dir.set(1, 0, 0);
|
if (i === 1) dir.set(0, 1, 0);
|
if (i === 2) dir.set(0, 0, 1);
|
forceArrows[i].setDirection(dir);
|
}
|
}
|
}
|
}
|
|
// 更新力矩的弧形
|
for (let i = 0; i < 3; i++) {
|
const torque = forceData[i + 3];
|
|
// 显示或隐藏力矩弧形
|
if (torqueArcs[i]) {
|
if (Math.abs(torque) < 0.1) {
|
torqueArcs[i].visible = false;
|
} else {
|
torqueArcs[i].visible = true;
|
|
// 根据力矩方向旋转弧形
|
const arc = torqueArcs[i];
|
arc.rotation.set(0, 0, 0); // 重置旋转
|
|
// 根据力矩大小和方向设置旋转
|
const rotationAngle = (Math.abs(torque) / maxTorque) * Math.PI / 2;
|
|
if (i === 0) { // Mx
|
arc.rotation.x = torque > 0 ? rotationAngle : -rotationAngle;
|
} else if (i === 1) { // My
|
arc.rotation.y = torque > 0 ? rotationAngle : -rotationAngle;
|
} else if (i === 2) { // Mz
|
arc.rotation.z = torque > 0 ? rotationAngle : -rotationAngle;
|
}
|
}
|
}
|
}
|
}
|
|
// 动画循环
|
function animate() {
|
if (!scene || !camera || !renderer || !controls) return;
|
|
requestAnimationFrame(animate);
|
|
// 更新控制器
|
controls.update();
|
|
// 渲染场景
|
renderer.render(scene, camera);
|
}
|
|
// 处理窗口大小变化
|
function handleResize() {
|
if (!containerRef.value || !camera || !renderer) return;
|
|
const width = containerRef.value.clientWidth;
|
const height = containerRef.value.clientHeight;
|
|
camera.aspect = width / height;
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(width, height);
|
}
|
|
|
|
|
|
onMounted(() => {
|
initScene();
|
animate();
|
|
// createDataReceiver 现在直接注册回调并返回一个清理函数
|
createDataReceiver((forceData) => {
|
updateVisualization(forceData);
|
}); // 传递 isSerialPortConnected 函数
|
|
// 监听窗口大小变化
|
window.addEventListener('resize', handleResize);
|
});
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener('resize', handleResize);
|
|
// 清理Three.js资源
|
if (renderer) {
|
renderer.dispose();
|
containerRef.value?.removeChild(renderer.domElement);
|
}
|
|
// 清除场景中的所有对象
|
if (scene) {
|
scene.clear();
|
}
|
|
// 清空引用
|
scene = null;
|
camera = null;
|
renderer = null;
|
controls = null;
|
forceArrows = [];
|
torqueArcs = [];
|
});
|
</script>
|
|
<template>
|
<div class="three-container">
|
<div class="three-scene" ref="containerRef"></div>
|
<div class="data-panel">
|
<h3 style="color: #ffffff;">六维力数据</h3>
|
<div class="data-row">
|
<div class="data-item" style="color: #ff0000">
|
<span class="label">Fx:</span>
|
<span class="value">{{ currentData[0].toFixed(2) }} N</span>
|
</div>
|
<div class="data-item" style="color: #00ff00">
|
<span class="label">Fy:</span>
|
<span class="value">{{ currentData[1].toFixed(2) }} N</span>
|
</div>
|
<div class="data-item" style="color: #0000ff">
|
<span class="label">Fz:</span>
|
<span class="value">{{ currentData[2].toFixed(2) }} N</span>
|
</div>
|
</div>
|
<div class="data-row">
|
<div class="data-item" style="color: #ff00ff">
|
<span class="label">Mx:</span>
|
<span class="value">{{ currentData[3].toFixed(2) }} Nm</span>
|
</div>
|
<div class="data-item" style="color: #ffff00">
|
<span class="label">My:</span>
|
<span class="value">{{ currentData[4].toFixed(2) }} Nm</span>
|
</div>
|
<div class="data-item" style="color: #00ffff">
|
<span class="label">Mz:</span>
|
<span class="value">{{ currentData[5].toFixed(2) }} Nm</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.three-container {
|
width: 100%;
|
height: 540px;
|
margin: 20px 0;
|
display: flex;
|
flex-direction: column;
|
background-color: #ffffff;
|
border-radius: 8px;
|
overflow: hidden;
|
}
|
|
.three-scene {
|
width: 100%;
|
height: 450px;
|
position: relative;
|
}
|
|
.data-panel {
|
padding: 10px 20px;
|
background-color: #000000;
|
border-top: 1px solid #535353;
|
}
|
|
.data-panel h3 {
|
margin: 0 0 10px 0;
|
font-size: 16px;
|
text-align: center;
|
}
|
|
.data-row {
|
display: flex;
|
justify-content: space-around;
|
margin-bottom: 5px;
|
}
|
|
.data-item {
|
display: flex;
|
align-items: center;
|
font-weight: bold;
|
}
|
|
.label {
|
margin-right: 5px;
|
}
|
|
.value {
|
font-family: monospace;
|
}
|
</style>
|