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 "<drawing name>.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.
This commit is contained in:
AI Assistant 2026-08-23 18:02:09 +10:00
commit 56d1c0370e
5 changed files with 152 additions and 0 deletions

View file

@ -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));

View file

@ -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<std::path::PathBuf>),
/// 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

View file

@ -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<crate::scene::model::mesh_model::MeshModel> = 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),

View file

@ -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<Element<'a, Message>> = 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,

View file

@ -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(),