From 703b4ff86546e02f7bbaec6f211f3b65187f610f Mon Sep 17 00:00:00 2001 From: xin Date: Thu, 18 Jun 2026 15:16:11 +0800 Subject: [PATCH] feat: add circle drawing support in path planner --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/path_commands.rs | 7 ++- src-tauri/src/services/path_generator.rs | 79 ++++++++++++++++-------- src-tauri/tauri.conf.json | 2 +- src/components/planner/ScanCanvas.vue | 38 +++++++++--- src/types/path-plan.ts | 3 + src/views/HomeView.vue | 2 +- src/views/PathPlannerView.vue | 26 ++++++-- update.md | 4 ++ 说明书.md | 6 -- 11 files changed, 119 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 9d26426..140d3df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "happa-mission-plan", "private": true, - "version": "0.1.0", + "version": "0.0.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 158821e..e036e21 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spectral-insight-mission-plan" -version = "0.0.2" +version = "0.0.3" description = "Spectral Insight Mission Plan" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/path_commands.rs b/src-tauri/src/commands/path_commands.rs index e1e1385..28e6322 100644 --- a/src-tauri/src/commands/path_commands.rs +++ b/src-tauri/src/commands/path_commands.rs @@ -27,9 +27,11 @@ pub fn estimate_scan_time_minutes(params: ScanParams) -> f64 { services::path_generator::estimate_scan_time(¶ms) } + #[command] pub fn generate_scan_paths_multi( regions: Vec, + shapes: Vec, camera: CameraParams, coverage_rate: f64, speed_y_cm_s: f64, @@ -38,7 +40,7 @@ pub fn generate_scan_paths_multi( mode: ScanMode, ) -> Result { let records = services::path_generator::generate_paths_multi( - ®ions, &camera, coverage_rate, + ®ions, &shapes, &camera, coverage_rate, speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, &mode, ); let count = records.len() as u64; @@ -48,6 +50,7 @@ pub fn generate_scan_paths_multi( #[command] pub fn estimate_scan_time_multi_minutes( regions: Vec, + shapes: Vec, camera: CameraParams, coverage_rate: f64, speed_y_cm_s: f64, @@ -56,7 +59,7 @@ pub fn estimate_scan_time_multi_minutes( mode: ScanMode, ) -> f64 { services::path_generator::estimate_scan_time_multi( - ®ions, &camera, coverage_rate, + ®ions, &shapes, &camera, coverage_rate, speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, &mode, ) } diff --git a/src-tauri/src/services/path_generator.rs b/src-tauri/src/services/path_generator.rs index 4a3d691..9b66831 100644 --- a/src-tauri/src/services/path_generator.rs +++ b/src-tauri/src/services/path_generator.rs @@ -103,8 +103,10 @@ pub fn estimate_scan_time(params: &ScanParams) -> f64 { } /// Generate paths for multiple regions, concatenated into one record list. + pub fn generate_paths_multi( regions: &[ScanRegion], + shapes: &[String], camera: &crate::models::CameraParams, coverage_rate: f64, speed_y_cm_s: f64, @@ -113,24 +115,54 @@ pub fn generate_paths_multi( mode: &ScanMode, ) -> Vec { let mut all_records = Vec::new(); - for region in regions { - let params = ScanParams { - region: region.clone(), - camera: camera.clone(), - coverage_rate, - speed_y_cm_s, - speed_x_start_cm_s, - speed_x_scan_cm_s, - mode: mode.clone(), - }; - all_records.extend(generate_path(¶ms)); + for (i, region) in regions.iter().enumerate() { + let is_circle = shapes.get(i).map(|s| s == "Circle").unwrap_or(false); + let step = calc_footprint(camera.height_cm, camera.fov_degrees) + * (1.0 - coverage_rate / 100.0); + let line_count = if step <= 0.0 { 1 } else { ((region.y_max - region.y_min) / step).ceil() as u64 + 1 }; + + for li in 0..line_count { + let y = region.y_min + (li as f64) * step; + if y > region.y_max { break; } + + let (x_start, x_end) = if is_circle { + let cx = (region.x_min + region.x_max) / 2.0; + let cy = (region.y_min + region.y_max) / 2.0; + let rx = (region.x_max - region.x_min) / 2.0; + let ry = (region.y_max - region.y_min) / 2.0; + // Ellipse: ((x-cx)/rx)^2 + ((y-cy)/ry)^2 = 1 + let dy = (y - cy) / ry; + if dy.abs() > 1.0 { continue; } // outside circle + let half_w = rx * (1.0 - dy * dy).sqrt(); + (cx - half_w, cx + half_w) + } else { + (region.x_min, region.x_max) + }; + + let is_forward = li % 2 == 0; + let (scan_start, scan_end) = match mode { + ScanMode::Zigzag => { + if is_forward { (x_start, x_end) } else { (x_end, x_start) } + } + ScanMode::OneWay => (x_start, x_end), + }; + + all_records.push(PathLineRecord { + target_y_position: y, + speed_target_y_position: speed_y_cm_s, + target_x_min_position: scan_start, + speed_target_x_min_position: speed_x_start_cm_s, + target_x_max_position: scan_end, + speed_target_x_max_position: speed_x_scan_cm_s, + }); + } } all_records } -/// Estimate total time for multiple regions. pub fn estimate_scan_time_multi( regions: &[ScanRegion], + shapes: &[String], camera: &crate::models::CameraParams, coverage_rate: f64, speed_y_cm_s: f64, @@ -138,18 +170,13 @@ pub fn estimate_scan_time_multi( speed_x_scan_cm_s: f64, mode: &ScanMode, ) -> f64 { - let mut total = 0.0; - for region in regions { - let params = ScanParams { - region: region.clone(), - camera: camera.clone(), - coverage_rate, - speed_y_cm_s, - speed_x_start_cm_s, - speed_x_scan_cm_s, - mode: mode.clone(), - }; - total += estimate_scan_time(¶ms); - } - total + let records = generate_paths_multi(regions, shapes, camera, coverage_rate, + speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, mode); + if records.is_empty() { return 0.0; } + + let total_x = records.len() as f64 * (regions.iter().map(|r| (r.x_max - r.x_min).abs()).sum::() / regions.len() as f64) / speed_x_scan_cm_s; + let return_time = if matches!(mode, ScanMode::OneWay) { + (records.len() as f64 - 1.0) * speed_x_scan_cm_s / speed_x_start_cm_s.max(0.01) + } else { 0.0 }; + (total_x + return_time) / 60.0 } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5265a66..e4a6844 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Spectral Insight Mission Plan", - "version": "0.0.2", + "version": "0.0.3", "identifier": "com.spectral-insight.mission-plan", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/planner/ScanCanvas.vue b/src/components/planner/ScanCanvas.vue index fcd6af7..c77b80c 100644 --- a/src/components/planner/ScanCanvas.vue +++ b/src/components/planner/ScanCanvas.vue @@ -18,7 +18,7 @@ diff --git a/src/types/path-plan.ts b/src/types/path-plan.ts index 1362014..bf4d13e 100644 --- a/src/types/path-plan.ts +++ b/src/types/path-plan.ts @@ -1,8 +1,11 @@ +export type ScanShape = 'Rect' | 'Circle'; + export interface ScanRegion { xMin: number; xMax: number; yMin: number; yMax: number; + shape: ScanShape; } export interface CameraParams { diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index c1d4d77..2404278 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,7 +1,7 @@ @@ -55,13 +60,13 @@ import { ref, computed } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { useMessage } from 'naive-ui'; -import { ArrowBackOutline, SquareOutline, TrashOutline, MapOutline, SaveOutline } from '@vicons/ionicons5'; +import { ArrowBackOutline, SquareOutline, EllipseOutline, TrashOutline, MapOutline, SaveOutline } from '@vicons/ionicons5'; import { invoke } from '@tauri-apps/api/core'; import ScanCanvas from '../components/planner/ScanCanvas.vue'; import ScanParamPanel from '../components/planner/ScanParamPanel.vue'; import { useMissionStore } from '../stores/mission'; import type { PathLineRecord, PathLineFile } from '../types/path-line'; -import type { ScanRegion, CameraParams, ScanMode } from '../types/path-plan'; +import type { ScanRegion, CameraParams, ScanMode, ScanShape } from '../types/path-plan'; import type { PlannerDefaults } from '../types/mission'; import { DEVICE_FOV } from '../utils/constants'; @@ -74,6 +79,7 @@ const taskId = route.query.taskId ? Number(route.query.taskId) : null; const subTaskId = route.query.subTaskId ? String(route.query.subTaskId) : null; const drawingMode = ref(false); +const currentShape = ref('Rect'); function getDeviceFov(): number { if (taskId === null || subTaskId === null) return 30; @@ -104,11 +110,17 @@ invoke('load_planner_defaults').then(p => { camera.value = { fovDegrees: camera.value.fovDegrees, heightCm: p.heightCm }; }).catch(() => {}); -function toggleDrawing() { - drawingMode.value = !drawingMode.value; +function startDraw(shape: ScanShape) { + if (drawingMode.value && currentShape.value === shape) { + drawingMode.value = false; + } else { + drawingMode.value = true; + currentShape.value = shape; + } } function onAddRegion(region: ScanRegion) { + region.shape = currentShape.value; regions.value.push(region); message.success(`已添加框 #${regions.value.length}`); } @@ -148,6 +160,7 @@ async function generatePath() { try { const file = await invoke('generate_scan_paths_multi', { regions: regions.value, + shapes: regions.value.map(r => r.shape), camera: camera.value, coverageRate: coverageRate.value, speedYCmS: speedY.value, @@ -158,6 +171,7 @@ async function generatePath() { generatedRecords.value = file.records; const estTime = await invoke('estimate_scan_time_multi_minutes', { regions: regions.value, + shapes: regions.value.map(r => r.shape), camera: camera.value, coverageRate: coverageRate.value, speedYCmS: speedY.value, diff --git a/update.md b/update.md index 3850cb3..5e7b9e4 100644 --- a/update.md +++ b/update.md @@ -1,5 +1,9 @@ # Spectral Insight Mission Plan - 更新日志 +## v0.0.3 (2026-06-18) + +- docs: add 说明书.md + ## v0.0.2 - 路径查看弹窗改为全屏双栏布局(左侧表格 + 右侧 Canvas 画布) diff --git a/说明书.md b/说明书.md index 266e6cb..44abc11 100644 --- a/说明书.md +++ b/说明书.md @@ -166,13 +166,7 @@ Spectral Insight Mission Plan 是一款设备自动采集任务规划软件, --- -## 快捷键 -| 快捷键 | 功能 | -|--------|------| -| F12 | 打开/关闭开发者工具(DevTools) | - ---- ## 版本信息