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 <[email protected]>
This commit is contained in:
1 parent
2c3fec087e
commit
0d7e456bc1
35 files changed
+792
-5
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,10 @@ storytold "craft" family.
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
||||
<!-- ANCHOR: 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 |
|
||||
<!-- ANCHOR_END: architecture -->
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/src/generated/
|
||||
/src/reference/commands.md
|
||||
@@ -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"
|
||||
@@ -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}}
|
||||
]}
|
||||
@@ -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}}
|
||||
]}
|
||||
@@ -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}}
|
||||
]}
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)).
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../../CONTRIBUTING.md}}
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../control-protocol.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<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`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../oracle.md}}
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../parity.md}}
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../step-import.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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 `<file>.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.
|
||||
@@ -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}}
|
||||
]}
|
||||
```
|
||||
|
||||

|
||||
|
||||
Units are millimetres and degrees unless an expression says otherwise (`"1 in"`, `"30 deg"`),
|
||||
and Z is up.
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../../mcp.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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,29 @@
|
||||
# A tour of the window
|
||||
|
||||

|
||||
|
||||
- **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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -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<Vec<std::path::PathBuf>, 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 <id> '<json>'`, 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!("<br>``{}``", 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])),
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user