From d70cc467122f9774bb0fa8b792532a638efc91c5 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 28 Sep 2026 19:18:53 +0300 Subject: [PATCH] fix(web): STEP export no longer panics; STEP/STL exports download in the browser The STEP header timestamp used SystemTime, which panics on wasm, and the exports wrote through std::fs, which the browser does not have. Fixes #761 Co-Authored-By: Claude Opus 5.5 --- src/app/update/file.rs | 19 +++++++++++++++++-- src/io/step.rs | 16 +++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index c5cbfebb4..7d9455805 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -19,6 +19,21 @@ use codec::{EntityType as AcadEntityType, Handle}; use iced::time::Instant; use iced::{mouse, Point, Task}; +/// Write an exported file: to disk on the desktop, as a browser download on +/// the web, where there is no filesystem behind the dialog's path. (#761) +fn write_export(path: &std::path::Path, bytes: &[u8]) -> Result<(), String> { + #[cfg(target_arch = "wasm32")] + { + let name = path.file_name().map_or_else(|| "export".into(), |n| n.to_string_lossy()); + crate::sys::download_bytes(&name, bytes); + Ok(()) + } + #[cfg(not(target_arch = "wasm32"))] + { + std::fs::write(path, bytes).map_err(|e| e.to_string()) + } +} + pub(super) fn background_task(work: F, map: M) -> Task where T: Send + 'static, @@ -2006,7 +2021,7 @@ impl OpenCADStudio { 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())?; - std::fs::write(&worker_path, bytes).map_err(|e| e.to_string()) + write_export(&worker_path, &bytes) }, move |result| Message::StlExportFinished(path, result), ) @@ -2027,7 +2042,7 @@ impl OpenCADStudio { let mesh_refs: Vec<_> = meshes.iter().collect(); let text = crate::io::step::build_step(&mesh_refs) .ok_or_else(|| "no mesh data to export".to_string())?; - std::fs::write(&worker_path, text.as_bytes()).map_err(|e| e.to_string()) + write_export(&worker_path, text.as_bytes()) }, move |result| Message::StepExportFinished(path, result), ) diff --git a/src/io/step.rs b/src/io/step.rs index 007085489..a71520445 100644 --- a/src/io/step.rs +++ b/src/io/step.rs @@ -255,11 +255,17 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { /// Returns an ISO 8601-like timestamp string for the STEP file header. fn chrono_timestamp() -> String { // Use seconds since Unix epoch for a simple timestamp without chrono dep. - use std::time::{SystemTime, UNIX_EPOCH}; - let secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); + // `SystemTime::now` panics in the browser; ask the page's clock there. (#761) + #[cfg(target_arch = "wasm32")] + let secs = (js_sys::Date::now() / 1000.0) as u64; + #[cfg(not(target_arch = "wasm32"))] + let secs = { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + }; // Format: YYYY-MM-DDTHH:MM:SS (approximate UTC from epoch seconds). let s = secs; let mins = s / 60;