merge: async mesh import with Apple Events and convert menu

This commit is contained in:
jess 2026-04-02 12:06:40 -07:00
commit 158719ef8b
1 changed files with 48 additions and 31 deletions

View File

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