forked from 1b-agent/opencadstudio-upstream
feat(wasm): compile the library for wasm32 (issue #45 groundwork)
First step toward a web build: the library now compiles for wasm32-unknown-unknown with --no-default-features. - Add a `solid3d` feature (default on) gating truck-meshalgo / truck-shapeops / lzma-sys. These pull vtkio → xz2 → lzma-sys (C), which cannot cross-compile to wasm. Without the feature the 3-D solid path (tessellation, booleans, ACIS import) becomes no-ops; 2-D CAD is unaffected. Native builds keep the feature by default. - Move native-only crates (ureq, open) to non-wasm target deps and add cfg shims: update-check is a no-op on wasm, and a new `sys` module provides open_url + handle_path (rfd FileHandle → PathBuf; synthesizes a name-based path on the web). Native build + all 75 tests unchanged. Browser runtime wiring (entry point, trunk, real file I/O) is a follow-up. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
3666c7fa69
commit
b2e20c95fe
12 files changed
+123
-20
No files matched your search
+16
-5
@@ -12,6 +12,15 @@ members = ["crates/ocs_plugin_api"]
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["solid3d"]
|
||||
# 3-D solid modelling: B-rep tessellation (truck-meshalgo) and booleans
|
||||
# (truck-shapeops). Disabled for wasm builds — truck-meshalgo pulls
|
||||
# vtkio → xz2 → lzma-sys (a C library) that does not cross-compile to
|
||||
# wasm32. Without this feature the app is a 2-D CAD editor; the Model tab,
|
||||
# solid tessellation and ACIS import become no-ops.
|
||||
solid3d = ["dep:truck-meshalgo", "dep:truck-shapeops", "dep:lzma-sys"]
|
||||
|
||||
[dependencies]
|
||||
# Stable, dependency-free add-on contract (manifest + ribbon/CadModule types).
|
||||
# Plugin authors target this crate's semver, not OpenCADStudio internals.
|
||||
@@ -22,23 +31,21 @@ ocs_plugin_api = { path = "crates/ocs_plugin_api" }
|
||||
# the .app crashes on launch on machines without that exact library (#56).
|
||||
# Forcing the `static` feature builds liblzma from the bundled source, making
|
||||
# the binary self-contained on every platform.
|
||||
lzma-sys = { version = "0.1", features = ["static"] }
|
||||
lzma-sys = { version = "0.1", features = ["static"], optional = true }
|
||||
iced = { version = "0.14", features = ["image", "svg", "advanced", "canvas", "smol"] }
|
||||
bytemuck = { version = "1.25", features = ["derive"] }
|
||||
glam = { version = "0.33", features = ["bytemuck"] }
|
||||
truck-modeling = "0.6"
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation"] }
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation"], optional = true }
|
||||
truck-polymesh = "0.6"
|
||||
rfd = "0.17"
|
||||
acadrust = "0.3.4"
|
||||
open = "5"
|
||||
printpdf = "0.9.1"
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
rayon = "1"
|
||||
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
||||
inventory = "0.3"
|
||||
truck-shapeops = "0.4"
|
||||
truck-shapeops = { version = "0.4", optional = true }
|
||||
# Fast non-cryptographic hasher. Handle/u64 keys dominate the hot block,
|
||||
# hatch, draw-depth and wire caches; the default SipHash is overkill there.
|
||||
rustc-hash = "2"
|
||||
@@ -68,3 +75,7 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
||||
# Z-decode fix, and CANNOSCALE/CANNOSCALEVALUE header support on top of
|
||||
# upstream. Revert to upstream once those PRs land.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
open = "5"
|
||||
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
||||
+3
-3
@@ -2489,7 +2489,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"DONATE" => {
|
||||
let _ = open::that("https://patreon.com/HakanSeven12");
|
||||
crate::sys::open_url("https://patreon.com/HakanSeven12");
|
||||
self.command_line.push_info("Opening Patreon page...");
|
||||
}
|
||||
|
||||
@@ -2669,7 +2669,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"REPORT" => {
|
||||
let _ = open::that("https://github.com/HakanSeven12/OpenCADStudio/issues/new");
|
||||
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/issues/new");
|
||||
self.command_line.push_info("Opening feedback page...");
|
||||
}
|
||||
|
||||
@@ -2682,7 +2682,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"CHANGELOG" => {
|
||||
let _ = open::that("https://github.com/HakanSeven12/OpenCADStudio/releases");
|
||||
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases");
|
||||
self.command_line.push_info("Opening release notes...");
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -408,7 +408,7 @@ impl OpenCADStudio {
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
Some(h) => Ok(h.path().to_path_buf()),
|
||||
Some(h) => Ok(crate::sys::handle_path(&h)),
|
||||
None => Err("Cancelled".to_string()),
|
||||
}
|
||||
},
|
||||
@@ -443,7 +443,7 @@ impl OpenCADStudio {
|
||||
.add_filter("DWG Files", &["dwg"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf());
|
||||
.map(|h| crate::sys::handle_path(&h));
|
||||
(name, path)
|
||||
},
|
||||
|(name, path)| Message::WblockSaveResult(name, path),
|
||||
@@ -497,7 +497,7 @@ impl OpenCADStudio {
|
||||
.add_filter("All Files", &["*"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf());
|
||||
.map(|h| crate::sys::handle_path(&h));
|
||||
(csv_clone, path)
|
||||
},
|
||||
|(csv, path)| Message::DataExtractionSaveResult(csv, path),
|
||||
@@ -540,7 +540,7 @@ impl OpenCADStudio {
|
||||
.add_filter("All Files", &["*"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
.map(|h| crate::sys::handle_path(&h))
|
||||
},
|
||||
Message::StlExportPath,
|
||||
)
|
||||
@@ -594,7 +594,7 @@ impl OpenCADStudio {
|
||||
.add_filter("All Files", &["*"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
.map(|h| crate::sys::handle_path(&h))
|
||||
},
|
||||
Message::StepExportPath,
|
||||
)
|
||||
@@ -637,7 +637,7 @@ impl OpenCADStudio {
|
||||
.add_filter("All Files", &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
.map(|h| crate::sys::handle_path(&h))
|
||||
},
|
||||
Message::ObjImportPath,
|
||||
),
|
||||
@@ -5730,7 +5730,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
Message::UpdateNoticeOpenRelease => {
|
||||
let _ = open::that(crate::update_check::RELEASES_PAGE);
|
||||
crate::sys::open_url(crate::update_check::RELEASES_PAGE);
|
||||
if let Some(id) = self.update_notice_window.take() {
|
||||
window::close(id)
|
||||
} else {
|
||||
@@ -6313,7 +6313,7 @@ impl OpenCADStudio {
|
||||
.add_filter("All Files", &["*"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
.map(|h| crate::sys::handle_path(&h))
|
||||
},
|
||||
Message::PlotStylePanelSavePath,
|
||||
)
|
||||
|
||||
+3
-3
@@ -40,7 +40,7 @@ pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||
.add_filter("All Files", &["*"])
|
||||
.pick_file()
|
||||
.await?;
|
||||
let path = handle.path().to_path_buf();
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||
Some((path, size))
|
||||
}
|
||||
@@ -250,7 +250,7 @@ pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
|
||||
.add_filter("All Files", &["*"])
|
||||
.pick_file()
|
||||
.await?;
|
||||
plot_style::PlotStyleTable::load(handle.path()).ok()
|
||||
plot_style::PlotStyleTable::load(&crate::sys::handle_path(&handle)).ok()
|
||||
}
|
||||
|
||||
// ── Image file picker ─────────────────────────────────────────────────────
|
||||
@@ -267,7 +267,7 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||
.pick_file()
|
||||
.await
|
||||
.ok_or_else(|| "Cancelled".to_string())?;
|
||||
let path = handle.path().to_path_buf();
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let img = image::open(&path).map_err(|e| e.to_string())?;
|
||||
let (w, h) = image::GenericImageView::dimensions(&img);
|
||||
Ok((path, w, h))
|
||||
|
||||
@@ -62,7 +62,7 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
|
||||
.add_filter("All Files", &["*"])
|
||||
.save_file()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
.map(|h| crate::sys::handle_path(&h))
|
||||
}
|
||||
|
||||
// ── PDF builder ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,4 +11,5 @@ pub mod patterns;
|
||||
pub mod scene;
|
||||
pub mod snap;
|
||||
pub mod ui;
|
||||
pub mod sys;
|
||||
pub mod update_check;
|
||||
@@ -15,6 +15,7 @@ mod patterns;
|
||||
mod scene;
|
||||
mod snap;
|
||||
mod ui;
|
||||
mod sys;
|
||||
mod update_check;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
pub mod acad_to_truck;
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub mod acis_to_truck;
|
||||
/// Without `solid3d` (e.g. wasm) ACIS/SAT meshing is unavailable; the entry
|
||||
/// point stays so callers compile, returning no mesh.
|
||||
#[cfg(not(feature = "solid3d"))]
|
||||
pub mod acis_to_truck {
|
||||
use crate::scene::model::mesh_model::MeshLodSet;
|
||||
use acadrust::entities::acis::SatDocument;
|
||||
pub fn tessellate_sat_truck(
|
||||
_sat: &SatDocument,
|
||||
_name: String,
|
||||
_color: [f32; 4],
|
||||
_facet_res: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub mod truck_tess;
|
||||
pub mod tessellate;
|
||||
pub mod tess_util;
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
// The caller (acad_to_truck / tessellate) decides which function to call based on
|
||||
// the truck topology level of the object.
|
||||
|
||||
#[cfg(feature = "solid3d")]
|
||||
use truck_meshalgo::tessellation::{MeshableShape, MeshedShape};
|
||||
use truck_modeling::base::{BoundedCurve, ParameterDivision1D};
|
||||
use truck_modeling::{Edge, Shell, Solid, Vertex, Wire};
|
||||
#[cfg(feature = "solid3d")]
|
||||
use truck_polymesh::PolygonMesh;
|
||||
|
||||
|
||||
@@ -140,21 +142,45 @@ pub fn tessellate_wire(w: &Wire, offset: [f64; 3]) -> TruckTessResult {
|
||||
// ── Shell ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub fn tessellate_shell(s: &Shell, offset: [f64; 3]) -> TruckTessResult {
|
||||
let meshed = s.triangulation(MESH_TOL);
|
||||
polygon_to_result(meshed.to_polygon(), offset)
|
||||
}
|
||||
|
||||
/// Without `solid3d` (e.g. wasm) there is no mesher; a shell yields no mesh.
|
||||
#[cfg(not(feature = "solid3d"))]
|
||||
pub fn tessellate_shell(_s: &Shell, _offset: [f64; 3]) -> TruckTessResult {
|
||||
TruckTessResult::Mesh {
|
||||
verts: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Solid ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub fn tessellate_solid(s: &Solid, offset: [f64; 3]) -> TruckTessResult {
|
||||
let meshed = s.triangulation(MESH_TOL);
|
||||
polygon_to_result(meshed.to_polygon(), offset)
|
||||
}
|
||||
|
||||
/// Without `solid3d` (e.g. wasm) there is no mesher; a solid yields no mesh.
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(feature = "solid3d"))]
|
||||
pub fn tessellate_solid(_s: &Solid, _offset: [f64; 3]) -> TruckTessResult {
|
||||
TruckTessResult::Mesh {
|
||||
verts: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "solid3d")]
|
||||
fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult {
|
||||
let verts: Vec<[f32; 3]> = mesh
|
||||
.positions()
|
||||
|
||||
@@ -130,10 +130,12 @@ pub enum Bool {
|
||||
Intersect,
|
||||
}
|
||||
|
||||
#[cfg(feature = "solid3d")]
|
||||
const BOOL_TOL: f64 = 0.05;
|
||||
|
||||
/// Combine two solids. `Subtract` removes `b` from `a`. Returns `None` when the
|
||||
/// operation fails (e.g. the solids don't actually overlap).
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub fn boolean(op: Bool, a: &Solid, b: &Solid) -> Option<Solid> {
|
||||
match op {
|
||||
Bool::Union => truck_shapeops::or(a, b, BOOL_TOL),
|
||||
@@ -146,6 +148,12 @@ pub fn boolean(op: Bool, a: &Solid, b: &Solid) -> Option<Solid> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Without `solid3d` (e.g. wasm) there is no boolean kernel.
|
||||
#[cfg(not(feature = "solid3d"))]
|
||||
pub fn boolean(_op: Bool, _a: &Solid, _b: &Solid) -> Option<Solid> {
|
||||
None
|
||||
}
|
||||
|
||||
// ── Tessellation ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tessellate a truck `Solid` into a single-LOD `MeshLodSet` (world-space,
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Small platform shims for things the desktop build does natively but the web
|
||||
// (wasm) build must handle differently or skip.
|
||||
|
||||
/// Open a URL in the user's browser. On the desktop this launches the default
|
||||
/// handler; on the web the app already *is* in a browser, so for now this is a
|
||||
/// no-op (a real implementation would call `window.open`).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn open_url(url: &str) {
|
||||
let _ = open::that(url);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn open_url(_url: &str) {
|
||||
// TODO(web): web_sys::window().open_with_url(_url)
|
||||
}
|
||||
|
||||
/// Turn an `rfd` file handle into a `PathBuf` the rest of the app keys on.
|
||||
///
|
||||
/// Desktop returns the real filesystem path. The browser has no path, so we
|
||||
/// synthesize one from the file name — enough for the app to compile and track
|
||||
/// the document name; actual byte I/O on the web reads the handle directly
|
||||
/// (a follow-up).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf {
|
||||
h.path().to_path_buf()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf {
|
||||
std::path::PathBuf::from(h.file_name())
|
||||
}
|
||||
@@ -28,6 +28,7 @@ pub struct UpdateInfo {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn check_for_update() -> Option<UpdateInfo> {
|
||||
std::thread::spawn(fetch_latest_if_outdated)
|
||||
.join()
|
||||
@@ -35,6 +36,14 @@ pub async fn check_for_update() -> Option<UpdateInfo> {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// On wasm there is no background thread or blocking HTTP client; the web build
|
||||
/// skips the self-update check (the page is always served fresh).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn check_for_update() -> Option<UpdateInfo> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(std::time::Duration::from_secs(5)))
|
||||
|
||||
Reference in new issue
Block a user