build(web): restore the wasm build — gate native-only deps off wasm

The web build had bit-rotted: native-only features added since it last
compiled pulled crates that don't build for wasm32-unknown-unknown.

- printpdf (PDF export / printing) → native-only target; the web build gets
  stub export_pdf / print_wires. It pulls a wasm-incompatible memchr 1.0.2
  via lopdf → nom_locate, and the web has no filesystem.
- getrandom 0.3 (via ahash via acadrust) → enable its wasm_js feature and add
  the matching --cfg getrandom_backend rustflag in .cargo/config.toml.
- ocs_plugin_api "host" feature (out-of-process plugin runtime: interprocess,
  libloading, memmap2, rkyv) → native-only target; the web gets the
  dependency-free manifest/ribbon contract. The host code was already
  wasm-gated; only the Cargo wiring pulled it in unconditionally.

`trunk build --release` succeeds again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-25 14:11:18 +03:00
commit 44fa9df278
5 changed files with 75 additions and 3 deletions

6
.cargo/config.toml Normal file
View file

@ -0,0 +1,6 @@
# getrandom 0.3 (pulled by ahash via acadrust) needs this cfg to select its
# browser backend on wasm32-unknown-unknown, alongside its `wasm_js` feature
# (enabled in Cargo.toml for the wasm target). Without it the web build fails
# with "wasm32-unknown-unknown targets are not supported by default".
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']

1
Cargo.lock generated
View file

