From 56d1c0370ec5a81a79bba749c00c2b36f2741451 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 23 Aug 2026 18:02:09 +1000 Subject: [PATCH] Add a real Export to CAM ribbon button + filename dialog Previously the CMMS's STL-export-and-navigate flow was only reachable by typing STLOUT/EXPORTSTL into the command line, with no visible button and no way to choose the filename. Adds a proper UI entry point: - Ribbon: a new quick-access button next to Save As (same style/position, GEAR icon - no dedicated CAM/CNC icon asset exists), command EXPORTCAM. - A filename dialog (ModalKind::CamExportDialog, cam_export_filename field) mirrors the existing Save-As dialog's shape minus the format picker (always STL) - defaults to ".stl", editable before confirming. - STLOUT/EXPORTSTL commands are unchanged (still instant-export using the drawing's own name, no popup) - this is a second, separate entry point, not a replacement. --- src/app/commands/display.rs | 7 ++++ src/app/mod.rs | 13 ++++++++ src/app/update/mod.rs | 60 ++++++++++++++++++++++++++++++++++ src/app/view/modal.rs | 64 +++++++++++++++++++++++++++++++++++++ src/ui/ribbon/mod.rs | 8 +++++ 5 files changed, 152 insertions(+) diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 0b3a06e9..ed3760e7 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -528,6 +528,13 @@ impl OpenCADStudio { return Some(Task::done(Message::StlExport)); } + // CMMS embedding (not upstream): "Export to CAM" ribbon button - + // opens a filename dialog first, unlike STLOUT/EXPORTSTL above + // which export immediately using the drawing's own name. + "EXPORTCAM" => { + return Some(Task::done(Message::CamExportDialogOpen)); + } + // STEPOUT — export 3D meshes to STEP AP203 format "STEPOUT" | "EXPORTSTEP" | "STPOUT" => { return Some(Task::done(Message::StepExport)); diff --git a/src/app/mod.rs b/src/app/mod.rs index ee40b30f..e0bb9933 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1012,6 +1012,10 @@ pub(super) struct OpenCADStudio { save_dialog_filename: String, /// True when triggered from the unsaved-changes flow. save_dialog_for_unsaved: bool, + /// CMMS embedding (not upstream): editable filename for the "Export to + /// CAM" dialog, e.g. "Bracket.stl" - separate from save_dialog_filename + /// so the two dialogs never clobber each other's typed text. + cam_export_filename: String, /// User preference for the first save of a new/unsaved drawing. default_save_format: String, /// Persisted interface language selection. @@ -1562,6 +1566,9 @@ pub enum ModalKind { DimStyle, Unsaved, SaveDialog, + /// CMMS embedding (not upstream): filename popup for "Export to CAM" + /// (Save-As's dialog, minus the format picker - always STL). + CamExportDialog, Recovery, RecoveryPrompt, Options, @@ -1809,6 +1816,11 @@ pub enum Message { SaveDialogFilenameChanged(String), SaveDialogConfirm, SaveDialogCancel, + // ── CMMS embedding (not upstream): "Export to CAM" filename dialog ───── + CamExportDialogOpen, + CamExportFilenameChanged(String), + CamExportConfirm, + CamExportCancel, /// Destination picked in the native OS save dialog (`None` = cancelled). SaveDialogPathPicked(Option), /// Open the application-wide Options dialog. @@ -3264,6 +3276,7 @@ impl OpenCADStudio { save_dialog_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), save_dialog_filename: "drawing.dwg".to_string(), save_dialog_for_unsaved: false, + cam_export_filename: "drawing.stl".to_string(), default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), language: crate::i18n::Language::default(), // Plot style diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 67ad5ee8..27f6f648 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1173,6 +1173,66 @@ impl OpenCADStudio { Message::SaveDialogCancel => self.close_save_dialog_window(), + // CMMS embedding (not upstream): "Export to CAM" filename dialog. + #[cfg(target_arch = "wasm32")] + Message::CamExportDialogOpen => { + let i = self.active_tab; + if self.tabs[i].scene.meshes.is_empty() { + self.command_line + .push_error(crate::t!("STLOUT: no 3D mesh data in this drawing.").as_ref()); + return Task::none(); + } + self.cam_export_filename = format!("{}.stl", self.tabs[i].tab_display_name()); + self.active_modal = Some(crate::app::ModalKind::CamExportDialog); + Task::none() + } + #[cfg(not(target_arch = "wasm32"))] + Message::CamExportDialogOpen => Task::none(), + + Message::CamExportFilenameChanged(name) => { + self.cam_export_filename = name; + Task::none() + } + + Message::CamExportCancel => { + if self.active_modal == Some(crate::app::ModalKind::CamExportDialog) { + self.active_modal = None; + self.reset_modal_geometry(); + } + Task::none() + } + + #[cfg(target_arch = "wasm32")] + Message::CamExportConfirm => { + self.active_modal = None; + self.reset_modal_geometry(); + let i = self.active_tab; + let meshes: Vec = self.tabs[i] + .scene + .meshes + .values() + .filter_map(|s| s.lods.first().cloned()) + .collect(); + let mut filename = self.cam_export_filename.trim().to_string(); + if filename.is_empty() { + filename = format!("{}.stl", self.tabs[i].tab_display_name()); + } + if std::path::Path::new(&filename).extension().is_none() { + filename = format!("{filename}.stl"); + } + Task::perform( + async move { + let mesh_refs: Vec<_> = meshes.iter().collect(); + let bytes = crate::io::stl::build_stl(&mesh_refs) + .ok_or_else(|| "no mesh data to export".to_string())?; + crate::sys::post_stl_export_to_host(&bytes, &filename).await + }, + Message::CmmsStlExportResult, + ) + } + #[cfg(not(target_arch = "wasm32"))] + Message::CamExportConfirm => Task::none(), + #[cfg(not(target_arch = "wasm32"))] Message::SaveDialogPathPicked(picked) => self.on_save_dialog_path_picked(picked), diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 0c52554a..a3ab99a0 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -45,6 +45,10 @@ impl OpenCADStudio { Some(K::PointStyle) => crate::tr!("modal", "point-style"), Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"), Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"), + // CMMS embedding (not upstream): plain literal, not crate::tr!() + // - not worth wiring a new key into every locale catalog file + // for one CMMS-only dialog. + Some(K::CamExportDialog) => "Export to CAM".to_string(), Some(K::Recovery) => crate::tr!("modal", "recovery-report"), Some(K::RecoveryPrompt) => crate::tr!("modal", "recovery-prompt"), None => String::new(), @@ -1358,6 +1362,11 @@ impl OpenCADStudio { ) }) } + super::super::ModalKind::CamExportDialog => { + automatic_flow(ex, |flow| { + cam_export_dialog_window(&self.cam_export_filename, flow) + }) + } super::super::ModalKind::Recovery => { let report = self.recovery_report.as_ref()?; sized_flow(ex, 680, 460, |flow| { @@ -1522,6 +1531,61 @@ fn save_as_dialog_window<'a>( .into() } +/// CMMS embedding (not upstream): filename popup for "Export to CAM" - +/// same visual shape as save_as_dialog_window, minus the format picker +/// (always STL, there's nothing to choose). +fn cam_export_dialog_window<'a>( + filename: &'a str, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let label = |s: Cow<'static, str>| text(s).size(11).style(dialog_muted_text_style); + let field_width = if matches!(sizing.width, iced::Length::Fill) { + Fill + } else { + iced::Length::Shrink + }; + + let mut items: Vec> = Vec::new(); + items.push(text("Export to CAM").size(14).into()); + items.push(Space::new().height(12).into()); + items.push( + row![ + label(Cow::Borrowed("File name:")).width(70), + iced::widget::text_input("drawing.stl", filename) + .on_input(Message::CamExportFilenameChanged) + .size(13) + .padding([5, 8]) + .width(field_width), + ] + .align_y(iced::Alignment::Center) + .spacing(6) + .width(sizing.width) + .into(), + ); + items.push(Space::new().height(16).into()); + items.push( + row![ + Space::new().width(Fit), + dialog_button(t!("Export"), Message::CamExportConfirm, button::primary), + Space::new().width(8), + dialog_button(t!("Cancel"), Message::CamExportCancel, button::secondary), + ] + .into(), + ); + + let body = column(items) + .spacing(0) + .width(sizing.width) + .height(sizing.height); + + container(body) + .style(dialog_body_style) + .padding([14, 16]) + .width(sizing.width) + .height(sizing.height) + .into() +} + fn unsaved_changes_dialog_window( name: &str, sizing: crate::ui::modal::ModalSizing, diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index abcccd86..2fb4e81c 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -410,6 +410,14 @@ impl Ribbon { quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN").into(), quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE").into(), quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS").into(), + // CMMS embedding (not upstream): "Export to CAM" - reuses the + // GEAR icon (no dedicated CAM/CNC icon asset exists) since it's + // the closest thematic fit already bundled. No-ops gracefully + // outside the CMMS (native build: dialog doesn't open; standalone + // web: opens, but the export POST fails with a clear "not + // running inside the CMMS" message) rather than being hidden - + // same always-visible treatment as New/Open/Print here. + quick_access_btn(crate::ui::icons::GEAR, "Export to CAM", "EXPORTCAM").into(), quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT").into(), render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown).into(), render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown).into(),