fix(wasm): make the web build actually run in the browser
With the wasm bundle building, get it rendering in a browser:
- Enable iced `webgl` so wgpu uses WebGL2 where WebGPU is absent
(e.g. Firefox by default), and `fira-sans` so UI text has an
embedded font (the web has no system fonts) — without it every
label was blank.
- Replace `std::time::Instant` with `iced::time::Instant` (web-safe);
`Instant::now()` panics ("time not implemented") on wasm and crashed
at boot via the command line.
- Skip the batched-hatch pipeline on wasm: its bind group needs
vertex-stage storage buffers, which WebGL2 lacks (validation error
on "New drawing"). Fields become Option; upload/draw guard on them.
Hatches don't draw on the web for now; everything else does.
- Fix the trunk feature flag (`data-cargo-no-default-features`) and
ignore the `dist/` build output.
Native build + 75 tests unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f2c4a1736a
commit
c49ad887b8
11 changed files with 55 additions and 14 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,6 +3,9 @@
|
|||
debug
|
||||
target
|
||||
|
||||
# Trunk web build output (wasm/js bundle)
|
||||
dist
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
|
|
|
|||
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
[build]
|
||||
target = "index.html"
|
||||
# Note: the `solid3d` feature is disabled via `data-cargo-no-default-features`
|
||||
# on the rust <link> in index.html (truck-meshalgo → lzma-sys C deps cannot
|
||||
# cross-compile to wasm).
|
||||
|
||||
[serve]
|
||||
# Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</style>
|
||||
<!-- Trunk builds the wasm32 binary without the `solid3d` feature (no 3-D
|
||||
solid kernel / vtkio C deps on the web). See issue #45. -->
|
||||
<link data-trunk rel="rust" data-bin="OpenCADStudio" data-no-default-features />
|
||||
<link data-trunk rel="rust" data-bin="OpenCADStudio" data-cargo-no-default-features />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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<wgpu::RenderPipeline>,
|
||||
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<wgpu::BindGroupLayout>,
|
||||
image_bgl1: wgpu::BindGroupLayout,
|
||||
depth_texture_size: Size<u32>,
|
||||
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<wgpu::BindGroupLayout>,
|
||||
Option<wgpu::RenderPipeline>,
|
||||
) = (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<HatchModel> =
|
||||
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(..));
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Loading…
Reference in a new issue