@ -24,6 +24,7 @@ dependencies = [
"env_logger", "env_logger",
"flate2", "flate2",
"fontdb", "fontdb",
"getrandom 0.3.4",
"glam 0.33.0", "glam 0.33.0",
"iced", "iced",
"image", "image",

View file

@ -24,7 +24,9 @@ solid3d = ["dep:truck-meshalgo", "dep:truck-shapeops", "dep:lzma-sys"]
[dependencies] [dependencies]
# Stable, dependency-free add-on contract (manifest + ribbon/CadModule types). # Stable, dependency-free add-on contract (manifest + ribbon/CadModule types).
# Plugin authors target this crate's semver, not OpenCADStudio internals. # Plugin authors target this crate's semver, not OpenCADStudio internals.
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } # The `host` feature (out-of-process plugin runtime: interprocess, libloading,
# memmap2, rkyv) is native-only — see the per-target sections below; the web
# build gets the dependency-free manifest/ribbon contract.
# vtkio (pulled in transitively via truck-meshalgo) depends on xz2 → lzma-sys, # vtkio (pulled in transitively via truck-meshalgo) depends on xz2 → lzma-sys,
# which by default links the system liblzma dynamically. On macOS that bakes a # which by default links the system liblzma dynamically. On macOS that bakes a
# Homebrew dylib path (/opt/homebrew/.../liblzma.5.dylib) into the binary, so # Homebrew dylib path (/opt/homebrew/.../liblzma.5.dylib) into the binary, so
@ -44,7 +46,6 @@ clap = { version = "4", features = ["derive"] }
# Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics). # Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics).
env_logger = "0.11" env_logger = "0.11"
acadrust = "0.3.4" acadrust = "0.3.4"
printpdf = "0.9.1"
flate2 = "1" flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
inventory = "0.3" inventory = "0.3"
@ -80,14 +81,25 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" } acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Native enables the plugin host runtime (out-of-process plugins).
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
rayon = "1" rayon = "1"
open = "5" open = "5"
# PDF export / printing. Native-only: printpdf pulls a wasm-incompatible
# `memchr` (1.0.2) via lopdf → nom_locate, and the web build has no filesystem.
printpdf = "0.9.1"
ureq = { version = "3", default-features = false, features = ["rustls"] } ureq = { version = "3", default-features = false, features = ["rustls"] }
# Parse the GitHub Releases API response for the plugin marketplace. # Parse the GitHub Releases API response for the plugin marketplace.
serde_json = "1" serde_json = "1"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
# Web gets the dependency-free manifest/ribbon contract only (no plugin host).
ocs_plugin_api = { path = "crates/ocs_plugin_api" }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
# ahash (via acadrust) pulls getrandom 0.3, which needs the `wasm_js` backend
# enabled to support wasm32-unknown-unknown (plus the matching rustflag in
# .cargo/config.toml).
getrandom = { version = "0.3", features = ["wasm_js"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
# fetch() returns a JS Promise; bridge it to a Rust future for the per-script # fetch() returns a JS Promise; bridge it to a Rust future for the per-script
# web font loader (#141). # web font loader (#141).

View file

@ -9,15 +9,43 @@
// drawing origin at the paper origin. // drawing origin at the paper origin.
use crate::io::plot_style::PlotStyleTable; use crate::io::plot_style::PlotStyleTable;
use crate::scene::model::hatch_model::{HatchModel, HatchPattern}; use crate::scene::model::hatch_model::HatchModel;
#[cfg(not(target_arch = "wasm32"))]
use crate::scene::model::hatch_model::HatchPattern;
use crate::scene::WireModel; use crate::scene::WireModel;
#[cfg(not(target_arch = "wasm32"))]
use printpdf::{ use printpdf::{
Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PaintMode, PdfDocument, PdfPage, Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PaintMode, PdfDocument, PdfPage,
PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, WindingOrder, PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, WindingOrder,
}; };
#[cfg(not(target_arch = "wasm32"))]
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
// The web build has no `printpdf` (it pulls a wasm-incompatible `memchr` via
// lopdf → nom_locate) and no filesystem, so PDF export is native-only; the web
// build gets these stubs so the call sites still compile.
#[cfg(target_arch = "wasm32")]
pub fn export_pdf(
_wires: &[WireModel],
_hatches: &[HatchModel],
_wipeouts: &[HatchModel],
_paper_w: f64,
_paper_h: f64,
_offset_x: f32,
_offset_y: f32,
_rotation_deg: i32,
_path: &Path,
_plot_style: Option<&PlotStyleTable>,
) -> Result<(), String> {
Err("PDF export is not available in the web version.".into())
}
#[cfg(target_arch = "wasm32")]
pub async fn pick_pdf_path_owned(_stem: String) -> Option<std::path::PathBuf> {
None
}
// ── Public entry point ──────────────────────────────────────────────────── // ── Public entry point ────────────────────────────────────────────────────
/// Export `wires` to a PDF file. /// Export `wires` to a PDF file.
@ -26,6 +54,7 @@ use std::path::Path;
/// - `offset_x` / `offset_y`: added to every wire coordinate so the drawing /// - `offset_x` / `offset_y`: added to every wire coordinate so the drawing
/// origin maps to the bottom-left corner of the page. /// origin maps to the bottom-left corner of the page.
/// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page. /// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page.
#[cfg(not(target_arch = "wasm32"))]
pub fn export_pdf( pub fn export_pdf(
wires: &[WireModel], wires: &[WireModel],
hatches: &[HatchModel], hatches: &[HatchModel],
@ -54,6 +83,7 @@ pub fn export_pdf(
} }
/// Show a PDF save-file dialog and return the chosen path (or None if cancelled). /// Show a PDF save-file dialog and return the chosen path (or None if cancelled).
#[cfg(not(target_arch = "wasm32"))]
pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> { pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
rfd::AsyncFileDialog::new() rfd::AsyncFileDialog::new()
.set_title("Export as PDF") .set_title("Export as PDF")
@ -67,6 +97,7 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
// ── PDF builder ─────────────────────────────────────────────────────────── // ── PDF builder ───────────────────────────────────────────────────────────
#[cfg(not(target_arch = "wasm32"))]
fn build_pdf( fn build_pdf(
wires: &[WireModel], wires: &[WireModel],
hatches: &[HatchModel], hatches: &[HatchModel],
@ -235,6 +266,7 @@ fn build_pdf(
doc.save(&PdfSaveOptions::default(), &mut warnings) doc.save(&PdfSaveOptions::default(), &mut warnings)
} }
#[cfg(not(target_arch = "wasm32"))]
fn flush_line(ops: &mut Vec<Op>, pts: &[LinePoint]) { fn flush_line(ops: &mut Vec<Op>, pts: &[LinePoint]) {
if pts.len() < 2 { if pts.len() < 2 {
return; return;
@ -252,6 +284,7 @@ fn flush_line(ops: &mut Vec<Op>, pts: &[LinePoint]) {
/// rings so islands and holes render correctly under the even-odd rule. /// rings so islands and holes render correctly under the even-odd rule.
/// Mirrors `scene::paper_canvas::draw_hatch`: solid → fill, pattern → outline, /// Mirrors `scene::paper_canvas::draw_hatch`: solid → fill, pattern → outline,
/// gradient → solid fill of the averaged colour. /// gradient → solid fill of the averaged colour.
#[cfg(not(target_arch = "wasm32"))]
fn emit_hatch(ops: &mut Vec<Op>, hatch: &HatchModel, ox: f32, oy: f32) { fn emit_hatch(ops: &mut Vec<Op>, hatch: &HatchModel, ox: f32, oy: f32) {
if hatch.boundary.is_empty() { if hatch.boundary.is_empty() {
return; return;

View file

@ -7,15 +7,34 @@
// //
// The function is async so the UI remains responsive while the job is queued. // The function is async so the UI remains responsive while the job is queued.
#[cfg(not(target_arch = "wasm32"))]
use crate::io::pdf_export; use crate::io::pdf_export;
use crate::io::plot_style::PlotStyleTable; use crate::io::plot_style::PlotStyleTable;
use crate::scene::model::hatch_model::HatchModel; use crate::scene::model::hatch_model::HatchModel;
use crate::scene::WireModel; use crate::scene::WireModel;
// Printing routes through the native PDF pipeline + the OS print command, so it
// is native-only; the web build gets a stub so the call site still compiles.
#[cfg(target_arch = "wasm32")]
pub async fn print_wires(
_wires: Vec<WireModel>,
_hatches: Vec<HatchModel>,
_wipeouts: Vec<HatchModel>,
_paper_w: f64,
_paper_h: f64,
_offset_x: f32,
_offset_y: f32,
_rotation_deg: i32,
_plot_style: Option<PlotStyleTable>,
) -> Result<String, String> {
Err("Printing is not available in the web version.".into())
}
/// Render `wires` (plus hatch / wipeout fills) to a temp PDF and dispatch it /// Render `wires` (plus hatch / wipeout fills) to a temp PDF and dispatch it
/// to the default system printer. /// to the default system printer.
/// ///
/// Returns `Ok(printer_name)` on success or `Err(message)` on failure. /// Returns `Ok(printer_name)` on success or `Err(message)` on failure.
#[cfg(not(target_arch = "wasm32"))]
pub async fn print_wires( pub async fn print_wires(
wires: Vec<WireModel>, wires: Vec<WireModel>,
hatches: Vec<HatchModel>, hatches: Vec<HatchModel>,
@ -47,6 +66,7 @@ pub async fn print_wires(
} }
/// Platform-specific dispatch of a PDF path to the system printer. /// Platform-specific dispatch of a PDF path to the system printer.
#[cfg(not(target_arch = "wasm32"))]
fn dispatch_to_printer(path: &std::path::Path) -> Result<String, String> { fn dispatch_to_printer(path: &std::path::Path) -> Result<String, String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {