From 0d7e456bc1d78a3442bbe1208615018ea04b21a3 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Fri, 9 Oct 2026 12:49:49 +0000 Subject: [PATCH] Documentation site and CI workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An mdBook under docs/book: a user guide (getting started, UI tour, sketching, features, parameters, components and joints, sheet metal, plastic, files, shortcuts, MCP) and a developer guide (CONTRIBUTING, architecture, command engine, kernel boundary, persistent naming, test harnesses, and the existing docs/*.md pages). `cargo xtask book` generates the command reference from `solvecraft-cli commands` and renders the screenshots with `solvecraft-cli snapshot`, then runs mdbook. .github/workflows/pages.yml publishes the book at /solvecraft/ and the web app at /solvecraft/app/ (skipped while the repository is private). .github/workflows/ci.yml runs `cargo xtask ci` on pull requests. README and Help → Documentation link to the book. Fixes #28 Fixes #8 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 27 ++++++ .github/workflows/pages.yml | 65 +++++++++++++ ATTRIBUTION.md | 5 + README.md | 6 ++ crates/ui-egui/src/help.rs | 15 ++- docs/book/.gitignore | 2 + docs/book/book.toml | 29 ++++++ docs/book/scripts/enclosure.json | 7 ++ docs/book/scripts/revolve.json | 11 +++ docs/book/scripts/sheet-metal.json | 9 ++ docs/book/src/SUMMARY.md | 34 +++++++ docs/book/src/dev/architecture.md | 22 +++++ docs/book/src/dev/commands.md | 38 ++++++++ docs/book/src/dev/contributing.md | 1 + docs/book/src/dev/control-protocol.md | 1 + docs/book/src/dev/kernel.md | 37 ++++++++ docs/book/src/dev/naming.md | 22 +++++ docs/book/src/dev/oracle.md | 1 + docs/book/src/dev/parity.md | 1 + docs/book/src/dev/step-import.md | 1 + docs/book/src/dev/testing.md | 40 ++++++++ docs/book/src/guide/assemblies.md | 22 +++++ docs/book/src/guide/features.md | 24 +++++ docs/book/src/guide/files.md | 19 ++++ docs/book/src/guide/getting-started.md | 56 +++++++++++ docs/book/src/guide/mcp.md | 1 + docs/book/src/guide/parameters.md | 21 +++++ docs/book/src/guide/plastic.md | 13 +++ docs/book/src/guide/sheet-metal.md | 15 +++ docs/book/src/guide/shortcuts.md | 29 ++++++ docs/book/src/guide/sketching.md | 34 +++++++ docs/book/src/guide/ui-tour.md | 29 ++++++ docs/book/src/introduction.md | 32 +++++++ xtask/src/book.rs | 123 +++++++++++++++++++++++++ xtask/src/main.rs | 5 + 35 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pages.yml create mode 100644 docs/book/.gitignore create mode 100644 docs/book/book.toml create mode 100644 docs/book/scripts/enclosure.json create mode 100644 docs/book/scripts/revolve.json create mode 100644 docs/book/scripts/sheet-metal.json create mode 100644 docs/book/src/SUMMARY.md create mode 100644 docs/book/src/dev/architecture.md create mode 100644 docs/book/src/dev/commands.md create mode 100644 docs/book/src/dev/contributing.md create mode 100644 docs/book/src/dev/control-protocol.md create mode 100644 docs/book/src/dev/kernel.md create mode 100644 docs/book/src/dev/naming.md create mode 100644 docs/book/src/dev/oracle.md create mode 100644 docs/book/src/dev/parity.md create mode 100644 docs/book/src/dev/step-import.md create mode 100644 docs/book/src/dev/testing.md create mode 100644 docs/book/src/guide/assemblies.md create mode 100644 docs/book/src/guide/features.md create mode 100644 docs/book/src/guide/files.md create mode 100644 docs/book/src/guide/getting-started.md create mode 100644 docs/book/src/guide/mcp.md create mode 100644 docs/book/src/guide/parameters.md create mode 100644 docs/book/src/guide/plastic.md create mode 100644 docs/book/src/guide/sheet-metal.md create mode 100644 docs/book/src/guide/shortcuts.md create mode 100644 docs/book/src/guide/sketching.md create mode 100644 docs/book/src/guide/ui-tour.md create mode 100644 docs/book/src/introduction.md create mode 100644 xtask/src/book.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1d65c2a8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +# Every pull request must pass `cargo xtask ci`: fmt, clippy -D warnings, tests in release mode, +# the asset and layer checks, and the wasm build check. +name: CI + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo xtask ci diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..6f57d857 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,65 @@ +# The documentation site on GitHub Pages: the book (docs/book) at /solvecraft/ and the web app +# at /solvecraft/app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the +# repository is private. +name: Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + MDBOOK_VERSION: v0.5.4 + TRUNK_VERSION: v0.21.14 + +jobs: + build: + if: ${{ !github.event.repository.private }} + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: Install mdbook and trunk + run: | + mkdir -p "$HOME/.local/bin" + curl -sSL "https://github.com/rust-lang/mdBook/releases/download/${MDBOOK_VERSION}/mdbook-${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C "$HOME/.local/bin" + curl -sSL "https://github.com/trunk-rs/trunk/releases/download/${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Build the book + run: cargo xtask book + - name: Build the web app + working-directory: apps/solvecraft-web + run: trunk build --release --public-url /solvecraft/app/ + - name: Assemble the site + run: | + mkdir -p site + cp -r target/book/. site/ + cp -r dist/web site/app + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 3e16f5c6..52f51e30 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -18,6 +18,11 @@ Generated-in-code assets are original and have no file to list: | File | Author | Source | Licence | |---|---|---|---| | docs/screenshots/sample-plate.png | SolveCraft contributors | screenshot of SolveCraft itself (`solvecraft --sample`) | MIT OR Apache-2.0 | +| docs/book/src/generated/sample-plate.png | SolveCraft contributors | copy of docs/screenshots/sample-plate.png made by `cargo xtask book` (gitignored) | MIT OR Apache-2.0 | +| docs/book/src/generated/bracket.png | SolveCraft contributors | rendered by `cargo xtask book` (`solvecraft-cli snapshot examples/bracket.json`; gitignored) | MIT OR Apache-2.0 | +| docs/book/src/generated/enclosure.png | SolveCraft contributors | rendered by `cargo xtask book` (`solvecraft-cli snapshot docs/book/scripts/enclosure.json`; gitignored) | MIT OR Apache-2.0 | +| docs/book/src/generated/revolve.png | SolveCraft contributors | rendered by `cargo xtask book` (`solvecraft-cli snapshot docs/book/scripts/revolve.json`; gitignored) | MIT OR Apache-2.0 | +| docs/book/src/generated/sheet-metal.png | SolveCraft contributors | rendered by `cargo xtask book` (`solvecraft-cli snapshot docs/book/scripts/sheet-metal.json`; gitignored) | MIT OR Apache-2.0 | | crates/sketch/fonts/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in crates/sketch/fonts/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 | ## Code adapted from sibling projects diff --git a/README.md b/README.md index 7a66b91c..86911d83 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ storytold "craft" family. ![SolveCraft with the sample plate](docs/screenshots/sample-plate.png) +**Documentation:** the [SolveCraft book](https://bherbruck.github.io/solvecraft/) (user guide, +developer guide and command reference; sources in [docs/book](docs/book)), and the +[web app](https://bherbruck.github.io/solvecraft/app/) to try it in the browser. + **Status: early but usable for simple parts.** 76 of 76 reference parts built in Fusion rebuild in SolveCraft with the same volume, area and topology ([docs/oracle.md](docs/oracle.md)); command parity with Fusion's Design workspace is tracked in [docs/parity.md](docs/parity.md). @@ -90,6 +94,7 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md). ## Architecture + | Layer | Crates | Role | |---|---|---| | L0 | `geom` | vectors, planes, profiles, meshes and their measures | @@ -99,6 +104,7 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md). | L4 | `engine` | the session and the command registry (everything is a command) | | L5 | `ui-egui`, `mcp` | the swappable desktop front end; the MCP server (headless or bridged to the app) | | apps | `solvecraft`, `solvecraft-cli` | desktop app, headless CLI | + ## Contributing diff --git a/crates/ui-egui/src/help.rs b/crates/ui-egui/src/help.rs index cba2ce03..52e6c977 100644 --- a/crates/ui-egui/src/help.rs +++ b/crates/ui-egui/src/help.rs @@ -1,6 +1,6 @@ -//! The Help menu (the ? button on the application bar): About SolveCraft (version, commit, build -//! date, licences, attributions, fonts), the keyboard shortcuts (read only), and Report an Issue, -//! which copies diagnostic information to the clipboard. +//! The Help menu (the ? button on the application bar): Documentation (the online book), About +//! SolveCraft (version, commit, build date, licences, attributions, fonts), the keyboard shortcuts +//! (read only), and Report an Issue, which copies diagnostic information to the clipboard. use egui::{RichText, vec2}; @@ -10,6 +10,8 @@ use crate::theme::Tokens; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT"); pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE"); +/// The user and developer guide (docs/book, published by .github/workflows/pages.yml). +pub const DOCS_URL: &str = "https://bherbruck.github.io/solvecraft/"; const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md"); const LICENSE_MIT: &str = include_str!("../../../LICENSE-MIT"); const LICENSE_APACHE: &str = include_str!("../../../LICENSE-APACHE"); @@ -42,10 +44,11 @@ pub fn diagnostics(app: &SolveApp) -> String { ) } -/// Run a Help menu item: `about`, `shortcuts`, `report`. +/// Run a Help menu item: `docs`, `about`, `shortcuts`, `report`. pub fn run(app: &mut SolveApp, ctx: &egui::Context, item: &str) { app.help.menu = false; match item { + "docs" => ctx.open_url(egui::OpenUrl::new_tab(DOCS_URL)), "about" => app.help.about = true, "shortcuts" => { app.keymap.open = true; @@ -65,7 +68,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) { let mut pick: Option<&str> = None; let resp = egui::Area::new(egui::Id::new("sc_help_menu")).fixed_pos(anchor).constrain(true).order(egui::Order::Foreground).show(ctx, |ui| { crate::context_menu::menu_frame(ui, |ui| { - for (id, label) in [("about", "About SolveCraft"), ("shortcuts", "Keyboard Shortcuts"), ("report", "Report an Issue…")] { + for (id, label) in + [("docs", "Documentation"), ("about", "About SolveCraft"), ("shortcuts", "Keyboard Shortcuts"), ("report", "Report an Issue…")] + { if crate::context_menu::list_row(ui, &crate::context_menu::Item::action(id, label, "")).is_some() { pick = Some(id); } diff --git a/docs/book/.gitignore b/docs/book/.gitignore new file mode 100644 index 00000000..14e59e8e --- /dev/null +++ b/docs/book/.gitignore @@ -0,0 +1,2 @@ +/src/generated/ +/src/reference/commands.md diff --git a/docs/book/book.toml b/docs/book/book.toml new file mode 100644 index 00000000..0f872bca --- /dev/null +++ b/docs/book/book.toml @@ -0,0 +1,29 @@ +[book] +title = "SolveCraft" +description = "An open-source parametric 3D CAD application in pure Rust: user guide and developer guide." +authors = ["SolveCraft contributors"] +language = "en" +src = "src" + +[build] +build-dir = "../../target/book" +create-missing = false + +[output.html] +git-repository-url = "https://github.com/bherbruck/solvecraft" +edit-url-template = "https://github.com/bherbruck/solvecraft/edit/main/docs/book/{path}" +site-url = "/solvecraft/" +no-section-label = false + +[output.html.search] +enable = true + +[output.html.redirect] +# Pages included from the repository (CONTRIBUTING.md, docs/*.md) keep their repository-relative +# links; these send them to the right place. +"/dev/mcp.html" = "../guide/mcp.html" +"/dev/docs/oracle.html" = "../oracle.html" +"/dev/README.html" = "https://github.com/bherbruck/solvecraft/blob/main/README.md" +"/dev/ATTRIBUTION.html" = "https://github.com/bherbruck/solvecraft/blob/main/ATTRIBUTION.md" +"/dev/AGENTS.html" = "https://github.com/bherbruck/solvecraft/blob/main/AGENTS.md" +"/dev/ROADMAP.html" = "https://github.com/bherbruck/solvecraft/blob/main/ROADMAP.md" diff --git a/docs/book/scripts/enclosure.json b/docs/book/scripts/enclosure.json new file mode 100644 index 00000000..6c287acf --- /dev/null +++ b/docs/book/scripts/enclosure.json @@ -0,0 +1,7 @@ +{"commands": [ + {"command": "solid.box", "params": {"length": 70, "width": 45, "height": 25, "body_name": "Enclosure"}}, + {"command": "solid.fillet", "params": {"edges": [[0, 0, 12], [70, 0, 12], [70, 45, 12], [0, 45, 12]], "radius": 6}}, + {"command": "solid.shell", "params": {"faces": [[35, 22, 25]], "thickness": 2}}, + {"command": "plastic.boss", "params": {"position": [12, 12, 2], "diameter": 7, "height": 14, "hole_diameter": 2.5}}, + {"command": "plastic.boss", "params": {"position": [58, 33, 2], "diameter": 7, "height": 14, "hole_diameter": 2.5}} +]} diff --git a/docs/book/scripts/revolve.json b/docs/book/scripts/revolve.json new file mode 100644 index 00000000..389c8163 --- /dev/null +++ b/docs/book/scripts/revolve.json @@ -0,0 +1,11 @@ +{"commands": [ + {"command": "sketch.create", "params": {"plane": "XZ", "name": "Section"}}, + {"command": "sketch.line", "params": {"points": [[6, 0], [30, 0], [30, 4], [12, 4], [12, 20], [6, 20]], "closed": true}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.revolve", "params": {"axis": "Z", "body_name": "Hub"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Bolt holes"}}, + {"command": "sketch.circle.center", "params": {"center": [22, 0], "diameter": 5}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 4, "operation": "cut", "name": "Bolt hole"}}, + {"command": "solid.pattern.circular", "params": {"features": ["Bolt hole"], "axis": "Z", "count": 6}} +]} diff --git a/docs/book/scripts/sheet-metal.json b/docs/book/scripts/sheet-metal.json new file mode 100644 index 00000000..08b37bb1 --- /dev/null +++ b/docs/book/scripts/sheet-metal.json @@ -0,0 +1,9 @@ +{"commands": [ + {"command": "sheet.manage_rules", "params": {"name": "Steel 1.5", "thickness": "1.5 mm", "bend_radius": "1.5 mm", "k_factor": 0.44, "active": true}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [80, 50]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "sheet.flange", "params": {"type": "base"}}, + {"command": "sheet.flange", "params": {"edges": [[40, 0, 1.5]], "height": 20}}, + {"command": "sheet.flange", "params": {"edges": [[40, 50, 1.5]], "height": 20}} +]} diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md new file mode 100644 index 00000000..f3d18c5f --- /dev/null +++ b/docs/book/src/SUMMARY.md @@ -0,0 +1,34 @@ +# Summary + +[Introduction](introduction.md) + +# User guide + +- [Getting started](guide/getting-started.md) +- [A tour of the window](guide/ui-tour.md) +- [Sketching and constraints](guide/sketching.md) +- [Solid features](guide/features.md) +- [Parameters and expressions](guide/parameters.md) +- [Components and joints](guide/assemblies.md) +- [Sheet metal](guide/sheet-metal.md) +- [Plastic parts](guide/plastic.md) +- [Import and export](guide/files.md) +- [Keyboard shortcuts](guide/shortcuts.md) +- [Driving SolveCraft with an AI agent (MCP)](guide/mcp.md) + +# Developer guide + +- [Contributing](dev/contributing.md) +- [Architecture](dev/architecture.md) +- [The command engine](dev/commands.md) +- [The kernel boundary and truck](dev/kernel.md) +- [Persistent naming](dev/naming.md) +- [Test harnesses](dev/testing.md) +- [Control protocol](dev/control-protocol.md) +- [Fusion oracle](dev/oracle.md) +- [Command parity](dev/parity.md) +- [STEP import](dev/step-import.md) + +# Reference + +- [Command reference](reference/commands.md) diff --git a/docs/book/src/dev/architecture.md b/docs/book/src/dev/architecture.md new file mode 100644 index 00000000..1a6a2f7d --- /dev/null +++ b/docs/book/src/dev/architecture.md @@ -0,0 +1,22 @@ +# Architecture + +SolveCraft is a Cargo workspace in layers. A crate only depends on crates in lower layers, and +`cargo xtask layers` fails the build otherwise. + +{{#include ../../../../README.md:architecture}} + +The web app, `apps/solvecraft-web`, is the desktop front end compiled to wasm with +[trunk](https://trunkrs.dev). `xtask` holds the repository's own tooling: `cargo xtask ci`, +`layers`, `assets`, `oracle`, `parity`, `step-corpus` and `book`. + +A few rules follow from the layering: + +- **Only `crates/kernel` touches truck.** Everything above it sees SolveCraft's own types: + bodies, faces, edges, meshes and measures. See [The kernel boundary](kernel.md). +- **The document knows nothing about commands or the UI.** `crates/doc` holds the parameters, + the feature timeline and the component tree, and evaluates them into a model. +- **Everything the user can do is a command** in `crates/engine`. See + [The command engine](commands.md). +- **The front end is swappable.** Nothing below `ui-egui` depends on egui. The CLI and the MCP + server run the same engine without a window, and `crates/render` has a CPU rasterizer so + headless screenshots work anywhere. diff --git a/docs/book/src/dev/commands.md b/docs/book/src/dev/commands.md new file mode 100644 index 00000000..798cad16 --- /dev/null +++ b/docs/book/src/dev/commands.md @@ -0,0 +1,38 @@ +# The command engine + +Every user-visible action is a **command**: a `CommandSpec` registered in +`crates/engine/src/cmd/`, with a dotted, lower-case id (`solid.extrude`, `sketch.create`, +`timeline.rollback`). The toolbar, dialogs, marking menu, S box, scripts, the CLI, the control +channel and MCP all run commands through `Session::execute(id, params)`. Commands take JSON +parameters and never open dialogs; a dialog is a front-end form that builds the parameters. + +```rust +CommandSpec::new("solid.fillet", "Fillet", fillet) + .at("SOLID", "MODIFY") // toolbar tab and panel ("" = not on the toolbar) + .icon("fillet") // drawn in code, crates/ui-egui/src/icons.rs + .key("F") // default shortcut + .params("edges: [[x,y,z] | {body, index}]; radius: expr; …"), +``` + +`run` is `fn(&mut Session, &Value) -> Result`, and `enabled` is +`fn(&Session) -> Result<(), String>`; the error string is the disabled-button tooltip. The +`params` string is the one-line documentation shown by `solvecraft-cli commands`, MCP's +`list_commands` and the [command reference](../reference/commands.md). + +A command that changes the design is undoable by default: the session snapshots the document +before it runs, and a failing command leaves the document unchanged. Feature commands add a +feature to the timeline and let the document evaluate it; they validate their inputs first +(units, ranges, references) and return `BadParams` instead of building a broken feature. + +Ids used before the 2026-10 rename still resolve through `crates/engine/src/legacy_ids.rs`, so +old scripts, recipes, saved shortcuts and toolbar layouts keep working. + +## Adding a command + +1. Add a `CommandSpec` to the right file in `crates/engine/src/cmd/` and implement `run`. +2. Write engine tests that measure the result: volume, area, and face/edge/vertex counts + against values you can derive by hand. +3. The hostile-input test (`hostile_params_never_panic` in `crates/engine/src/tests.rs`) runs + your command automatically with every documented parameter set to junk. It must return + errors, never panic. +4. For UI work, add a scenario (see [Test harnesses](testing.md)). diff --git a/docs/book/src/dev/contributing.md b/docs/book/src/dev/contributing.md new file mode 100644 index 00000000..a3ac1662 --- /dev/null +++ b/docs/book/src/dev/contributing.md @@ -0,0 +1 @@ +{{#include ../../../../CONTRIBUTING.md}} diff --git a/docs/book/src/dev/control-protocol.md b/docs/book/src/dev/control-protocol.md new file mode 100644 index 00000000..5e076571 --- /dev/null +++ b/docs/book/src/dev/control-protocol.md @@ -0,0 +1 @@ +{{#include ../../../control-protocol.md}} diff --git a/docs/book/src/dev/kernel.md b/docs/book/src/dev/kernel.md new file mode 100644 index 00000000..6611b819 --- /dev/null +++ b/docs/book/src/dev/kernel.md @@ -0,0 +1,37 @@ +# The kernel boundary and truck + +`crates/kernel` is SolveCraft's B-rep kernel. It wraps +[truck](https://github.com/ricosjp/truck) (Apache-2.0) for topology, NURBS geometry, booleans, +tessellation and STEP parsing, and it is the only crate allowed to use truck. Callers get +SolveCraft types (`Body`, face and edge indices, meshes, measures) and a +`KernelError` instead of truck types and panics. + +## `guard` + +truck can panic on geometry it doesn't handle. Every call into it goes through +`solvecraft_kernel::guard`, which catches the panic and turns it into +`KernelError::Internal`, leaving the model unchanged: + +```rust +pub fn guard(what: &str, f: impl FnOnce() -> Result) -> Result +``` + +A failing feature shows an error on the timeline; it never takes the app down. + +## What is our own + +truck has no fillets, chamfers, shells or face edits, so these are SolveCraft code in the +kernel: constant, chord and variable-radius fillets with corner blends, chamfers, shell and +offset, Delete Face, Split Face, Replace Face, sewing, STEP and IGES reading and writing beyond +truck's parser, and face provenance (which input face each output face came from, for +[persistent naming](naming.md)). + +## Vendored truck crates + +Patched copies of `truck-shapeops`, `truck-stepio` and `truck-meshalgo` live in `vendor/`, +wired in with `[patch.crates-io]`. The patches fix robustness bugs found by our tests and the +STEP corpus. `vendor/` is excluded from our fmt and clippy rules, and each patch should go +upstream when it can. + +Some truck assertions only run in debug builds and can fail on valid geometry, so tests and CI +run with `--release`. diff --git a/docs/book/src/dev/naming.md b/docs/book/src/dev/naming.md new file mode 100644 index 00000000..43049955 --- /dev/null +++ b/docs/book/src/dev/naming.md @@ -0,0 +1,22 @@ +# Persistent naming + +A fillet stores the edges it rounds, and a sketch stores the face it sits on. If those were face +and edge *indices*, an upstream edit that adds a face would move the fillet to the wrong edge. +SolveCraft stores **names** instead (`crates/doc/src/naming.rs`): + +- A face of an evaluated body is named after how it was made. A face lying on a face of the + feature's input keeps that face's name; a face new to the feature is named by its role: + `F:side:`, `F:start` and `:end`, `F:blend:` and + so on. +- Pieces of a split face get `#1`, `#2`, … in order along the longest axis. +- An edge is named by the two faces it separates. +- Pattern and mirror copies are named after their sources plus the instance. + +Names are computed lazily, the first time something asks, from the body, the feature that made +it and the model just before that feature. Face provenance from the kernel (which input face an +output face came from) does most of the work. + +When a feature evaluates, each stored reference is resolved by name. If no face has the name +any more, the reference falls back to the geometry it recorded (a point on the face, a normal), +and the feature shows a warning so the user can check it. A reference that matches nothing is +an error, never a guess. diff --git a/docs/book/src/dev/oracle.md b/docs/book/src/dev/oracle.md new file mode 100644 index 00000000..cb69b071 --- /dev/null +++ b/docs/book/src/dev/oracle.md @@ -0,0 +1 @@ +{{#include ../../../oracle.md}} diff --git a/docs/book/src/dev/parity.md b/docs/book/src/dev/parity.md new file mode 100644 index 00000000..a15eb493 --- /dev/null +++ b/docs/book/src/dev/parity.md @@ -0,0 +1 @@ +{{#include ../../../parity.md}} diff --git a/docs/book/src/dev/step-import.md b/docs/book/src/dev/step-import.md new file mode 100644 index 00000000..cb48057c --- /dev/null +++ b/docs/book/src/dev/step-import.md @@ -0,0 +1 @@ +{{#include ../../../step-import.md}} diff --git a/docs/book/src/dev/testing.md b/docs/book/src/dev/testing.md new file mode 100644 index 00000000..1be9377e --- /dev/null +++ b/docs/book/src/dev/testing.md @@ -0,0 +1,40 @@ +# Test harnesses + +`cargo xtask ci` runs everything a pull request must pass: fmt, clippy with `-D warnings`, the +tests in release mode, `xtask layers`, `xtask assets` and the wasm build check. + +| Harness | Where | What it catches | +|---|---|---| +| Unit and engine tests | next to the code, `*_tests.rs` | measured results: volume, area, topology counts against derived values | +| Hostile input | `hostile_params_never_panic`, `crates/engine/src/tests.rs` | every command × every documented parameter × junk values: errors, never panics | +| Sketch fuzz | `crates/engine/src/cmd/sketch_fuzz_tests.rs` | random sketches through the solver and profile finder | +| UI scenarios | `crates/ui-egui/tests/scenarios/*.json` | the real app, headless: clicks, keys, drags, assertions | +| Layout tests | `crates/ui-egui/tests/ui_layout.rs`, `window_widths.rs` | panels and dialogs at every window width | +| Design-file corpus | `apps/solvecraft-cli/tests/corpus` | every saved file version still opens and evaluates the same | +| Kill mid-save | `apps/solvecraft-cli/tests/kill_mid_save.rs` | a save killed at any moment leaves a readable file | +| Fusion oracle | `cargo xtask oracle` → [docs/oracle.md](oracle.md) | parts rebuilt from recipes, compared with Fusion's measurements | +| STEP corpus | `cargo xtask step-corpus` → [docs/step-import.md](step-import.md) | STEP files from Fusion opened and measured | + +## UI scenarios + +A scenario is a JSON list of steps run against the real app with a headless egui context: + +```json +[ + {"start": "sketch.create"}, + {"click": {"plane": "XY"}}, + {"start": "sketch.rectangle.two_point"}, + {"click": {"sketch": [0, 0]}}, + {"click": {"sketch": [40, 30]}}, + {"expect": {"errors": 0}} +] +``` + +A new file in `crates/ui-egui/tests/scenarios/` is picked up automatically. To record a bug +before it's fixed, add a `{"pending": "why"}` step; its test is then ignored with that reason, and +the fix removes the step. + +## Release mode + +CI runs the tests with `--release`, because some truck assertions only exist in debug builds +and fail on valid geometry. If a test fails only in debug, say so in the PR. diff --git a/docs/book/src/guide/assemblies.md b/docs/book/src/guide/assemblies.md new file mode 100644 index 00000000..94aefac0 --- /dev/null +++ b/docs/book/src/guide/assemblies.md @@ -0,0 +1,22 @@ +# Components and joints + +A design is a tree of **components**. **New Component** (`component.create`) adds one, and +**Create Components from Bodies** (`component.from_bodies`) turns bodies into components. Each +component has its own origin, bodies, sketches and timeline entries. The **active component** +(the radio button in the browser) receives new sketches and features; the others are drawn +faded. + +A component is placed by its **occurrences**. Paste creates another instance of the same +component, and **Paste New** creates an independent copy. Move an occurrence with the move +triad, then **Capture Position** to keep it or **Revert** to put it back. SolveCraft asks before +the next command if a move wasn't captured. **Ground** fixes an occurrence in place. + +**Joints** (`joint.create`, J) connect two components by joint origins that snap to faces, edges, +circles and vertices: rigid, revolute, slider, cylindrical, pin-slot, planar and ball, with +offsets, flip and limits. **As-built Joint** keeps the current position, **Rigid Group** locks +components together, **Drive Joints** moves a joint through its values, and **Motion Link** +couples two joints. Contact sets stop motion where bodies would pass through each other, +**Motion Study** keys joint values along a timeline, and **Interference** shows overlapping +volumes in red. + +STEP export writes the component tree as an assembly. diff --git a/docs/book/src/guide/features.md b/docs/book/src/guide/features.md new file mode 100644 index 00000000..cc69c0fb --- /dev/null +++ b/docs/book/src/guide/features.md @@ -0,0 +1,24 @@ +# Solid features + +Features live on the **timeline** and rebuild incrementally from the first one that changed. +Each one can be edited (double-click), suppressed, renamed, reordered (if nothing it depends on +moves after it), rolled back to, and deleted. Deleting asks first when other features depend on +it. + +| Panel | Features | +|---|---| +| Create | extrude (one side, two sides, symmetric, taper, start offset, to a distance or through all; new body, join, cut, intersect), revolve, sweep, loft, box, cylinder, sphere, torus, coil, pipe, rib, web, emboss, hole (simple, counterbore, countersink, tapped; on faces or at sketch points), thread, rectangular/circular/path patterns, mirror | +| Modify | press pull, fillet, chamfer, shell, draft, scale, combine, split body, offset face, replace face, move/copy, align, remove | +| Construct | offset plane, plane at angle | +| Inspect | measure, section analysis, interference, curvature comb and map, zebra, draft, minimum radius, isocurves, centre of mass | + +Fillets and chamfers handle straight edges between planar faces, whole smooth loops, runs of +neighbouring edges with mitred or spherical corners, and closed chains of curved edges (a rolling +ball). See the roadmap's known limitations for the rest. + +Feature references to faces and edges are stored by name, so an upstream edit that moves geometry +keeps them on the right face (see [Persistent naming](../dev/naming.md)). When a reference can't +be found, the feature warns instead of silently picking another face. + +Press **Delete** on anything selected (bodies, faces, sketches, features, planes, components, +canvases). It runs as one undoable step and lists the features that would go with it or fail. diff --git a/docs/book/src/guide/files.md b/docs/book/src/guide/files.md new file mode 100644 index 00000000..663bd9c2 --- /dev/null +++ b/docs/book/src/guide/files.md @@ -0,0 +1,19 @@ +# Import and export + +| Format | Read | Write | +|---|---|---| +| `.solvecraft` (JSON design, versioned) | yes | yes | +| STEP (AP203, AP214, AP242) | solids, assemblies, units, names, colours | AP242 with names, colours and the component tree | +| IGES 5.3 | manifold solids, or trimmed surfaces sewn into solids | manifold solids with names and colours | +| 3MF, STL, OBJ | mesh bodies | yes | +| DXF, SVG | into a sketch | sketches and flat patterns as DXF | +| PNG, JPEG | canvases and decals | | + +Opening a STEP or IGES file creates a design with the file's bodies as a base feature, and +inserting one adds them to the current design. Later features build on them. Mesh bodies render, +measure, move and export, but solid features need B-rep bodies. + +Saves are atomic: a crash at any moment leaves either the old file or the new one, and the +replaced version is kept as `.bak`. Unsaved changes are autosaved to a recovery folder, +and the next launch offers to recover them. Older design files are upgraded on open, and files +from a newer version are refused with a message. diff --git a/docs/book/src/guide/getting-started.md b/docs/book/src/guide/getting-started.md new file mode 100644 index 00000000..9112935e --- /dev/null +++ b/docs/book/src/guide/getting-started.md @@ -0,0 +1,56 @@ +# Getting started + +## Desktop + +Download the latest build from the +[releases page](https://github.com/bherbruck/solvecraft/releases), or build it yourself with +stable Rust (1.90 or newer): + +```sh +git clone https://github.com/bherbruck/solvecraft && cd solvecraft +cargo run --release -p solvecraft -- --sample # opens with a sample part +``` + +`solvecraft part.step` opens a STEP, IGES, 3MF, STL or OBJ file as a new design, and +`solvecraft design.solvecraft` opens a saved design. Windows builds cross-compile from Linux with +`cargo xwin build --release --target x86_64-pc-windows-msvc -p solvecraft`. + +## Web + +The same app runs in the browser: [open SolveCraft on the web](../app/) (`?sample` opens the sample +part, `?webgl` forces WebGL2 where WebGPU is missing). Designs are saved in the browser's private +file storage, which needs HTTPS or localhost. To build it yourself: + +```sh +rustup target add wasm32-unknown-unknown +cd apps/solvecraft-web && trunk serve --release +``` + +## CLI + +`solvecraft-cli` runs the same commands without a window: + +```sh +cargo run --release -p solvecraft-cli -- commands # every command, as JSON +cargo run --release -p solvecraft-cli -- run examples/bracket.json --out bracket.step +cargo run --release -p solvecraft-cli -- eval examples/bracket.json # volume, area, faces… +cargo run --release -p solvecraft-cli -- snapshot examples/bracket.json --out bracket.png +cargo run --release -p solvecraft-cli -- exec solid.box '{"length": 20, "width": 10, "height": 5}' +``` + +A script is a list of commands: + +```json +{"commands": [ + {"command": "sketch.create", "params": {"plane": "XY"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 20}}, + {"command": "solid.fillet", "params": {"edges": [[0, 0, 10]], "radius": 3}} +]} +``` + +![The bracket example, rendered by solvecraft-cli snapshot](../generated/bracket.png) + +Units are millimetres and degrees unless an expression says otherwise (`"1 in"`, `"30 deg"`), +and Z is up. diff --git a/docs/book/src/guide/mcp.md b/docs/book/src/guide/mcp.md new file mode 100644 index 00000000..49f0c0a9 --- /dev/null +++ b/docs/book/src/guide/mcp.md @@ -0,0 +1 @@ +{{#include ../../../mcp.md}} diff --git a/docs/book/src/guide/parameters.md b/docs/book/src/guide/parameters.md new file mode 100644 index 00000000..b3da3ecb --- /dev/null +++ b/docs/book/src/guide/parameters.md @@ -0,0 +1,21 @@ +# Parameters and expressions + +Every dimension and feature input is a **model parameter** (`d1`, `d2`, …). **User parameters** +are ones you add yourself, with a unit, an expression and an optional comment. Change them in +**Change Parameters** (`parameters.change`). The timeline rebuilds from the first feature that +uses them. + +Expressions are unit-aware: `2 * width + 5 mm`, `angle / 2`, `sqrt(area)`, `1 in + 3 mm`, using +`+ - * / ^`, parentheses, `sin cos tan asin acos atan atan2 sqrt abs min max floor ceil round mod +trunc` and unit conversion. A length where an angle is expected (or the other way round) is an +error, not a silent conversion. Cycles are reported with their path (`a → b → a`), renaming a +parameter updates every expression, dimension and feature input that uses it, and a parameter +in use can't be deleted (the error lists its users). + +The design's units (Document Settings in the browser: mm, cm, m, in or ft) set how a bare number +typed into a feature is read. In an inch design `2` means 2 in. Changing the units never moves +existing geometry. + +Parameters export to and import from CSV or JSON (`parameters.export`, `parameters.import`). +**Configurations** (`config.*`) keep a table of rows, each with its own parameter values and +suppressed features, switched with `config.activate`. diff --git a/docs/book/src/guide/plastic.md b/docs/book/src/guide/plastic.md new file mode 100644 index 00000000..50389c84 --- /dev/null +++ b/docs/book/src/guide/plastic.md @@ -0,0 +1,13 @@ +# Plastic parts + +The PLASTIC tab has a small enclosure subset: + +- **Boss** (`plastic.boss`): screw bosses with a hole, fillets and optional ribs. +- **Lip / Groove** (`plastic.lip`): the mating lip and groove along a shell's opening. +- **Snap Fit** (`plastic.snap_fit`): a cantilever snap hook and its catch. +- **Rest** (`plastic.rest`): a pad for a part to rest on. +- **Plastic Rules** (`plastic.manage_rules`, `plastic.assign_rule`): wall thickness, draft and + clearance per material, used as the features' defaults. + +Rib and web (on the SOLID tab) complete the set. These features are checked against +hand-computed geometry rather than a Fusion oracle. diff --git a/docs/book/src/guide/sheet-metal.md b/docs/book/src/guide/sheet-metal.md new file mode 100644 index 00000000..78d5c7de --- /dev/null +++ b/docs/book/src/guide/sheet-metal.md @@ -0,0 +1,15 @@ +# Sheet metal + +The SHEET METAL tab builds parts from a sheet of constant thickness under a **sheet metal rule** +(thickness, bend radius, K-factor), set in **Sheet Metal Rules** (`sheet.manage_rules`). + +- **Flange** (`sheet.flange`): a base flange from a sketch profile, a contour flange from an open + sketch, or an edge flange from a part edge. +- **Hem** (`sheet.hem`) and **Fold** (`sheet.fold`) along a sketch line. +- **Unfold / Refold** (`sheet.unfold`, `sheet.refold`) for working on the flat part. +- **Convert** (`sheet.convert`) turns a solid of constant thickness into sheet metal. +- **Create Flat Pattern** (`sheet.flat_pattern`) shows the flat part with its bend lines and a + bend table, and exports it as DXF. + +Bends follow the rule's K-factor, so flat lengths match what a press brake will make. The +oracle checks these against parts built in Fusion. diff --git a/docs/book/src/guide/shortcuts.md b/docs/book/src/guide/shortcuts.md new file mode 100644 index 00000000..52b29d3d --- /dev/null +++ b/docs/book/src/guide/shortcuts.md @@ -0,0 +1,29 @@ +# Keyboard shortcuts + +Command shortcuts can be changed in File › Keyboard Shortcuts; these are the defaults. + +| Key | Command | Key | Command | +|---|---|---|---| +| L | Line | E | Extrude | +| R | Two-Point Rectangle | Q | Press Pull | +| C | Center Diameter Circle | F | Fillet | +| D | Sketch Dimension | H | Hole | +| X | Construction | M | Move/Copy | +| O | Offset | J | Joint | +| T | Trim | I | Measure | +| P | Project | A | Appearance | +| Ctrl+Z | Undo | Ctrl+Y | Redo | +| Ctrl+N | New Design | Ctrl+O | Open | +| Ctrl+S | Save | Delete | Delete the selection | + +Keys handled by the window itself: + +| Key | Action | +|---|---| +| S | the S box: search every command at the cursor | +| V | show or hide the selected bodies, sketches and planes | +| F2 | rename the selected browser or timeline item | +| F6 | fit the view to the model | +| Esc | cancel the running command, or clear the selection | + +Help › Keyboard Shortcuts in the app lists the current bindings, including any you changed. diff --git a/docs/book/src/guide/sketching.md b/docs/book/src/guide/sketching.md new file mode 100644 index 00000000..d10bf732 --- /dev/null +++ b/docs/book/src/guide/sketching.md @@ -0,0 +1,34 @@ +# Sketching and constraints + +A sketch lies on a plane: an origin plane, a construction plane or a planar face. Start one with +**Create Sketch** (`sketch.create`) and click a plane, or pick a face first. **Finish Sketch** +returns to the model. + +## Geometry + +Lines and polylines (L), rectangles (two-point R, three-point, centre), circles (centre C, +two-point, three-point, tangent), arcs (three-point, centre, tangent), polygons, slots, ellipses, +fit-point and control-point splines, conics, points and text. **Construction** (X) turns curves +into reference geometry that doesn't form profiles. Modify tools include trim (T), extend, break, +fillet, chamfer, offset (O), mirror, patterns, move/copy and scale. + +**Project** (P) and **Intersect** bring model edges into the sketch as linked geometry that +follows later changes. Sketching on a face projects its edges automatically. + +## Constraints and dimensions + +The solver is SolveCraft's own (damped Gauss–Newton with degree-of-freedom analysis). Constraints: +coincident, horizontal/vertical, parallel, perpendicular, tangent, equal, concentric, collinear, +midpoint, symmetry, fix, curvature (G2) and polygon. **Sketch Dimension** (D) adds linear, +aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a parameter +(`d1`, `d2`, …), so it can hold an expression. + +Under-constrained geometry is blue and fully constrained geometry turns dark, and the status +shows the remaining degrees of freedom. A constraint or dimension that would over-constrain the +sketch is refused rather than applied. Drag a point to move it; the solver keeps every +constraint while you drag. + +## Profiles + +Closed loops become profiles automatically, including regions split by crossing lines and +islands inside outlines. Extrude, revolve, sweep and loft pick profiles. diff --git a/docs/book/src/guide/ui-tour.md b/docs/book/src/guide/ui-tour.md new file mode 100644 index 00000000..ee6d96ea --- /dev/null +++ b/docs/book/src/guide/ui-tour.md @@ -0,0 +1,29 @@ +# A tour of the window + +![The SolveCraft window](../generated/sample-plate.png) + +- **Title bar.** On Windows and Linux the application bar is the window's title bar: File, save, + undo and redo, the start page, the theme, Help, and one tab per open design (each with its own + undo history, selection and camera). +- **Toolbar.** The workspace switcher (DESIGN) sits on the left. The tabs (SOLID, SURFACE, + SHEET METAL, PLASTIC, UTILITIES, and SKETCH while you sketch) hold panels of commands. Click a + panel's name for all its commands. +- **Browser** (left). The design as a tree: document settings and units, named views, the + origin, bodies, sketches, construction planes, canvases, joints and components. The eye shows or + hides an item, a component's radio button makes it the active component, and right-clicking any + row gives that item's menu. Rows drag into groups and other components. +- **Viewport.** Click to select, Shift or Ctrl to add, drag for a window or crossing box. The + wheel zooms at the cursor, the middle button pans and Shift+middle orbits (the mouse preset in + Preferences switches to SolidWorks or Inventor style). The view cube and the navigation bar + change the view. Right-click for the marking menu: eight commands around the cursor, plus a + menu for whatever is under it. +- **Dialogs** dock on the right with a live preview. Most features also show drag handles and a + value box in the viewport. +- **Timeline** (bottom). Every feature in order. Double-click to edit, drag the marker to roll + back, and drag features to reorder them. +- **S box.** Press S anywhere for a command search at the cursor. Pin the commands you use most + to its top. + +Preferences (File › Preferences) hold the theme, the units of new designs, the mouse preset, the +visual style and autosave. Help › Keyboard Shortcuts lists every key, and File › Keyboard +Shortcuts changes them. diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md new file mode 100644 index 00000000..daef1489 --- /dev/null +++ b/docs/book/src/introduction.md @@ -0,0 +1,32 @@ +# SolveCraft + +SolveCraft is an open-source parametric 3D CAD application written in pure Rust. It works the way +Fusion-style modellers do: sketch on a plane, constrain and dimension the sketch, turn profiles +into solids with features on a timeline, and change a parameter to watch the whole timeline +rebuild. + +![The sample plate in SolveCraft](generated/sample-plate.png) + +It runs as: + +- a **desktop app** for Windows and Linux (egui + wgpu); +- a **web app** in the browser (wasm, WebGPU or WebGL2), [try it here](app/); +- a **headless CLI**, `solvecraft-cli`, that runs command scripts, measures and exports; +- an **MCP server** that AI agents model with, headless or driving the running app. + +Everything the user can do is a *command* with JSON parameters. The toolbar, the dialogs, scripts, +the CLI, the control channel and MCP all run the same commands, so anything you can click you can +also script, test or hand to an agent. + +SolveCraft is early. Sketching, solid modelling, parameters, components with joints, sheet metal +and a plastic-enclosure subset work; surfaces, drawings, CAM and simulation are deferred. The +[roadmap](https://github.com/bherbruck/solvecraft/blob/main/ROADMAP.md) has the details, and +[command parity](dev/parity.md) and the [Fusion oracle](dev/oracle.md) measure how far along it +is. + +This book has two halves: the **user guide** for people modelling with SolveCraft, and the +**developer guide** for people working on it. The [command reference](reference/commands.md) is +generated from the code on every build. + +SolveCraft is licensed MIT OR Apache-2.0. It is a clean-room implementation and is not affiliated +with Autodesk; Fusion is a trademark of Autodesk, Inc. diff --git a/xtask/src/book.rs b/xtask/src/book.rs new file mode 100644 index 00000000..cdd18a7f --- /dev/null +++ b/xtask/src/book.rs @@ -0,0 +1,123 @@ +//! `cargo xtask book`: generate the book's command reference and screenshots, then build it. +//! +//! The command reference (`docs/book/src/reference/commands.md`) comes from +//! `solvecraft-cli commands`, so it can't drift from the registry. The screenshots in +//! `docs/book/src/generated/` are CPU renders by `solvecraft-cli snapshot` of `examples/*.json` +//! and `docs/book/scripts/*.json`, plus the app screenshot from `docs/screenshots/`. Both are +//! gitignored and attributed in ATTRIBUTION.md. The book itself is built by `mdbook` (a separate +//! tool, not a dependency) into `target/book`. + +use std::path::Path; + +use serde_json::Value; + +use crate::cargo; + +/// Toolbar tabs in toolbar order; commands not on the toolbar come last, grouped by id prefix. +const TABS: &[&str] = &["SOLID", "SKETCH", "SURFACE", "SHEET METAL", "PLASTIC"]; +const SNAPSHOT_SIZE: (&str, &str) = ("960", "600"); + +pub fn run(root: &Path, build: bool) -> Result<(), String> { + let book = root.join("docs/book"); + let mut c = cargo(); + c.args(["build", "-q", "--release", "-p", "solvecraft-cli"]); + crate::run(c, "cargo build -p solvecraft-cli")?; + + let out = cli().arg("commands").output().map_err(|e| format!("solvecraft-cli: {e}"))?; + if !out.status.success() { + return Err(format!("solvecraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr))); + } + let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?; + let md = reference(cmds.as_array().map(Vec::as_slice).unwrap_or_default()); + write(&book.join("src/reference/commands.md"), &md)?; + + let gen_dir = book.join("src/generated"); + std::fs::create_dir_all(&gen_dir).map_err(|e| format!("{}: {e}", gen_dir.display()))?; + let mut scripts = vec![root.join("examples/bracket.json")]; + scripts.extend(json_files(&book.join("scripts"))?); + for s in &scripts { + let stem = s.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_default(); + let png = gen_dir.join(format!("{stem}.png")); + let mut c = cli(); + c.arg("snapshot").arg(s).arg("--out").arg(&png).args(["--width", SNAPSHOT_SIZE.0, "--height", SNAPSHOT_SIZE.1]); + let o = c.output().map_err(|e| format!("solvecraft-cli snapshot: {e}"))?; + if !o.status.success() { + return Err(format!("snapshot of {} failed:\n{}", s.display(), String::from_utf8_lossy(&o.stderr))); + } + println!("book: {}", png.strip_prefix(root).unwrap_or(&png).display()); + } + let shot = "sample-plate.png"; + std::fs::copy(root.join("docs/screenshots").join(shot), gen_dir.join(shot)).map_err(|e| format!("{shot}: {e}"))?; + + if build { + let mut c = std::process::Command::new("mdbook"); + c.arg("build").arg(&book); + crate::run(c, "mdbook build docs/book").map_err(|e| format!("{e}\n(install mdbook with `cargo install mdbook`)"))?; + println!("book: built into target/book"); + } + Ok(()) +} + +/// The release solvecraft-cli just built. +fn cli() -> std::process::Command { + let mut c = cargo(); + c.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--"]); + c +} + +fn write(path: &Path, text: &str) -> Result<(), String> { + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + } + std::fs::write(path, text).map_err(|e| format!("{}: {e}", path.display())) +} + +fn json_files(dir: &Path) -> Result, String> { + let rd = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut v: Vec<_> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "json")).collect(); + v.sort(); + Ok(v) +} + +/// The command reference page: one section per toolbar tab and panel, in registry order. +fn reference(cmds: &[Value]) -> String { + let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string(); + let mut md = format!( + "# Command reference\n\nGenerated by `cargo xtask book` from `solvecraft-cli commands` ({} commands). Every command runs the same \ + way from the toolbar, a script, `solvecraft-cli exec ''`, the control channel and MCP. Parameters marked `?` are \ + optional; `expr` is a number or an expression with units.\n", + cmds.len() + ); + let mut sections: Vec<(String, Vec<&Value>)> = Vec::new(); + for tab in TABS { + for c in cmds.iter().filter(|c| s(c, "tab") == *tab) { + let title = format!("{tab} › {}", s(c, "panel")); + add(&mut sections, title, c); + } + } + for c in cmds.iter().filter(|c| !TABS.contains(&s(c, "tab").as_str())) { + let id = s(c, "id"); + let prefix = id.split('.').next().unwrap_or_default().to_string(); + add(&mut sections, format!("Other: {prefix}.*"), c); + } + for (title, list) in sections { + md.push_str(&format!("\n## {title}\n\n")); + for c in list { + let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default(); + md.push_str(&format!("- `{}` **{}**{key}", s(c, "id"), s(c, "label"))); + let p = s(c, "params"); + if !p.is_empty() { + md.push_str(&format!("
``{}``", p.replace('\n', " "))); + } + md.push('\n'); + } + } + md +} + +fn add<'a>(sections: &mut Vec<(String, Vec<&'a Value>)>, title: String, c: &'a Value) { + match sections.iter_mut().find(|(t, _)| *t == title) { + Some((_, v)) => v.push(c), + None => sections.push((title, vec![c])), + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 5f40aebc..6e7f546d 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -4,6 +4,7 @@ //! `std::process::Command`. mod assets; +mod book; mod layers; mod parity; @@ -24,6 +25,9 @@ commands: step-corpus [DIR] import every DIR/*/part.step (default plan/fusion/oracle) and compare with measure.json; writes docs/step-import.md + book [--no-build] + generate docs/book's command reference and screenshots, then mdbook build + into target/book 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) "; @@ -37,6 +41,7 @@ fn main() -> ExitCode { Some("parity") => parity::run(&root(), rest.contains(&"--refresh")), Some("oracle") => cmd_oracle(), Some("step-corpus") => cmd_step_corpus(rest.first().copied()), + Some("book") => book::run(&root(), !rest.contains(&"--no-build")), Some("ci") => cmd_ci(), Some("wasm") => cmd_wasm(), Some("-h" | "--help" | "help") | None => {