web: FieldCraft in the browser (apps/fieldcraft-web)

The desktop's ModelApp in a web page through eframe's web runner
(WebGPU, WebGL2 fallback, `?webgl` to force it), built with Trunk.
The browser services sit behind the UI's host-service seams: File >
Open uses the browser's file picker, dropped files are read
asynchronously, and both go into the page's files; Save, Save As and
Write Deck hand the written file out as a download. Autosaves go to the
recovery folder as on the desktop and are mirrored to local storage, so
File > Recover offers what a closed or crashed page left. `?sample=NAME`
opens a sample. A loading screen with the app icon, and a message
instead of the app when neither WebGPU nor WebGL2 is available.

`cargo xtask wasm` (and so `cargo xtask ci`) now checks fieldcraft-web
for wasm32 with the library crates. docs/web.md: running it locally,
what works in the browser and what does not.

Part of #119

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 10:47:41 -04:00
1 parent 5011ae5896
commit ebd2493051
13 files changed
+756 -15

No files matched your search

+14 -4
View File
@@ -43,7 +43,7 @@ commands:
against their references and fixtures; write docs/oracle.md after a full run;
--bless freezes a passing case's results in its fixture.json; --quick skips
cases marked slow (release build)
wasm cargo check every library crate for wasm32-unknown-unknown
wasm cargo check every library crate and the web app for wasm32-unknown-unknown
ci fmt --check, assets, licences --check, layers, clippy -D warnings, then the tests
(not the heavy ones) with the wasm check alongside (stops at the first failing
step)
@@ -160,7 +160,8 @@ fn cmd_layers() -> Result<(), String> {
}
}
/// Never break wasm: every layered crate (everything below the apps) must build for the browser.
/// Never break wasm: every layered crate (everything below the apps) and the web app must build
/// for the browser.
fn cmd_wasm() -> Result<(), String> {
let (c, n) = wasm_check()?;
run(c, &format!("cargo check --target wasm32-unknown-unknown ({n} crates)"))?;
@@ -168,12 +169,21 @@ fn cmd_wasm() -> Result<(), String> {
Ok(())
}
/// The app that runs in the browser (apps/fieldcraft-web), checked with the layered crates.
const WEB_APP: &str = "fieldcraft-web";
/// The wasm check and how many crates it covers. It uses the ci profile so the host build
/// scripts and proc macros built for clippy and the tests are reused.
fn wasm_check() -> Result<(Command, usize), String> {
let crates = layers::from_metadata(&metadata()?)?;
let set: Vec<String> =
crates.iter().filter(|c| matches!(layers::classify(&c.name), Some(layers::Class::Layer(_)))).map(|c| c.name.clone()).collect();
let set: Vec<String> = crates
.iter()
.filter(|c| c.name == WEB_APP || matches!(layers::classify(&c.name), Some(layers::Class::Layer(_))))
.map(|c| c.name.clone())
.collect();
if !set.iter().any(|c| c == WEB_APP) {
return Err(format!("wasm: the web app `{WEB_APP}` is not in the workspace"));
}
let mut c = cargo();
c.args(["check", "--target", "wasm32-unknown-unknown", "--profile", "ci"]);
for p in &set {