README.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src-tauri/Cargo.toml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src-tauri/src/lib.rs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src-tauri/src/main.rs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src-tauri/src/ws_client.rs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/App.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/ForceChart.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/GaugeDisplay.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/TableDisplay.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/ThreeDDisplay copy.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/ThreeDDisplay.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pipe_client.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/utils/dataFetcher.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
README.md
@@ -2,6 +2,10 @@ è¿ä¸ªæ¨¡æ¿æ¨å¨å¸®å©æ¨å¼å§ä½¿ç¨ Vue 3 å TypeScript å¨ Vite ä¸è¿è¡å¼åã该模æ¿ä½¿ç¨ Vue 3 `<script setup>` SFCsï¼è¯·æ¥ç [script setup ææ¡£](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) äºè§£æ´å¤ä¿¡æ¯ã ## 项ç®ä»ç» æ¬é¡¹ç®ä¸º"å ç»´åä¼ æå¨æµè¯è½¯ä»¶"ï¼åºäº Tauri + Vue3 + TypeScript å¼åã软件éè¿ç®¡ééä¿¡å®æ¶æ¥æ¶å ç»´åï¼Fx, Fy, Fz, Mx, My, Mzï¼æ°æ®ï¼æ¯ææ²çº¿å¾ã仪表çãæ°æ®è¡¨æ ¼å 3D åæ è½´çå¤ç§å¯è§åæ¹å¼ï¼æ¹ä¾¿ç¨æ·å¯¹å ç»´åä¼ æå¨è¿è¡è°è¯ãæµè¯åæ°æ®åæãéç¨äºç§ç ãå·¥ä¸çå¤ç§åºç¨åºæ¯ã ## æ¨èç IDE 设置 - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) src-tauri/Cargo.toml
@@ -25,3 +25,9 @@ tokio = { version = "1.45.0", features = ["rt", "net", "time"] } rand = "0.8.5" once_cell = "1.19" tokio-tungstenite = "0.21" futures-util = "0.3" tungstenite = "0.21" anyhow = "1.0" src-tauri/src/lib.rs
@@ -1,5 +1,9 @@ mod tokio_utils; mod pipe_server; mod ws_client; use crate::ws_client::LATEST_WS_DATA; use std::sync::Mutex; use tauri::async_runtime::spawn; @@ -9,6 +13,10 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use crate::pipe_server::start_pipe_server; #[tauri::command] fn get_latest_ws_data() -> Option<String> { LATEST_WS_DATA.lock().unwrap().clone() } #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) @@ -38,6 +46,8 @@ .map_err(|e| e.to_string()) } // #[cfg_attr(mobile, tauri::mobile_entry_point)] // pub fn run() { // tauri::Builder::default() @@ -60,10 +70,12 @@ backend_task: false, })) // Add a command we can use to check .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message]) .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message, get_latest_ws_data]) // Use the setup hook to execute setup related tasks // Runs before the main loop, so no windows are yet created .setup(|app| { // å¯å¨ WebSocket 客æ·ç«¯ï¼å¼æ¥ä»»å¡ï¼ ws_client::start_ws_client(); // Spawn setup as a non-blocking task so the windows can be // created and ran while it executes spawn(setup(app.handle().clone())); src-tauri/src/main.rs
@@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { six_axis_force_lib::run() six_axis_force_lib::run(); } src-tauri/src/ws_client.rs
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,45 @@ use std::sync::{Arc, Mutex}; use once_cell::sync::Lazy; use tokio::runtime::Runtime; use tokio_tungstenite::connect_async; use futures_util::{StreamExt}; // å ¨å±ç¼åææ°å ç»´åæ°æ® pub static LATEST_WS_DATA: Lazy<Arc<Mutex<Option<String>>>> = Lazy::new(|| Arc::new(Mutex::new(None))); // WebSocket æå¡ç«¯å°åï¼å¯æ ¹æ®å®é æ åµä¿®æ¹ï¼ const WS_SERVER_URL: &str = "ws://192.168.21.25:5000"; pub fn start_ws_client() { // å¯å¨ tokio runtime std::thread::spawn(|| { let rt = Runtime::new().unwrap(); rt.block_on(async move { if let Err(e) = ws_client_task().await { eprintln!("WebSocket 客æ·ç«¯å¯å¨å¤±è´¥: {}", e); } }); }); } async fn ws_client_task() -> anyhow::Result<()> { println!("å°è¯è¿æ¥ WebSocket æå¡ç«¯: {}", WS_SERVER_URL); let (ws_stream, _) = connect_async(WS_SERVER_URL).await?; println!("WebSocket å·²è¿æ¥"); let (_, mut read) = ws_stream.split(); while let Some(msg) = read.next().await { match msg { Ok(tungstenite::Message::Text(text)) => { // å设æå¡ç«¯æ¨éçå°±æ¯å ç»´åæ°æ®ç JSON å符串 let mut data = LATEST_WS_DATA.lock().unwrap(); *data = Some(text.clone()); }, Ok(_) => {}, Err(e) => { eprintln!("WebSocket 读åé误: {}", e); break; } } } Ok(()) } src/App.vue
@@ -12,6 +12,7 @@ const showChart = ref(false); const pipeName = 'tauri-pipe-server'; const currentDisplay = ref('line'); // é»è®¤æ¾ç¤ºæ²çº¿å¾ const dataSource = ref<'pipe' | 'ws'>('ws'); // é»è®¤æ°æ®æºä¸ºç®¡é async function startPipeServer() { try { @@ -43,6 +44,13 @@ <template> <TitleBar :currentDisplay="currentDisplay" @start-service="startPipeServer" @select-display="(mode) => currentDisplay = mode" /> <main class="container"> <!-- <div class="data-source-switch"> <label>æ°æ®æºï¼</label> <select v-model="dataSource"> <option value="pipe">管é</option> <option value="ws">WebSocket</option> </select> </div> --> <div class="display-mode-icons"> <button :class="['icon-button', { active: currentDisplay === 'line' }]" @click="currentDisplay = 'line'" title="æ²çº¿å¾"> @@ -54,9 +62,9 @@ <button :class="['icon-button', { active: currentDisplay === 'table' }]" @click="currentDisplay = 'table'" title="æ°æ®è¡¨æ ¼"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="7" rx="2"/><path d="M10 12H6"/><path d="M18 12h-4"/><path d="M10 16H6"/><path d="M18 16h-4"/></svg> </button> <!-- <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3Dåæ è½´"> <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3Dåæ è½´"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5l10-5l-10-5z"/><path d="M2 17l10 5l10-5"/><path d="M2 12l10 5l10-5"/></svg> </button> --> </button> </div> <div class="header-row"> <h2>Force Sensor Data Visualization</h2> @@ -66,10 +74,10 @@ <!-- æ¾ç¤ºåä¼ æå¨æ°æ® --> <div v-if="showChart" class="chart-wrapper"> <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" /> <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" /> <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" /> <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" /> <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" :dataSource="dataSource" /> <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" :dataSource="dataSource" /> <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" :dataSource="dataSource" /> <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" :dataSource="dataSource" /> </div> <!-- <div class="input-group"> @@ -124,6 +132,13 @@ color: #000; font-size: 0.9em; } .data-source-switch { margin-bottom: 16px; display: flex; align-items: center; gap: 8px; } </style> <style> src/components/ForceChart.vue
@@ -1,11 +1,7 @@ <script setup lang="ts"> import { ref, onMounted, onUnmounted} from 'vue'; import { ref, onMounted, onUnmounted } from 'vue'; import * as echarts from 'echarts'; import { sendToPipe } from '../pipe_client'; const props = defineProps<{ pipeName: string; }>(); import { createDataReceiver } from '../utils/dataFetcher'; const chartContainer = ref<HTMLElement | null>(null); let chart: echarts.ECharts | null = null; @@ -64,37 +60,43 @@ name: 'Fx', type: 'line', data: dataHistory.value[0], smooth: true smooth: true, showSymbol: false }, { name: 'Fy', type: 'line', data: dataHistory.value[1], smooth: true smooth: true, showSymbol: false }, { name: 'Fz', type: 'line', data: dataHistory.value[2], smooth: true smooth: true, showSymbol: false }, { name: 'Mx', type: 'line', data: dataHistory.value[3], smooth: true smooth: true, showSymbol: false }, { name: 'My', type: 'line', data: dataHistory.value[4], smooth: true smooth: true, showSymbol: false }, { name: 'Mz', type: 'line', data: dataHistory.value[5], smooth: true smooth: true, showSymbol: false } ] }; @@ -150,53 +152,22 @@ chart?.resize(); } // ç¶æåéï¼ç¨äºæ§å¶é误æ¾ç¤ºé¢ç const errorCount = ref(0); const maxConsecutiveErrors = 5; const showingError = ref(false); // æ¥æ¶ç®¡éæ°æ®ç彿° async function receiveForceData() { try { // ä»ç®¡éæ¥æ¶æ°æ® const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); // æåæ¥æ¶æ°æ®ï¼éç½®éè¯¯è®¡æ° errorCount.value = 0; if (showingError.value) { showingError.value = false; console.log('管ééä¿¡å·²æ¢å¤'); } // è§£ææ¥æ¶å°çæ°æ® try { const forceData = JSON.parse(response); if (Array.isArray(forceData) && forceData.length === 6) { updateChart(forceData); } } catch (e) { console.warn('è§£ææ°æ®å¤±è´¥:', e); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount.value++; // åªå¨è¿ç»é误达å°é弿¶æ¾ç¤ºé误信æ¯ï¼é¿å æ¥å¿å·å± if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { showingError.value = true; console.error('管ééä¿¡æç»å¤±è´¥ï¼è¯·æ£æ¥æå¡ç«¯ç¶æ:', err); } } } // 宿¶è·åæ°æ® let dataTimer: number | null = null; onMounted(() => { initChart(); // æ¯ç§è·å䏿¬¡æ°æ® dataTimer = window.setInterval(receiveForceData, 1000); const receiveData = createDataReceiver( (forceData) => { updateChart(forceData); }); // æ¯100msè·å䏿¬¡æ°æ® dataTimer = window.setInterval(receiveData, 100); // çå¬çªå£å¤§å°ååï¼è°æ´å¾è¡¨å¤§å° window.addEventListener('resize', handleResize); }); onUnmounted(() => { src/components/GaugeDisplay.vue
@@ -1,11 +1,7 @@ <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import * as echarts from 'echarts'; import { sendToPipe } from '../pipe_client'; const props = defineProps<{ pipeName: string; }>(); import { createDataReceiver } from '../utils/dataFetcher'; const chartContainer = ref<HTMLElement | null>(null); let chart: echarts.ECharts | null = null; @@ -38,8 +34,8 @@ { name: 'Fx', type: 'gauge', min: -100, max: 100, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['16.7%', '30%'], @@ -61,8 +57,8 @@ { name: 'Fy', type: 'gauge', min: -100, max: 100, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['50%', '30%'], @@ -84,8 +80,8 @@ { name: 'Fz', type: 'gauge', min: -100, max: 100, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['83.3%', '30%'], @@ -107,8 +103,8 @@ { name: 'Mx', type: 'gauge', min: -10, max: 10, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['16.7%', '75%'], @@ -130,8 +126,8 @@ { name: 'My', type: 'gauge', min: -10, max: 10, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['50%', '75%'], @@ -153,8 +149,8 @@ { name: 'Mz', type: 'gauge', min: -10, max: 10, min: 1000, max: 4000, splitNumber: 10, radius: '30%', center: ['83.3%', '75%'], @@ -209,46 +205,17 @@ }); } // ç¶æåéï¼ç¨äºæ§å¶é误æ¾ç¤ºé¢ç const errorCount = ref(0); const maxConsecutiveErrors = 5; const showingError = ref(false); // æ¥æ¶ç®¡éæ°æ®ç彿° async function receiveData() { try { const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); // éç½®éè¯¯è®¡æ° errorCount.value = 0; showingError.value = false; // è§£ææ°æ® try { const forceData = JSON.parse(response); if (Array.isArray(forceData) && forceData.length === 6) { updateChart(forceData); } } catch (parseErr) { console.error('æ°æ®è§£æé误:', parseErr); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount.value++; // åªå¨è¿ç»é误达å°éå¼ä¸å°æªæ¾ç¤ºéè¯¯æ¶æ¾ç¤º if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { console.error('管éé信失败:', err); showingError.value = true; } } } let dataInterval: number | null = null; onMounted(() => { initChart(); const receiveData = createDataReceiver((forceData) => { updateChart(forceData); }); // æ¯200msè·å䏿¬¡æ°æ® dataInterval = window.setInterval(receiveData, 200); src/components/TableDisplay.vue
@@ -1,10 +1,6 @@ <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import { sendToPipe } from '../pipe_client'; const props = defineProps<{ pipeName: string; }>(); import { createDataReceiver } from '../utils/dataFetcher'; interface ForceData { timestamp: string; @@ -42,44 +38,14 @@ } } // ç¶æåéï¼ç¨äºæ§å¶é误æ¾ç¤ºé¢ç const errorCount = ref(0); const maxConsecutiveErrors = 5; const showingError = ref(false); // æ¥æ¶ç®¡éæ°æ®ç彿° async function receiveData() { try { const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); // éç½®éè¯¯è®¡æ° errorCount.value = 0; showingError.value = false; // è§£ææ°æ® try { const forceData = JSON.parse(response); if (Array.isArray(forceData) && forceData.length === 6) { updateTable(forceData); } } catch (parseErr) { console.error('æ°æ®è§£æé误:', parseErr); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount.value++; // åªå¨è¿ç»é误达å°éå¼ä¸å°æªæ¾ç¤ºéè¯¯æ¶æ¾ç¤º if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { console.error('管éé信失败:', err); showingError.value = true; } } } let dataInterval: number | null = null; onMounted(() => { const receiveData = createDataReceiver( (forceData) => { updateTable(forceData); }); // æ¯500msè·å䏿¬¡æ°æ® dataInterval = window.setInterval(receiveData, 500); }); src/components/ThreeDDisplay copy.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,525 @@ <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 { sendToPipe } from '../pipe_client'; const props = defineProps<{ pipeName: string; }>(); 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); } // ç¶æåéï¼ç¨äºæ§å¶é误æ¾ç¤ºé¢ç const errorCount = ref(0); const maxConsecutiveErrors = 5; const showingError = ref(false); // æ¥æ¶ç®¡éæ°æ®ç彿° async function receiveForceData() { try { // ä»ç®¡éæ¥æ¶æ°æ® const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); // æåæ¥æ¶æ°æ®ï¼éç½®éè¯¯è®¡æ° errorCount.value = 0; if (showingError.value) { showingError.value = false; console.log('管ééä¿¡å·²æ¢å¤'); } // è§£ææ¥æ¶å°çæ°æ® try { const forceData = JSON.parse(response); if (Array.isArray(forceData) && forceData.length === 6) { updateVisualization(forceData); } } catch (e) { console.warn('è§£ææ°æ®å¤±è´¥:', e); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount.value++; // åªå¨è¿ç»é误达å°é弿¶æ¾ç¤ºé误信æ¯ï¼é¿å æ¥å¿å·å± if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { showingError.value = true; console.error('管ééä¿¡æç»å¤±è´¥ï¼è¯·æ£æ¥æå¡ç«¯ç¶æ:', err); } } } // 宿¶è·åæ°æ® let dataTimer: number | null = null; onMounted(() => { initScene(); // æ¯ç§è·å䏿¬¡æ°æ® dataTimer = window.setInterval(receiveForceData, 3000); }); onUnmounted(() => { // æ¸ ç宿¶å¨åäºä»¶çå¬ if (dataTimer !== null) { clearInterval(dataTimer); } 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> src/components/ThreeDDisplay.vue
@@ -2,11 +2,8 @@ import { ref, onMounted, onUnmounted } from 'vue'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { sendToPipe } from '../pipe_client'; import { createDataReceiver } from '../utils/dataFetcher'; const props = defineProps<{ pipeName: string; }>(); const containerRef = ref<HTMLElement | null>(null); let scene: THREE.Scene | null = null; @@ -358,53 +355,25 @@ renderer.setSize(width, height); } // ç¶æåéï¼ç¨äºæ§å¶é误æ¾ç¤ºé¢ç const errorCount = ref(0); const maxConsecutiveErrors = 5; const showingError = ref(false); // æ¥æ¶ç®¡éæ°æ®ç彿° async function receiveForceData() { try { // ä»ç®¡éæ¥æ¶æ°æ® const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); // æåæ¥æ¶æ°æ®ï¼éç½®éè¯¯è®¡æ° errorCount.value = 0; if (showingError.value) { showingError.value = false; console.log('管ééä¿¡å·²æ¢å¤'); } // è§£ææ¥æ¶å°çæ°æ® try { const forceData = JSON.parse(response); if (Array.isArray(forceData) && forceData.length === 6) { updateVisualization(forceData); } } catch (e) { console.warn('è§£ææ°æ®å¤±è´¥:', e); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount.value++; // åªå¨è¿ç»é误达å°é弿¶æ¾ç¤ºé误信æ¯ï¼é¿å æ¥å¿å·å± if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { showingError.value = true; console.error('管ééä¿¡æç»å¤±è´¥ï¼è¯·æ£æ¥æå¡ç«¯ç¶æ:', err); } } } // 宿¶è·åæ°æ® let dataTimer: number | null = null; onMounted(() => { initScene(); animate(); // æ¯ç§è·å䏿¬¡æ°æ® dataTimer = window.setInterval(receiveForceData, 3000); const receiveData = createDataReceiver( (forceData) => { updateVisualization(forceData); }); // æ¯100msè·å䏿¬¡æ°æ® dataTimer = window.setInterval(receiveData, 100); // çå¬çªå£å¤§å°åå window.addEventListener('resize', handleResize); }); onUnmounted(() => { src/pipe_client.ts
@@ -32,4 +32,15 @@ // ææéè¯é½å¤±è´¥ï¼æåºæåä¸ä¸ªé误 throw lastError; } export async function getLatestWsData(): Promise<string | null> { try { const result = await invoke<string | null>('get_latest_ws_data'); console.log("getlatestwsData", result); return result; } catch (err) { console.error('è·å WebSocket æ°æ®å¤±è´¥:', err); return null; } } src/utils/dataFetcher.ts
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,68 @@ import { getLatestWsData } from '../pipe_client'; // const pipeName = 'tauri-pipe-server'; // const dataSource = 'ws'; // pipe|ws export async function fetchForceData() { try { let response: string | null = null; // if (dataSource === 'pipe') { // if (!pipeName) throw new Error('Pipe name is required for pipe data source'); // response = await sendToPipe(pipeName, 'GET_FORCE_DATA'); // } else if (dataSource === 'ws') { response = await getLatestWsData(); if (!response) return null; // } if (!response) return null; const jsonData = JSON.parse(response); const forceData = jsonData[0]; if (Array.isArray(forceData) && forceData.length === 6) { // å°æ°ç»ä¸çæ¯ä¸ªæ°åå«*4000/4194303 for (let i = 0; i < forceData.length; i++) { forceData[i] = forceData[i] * 4000 / 4194303; } return forceData; } return null; } catch (error) { console.error('Error fetching force data:', error); return null; } } export function createDataReceiver( callback: (data: number[]) => void) { let errorCount = 0; const maxConsecutiveErrors = 5; let showingError = false; return async () => { try { const forceData = await fetchForceData(); console.log('forceData111', forceData); if (forceData) { // éç½®éè¯¯è®¡æ° errorCount = 0; if (showingError) { showingError = false; console.log('æ°æ®éä¿¡å·²æ¢å¤'); } console.log('forceData222', forceData); callback(forceData); } } catch (err) { // å¢å éè¯¯è®¡æ° errorCount++; // åªå¨è¿ç»é误达å°é弿¶æ¾ç¤ºé误信æ¯ï¼é¿å æ¥å¿å·å± if (errorCount >= maxConsecutiveErrors && !showingError) { showingError = true; console.error('æ°æ®éä¿¡æç»å¤±è´¥ï¼è¯·æ£æ¥æå¡ç«¯ç¶æ:', err); } } }; }