Initial commit: Happa Mission Plan v0.1.0
Task planning and path generation software for automated data collection equipment (hyperspectral cameras, DSLR, depth cameras). - Generate mission plans (JSON) with subtasks per device - Generate scan path binary files (.RecordLine3) - Multi-rectangle area planning with reference map support - Device-specific FOV defaults (Pika L 17.6°, Pika NIR 21.7°, etc.) - Timeline scheduling with constraint validation - Tauri 2.x + Vue 3 + Naive UI + Pinia
This commit is contained in:
76
src-tauri/src/commands/defaults_commands.rs
Normal file
76
src-tauri/src/commands/defaults_commands.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use tauri::command;
|
||||
use crate::error::AppError;
|
||||
use crate::models::{MissionScanConfig, PlannerDefaults};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn defaults_dir() -> PathBuf {
|
||||
// Save next to the executable
|
||||
std::env::current_exe()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new("."))
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_default_scan_config(config: MissionScanConfig) -> Result<(), AppError> {
|
||||
let path = defaults_dir().join("scan_defaults.json");
|
||||
let content = serde_json::to_string_pretty(&config)?;
|
||||
std::fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn load_default_scan_config() -> Result<MissionScanConfig, AppError> {
|
||||
let path = defaults_dir().join("scan_defaults.json");
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: MissionScanConfig = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
Ok(MissionScanConfig::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_default_background(path: String) -> Result<(), AppError> {
|
||||
let file_path = defaults_dir().join("background_default.txt");
|
||||
std::fs::write(file_path, &path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn load_default_background() -> Result<Option<String>, AppError> {
|
||||
let file_path = defaults_dir().join("background_default.txt");
|
||||
if file_path.exists() {
|
||||
let content = std::fs::read_to_string(file_path)?;
|
||||
let trimmed = content.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_planner_defaults(config: PlannerDefaults) -> Result<(), AppError> {
|
||||
let path = defaults_dir().join("planner_defaults.json");
|
||||
let content = serde_json::to_string_pretty(&config)?;
|
||||
std::fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn load_planner_defaults() -> Result<PlannerDefaults, AppError> {
|
||||
let path = defaults_dir().join("planner_defaults.json");
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: PlannerDefaults = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
Ok(PlannerDefaults::default())
|
||||
}
|
||||
}
|
||||
10
src-tauri/src/commands/devtools_commands.rs
Normal file
10
src-tauri/src/commands/devtools_commands.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use tauri::{command, Webview};
|
||||
|
||||
#[command]
|
||||
pub async fn toggle_devtools(webview: Webview) {
|
||||
if webview.is_devtools_open() {
|
||||
webview.close_devtools();
|
||||
} else {
|
||||
webview.open_devtools();
|
||||
}
|
||||
}
|
||||
20
src-tauri/src/commands/file_commands.rs
Normal file
20
src-tauri/src/commands/file_commands.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use std::path::Path;
|
||||
use tauri::command;
|
||||
use crate::error::AppError;
|
||||
use crate::io;
|
||||
use crate::models::MissionPlan;
|
||||
|
||||
#[command]
|
||||
pub fn load_mission(path: String) -> Result<MissionPlan, AppError> {
|
||||
io::json_reader::read_mission(Path::new(&path))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_mission(path: String, mission: MissionPlan) -> Result<(), AppError> {
|
||||
io::json_writer::write_mission(Path::new(&path), &mission)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn new_mission() -> MissionPlan {
|
||||
MissionPlan::new()
|
||||
}
|
||||
6
src-tauri/src/commands/mod.rs
Normal file
6
src-tauri/src/commands/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod file_commands;
|
||||
pub mod task_commands;
|
||||
pub mod validation_commands;
|
||||
pub mod path_commands;
|
||||
pub mod devtools_commands;
|
||||
pub mod defaults_commands;
|
||||
62
src-tauri/src/commands/path_commands.rs
Normal file
62
src-tauri/src/commands/path_commands.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use std::path::Path;
|
||||
use tauri::command;
|
||||
use crate::error::AppError;
|
||||
use crate::io;
|
||||
use crate::models::{PathLineFile, ScanParams, ScanRegion, CameraParams, ScanMode};
|
||||
use crate::services;
|
||||
|
||||
#[command]
|
||||
pub fn load_path_line(path: String) -> Result<PathLineFile, AppError> {
|
||||
io::binary_reader::read_path_line(Path::new(&path))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_path_line(path: String, file: PathLineFile) -> Result<(), AppError> {
|
||||
io::binary_writer::write_path_line(Path::new(&path), &file)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn generate_scan_path(params: ScanParams) -> Result<PathLineFile, AppError> {
|
||||
let records = services::path_generator::generate_path(¶ms);
|
||||
let count = records.len() as u64;
|
||||
Ok(PathLineFile { count, records })
|
||||
}
|
||||
|
||||
#[command]
|
||||
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<ScanRegion>,
|
||||
camera: CameraParams,
|
||||
coverage_rate: f64,
|
||||
speed_y_cm_s: f64,
|
||||
speed_x_start_cm_s: f64,
|
||||
speed_x_scan_cm_s: f64,
|
||||
mode: ScanMode,
|
||||
) -> Result<PathLineFile, AppError> {
|
||||
let records = services::path_generator::generate_paths_multi(
|
||||
®ions, &camera, coverage_rate,
|
||||
speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, &mode,
|
||||
);
|
||||
let count = records.len() as u64;
|
||||
Ok(PathLineFile { count, records })
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn estimate_scan_time_multi_minutes(
|
||||
regions: Vec<ScanRegion>,
|
||||
camera: CameraParams,
|
||||
coverage_rate: f64,
|
||||
speed_y_cm_s: f64,
|
||||
speed_x_start_cm_s: f64,
|
||||
speed_x_scan_cm_s: f64,
|
||||
mode: ScanMode,
|
||||
) -> f64 {
|
||||
services::path_generator::estimate_scan_time_multi(
|
||||
®ions, &camera, coverage_rate,
|
||||
speed_y_cm_s, speed_x_start_cm_s, speed_x_scan_cm_s, &mode,
|
||||
)
|
||||
}
|
||||
163
src-tauri/src/commands/task_commands.rs
Normal file
163
src-tauri/src/commands/task_commands.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use tauri::command;
|
||||
use crate::error::AppError;
|
||||
use crate::models::{MissionPlan, SubTask, SubTaskType, Task};
|
||||
|
||||
#[command]
|
||||
pub fn add_task(mission: MissionPlan) -> MissionPlan {
|
||||
let mut m = mission;
|
||||
let new_id = m.next_task_id();
|
||||
m.tasks.push(Task::new(new_id));
|
||||
m.task_count = m.tasks.len() as u32;
|
||||
m
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn copy_task(mission: MissionPlan, task_id: u32) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let src = m.tasks.iter()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?
|
||||
.clone();
|
||||
let new_id = m.next_task_id();
|
||||
let mut new_task = src;
|
||||
new_task.id = new_id;
|
||||
new_task.scheduled_time = "2000-01-01T00:00:00".to_string();
|
||||
new_task.start_time = None;
|
||||
new_task.end_time = None;
|
||||
new_task.duration_minutes = 0.0;
|
||||
new_task.estimated_duration_minutes = 0.0;
|
||||
new_task.status = "Waiting".to_string();
|
||||
// Reset subtask IDs
|
||||
for sub in &mut new_task.sub_tasks {
|
||||
sub.id = uuid::Uuid::new_v4().to_string();
|
||||
sub.start_time = None;
|
||||
sub.end_time = None;
|
||||
sub.duration_minutes = 0.0;
|
||||
sub.estimated_duration_minutes = 0.0;
|
||||
sub.status = "Waiting".to_string();
|
||||
}
|
||||
m.tasks.push(new_task);
|
||||
m.task_count = m.tasks.len() as u32;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn remove_task(mission: MissionPlan, task_id: u32) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let pos = m.tasks.iter().position(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
m.tasks.remove(pos);
|
||||
m.task_count = m.tasks.len() as u32;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn add_sub_task(
|
||||
mission: MissionPlan,
|
||||
task_id: u32,
|
||||
sub_task_type: SubTaskType,
|
||||
) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let task = m.tasks.iter_mut()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
|
||||
// Check: same type already exists
|
||||
if task.has_type(&sub_task_type) {
|
||||
return Err(AppError::Validation(format!(
|
||||
"Task #{} already has a {} sub-task",
|
||||
task_id,
|
||||
sub_task_type.label()
|
||||
)));
|
||||
}
|
||||
|
||||
task.sub_tasks.push(SubTask::new(sub_task_type));
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn remove_sub_task(
|
||||
mission: MissionPlan,
|
||||
task_id: u32,
|
||||
sub_task_id: String,
|
||||
) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let task = m.tasks.iter_mut()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
|
||||
let pos = task.sub_tasks.iter().position(|s| s.id == sub_task_id)
|
||||
.ok_or(AppError::SubTaskNotFound(sub_task_id))?;
|
||||
task.sub_tasks.remove(pos);
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn update_sub_task(
|
||||
mission: MissionPlan,
|
||||
task_id: u32,
|
||||
sub_task: SubTask,
|
||||
) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let task = m.tasks.iter_mut()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
|
||||
let pos = task.sub_tasks.iter().position(|s| s.id == sub_task.id)
|
||||
.ok_or(AppError::SubTaskNotFound(sub_task.id.clone()))?;
|
||||
task.sub_tasks[pos] = sub_task;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn update_task(
|
||||
mission: MissionPlan,
|
||||
task: Task,
|
||||
) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let pos = m.tasks.iter().position(|t| t.id == task.id)
|
||||
.ok_or(AppError::TaskNotFound(task.id))?;
|
||||
m.tasks[pos] = task;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn reorder_sub_tasks(
|
||||
mission: MissionPlan,
|
||||
task_id: u32,
|
||||
from_index: usize,
|
||||
to_index: usize,
|
||||
) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
let task = m.tasks.iter_mut()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
|
||||
if from_index >= task.sub_tasks.len() || to_index >= task.sub_tasks.len() {
|
||||
return Err(AppError::Validation("Index out of bounds".into()));
|
||||
}
|
||||
|
||||
let sub = task.sub_tasks.remove(from_index);
|
||||
task.sub_tasks.insert(to_index, sub);
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn validate_sub_task_order(
|
||||
mission: MissionPlan,
|
||||
task_id: u32,
|
||||
) -> Result<bool, AppError> {
|
||||
let task = mission.tasks.iter()
|
||||
.find(|t| t.id == task_id)
|
||||
.ok_or(AppError::TaskNotFound(task_id))?;
|
||||
|
||||
let mut found_camera = false;
|
||||
for sub in &task.sub_tasks {
|
||||
if sub.sub_task_type.is_camera_type() {
|
||||
found_camera = true;
|
||||
} else if found_camera && sub.sub_task_type.is_hyperspectral() {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
17
src-tauri/src/commands/validation_commands.rs
Normal file
17
src-tauri/src/commands/validation_commands.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::MissionPlan;
|
||||
use crate::services;
|
||||
use tauri::command;
|
||||
|
||||
#[command]
|
||||
pub fn calculate_schedule(mission: MissionPlan) -> Result<MissionPlan, AppError> {
|
||||
let mut m = mission;
|
||||
services::scheduler::calculate_schedule(&mut m)?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn validate_mission(mission: MissionPlan) -> Vec<services::validator::ValidationIssue> {
|
||||
let validator = services::validator::Validator::new();
|
||||
validator.validate(&mission)
|
||||
}
|
||||
Reference in New Issue
Block a user