Documentation site and CI workflows

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 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 13:25:38 +00:00
1 parent 2c3fec087e
commit 0d7e456bc1
35 files changed
+792 -5

No files matched your search

+22
View File
@@ -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.
+38
View File
@@ -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<Value>`, 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)).
+1
View File
@@ -0,0 +1 @@
{{#include ../../../../CONTRIBUTING.md}}
+1
View File
@@ -0,0 +1 @@
{{#include ../../../control-protocol.md}}
+37
View File
@@ -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<T>(what: &str, f: impl FnOnce() -> Result<T>) -> Result<T>
```
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`.
+22
View File
@@ -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<feature>:side:<sketch curve>`, `F<feature>:start` and `:end`, `F<feature>:blend:<k>` 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.
+1
View File
@@ -0,0 +1 @@
{{#include ../../../oracle.md}}
+1
View File
@@ -0,0 +1 @@
{{#include ../../../parity.md}}
+1
View File
@@ -0,0 +1 @@
{{#include ../../../step-import.md}}
+40
View File
@@ -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.