From 02d490e7144f872ebbc783692847385f1e9f4962 Mon Sep 17 00:00:00 2001 From: xin Date: Thu, 18 Jun 2026 16:35:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20canvas=20drag/resize/selection,=20modal?= =?UTF-8?q?=20planner,=20default=20OneWay=20(=E5=94=90=E8=B6=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Canvas: 坐标轴、顶点坐标、框拖拽、角点缩放、点击选中高亮 - 路径规划改为弹窗覆盖层,编辑状态不丢失 - 展开任务状态持久化到 Pinia store - 扫描模式默认 OneWay - 保存时 startTime/endTime/durationMinutes 清空为 "" - exposureTime/frameRate/captureIntervalSeconds 默认 0 - 去除生成航线后的扫描线文字标注 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/io/json_writer.rs | 22 ++- src-tauri/src/models/path_plan.rs | 6 +- src-tauri/src/models/task.rs | 6 +- src-tauri/tauri.conf.json | 2 +- src/components/planner/ScanCanvas.vue | 270 ++++++++------------------ src/stores/mission.ts | 85 +++----- src/views/MissionEditorView.vue | 26 ++- src/views/PathPlannerView.vue | 136 +++++-------- update.md | 4 + 12 files changed, 221 insertions(+), 342 deletions(-) diff --git a/package.json b/package.json index 630ebab..cca2636 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "happa-mission-plan", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6433060..8ac4f8d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3180,7 +3180,7 @@ dependencies = [ [[package]] name = "spectral-insight-mission-plan" -version = "0.0.4" +version = "0.0.5" dependencies = [ "byteorder", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 134e0f1..7fd214e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spectral-insight-mission-plan" -version = "0.0.5" +version = "0.0.6" description = "Spectral Insight Mission Plan" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/io/json_writer.rs b/src-tauri/src/io/json_writer.rs index 82f2264..49946ea 100644 --- a/src-tauri/src/io/json_writer.rs +++ b/src-tauri/src/io/json_writer.rs @@ -3,7 +3,27 @@ use crate::error::AppError; use crate::models::MissionPlan; pub fn write_mission(path: &Path, mission: &MissionPlan) -> Result<(), AppError> { - let content = serde_json::to_string_pretty(mission)?; + // Clear computed timing fields before saving + let mut cleaned = mission.clone(); + for task in &mut cleaned.tasks { + task.duration_minutes = 0.0; + task.estimated_duration_minutes = 0.0; + task.start_time = Some("".to_string()); + task.end_time = Some("".to_string()); + for sub in &mut task.sub_tasks { + sub.duration_minutes = 0.0; + sub.estimated_duration_minutes = 0.0; + sub.start_time = Some("".to_string()); + sub.end_time = Some("".to_string()); + if sub.capture_interval_seconds.is_none() { sub.capture_interval_seconds = Some(0); } + if sub.exposure_time.is_none() { sub.exposure_time = Some(0); } + if sub.frame_rate.is_none() { sub.frame_rate = Some(0); } + } + } + let content = serde_json::to_string_pretty(&cleaned)?; + let content = content + .replace("\"durationMinutes\": 0.0", "\"durationMinutes\": \"\"") + .replace("\"estimatedDurationMinutes\": 0.0", "\"estimatedDurationMinutes\": \"\""); std::fs::write(path, content)?; Ok(()) } diff --git a/src-tauri/src/models/path_plan.rs b/src-tauri/src/models/path_plan.rs index 0afec1c..053ff55 100644 --- a/src-tauri/src/models/path_plan.rs +++ b/src-tauri/src/models/path_plan.rs @@ -10,8 +10,12 @@ pub struct ScanRegion { pub y_min: f64, #[serde(rename = "yMax")] pub y_max: f64, + #[serde(default = "default_shape")] + pub shape: String, } +fn default_shape() -> String { "Rect".to_string() } + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CameraParams { #[serde(rename = "fovDegrees")] @@ -67,7 +71,7 @@ impl Default for PlannerDefaults { speed_y_cm_s: 5.0, speed_x_start_cm_s: 20.0, speed_x_scan_cm_s: 10.0, - mode: "Zigzag".to_string(), + mode: "OneWay".to_string(), } } } diff --git a/src-tauri/src/models/task.rs b/src-tauri/src/models/task.rs index 4e7f292..eef47b7 100644 --- a/src-tauri/src/models/task.rs +++ b/src-tauri/src/models/task.rs @@ -68,10 +68,10 @@ impl SubTask { Self { id: default_subtask_id(), sub_task_type, - capture_interval_seconds: None, + capture_interval_seconds: Some(0), default_render_band: Some(550), - exposure_time: None, - frame_rate: None, + exposure_time: Some(0), + frame_rate: Some(0), path_line_file_path: String::new(), duration_minutes: 0.0, estimated_duration_minutes: 0.0, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7ae96a4..f3b1c8d 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.5", + "version": "0.0.6", "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 95252a4..cf1402f 100644 --- a/src/components/planner/ScanCanvas.vue +++ b/src/components/planner/ScanCanvas.vue @@ -1,18 +1,8 @@ @@ -22,210 +12,112 @@ import type { ScanRegion, ScanMode, ScanShape } from '../../types/path-plan'; import type { PathLineRecord } from '../../types/path-line'; const props = defineProps<{ - regions: ScanRegion[]; - records: PathLineRecord[]; - mode: ScanMode; - backgroundImage?: string; - drawingMode: boolean; - drawingShape?: ScanShape; + records: PathLineRecord[]; mode: ScanMode; backgroundImage?: string; + drawingMode: boolean; drawingShape?: ScanShape; selectedIndex?: number; }>(); +const regions = defineModel('regions', { required: true }); +const emit = defineEmits<{ 'add-region': [region: ScanRegion]; 'select-region': [index: number]; 'deselect': [] }>(); -const emit = defineEmits<{ - 'add-region': [region: ScanRegion]; -}>(); - -const canvasContainer = ref(); -const canvas = ref(); -const isDrawing = ref(false); -const dragStart = ref({ x: 0, y: 0 }); -const drawingRect = ref(null); -const bgImage = ref(null); +const canvasContainer = ref(); const canvas = ref(); +const isDrawing = ref(false); const isDragging = ref(false); const isResizing = ref(false); +const resizeCorner = ref(''); const dragTarget = ref(-1); +const dragStart = ref({ x: 0, y: 0 }); const regionStart = ref(null); +const drawingRect = ref(null); const bgImage = ref(null); const padding = 40; - -function getCanvasSize() { - const rect = canvasContainer.value?.getBoundingClientRect(); - return { w: rect?.width || 600, h: rect?.height || 400 }; -} - -function toCanvas(clientX: number, clientY: number) { - const rect = canvas.value!.getBoundingClientRect(); - return { x: clientX - rect.left, y: clientY - rect.top }; -} - -function toWorld(cx: number, cy: number) { - const { w, h } = getCanvasSize(); - const drawW = w - padding * 2; - const drawH = h - padding * 2; - const worldX = (cx - padding) / drawW * 100; - const worldY = (cy - padding) / drawH * 100; - return { x: Math.max(0, Math.min(100, worldX)), y: Math.max(0, Math.min(100, 100 - worldY)) }; -} - -async function loadBackgroundImage(path: string | undefined) { - if (!path) { bgImage.value = null; draw(); return; } - try { - const { convertFileSrc } = await import('@tauri-apps/api/core'); - const assetUrl = convertFileSrc(path); - const img = new Image(); - img.onload = () => { bgImage.value = img; draw(); }; - img.onerror = () => { bgImage.value = null; draw(); }; - img.src = assetUrl; - } catch { bgImage.value = null; draw(); } -} - -function worldToCanvas(worldX: number, worldY: number) { - const { w, h } = getCanvasSize(); - const drawW = w - padding * 2; - const drawH = h - padding * 2; - const x = padding + (worldX / 100) * drawW; - const y = padding + ((100 - worldY) / 100) * drawH; - return { x, y }; -} - const COLORS = ['#2080f0', '#f0a020', '#18a058', '#d03050', '#a060e0', '#e06080']; +function gs() { const r = canvasContainer.value?.getBoundingClientRect(); return { w: r?.width || 600, h: r?.height || 400 }; } +function tc(cx: number, cy: number) { const r = canvas.value!.getBoundingClientRect(); return { x: cx - r.left, y: cy - r.top }; } +function tw(cx: number, cy: number) { const { w, h } = gs(); const dw = w - padding * 2, dh = h - padding * 2; return { x: Math.max(0, Math.min(100, (cx - padding) / dw * 100)), y: Math.max(0, Math.min(100, 100 - (cy - padding) / dh * 100)) }; } +function w2c(wx: number, wy: number) { const { w, h } = gs(); const dw = w - padding * 2, dh = h - padding * 2; return { x: padding + (wx / 100) * dw, y: padding + ((100 - wy) / 100) * dh }; } +function findR(p: { x: number; y: number }): number { const r = regions.value; for (let i = r.length - 1; i >= 0; i--) { if (p.x >= r[i].xMin && p.x <= r[i].xMax && p.y >= r[i].yMin && p.y <= r[i].yMax) return i; } return -1; } +function findC(p: { x: number; y: number }): { i: number; c: string } | null { const th = 3; for (let i = 0; i < regions.value.length; i++) { const r = regions.value[i]; for (const [c, cx, cy] of [['tl', r.xMin, r.yMax] as const, ['tr', r.xMax, r.yMax] as const, ['bl', r.xMin, r.yMin] as const, ['br', r.xMax, r.yMin] as const]) { if (Math.abs(p.x - cx) < th && Math.abs(p.y - cy) < th) return { i, c }; } } return null; } + +async function loadBG(p: string | undefined) { + if (!p) { bgImage.value = null; draw(); return; } + try { const { convertFileSrc } = await import('@tauri-apps/api/core'); const img = new Image(); img.onload = () => { bgImage.value = img; draw(); }; img.onerror = () => { bgImage.value = null; draw(); }; img.src = convertFileSrc(p); } catch { bgImage.value = null; draw(); } +} + function draw() { - const ctx = canvas.value?.getContext('2d'); - if (!ctx) return; + const ctx = canvas.value?.getContext('2d'); if (!ctx) return; + const { w, h } = gs(); const dpr = window.devicePixelRatio || 1; + canvas.value!.width = w * dpr; canvas.value!.height = h * dpr; + canvas.value!.style.width = w + 'px'; canvas.value!.style.height = h + 'px'; + ctx.scale(dpr, dpr); ctx.fillStyle = '#f8f8f8'; ctx.fillRect(0, 0, w, h); + if (bgImage.value) { ctx.globalAlpha = 0.5; ctx.drawImage(bgImage.value, 0, 0, w, h); ctx.globalAlpha = 1.0; } + const dw = w - padding * 2, dh = h - padding * 2, ax = 16; - const { w, h } = getCanvasSize(); - const dpr = window.devicePixelRatio || 1; - canvas.value!.width = w * dpr; - canvas.value!.height = h * dpr; - canvas.value!.style.width = w + 'px'; - canvas.value!.style.height = h + 'px'; - ctx.scale(dpr, dpr); + ctx.strokeStyle = '#e8e8e8'; ctx.lineWidth = 1; + for (let i = 0; i <= 10; i++) { const x = padding + (i / 10) * dw; ctx.beginPath(); ctx.moveTo(x, padding); ctx.lineTo(x, padding + dh); ctx.stroke(); } + for (let i = 0; i <= 10; i++) { const y = padding + (i / 10) * dh; ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(padding + dw, y); ctx.stroke(); } + ctx.strokeStyle = '#666'; ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.moveTo(padding - ax, padding + dh); ctx.lineTo(padding + dw + ax, padding + dh); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(padding, padding + dh + ax); ctx.lineTo(padding, padding - ax); ctx.stroke(); + ctx.fillStyle = '#888'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; + for (let i = 0; i <= 10; i++) { const x = padding + (i / 10) * dw; ctx.fillText(String(i * 10), x, padding + dh + ax + 12); ctx.beginPath(); ctx.moveTo(x, padding + dh + ax - 3); ctx.lineTo(x, padding + dh + ax + 3); ctx.stroke(); } + ctx.textAlign = 'right'; + for (let i = 0; i <= 10; i++) { const y = padding + (i / 10) * dh; ctx.fillText(String((10 - i) * 10), padding - ax - 4, y + 4); ctx.beginPath(); ctx.moveTo(padding - ax - 3, y); ctx.lineTo(padding - ax + 3, y); ctx.stroke(); } + ctx.textAlign = 'start'; ctx.font = '11px sans-serif'; + ctx.fillText('X (cm)', padding + dw + ax - 10, padding + dh + ax + 24); + ctx.save(); ctx.translate(padding - ax - 24, 10); ctx.rotate(-Math.PI / 2); ctx.fillText('Y (cm)', 0, 0); ctx.restore(); + ctx.strokeStyle = '#f0a020'; ctx.lineWidth = 1; ctx.setLineDash([4, 4]); const z1 = w2c(0, 0), z2 = w2c(100, 100); ctx.strokeRect(z1.x, z1.y, z2.x - z1.x, z2.y - z1.y); ctx.setLineDash([]); - ctx.fillStyle = '#f8f8f8'; - ctx.fillRect(0, 0, w, h); - - if (bgImage.value) { - ctx.globalAlpha = 0.5; - ctx.drawImage(bgImage.value, 0, 0, w, h); - ctx.globalAlpha = 1.0; + for (let i = 0; i < regions.value.length; i++) { + const r = regions.value[i]; const p1 = w2c(r.xMin, r.yMin), p2 = w2c(r.xMax, r.yMax); + const c = COLORS[i % COLORS.length]; const sel = i === props.selectedIndex; + ctx.strokeStyle = sel ? '#ff0000' : c; ctx.lineWidth = sel ? 3 : 2; + if (r.shape === 'Circle') { ctx.beginPath(); ctx.ellipse((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, Math.abs(p2.x - p1.x) / 2, Math.abs(p2.y - p1.y) / 2, 0, 0, Math.PI * 2); ctx.stroke(); } + else { ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); ctx.fillStyle = '#fff'; for (const [cx, cy] of [[p1.x, p1.y], [p2.x, p1.y], [p1.x, p2.y], [p2.x, p2.y]]) { ctx.fillRect(cx - 4, cy - 4, 8, 8); ctx.strokeRect(cx - 4, cy - 4, 8, 8); } } + ctx.fillStyle = c; ctx.font = 'bold 12px sans-serif'; ctx.fillText(`#${i + 1}`, p1.x + 4, p1.y - 4); + ctx.font = '10px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`(${r.xMin.toFixed(1)}, ${r.yMin.toFixed(1)})`, p1.x + 4, p2.y + 14); + ctx.textAlign = 'right'; ctx.fillText(`(${r.xMax.toFixed(1)}, ${r.yMax.toFixed(1)})`, p2.x - 4, p1.y - 8); ctx.textAlign = 'start'; } - - const drawW = w - padding * 2; - const drawH = h - padding * 2; - - // Grid - ctx.strokeStyle = '#e8e8e8'; - ctx.lineWidth = 1; - for (let i = 0; i <= 10; i++) { - const x = padding + (i / 10) * drawW; - ctx.beginPath(); ctx.moveTo(x, padding); ctx.lineTo(x, padding + drawH); ctx.stroke(); - } - - // Draw all regions (rect or circle) - for (let i = 0; i < props.regions.length; i++) { - const r = props.regions[i]; - const p1 = worldToCanvas(r.xMin, r.yMin); - const p2 = worldToCanvas(r.xMax, r.yMax); - ctx.strokeStyle = COLORS[i % COLORS.length]; - ctx.lineWidth = 2; - if (r.shape === 'Circle') { - const cx = (p1.x + p2.x) / 2; - const cy = (p1.y + p2.y) / 2; - const rx = Math.abs(p2.x - p1.x) / 2; - const ry = Math.abs(p2.y - p1.y) / 2; - ctx.beginPath(); - ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); - ctx.stroke(); - } else { - ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); - } - // Label - ctx.fillStyle = COLORS[i % COLORS.length]; - ctx.font = 'bold 12px sans-serif'; - ctx.fillText(`#${i + 1}`, p1.x + 4, p1.y - 4); - } - - // Draw current drawing shape if (drawingRect.value) { - const p1 = worldToCanvas(drawingRect.value.xMin, drawingRect.value.yMin); - const p2 = worldToCanvas(drawingRect.value.xMax, drawingRect.value.yMax); - ctx.strokeStyle = '#f00'; - ctx.lineWidth = 2; - ctx.setLineDash([5, 5]); - if (props.drawingShape === 'Circle') { - const cx = (p1.x + p2.x) / 2; - const cy = (p1.y + p2.y) / 2; - const rx = Math.abs(p2.x - p1.x) / 2; - const ry = Math.abs(p2.y - p1.y) / 2; - ctx.beginPath(); - ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); - ctx.stroke(); - } else { - ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); - } - ctx.setLineDash([]); + const p1 = w2c(drawingRect.value.xMin, drawingRect.value.yMin), p2 = w2c(drawingRect.value.xMax, drawingRect.value.yMax); + ctx.strokeStyle = '#f00'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); + if (props.drawingShape === 'Circle') { ctx.beginPath(); ctx.ellipse((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, Math.abs(p2.x - p1.x) / 2, Math.abs(p2.y - p1.y) / 2, 0, 0, Math.PI * 2); ctx.stroke(); } + else { ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); } ctx.setLineDash([]); } - - // Draw scan paths if (props.records.length > 0) { - for (const record of props.records) { - const start = worldToCanvas(record.targetXMinPosition, record.targetYPosition); - const end = worldToCanvas(record.targetXMaxPosition, record.targetYPosition); - ctx.beginPath(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); - ctx.strokeStyle = '#18a058'; ctx.lineWidth = 1.5; ctx.stroke(); - const angle = Math.atan2(end.y - start.y, end.x - start.x); - ctx.beginPath(); - ctx.moveTo(end.x, end.y); - ctx.lineTo(end.x - 8 * Math.cos(angle - 0.4), end.y - 8 * Math.sin(angle - 0.4)); - ctx.lineTo(end.x - 8 * Math.cos(angle + 0.4), end.y - 8 * Math.sin(angle + 0.4)); - ctx.closePath(); ctx.fillStyle = '#18a058'; ctx.fill(); - } + for (const r of props.records) { const s = w2c(r.targetXMinPosition, r.targetYPosition), e = w2c(r.targetXMaxPosition, r.targetYPosition); ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(e.x, e.y); ctx.strokeStyle = '#18a058'; ctx.lineWidth = 1.5; ctx.stroke(); const a = Math.atan2(e.y - s.y, e.x - s.x); ctx.beginPath(); ctx.moveTo(e.x, e.y); ctx.lineTo(e.x - 6 * Math.cos(a - 0.4), e.y - 6 * Math.sin(a - 0.4)); ctx.lineTo(e.x - 6 * Math.cos(a + 0.4), e.y - 6 * Math.sin(a + 0.4)); ctx.closePath(); ctx.fillStyle = '#18a058'; ctx.fill(); } } } function onMouseDown(e: MouseEvent) { - if (!props.drawingMode) return; - isDrawing.value = true; - dragStart.value = toCanvas(e.clientX, e.clientY); + const cp = tc(e.clientX, e.clientY); + if (props.drawingMode) { isDrawing.value = true; dragStart.value = cp; return; } + const wp = tw(cp.x, cp.y); + const corner = findC(wp); + if (corner && regions.value[corner.i].shape !== 'Circle') { + isResizing.value = true; resizeCorner.value = corner.c; dragTarget.value = corner.i; + regionStart.value = { ...regions.value[corner.i] }; dragStart.value = cp; return; + } + const hit = findR(wp); + if (hit >= 0) { isDragging.value = true; dragTarget.value = hit; regionStart.value = { ...regions.value[hit] }; dragStart.value = cp; emit('select-region', hit); return; } + // Clicked empty space + if (!props.drawingMode) { emit('deselect'); } } function onMouseMove(e: MouseEvent) { - if (!isDrawing.value || !props.drawingMode) return; - const p = toCanvas(e.clientX, e.clientY); - const p1 = toWorld(dragStart.value.x, dragStart.value.y); - const p2 = toWorld(p.x, p.y); - drawingRect.value = { - shape: props.drawingShape ? props.drawingShape : "Rect", - xMin: Math.min(p1.x, p2.x), xMax: Math.max(p1.x, p2.x), - yMin: Math.min(p1.y, p2.y), yMax: Math.max(p1.y, p2.y), - }; - draw(); + const cp = tc(e.clientX, e.clientY); + if (isDrawing.value && props.drawingMode) { const p1 = tw(dragStart.value.x, dragStart.value.y), p2 = tw(cp.x, cp.y); drawingRect.value = { shape: props.drawingShape || 'Rect', xMin: Math.min(p1.x, p2.x), xMax: Math.max(p1.x, p2.x), yMin: Math.min(p1.y, p2.y), yMax: Math.max(p1.y, p2.y) }; draw(); return; } + if (isDragging.value && dragTarget.value >= 0 && regionStart.value) { const dx = (cp.x - dragStart.value.x) / (gs().w - padding * 2) * 100, dy = -(cp.y - dragStart.value.y) / (gs().h - padding * 2) * 100; const rs = regionStart.value, rw = rs.xMax - rs.xMin, rh = rs.yMax - rs.yMin; const arr = [...regions.value]; arr[dragTarget.value] = { ...arr[dragTarget.value], xMin: Math.max(0, Math.min(100 - rw, rs.xMin + dx)), xMax: Math.max(rs.xMin + dx + 1, Math.min(100, rs.xMax + dx)), yMin: Math.max(0, Math.min(100 - rh, rs.yMin + dy)), yMax: Math.max(rs.yMin + dy + 1, Math.min(100, rs.yMax + dy)) }; regions.value = arr; draw(); return; } + if (isResizing.value && dragTarget.value >= 0 && regionStart.value) { const p1 = tw(dragStart.value.x, dragStart.value.y), p2 = tw(cp.x, cp.y); const dx = p2.x - p1.x, dy = p2.y - p1.y, rs = regionStart.value; let xMin = rs.xMin, xMax = rs.xMax, yMin = rs.yMin, yMax = rs.yMax; const c = resizeCorner.value; if (c.includes('l')) xMin = Math.min(rs.xMin + dx, rs.xMax - 1); if (c.includes('r')) xMax = Math.max(rs.xMax + dx, rs.xMin + 1); if (c.includes('b')) yMin = Math.min(rs.yMin + dy, rs.yMax - 1); if (c.includes('t')) yMax = Math.max(rs.yMax + dy, rs.yMin + 1); xMin = Math.max(0, xMin); xMax = Math.min(100, xMax); yMin = Math.max(0, yMin); yMax = Math.min(100, yMax); if (xMax - xMin > 1 && yMax - yMin > 1) { const arr = [...regions.value]; arr[dragTarget.value] = { ...arr[dragTarget.value], xMin, xMax, yMin, yMax }; regions.value = arr; draw(); } } } function onMouseUp() { - if (!isDrawing.value) return; - isDrawing.value = false; - if (drawingRect.value) { - const r = drawingRect.value; - if (r.xMax - r.xMin > 1 && r.yMax - r.yMin > 1) { - emit('add-region', { ...r, shape: props.drawingShape ? props.drawingShape : 'Rect' }); - } - drawingRect.value = null; - draw(); - } + if (isDrawing.value) { isDrawing.value = false; if (drawingRect.value) { const r = drawingRect.value; if (r.xMax - r.xMin > 1 && r.yMax - r.yMin > 1) emit('add-region', { ...r, shape: props.drawingShape || 'Rect' }); drawingRect.value = null; draw(); } return; } + if (isDragging.value || isResizing.value) { isDragging.value = false; isResizing.value = false; dragTarget.value = -1; regionStart.value = null; } } -onMounted(() => { - loadBackgroundImage(props.backgroundImage); - window.addEventListener('resize', draw); -}); - -watch(() => [props.regions, props.records, props.mode, props.drawingMode, props.drawingShape], () => { draw(); }); -watch(() => props.backgroundImage, (v) => { loadBackgroundImage(v); }); +onMounted(() => { loadBG(props.backgroundImage); window.addEventListener('resize', draw); }); +watch(() => [regions.value, props.records, props.mode, props.drawingMode, props.drawingShape, props.selectedIndex], () => { draw(); }); +watch(() => props.backgroundImage, (v) => { loadBG(v); }); diff --git a/src/stores/mission.ts b/src/stores/mission.ts index f5e3da4..b701cfe 100644 --- a/src/stores/mission.ts +++ b/src/stores/mission.ts @@ -10,38 +10,20 @@ export const useMissionStore = defineStore('mission', () => { const mission = ref(createEmptyMission()); const currentFilePath = ref(null); const isDirty = ref(false); + const expandedTaskId = ref(null); const taskCount = computed(() => mission.value.tasks.length); - // Auto-persist scan config and background when they change - let saveTimer: ReturnType | null = null; - watch( - () => ({ sc: mission.value.scanConfig, bg: mission.value.backgroundImage }), - () => { - if (saveTimer) clearTimeout(saveTimer); - saveTimer = setTimeout(() => { - invoke('save_default_scan_config', { config: mission.value.scanConfig }).catch(() => {}); - invoke('save_default_background', { path: mission.value.backgroundImage || '' }).catch(() => {}); - }, 500); - }, - { deep: true } - ); - async function createNew() { mission.value = createEmptyMission(); - // Load persisted defaults try { const defaults = await invoke('load_default_scan_config'); mission.value.scanConfig = defaults; - } catch { - // use defaults - } + } catch { /* ignore */ } try { const bg = await invoke('load_default_background'); mission.value.backgroundImage = bg; - } catch { - // ignore - } + } catch { /* ignore */ } currentFilePath.value = null; isDirty.value = false; } @@ -80,40 +62,25 @@ export const useMissionStore = defineStore('mission', () => { } async function addSubTask(taskId: number, subTaskType: SubTaskType) { - const result = await invoke('add_sub_task', { - mission: mission.value, - taskId, - subTaskType, - }); + const result = await invoke('add_sub_task', { mission: mission.value, taskId, subTaskType }); mission.value = result; isDirty.value = true; } async function removeSubTask(taskId: number, subTaskId: string) { - const result = await invoke('remove_sub_task', { - mission: mission.value, - taskId, - subTaskId, - }); + const result = await invoke('remove_sub_task', { mission: mission.value, taskId, subTaskId }); mission.value = result; isDirty.value = true; } async function updateSubTask(taskId: number, subTask: SubTask) { - const result = await invoke('update_sub_task', { - mission: mission.value, - taskId, - subTask, - }); + const result = await invoke('update_sub_task', { mission: mission.value, taskId, subTask }); mission.value = result; isDirty.value = true; } async function updateTask(task: Task) { - const result = await invoke('update_task', { - mission: mission.value, - task, - }); + const result = await invoke('update_task', { mission: mission.value, task }); mission.value = result; isDirty.value = true; } @@ -138,24 +105,26 @@ export const useMissionStore = defineStore('mission', () => { isDirty.value = true; } + // Auto-persist scan config and background when they change + let saveTimer: ReturnType | null = null; + watch( + () => ({ sc: mission.value.scanConfig, bg: mission.value.backgroundImage }), + () => { + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(() => { + invoke('save_default_scan_config', { config: mission.value.scanConfig }).catch(() => {}); + invoke('save_default_background', { path: mission.value.backgroundImage || '' }).catch(() => {}); + }, 500); + }, + { deep: true } + ); + return { - mission, - currentFilePath, - isDirty, - taskCount, - createNew, - loadMission, - saveMission, - addTask, - copyTask, - removeTask, - addSubTask, - removeSubTask, - updateSubTask, - updateTask, - calculateSchedule, - runValidation, - updateScanConfig, - setBackgroundImage, + mission, currentFilePath, isDirty, expandedTaskId, taskCount, + createNew, loadMission, saveMission, + addTask, copyTask, removeTask, + addSubTask, removeSubTask, updateSubTask, updateTask, + calculateSchedule, runValidation, + updateScanConfig, setBackgroundImage, }; }); diff --git a/src/views/MissionEditorView.vue b/src/views/MissionEditorView.vue index d91bdf6..d8356e8 100644 --- a/src/views/MissionEditorView.vue +++ b/src/views/MissionEditorView.vue @@ -59,7 +59,7 @@ - + 取消 + +
+ +
@@ -217,6 +225,7 @@ import type { SubTask } from '../types/task'; import type { PathLineRecord } from '../types/path-line'; import type { DataTableColumn } from 'naive-ui'; import HelpIcon from '../components/common/HelpIcon.vue'; +import PathPlannerView from '../views/PathPlannerView.vue'; const router = useRouter(); const message = useMessage(); @@ -224,6 +233,16 @@ const dialog = useDialog(); const missionStore = useMissionStore(); const validationStore = useValidationStore(); +const expandedTask = computed({ + get: () => missionStore.expandedTaskId, + set: (v) => (missionStore.expandedTaskId = v), +}); + +const showPlanner = ref(false); +const plannerTaskId = ref(null); +const plannerSubTaskId = ref(null); + +// function onExpandedChange(val: string | null) { const showTypeModal = ref(false); const typeModalTaskId = ref(null); @@ -364,9 +383,10 @@ async function browsePathLine(sub: SubTask) { } function goToPlanner(taskId: number, sub: SubTask) { - router.push({ path: '/planner', query: { taskId: String(taskId), subTaskId: sub.id } }); + plannerTaskId.value = taskId; + plannerSubTaskId.value = sub.id; + showPlanner.value = true; } - async function openPathFile(path: string) { try { const file = await invoke('load_path_line', { path }); diff --git a/src/views/PathPlannerView.vue b/src/views/PathPlannerView.vue index 9d20b3c..1703fed 100644 --- a/src/views/PathPlannerView.vue +++ b/src/views/PathPlannerView.vue @@ -1,13 +1,13 @@