diff --git a/.gitignore b/.gitignore index 7acd88e17..7d32e1b48 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ debug target +# Trunk web build output (wasm/js bundle) +dist + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index efa9b9789..8d460b549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6336,6 +6336,7 @@ dependencies = [ "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", @@ -6359,6 +6360,15 @@ dependencies = [ "wgpu-hal", ] +[[package]] +name = "wgpu-core-deps-wasm" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-core-deps-windows-linux-android" version = "27.0.0" diff --git a/Cargo.toml b/Cargo.toml index cb4476557..c4c8b5098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,3 +82,8 @@ ureq = { version = "3", default-features = false, features = ["rustls"] } [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1" +# Browsers without WebGPU (e.g. Firefox by default) need wgpu's WebGL2 backend; +# `webgl` enables it. `fira-sans` embeds the default UI font — the web has no +# system fonts, so without it all iced text (ribbon labels, command line) is +# blank. Features merge with the main `iced` dependency. +iced = { version = "0.14", features = ["webgl", "fira-sans"] } diff --git a/Trunk.toml b/Trunk.toml index e58869cdb..4c60188c6 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -8,6 +8,9 @@ [build] target = "index.html" +# Note: the `solid3d` feature is disabled via `data-cargo-no-default-features` +# on the rust in index.html (truck-meshalgo → lzma-sys C deps cannot +# cross-compile to wasm). [serve] # Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads diff --git a/index.html b/index.html index a58c53477..74066bf78 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ - + diff --git a/src/app/mod.rs b/src/app/mod.rs index 67b2e2953..5800db418 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -41,7 +41,7 @@ pub struct GripHover { pub handle: acadrust::Handle, pub grip_id: usize, pub screen: iced::Point, - pub started: std::time::Instant, + pub started: iced::time::Instant, } /// Open multi-functional-grip popup state. diff --git a/src/app/update.rs b/src/app/update.rs index 41ce74fd7..f3e695304 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -7711,7 +7711,7 @@ impl OpenCADStudio { handle, grip_id, screen: p, - started: std::time::Instant::now(), + started: iced::time::Instant::now(), }); self.grip_popup = None; } else if let Some(h) = self.grip_hover.as_mut() { diff --git a/src/io/mod.rs b/src/io/mod.rs index 8d4bd8cce..868cf29e8 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -61,7 +61,7 @@ pub async fn open_path_with_phase( let path2 = path.clone(); let phase2 = phase.clone(); let (doc, caches) = std::thread::spawn(move || -> Result<_, String> { - use std::time::Instant; + use iced::time::Instant; phase2.store(PHASE_PARSING, Ordering::Relaxed); let t_parse = Instant::now(); let mut doc = load_file(&path2)?; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index de5bd323c..61a3db3f3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1710,7 +1710,7 @@ impl Scene { Some([cx - w * margin, cy - h * margin, cx + w * margin, cy + h * margin]) }; let block = self.model_space_block_handle(); - let t_tess = std::time::Instant::now(); + let t_tess = iced::time::Instant::now(); let arc = Arc::new(self.wires_for_block_culled(block, tess_region, wpp, None, None)); self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0); self.last_tess_wires.set(arc.len()); @@ -1739,7 +1739,7 @@ impl Scene { } } let layout_block = self.current_layout_block_handle(); - let t_tess = std::time::Instant::now(); + let t_tess = iced::time::Instant::now(); let mut wires = self.wires_for_block(layout_block); self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0); self.last_tess_wires.set(wires.len()); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 7b4dc62d5..f8e781e50 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -35,8 +35,9 @@ pub struct Pipeline { hatch_pipeline: wgpu::RenderPipeline, /// Phase 4-B — single-draw batched hatch pipeline. Per-instance /// data lives in storage buffers; one draw call covers every - /// hatch in the frame. - hatch_batched_pipeline: wgpu::RenderPipeline, + /// hatch in the frame. `None` on WebGL2 (wasm), which lacks + /// vertex-stage storage buffers. + hatch_batched_pipeline: Option, image_pipeline: wgpu::RenderPipeline, mesh_pipeline: wgpu::RenderPipeline, /// Wireframe variant of the mesh pipeline (LineList topology, same @@ -56,8 +57,8 @@ pub struct Pipeline { uniform_bind_group: wgpu::BindGroup, hatch_bgl1: wgpu::BindGroupLayout, /// Group-1 layout for the batched hatch pipeline (storage buffers - /// for instances / boundary / families / dashes). - hatch_batched_bgl1: wgpu::BindGroupLayout, + /// for instances / boundary / families / dashes). `None` on WebGL2. + hatch_batched_bgl1: Option, image_bgl1: wgpu::BindGroupLayout, depth_texture_size: Size, depth_view: wgpu::TextureView, @@ -355,6 +356,16 @@ impl Pipeline { }); // ── Hatch batched pipeline (Phase 4-B) ───────────────────────────── + // WebGL2 (the wasm fallback backend) has no vertex-stage storage + // buffers, which this pipeline's bind group requires. Skip it on wasm + // and run without the batched-hatch optimization. + #[cfg(target_arch = "wasm32")] + let (hatch_batched_bgl1, hatch_batched_pipeline): ( + Option, + Option, + ) = (None, None); + #[cfg(not(target_arch = "wasm32"))] + let (hatch_batched_bgl1, hatch_batched_pipeline) = { let hatch_batched_bgl1 = hatch_batched_gpu::HatchBatchedGpu::bind_group_layout(device); let hatch_batched_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("hatch_batched.pipeline_layout"), @@ -406,6 +417,8 @@ impl Pipeline { multiview: None, cache: None, }); + (Some(hatch_batched_bgl1), Some(hatch_batched_pipeline)) + }; // ── Mesh pipeline ────────────────────────────────────────────────── let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { @@ -1152,10 +1165,15 @@ impl Pipeline { } pub fn upload_hatches(&mut self, device: &wgpu::Device, hatches: &[HatchModel]) { + // No batched pipeline (WebGL2) → nothing to upload. + let Some(bgl1) = &self.hatch_batched_bgl1 else { + self.gpu_hatch_batched = None; + return; + }; let renderable: Vec = hatches.iter().filter(|h| h.boundary.len() >= 3).cloned().collect(); self.gpu_hatch_batched = - hatch_batched_gpu::HatchBatchedGpu::build(device, &self.hatch_batched_bgl1, &renderable); + hatch_batched_gpu::HatchBatchedGpu::build(device, bgl1, &renderable); } pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) { @@ -1257,8 +1275,10 @@ impl Pipeline { // by `compute_hatch_lod`). Per-hatch viewport scissor // (paper-space MSPACE) isn't ported to the batched path // yet — follow-up if it shows up as a visual issue. - if let Some(batch) = &self.gpu_hatch_batched { - pass.set_pipeline(&self.hatch_batched_pipeline); + if let (Some(batch), Some(pipeline)) = + (&self.gpu_hatch_batched, &self.hatch_batched_pipeline) + { + pass.set_pipeline(pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); pass.set_bind_group(1, &batch.bind_group, &[]); pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..)); diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 0c4b0c894..316e8f2b2 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -1,6 +1,6 @@ //! OpenCADStudio-style command line — bottom panel with input and history -use std::time::Instant; +use iced::time::Instant; use crate::app::Message; use iced::widget::{button, column, container, opaque, row, scrollable, text, text_input};