diff --git a/crates/cord-gui/src/app.rs b/crates/cord-gui/src/app.rs index 8e98086..a554860 100644 --- a/crates/cord-gui/src/app.rs +++ b/crates/cord-gui/src/app.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Shadow, Subscription}; use iced::{mouse, keyboard, window, Point, Vector}; use std::path::PathBuf; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::{Arc, Mutex, OnceLock, mpsc}; use std::time::Duration; static OPEN_QUEUE: OnceLock>> = OnceLock::new(); @@ -45,6 +45,7 @@ pub struct App { cursor_line: usize, line_eval_text: Option, mesh_path: Option, + mesh_import_rx: Option>>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -87,6 +88,7 @@ pub enum Message { RenderAll, DecomposeMesh, ConvertToCordial, + MeshImportComplete(Result<(String, PathBuf), String>), Tick, } @@ -154,6 +156,7 @@ impl App { cursor_line: 0, line_eval_text: None, mesh_path: None, + mesh_import_rx: None, }; app.reparse(); @@ -335,6 +338,26 @@ impl App { } Message::DecomposeMesh => self.decompose_mesh(), Message::ConvertToCordial => self.convert_to_cordial(), + Message::MeshImportComplete(result) => { + match result { + Ok((source, path)) => { + self.source = text_editor::Content::with_text(&source); + self.undo_stack = vec![source]; + self.redo_stack.clear(); + self.mesh_path = Some(path.clone()); + self.current_path = Some(path.clone()); + self.dirty = false; + self.detect_mode(); + self.reparse(); + self.update_markdown(); + self.push_recent(&path); + self.status = Some(format!("opened: {}", path.display())); + } + Err(e) => { + self.status = Some(format!("mesh import error: {e}")); + } + } + } Message::Tick => { if !self.menu_ready { setup_native_menu(); @@ -347,6 +370,13 @@ impl App { self.open_path(&path); } } + if let Some(rx) = &self.mesh_import_rx { + if let Ok(result) = rx.try_recv() { + self.mesh_import_rx = None; + self.update(Message::MeshImportComplete(result)); + return; + } + } let new_line = self.source.cursor().position.line; if new_line != self.cursor_line { self.cursor_line = new_line; @@ -696,8 +726,18 @@ impl App { .unwrap_or("") .to_lowercase(); - let is_mesh = matches!(ext.as_str(), "obj" | "stl" | "3mf"); - let mut fallback_note: Option<&str> = None; + if matches!(ext.as_str(), "obj" | "stl" | "3mf") { + self.status = Some(format!("decomposing mesh: {}…", path.display())); + let (tx, rx) = mpsc::channel(); + let path_owned = path.to_path_buf(); + std::thread::spawn(move || { + let result = import_mesh(&path_owned) + .map(|source| (source, path_owned)); + let _ = tx.send(result); + }); + self.mesh_import_rx = Some(rx); + return; + } let result = match ext.as_str() { "zcd" => load_zcd(path), @@ -705,17 +745,6 @@ impl App { "cord" | "bin" => { Err("binary format (.cord) — no source to edit".into()) } - "obj" | "stl" | "3mf" => { - match import_mesh(path) { - Ok(imp) => { - if imp.is_fallback { - fallback_note = Some(" (bounding box — full decompose failed)"); - } - Ok(imp.source) - } - Err(e) => Err(e), - } - } "step" | "stp" => { Err(format!("import for .{ext} not yet implemented")) } @@ -728,14 +757,13 @@ impl App { self.undo_stack = vec![source]; self.redo_stack.clear(); self.current_path = Some(path.to_path_buf()); - self.mesh_path = if is_mesh { Some(path.to_path_buf()) } else { None }; + self.mesh_path = None; self.dirty = false; self.detect_mode(); self.reparse(); self.update_markdown(); self.push_recent(path); - let note = fallback_note.unwrap_or(""); - self.status = Some(format!("opened: {}{note}", path.display())); + self.status = Some(format!("opened: {}", path.display())); } Err(e) => { self.status = Some(format!("error: {e}")); @@ -1766,26 +1794,15 @@ fn load_zcd(path: &std::path::Path) -> Result { Err("no readable layers in .zcd".into()) } -struct MeshImport { - source: String, - is_fallback: bool, -} - -fn import_mesh(path: &std::path::Path) -> Result { +fn import_mesh(path: &std::path::Path) -> Result { use cord_decompile::mesh::TriangleMesh; use cord_decompile::{decompile, DecompileConfig}; let mesh = TriangleMesh::load(path).map_err(|e| e.to_string())?; let config = DecompileConfig::default(); match decompile(&mesh, &config) { - Ok(result) => Ok(MeshImport { - source: cord_sdf::sdf_to_cordial(&result.sdf), - is_fallback: false, - }), - Err(_) => Ok(MeshImport { - source: mesh_bounding_source(&mesh), - is_fallback: true, - }), + Ok(result) => Ok(cord_sdf::sdf_to_cordial(&result.sdf)), + Err(_) => Ok(mesh_bounding_source(&mesh)), } }