Web: apps/solvecraft-web (Trunk, eframe web runner, WebGPU or WebGL2), cargo xtask wasm in ci, web-time clocks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 20:35:55 +00:00
1 parent 9dfd7cad60
commit a737ddad3b
14 files changed
+197 -7

No files matched your search

Generated
+16
View File
@@ -3084,6 +3084,7 @@ dependencies = [
"solvecraft-kernel",
"solvecraft-sketch",
"thiserror 2.0.21",
"web-time",
]
[[package]]
@@ -3173,6 +3174,21 @@ dependencies = [
"serde",
"serde_json",
"solvecraft-engine",
"web-time",
]
[[package]]
name = "solvecraft-web"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"log",
"solvecraft-engine",
"solvecraft-ui-egui",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
+2
View File
@@ -41,6 +41,8 @@ image = { version = "0.25", default-features = false, features = ["png"] }
# ui
egui = "0.36"
eframe = { version = "0.36", default-features = false }
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
web-time = "1"
egui-wgpu = "0.36"
rfd = "0.17"
+4 -1
View File
@@ -45,11 +45,14 @@ cargo run --release -p solvecraft -- --sample # desktop app with a samp
cargo run --release -p solvecraft-cli -- commands # the command registry
cargo run --release -p solvecraft-cli -- run examples/bracket.json --out bracket.step
cargo run --release -p solvecraft-cli -- snapshot examples/bracket.json --out bracket.png
cargo xtask ci # fmt, clippy, tests, assets, layers
cargo xtask ci # fmt, clippy, tests, assets, layers, wasm
```
Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solvecraft`.
In the browser: `cd apps/solvecraft-web && trunk build --release --public-url ./` (WebGPU, or
WebGL2 with `?webgl`; `?sample` opens the sample part).
## Scripts
A script is a JSON list of commands, the same ones the UI runs:
+2 -2
View File
@@ -7,11 +7,11 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat
| Question | Answer |
|---|---|
| Where are we? | **M0 done; M1/M2 in progress.** Sketch with solver, extrude (taper, two-sided, through all), revolve, fillet/chamfer, holes, patterns, mirror, shell, draft, loft, sweep, split, construction planes, parameters that rebuild the timeline, STEP/STL export, desktop app with Fusion-style selection (hover/selected highlights, origin planes, click-to-pick sketch planes, multi-select, window/crossing box selection, animated view cube), CLI, MCP server, oracle harness. |
| Where are we? | **M0 done; M1/M2 in progress.** Sketch with solver, extrude (taper, two-sided, through all), revolve, fillet/chamfer, holes, patterns, mirror, shell, draft, loft, sweep, split, construction planes, parameters that rebuild the timeline, STEP/STL export, desktop and browser (wasm) app with Fusion-style selection (hover/selected highlights, origin planes, click-to-pick sketch planes, multi-select, window/crossing box selection, animated view cube), CLI, MCP server, oracle harness. |
| Command parity (SOLID + SKETCH toolbar) | **57 / 291 (20%)** — [docs/parity.md](docs/parity.md) |
| Fusion oracle | **23 / 29 parts match** Fusion's measurements — [docs/oracle.md](docs/oracle.md) |
| Tests | 85 (solver, profiles, kernel booleans/blends/measures, expressions, timeline, file formats, camera, engine end-to-end, hostile-input fuzz over every command) |
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering — green |
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
| Weighted parity estimate | **≈ 5%** of Fusion's Design workspace by importance (sketch + basic solids are the core, but surfaces, assemblies, sheet metal, CAM, drawings are untouched) |
| Time to a useful alpha (M0–M6) | ≈ **150 agent hours** remain |
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "solvecraft-web"
description = "SolveCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lints]
workspace = true
[[bin]]
name = "solvecraft-web"
path = "src/main.rs"
[dependencies]
solvecraft-engine = { workspace = true }
solvecraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location"] }
+10
View File
@@ -0,0 +1,10 @@
# Build with: trunk build --release (from apps/solvecraft-web)
[build]
target = "index.html"
dist = "../../dist/web"
public_url = "./"
filehash = true
[serve]
addresses = ["127.0.0.1"]
port = 8781
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>SolveCraft</title>
<link data-trunk rel="rust" data-bin="solvecraft-web" data-wasm-opt="z" data-weak-refs />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
#solvecraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#solvecraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
</head>
<body>
<canvas id="solvecraft_canvas"></canvas>
<div id="solvecraft_loading">Loading SolveCraft&hellip;</div>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
//! SolveCraft in the browser.
//!
//! Runs the same [`solvecraft_ui_egui::SolveApp`] as the desktop app through eframe's web runner
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from this
//! directory (output in `dist/web`). URL flags: `?webgl` forces WebGL2, `?sample` opens the
//! sample part.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(target_arch = "wasm32")]
mod web {
use solvecraft_engine::Session;
use solvecraft_ui_egui::{Services, SolveApp};
use wasm_bindgen::JsCast as _;
const CANVAS_ID: &str = "solvecraft_canvas";
const LOADING_ID: &str = "solvecraft_loading";
const DEPTH_BITS: u8 = 24;
struct Shell(SolveApp);
impl eframe::App for Shell {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.0.logic(ctx);
}
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
self.0.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.0.ui(ui);
}
}
fn query() -> String {
web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default()
}
pub fn start() {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
wasm_bindgen_futures::spawn_local(async {
let Some(document) = web_sys::window().and_then(|w| w.document()) else { return };
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
return;
};
let mut options = eframe::WebOptions { depth_buffer: DEPTH_BITS, ..Default::default() };
if query().contains("webgl")
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
{
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
}
let result = eframe::WebRunner::new()
.start(
canvas,
options,
Box::new(move |cc| {
let mut app = SolveApp::new(Session::default(), Services::default());
if let Some(rs) = &cc.wgpu_render_state {
app.set_wgpu(rs, DEPTH_BITS, 1);
}
if query().contains("sample") {
if let Err(e) = app.session.run_script(&solvecraft_engine::sample::script()) {
app.set_status(format!("sample: {e}"), true);
}
app.session.doc_mut().name = "Sample Plate".into();
app.session.mark_saved();
}
Ok(Box::new(Shell(app)))
}),
)
.await;
if let Some(el) = document.get_element_by_id(LOADING_ID) {
match result {
Ok(()) => el.remove(),
Err(e) => {
el.set_inner_html(&format!("<p>SolveCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>"))
}
}
}
});
}
}
#[cfg(target_arch = "wasm32")]
fn main() {
web::start();
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
eprintln!("solvecraft-web only runs in the browser: build it with `trunk build --release` in apps/solvecraft-web");
}
+1
View File
@@ -16,3 +16,4 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }
web-time = { workspace = true }
+2 -2
View File
@@ -115,8 +115,8 @@ fn fingerprint(prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_ba
h.finish()
}
fn now() -> std::time::Instant {
std::time::Instant::now()
fn now() -> web_time::Instant {
web_time::Instant::now()
}
impl Model {
+1
View File
@@ -15,4 +15,5 @@ egui-wgpu = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
web-time = { workspace = true }
image = { workspace = true }
+1 -1
View File
@@ -441,6 +441,6 @@ impl SolveApp {
}
pub fn now_ms() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
use web_time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
}
+1
View File
@@ -48,6 +48,7 @@ pub const TABLE: &[(&str, Class)] = &[
// apps and tooling
("solvecraft", Class::Exempt),
("cli", Class::Exempt),
("web", Class::Exempt),
("xtask", Class::Exempt),
];
+21 -1
View File
@@ -21,7 +21,8 @@ commands:
xtask/data/fusion-catalog.tsv from plan/fusion/menu-tree.json)
oracle replay plan/fusion/oracle/*/recipe.json and compare with measure.json;
writes docs/oracle.md
ci fmt --check, clippy -D warnings, test, assets, layers (stops at first failure)
wasm cargo check every library crate and the web app for wasm32-unknown-unknown
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
";
fn main() -> ExitCode {
@@ -33,6 +34,7 @@ fn main() -> ExitCode {
Some("parity") => parity::run(&root(), rest.contains(&"--refresh")),
Some("oracle") => cmd_oracle(),
Some("ci") => cmd_ci(),
Some("wasm") => cmd_wasm(),
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");
Ok(())
@@ -161,6 +163,23 @@ fn cmd_oracle() -> Result<(), String> {
Ok(())
}
/// 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 crates = layers::from_metadata(&metadata()?)?;
let mut set: Vec<String> =
crates.iter().filter(|c| matches!(layers::classify(&c.name), Some(layers::Class::Layer(_)))).map(|c| c.name.clone()).collect();
set.push("solvecraft-web".into());
let mut c = cargo();
c.args(["check", "--target", "wasm32-unknown-unknown"]);
for p in &set {
c.args(["-p", p]);
}
run(c, &format!("cargo check --target wasm32-unknown-unknown ({} crates)", set.len()))?;
println!("wasm: {} crates build for wasm32-unknown-unknown", set.len());
Ok(())
}
fn cmd_ci() -> Result<(), String> {
let mut c = cargo();
c.args(["fmt", "--all", "--", "--check"]);
@@ -173,6 +192,7 @@ fn cmd_ci() -> Result<(), String> {
run(c, "cargo test")?;
assets::run(&root())?;
cmd_layers()?;
cmd_wasm()?;
eprintln!("ci: all gates passed");
Ok(())
}