From ef64a9996fa70d2d464177220a4611b0a7387115 Mon Sep 17 00:00:00 2001 From: jess Date: Thu, 2 Apr 2026 11:56:46 -0700 Subject: [PATCH] convert SCAD to Cordial menu, Apple Event handler, status feedback --- bundle.sh | 7 +- crates/cord-gui/Cargo.toml | 3 +- crates/cord-gui/src/app.rs | 49 ++++++- crates/cord-gui/src/main.rs | 2 - examples/spe_contact_jig.scad | 63 +++++++++ static/vectors/cord.svg | 246 +--------------------------------- 6 files changed, 119 insertions(+), 251 deletions(-) create mode 100644 examples/spe_contact_jig.scad diff --git a/bundle.sh b/bundle.sh index 94d4d2e..95cb004 100755 --- a/bundle.sh +++ b/bundle.sh @@ -41,5 +41,10 @@ else echo "no icon svg or rsvg-convert not found, skipping icon" fi +LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" +if [ -x "${LSREGISTER}" ]; then + "${LSREGISTER}" -f "${BUNDLE}" + echo "registered file types with LaunchServices" +fi + echo "done: ${BUNDLE}" -echo "to register file types, run: open ${BUNDLE}" diff --git a/crates/cord-gui/Cargo.toml b/crates/cord-gui/Cargo.toml index cdb59f9..fd9e713 100644 --- a/crates/cord-gui/Cargo.toml +++ b/crates/cord-gui/Cargo.toml @@ -31,4 +31,5 @@ muda = "0.17" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" -objc2-foundation = { version = "0.3", features = ["NSAppleEventDescriptor", "NSAppleEventManager", "NSString"] } +objc2-foundation = { version = "0.3", features = ["NSAppleEventDescriptor", "NSAppleEventManager", "NSString", "NSURL", "NSArray"] } +objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSRunningApplication"] } diff --git a/crates/cord-gui/src/app.rs b/crates/cord-gui/src/app.rs index 75c73ad..8e98086 100644 --- a/crates/cord-gui/src/app.rs +++ b/crates/cord-gui/src/app.rs @@ -86,6 +86,7 @@ pub enum Message { RenderPlots, RenderAll, DecomposeMesh, + ConvertToCordial, Tick, } @@ -333,14 +334,16 @@ impl App { } } Message::DecomposeMesh => self.decompose_mesh(), + Message::ConvertToCordial => self.convert_to_cordial(), Message::Tick => { if !self.menu_ready { setup_native_menu(); self.menu_ready = true; } self.poll_menu_events(); - if let Ok(mut q) = open_queue().lock() { - for path in q.drain(..) { + if let Ok(mut queue) = open_queue().try_lock() { + if let Some(path) = queue.pop() { + drop(queue); self.open_path(&path); } } @@ -400,6 +403,7 @@ impl App { "export_scad" => Some(Message::ExportMesh(MeshFormat::Scad)), "undo" => Some(Message::Undo), "redo" => Some(Message::Redo), + "convert_cordial" => Some(Message::ConvertToCordial), "decompose" => Some(Message::DecomposeMesh), "render_objects" => Some(Message::RenderObjects), "render_plots" => Some(Message::RenderPlots), @@ -633,6 +637,46 @@ impl App { self.status = Some("decomposed mesh to Cordial".into()); } + fn convert_to_cordial(&mut self) { + if self.mode != InputMode::Scad { + self.status = Some("already in Cordial mode".into()); + return; + } + let src = self.source.text(); + let src = src.trim(); + + use cord_parse::lexer::Lexer; + use cord_parse::parser::Parser; + use cord_sdf::lower::lower_program; + + let tokens = match Lexer::new(src).tokenize() { + Ok(t) => t, + Err(e) => { self.status = Some(format!("parse error: {e}")); return; } + }; + let program = match Parser::new(tokens).parse_program() { + Ok(p) => p, + Err(e) => { self.status = Some(format!("parse error: {e}")); return; } + }; + let mut sdf = match lower_program(&program) { + Ok(s) => s, + Err(e) => { self.status = Some(format!("lower error: {e}")); return; } + }; + cord_sdf::simplify(&mut sdf); + let cordial = cord_sdf::sdf_to_cordial(&sdf); + + self.source = text_editor::Content::with_text(&cordial); + self.undo_stack.push(cordial); + if self.undo_stack.len() > UNDO_LIMIT { + self.undo_stack.remove(0); + } + self.redo_stack.clear(); + self.dirty = true; + self.mode = InputMode::Expr; + self.reparse(); + self.update_markdown(); + self.status = Some("converted SCAD to Cordial".into()); + } + fn open_dialog(&mut self) { let path = rfd::FileDialog::new() .add_filter("All Supported", &["crd", "cord", "zcd", "scad", "stl", "obj", "3mf", "step", "stp"]) @@ -2201,6 +2245,7 @@ fn setup_native_menu() { &MenuItem::with_id("export_step", "Export STEP\u{2026}", true, None), &MenuItem::with_id("export_scad", "Export OpenSCAD\u{2026}", true, None), &PredefinedMenuItem::separator(), + &MenuItem::with_id("convert_cordial", "Convert SCAD to Cordial", true, m(cmd_shift, Code::KeyC)), &MenuItem::with_id("decompose", "Decompose Mesh to Cordial", true, m(cmd_shift, Code::KeyD)), ]).unwrap(); diff --git a/crates/cord-gui/src/main.rs b/crates/cord-gui/src/main.rs index c79dcb6..67dcdfb 100644 --- a/crates/cord-gui/src/main.rs +++ b/crates/cord-gui/src/main.rs @@ -21,7 +21,6 @@ mod apple_events { use objc2::runtime::{AnyObject, NSObject}; use objc2::{define_class, msg_send, sel, AnyThread}; - // kCoreEventClass = 'aevt', kAEOpenDocuments = 'odoc', keyDirectObject = '----' const KAEVT: u32 = u32::from_be_bytes(*b"aevt"); const KODOC: u32 = u32::from_be_bytes(*b"odoc"); const KEY_DIRECT_OBJECT: u32 = u32::from_be_bytes(*b"----"); @@ -132,7 +131,6 @@ mod apple_events { andEventID: KODOC ]; } - // Leak the handler so it lives for the process lifetime std::mem::forget(handler); } } diff --git a/examples/spe_contact_jig.scad b/examples/spe_contact_jig.scad new file mode 100644 index 0000000..ddc4f04 --- /dev/null +++ b/examples/spe_contact_jig.scad @@ -0,0 +1,63 @@ +// SPE Press-Contact Jig +spe_length = 31.0; +spe_width = 12.0; +spe_thick = 0.35; +strip_count = 3; +strip_y = [3.1, 6.05, 9.0]; +strip_length = 18.0; +wire_dia = 0.65; +wall = 1.8; +floor_t = 1.2; +rim = 1.5; +tol = 0.15; +nest = 0.8; +lid_coverage = strip_length - 1.0; +tray_inner_l = spe_length + 2*tol; +tray_inner_w = spe_width + 2*tol; +tray_outer_l = tray_inner_l + 2*wall; +tray_outer_w = tray_inner_w + 2*wall; +lid_inner_l = lid_coverage + 2*tol; +lid_outer_l = lid_inner_l + 2*wall; +lid_outer_w = tray_outer_w; +blade_w = 1.2; +blade_drop = rim + 0.3; + +module bottom() { + difference() { + cube([tray_outer_l, tray_outer_w, floor_t + rim]); + translate([wall, wall, floor_t]) + cube([tray_inner_l + wall + 1, tray_inner_w, rim + 0.1]); + for (i = [0:strip_count-1]) { + wy = wall + tol + strip_y[i]; + translate([-0.1, wy - (wire_dia + tol)/2, floor_t]) + cube([wall + 0.2, wire_dia + tol, rim + 0.1]); + } + } +} + +module top() { + lip_inset = tol; + difference() { + cube([lid_outer_l, lid_outer_w, floor_t]); + for (i = [0:strip_count-1]) { + wy = wall + tol + strip_y[i]; + translate([-0.1, wy - (wire_dia + tol)/2, -0.1]) + cube([wall + 0.2, wire_dia + tol, floor_t + 0.2]); + } + } + translate([wall + lip_inset, wall + lip_inset, -nest]) + difference() { + cube([lid_inner_l - 2*lip_inset, tray_inner_w - 2*lip_inset, nest]); + translate([tol, tol, -0.1]) + cube([lid_inner_l - 2*lip_inset - 2*tol, tray_inner_w - 2*lip_inset - 2*tol, nest + 0.2]); + } + for (i = [0:strip_count-1]) { + wy = wall + tol + strip_y[i]; + translate([wall, wy - blade_w/2, -blade_drop]) + cube([lid_inner_l, blade_w, blade_drop]); + } +} + +bottom(); +translate([tray_outer_l + 8, 0, blade_drop]) + top(); diff --git a/static/vectors/cord.svg b/static/vectors/cord.svg index e228f3b..9a2f1fa 100644 --- a/static/vectors/cord.svg +++ b/static/vectors/cord.svg @@ -1,245 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file