Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8beaedfa4 | ||
|
|
4af2d06007 | ||
|
|
81ef51c6ce | ||
|
|
1fb4783f67 | ||
|
|
a94bf2562b | ||
|
|
26b7b7336f | ||
|
|
8539ac1b40 | ||
|
|
78aa2aacfb | ||
|
|
92dbd240ae | ||
|
|
71cda4e7ee | ||
|
|
a91596c1b8 | ||
|
|
7f98dd1c47 | ||
|
|
8cce89d78a | ||
|
|
6ce472c29e | ||
|
|
4e39869023 | ||
|
|
3f1b07d35c | ||
|
|
f6e7d754e5 | ||
|
|
6c06168fa7 | ||
|
|
af536a7907 | ||
|
|
ee1b56cac4 | ||
|
|
344e52e207 | ||
|
|
35555c0b9f | ||
|
|
1d3048062d | ||
|
|
da56d180bf | ||
|
|
33f0b9e3cc | ||
|
|
cb5144b6e2 | ||
|
|
cc2ee6a0f9 | ||
|
|
9bfd1158d0 | ||
|
|
58c0ed8a90 | ||
|
|
6dc6372b13 | ||
|
|
adecca41f6 | ||
|
|
36f49811ab | ||
|
|
09bf9cafdf |
No files matched your search
@@ -1,14 +1,19 @@
|
|||||||
SolveCraft is open-source parametric 3D CAD in Rust: sketch, constrain, extrude, and change a
|
SolidCraft is open-source parametric 3D CAD in Rust: sketch, constrain, extrude, and change a
|
||||||
parameter to watch the timeline rebuild. [Documentation](https://bherbruck.github.io/solvecraft/)
|
parameter to watch the timeline rebuild. [Documentation](https://bherbruck.github.io/solidcraft/)
|
||||||
· [try it in the browser](https://bherbruck.github.io/solvecraft/app/) ·
|
· [try it in the browser](https://bherbruck.github.io/solidcraft/app/) ·
|
||||||
[contributing](https://github.com/bherbruck/solvecraft/blob/main/CONTRIBUTING.md) ·
|
[contributing](https://github.com/bherbruck/solidcraft/blob/main/CONTRIBUTING.md) ·
|
||||||
[issues](https://github.com/bherbruck/solvecraft/issues). AI agents can model with it over MCP:
|
[issues](https://github.com/bherbruck/solidcraft/issues). AI agents can model with it over MCP:
|
||||||
`solvecraft-cli mcp` ([how](https://bherbruck.github.io/solvecraft/guide/mcp.html)).
|
`solvecraft-cli mcp` ([how](https://bherbruck.github.io/solidcraft/guide/mcp.html)).
|
||||||
|
|
||||||
|
Compatibility names are unchanged: existing `.solvecraft` designs, `solvecraft-cli`, package
|
||||||
|
IDs and archive filenames still work. Published 0.2.0 and 0.2.1 releases keep their original
|
||||||
|
SolveCraft branding. Documentation, the browser app and the Flatpak feed use the new
|
||||||
|
SolidCraft project Pages URLs; old-name Pages addresses are not permanent aliases.
|
||||||
|
|
||||||
**Which file?**
|
**Which file?**
|
||||||
- **Windows:** `SolveCraft-{version}-windows-x64.exe` runs as is. Or install the `.msi`, or unzip the
|
- **Windows:** `SolveCraft-{version}-windows-x64.exe` runs as is. Or install the `.msi`, or unzip the
|
||||||
`-portable.zip` (x64, x86 and arm64).
|
`-portable.zip` (x64, x86 and arm64).
|
||||||
- **macOS 11+** (Apple silicon and Intel): open the `.dmg` and drag SolveCraft onto Applications.
|
- **macOS 11+** (Apple silicon and Intel): open the `.dmg` and drag `SolveCraft.app` onto Applications.
|
||||||
The CLI is in `solvecraft-cli-{version}-macos-universal.zip`.
|
The CLI is in `solvecraft-cli-{version}-macos-universal.zip`.
|
||||||
- **Linux** (x86_64 and aarch64, glibc 2.35+): the `.AppImage` (`chmod +x`, then run it), the
|
- **Linux** (x86_64 and aarch64, glibc 2.35+): the `.AppImage` (`chmod +x`, then run it), the
|
||||||
`.deb` or `.rpm`, the `.flatpak` (`flatpak install --user <file>`), or the `.tar.gz`.
|
`.deb` or `.rpm`, the `.flatpak` (`flatpak install --user <file>`), or the `.tar.gz`.
|
||||||
@@ -16,10 +21,10 @@ parameter to watch the timeline rebuild. [Documentation](https://bherbruck.githu
|
|||||||
- **Web:** `solvecraft-web-{version}.zip` is a static site to host yourself.
|
- **Web:** `solvecraft-web-{version}.zip` is a static site to host yourself.
|
||||||
- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing`.
|
- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing`.
|
||||||
|
|
||||||
The builds aren't signed yet. On macOS, right-click SolveCraft in Applications and choose
|
The builds aren't signed yet. On macOS, right-click `SolveCraft.app` in Applications and choose
|
||||||
**Open** the first time (or run `xattr -dr com.apple.quarantine /Applications/SolveCraft.app`).
|
**Open** the first time (or run `xattr -dr com.apple.quarantine /Applications/SolveCraft.app`).
|
||||||
On Windows, SmartScreen may say "Windows protected your PC": **More info**, then **Run anyway**.
|
On Windows, SmartScreen may say "Windows protected your PC": **More info**, then **Run anyway**.
|
||||||
Licences are built in (Help ▸ About ▸ Licences, `solvecraft-cli licences`); the packages also
|
Licences are built in (Help ▸ About ▸ Licences, `solvecraft-cli licences`); the packages also
|
||||||
install `THIRD-PARTY-LICENSES.txt`. Known limitations:
|
install `THIRD-PARTY-LICENSES.txt`. Known limitations:
|
||||||
[ROADMAP.md](https://github.com/bherbruck/solvecraft/blob/main/ROADMAP.md#known-limitations).
|
[ROADMAP.md](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations).
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# The documentation site on GitHub Pages: the book (docs/book) at /solvecraft/ and the web app
|
# The documentation site on GitHub Pages: the book (docs/book) at /solidcraft/ and the web app
|
||||||
# at /solvecraft/app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the
|
# at /solidcraft/app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the
|
||||||
# repository is private.
|
# repository is private.
|
||||||
name: Pages
|
name: Pages
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
run: cargo xtask book
|
run: cargo xtask book
|
||||||
- name: Build the web app
|
- name: Build the web app
|
||||||
working-directory: apps/solvecraft-web
|
working-directory: apps/solvecraft-web
|
||||||
run: trunk build --release --public-url /solvecraft/app/
|
run: trunk build --release --public-url /solidcraft/app/
|
||||||
- name: Assemble the site
|
- name: Assemble the site
|
||||||
run: |
|
run: |
|
||||||
mkdir -p site
|
mkdir -p site
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
|
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
|
||||||
{ echo "version=$VERSION"; echo "date=$(date -u +%Y-%m-%d)"; echo "prerelease=$pre"; } >>"$GITHUB_OUTPUT"
|
{ echo "version=$VERSION"; echo "date=$(date -u +%Y-%m-%d)"; echo "prerelease=$pre"; } >>"$GITHUB_OUTPUT"
|
||||||
echo "### SolveCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
echo "### SolidCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# The app icon in every size, drawn by solvecraft-cli (no image files are committed), for the
|
# The app icon in every size, drawn by solvecraft-cli (no image files are committed), for the
|
||||||
# Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own.
|
# Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own.
|
||||||
@@ -357,7 +357,7 @@ jobs:
|
|||||||
sha256sum --check SHA256SUMS.txt
|
sha256sum --check SHA256SUMS.txt
|
||||||
# Stage all packages before publishing. Published tags are never replaced.
|
# Stage all packages before publishing. Published tags are never replaced.
|
||||||
gh release create "$tag" --draft --prerelease --latest=false \
|
gh release create "$tag" --draft --prerelease --latest=false \
|
||||||
--title "SolveCraft v$VERSION (nightly)" --target "$GITHUB_SHA" \
|
--title "SolidCraft v$VERSION (nightly)" --target "$GITHUB_SHA" \
|
||||||
--notes-file ../notes.md --generate-notes -- *
|
--notes-file ../notes.md --generate-notes -- *
|
||||||
gh release edit "$tag" --draft=false --prerelease --latest=false
|
gh release edit "$tag" --draft=false --prerelease --latest=false
|
||||||
echo "### Published nightly: $tag (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
echo "### Published nightly: $tag (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||||
@@ -386,15 +386,15 @@ jobs:
|
|||||||
while IFS= read -r asset; do
|
while IFS= read -r asset; do
|
||||||
if [ -n "$asset" ]; then gh release delete-asset "$tag" "$asset" --yes; fi
|
if [ -n "$asset" ]; then gh release delete-asset "$tag" "$asset" --yes; fi
|
||||||
done <<<"$assets"
|
done <<<"$assets"
|
||||||
gh release edit "$tag" --draft --title "SolveCraft v$VERSION" \
|
gh release edit "$tag" --draft --title "SolidCraft v$VERSION" \
|
||||||
--target "$GITHUB_SHA" --prerelease="$PRERELEASE"
|
--target "$GITHUB_SHA" --prerelease="$PRERELEASE"
|
||||||
gh release upload "$tag" --clobber -- *
|
gh release upload "$tag" --clobber -- *
|
||||||
else
|
else
|
||||||
gh release create "$tag" --draft --title "SolveCraft v$VERSION" \
|
gh release create "$tag" --draft --title "SolidCraft v$VERSION" \
|
||||||
--target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${pre[@]}" -- *
|
--target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${pre[@]}" -- *
|
||||||
fi
|
fi
|
||||||
{
|
{
|
||||||
echo "Draft **SolveCraft v$VERSION** at \`${GITHUB_SHA::9}\`:"
|
echo "Draft **SolidCraft v$VERSION** at \`${GITHUB_SHA::9}\`:"
|
||||||
echo
|
echo
|
||||||
sed 's/^/ /' SHA256SUMS.txt
|
sed 's/^/ /' SHA256SUMS.txt
|
||||||
} >>"$GITHUB_STEP_SUMMARY"
|
} >>"$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
# SolveCraft — instructions for agents
|
# SolidCraft — instructions for agents
|
||||||
|
|
||||||
SolveCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application in the style of
|
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
|
||||||
Autodesk Fusion: sketches with a constraint solver, a parametric timeline of features, a B-rep
|
sketches with a constraint solver, a parametric timeline of features, a B-rep
|
||||||
kernel, and a desktop UI that agents can drive. Siblings with the same conventions live next to
|
kernel, and a desktop UI that agents can drive. Siblings with the same conventions live next to
|
||||||
it in `storytold/`: `cadcraft` (AutoCAD-style, the closest relative), `photocraft`, `vectorcraft`,
|
it in `storytold/`: `cadcraft` (AutoCAD-style, the closest relative), `photocraft`, `vectorcraft`,
|
||||||
`gridcraft`, `designcraft`, `wordcraft`, `deckcraft`, `soundcraft`. Repos don't share code; when
|
`gridcraft`, `designcraft`, `wordcraft`, `deckcraft`, `soundcraft`. Repos don't share code; when
|
||||||
code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md.
|
code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md.
|
||||||
|
|
||||||
|
## Product name and compatibility
|
||||||
|
SolidCraft is the displayed product name; `solidcraft` is the repository name. Keep existing
|
||||||
|
`solvecraft` crate/binary names, `.solvecraft` files, format markers, environment variables,
|
||||||
|
settings/recovery paths and package IDs compatible. Published SolveCraft release artifacts
|
||||||
|
and attribution records keep their historical names. Public docs/web/Flatpak deployments
|
||||||
|
use project Pages `/solidcraft/`; do not add permanent old-name Pages aliases. See README's
|
||||||
|
compatibility notes.
|
||||||
|
|
||||||
## Start every session here
|
## Start every session here
|
||||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
||||||
and the relevant `plan/architecture.md` section. Behaviour reference: `plan/fusion/`
|
and the relevant `plan/architecture.md` section. Behaviour reference: `plan/fusion/`
|
||||||
@@ -20,7 +28,7 @@ code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md
|
|||||||
`plan/` is gitignored (local only).
|
`plan/` is gitignored (local only).
|
||||||
|
|
||||||
## Never crash
|
## Never crash
|
||||||
People trust SolveCraft with their designs; a crash loses their work. **This outranks feature work.**
|
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||||
@@ -44,7 +52,7 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
|
|||||||
copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs), SolveSpace,
|
copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs), SolveSpace,
|
||||||
CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. The sketch solver and the blend
|
CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. The sketch solver and the blend
|
||||||
operations are our own.
|
operations are our own.
|
||||||
- **Assets — absolutely essential.** SolveCraft contains **no Autodesk iconography, images,
|
- **Assets — absolutely essential.** SolidCraft contains **no Autodesk iconography, images,
|
||||||
fonts or artwork — ever.** Every icon is drawn in code (`crates/ui-egui/src/icons.rs`). Any
|
fonts or artwork — ever.** Every icon is drawn in code (`crates/ui-egui/src/icons.rs`). Any
|
||||||
file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||||
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
|
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
|
||||||
@@ -64,7 +72,7 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
|
|||||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||||
assets, layers). Commit after every feature arc that builds.
|
assets, layers). Commit after every feature arc that builds.
|
||||||
- **Publishing**: `origin` is github.com/bherbruck/solvecraft (private). After each commit that
|
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. After each commit that
|
||||||
passes `cargo xtask ci`: `git pull --rebase origin main` (re-run ci if it brought changes),
|
passes `cargo xtask ci`: `git pull --rebase origin main` (re-run ci if it brought changes),
|
||||||
then `git push`. Never force-push. Never commit `plan/`, build dirs (`target*/`) or anything
|
then `git push`. Never force-push. Never commit `plan/`, build dirs (`target*/`) or anything
|
||||||
produced by Fusion (oracle STEP/STL/PNG); check `git ls-files` before pushing.
|
produced by Fusion (oracle STEP/STL/PNG); check `git ls-files` before pushing.
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
# SolveCraft — instructions for agents
|
# SolidCraft — instructions for agents
|
||||||
|
|
||||||
SolveCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application in the style of
|
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
|
||||||
Autodesk Fusion: sketches with a constraint solver, a parametric timeline of features, a B-rep
|
sketches with a constraint solver, a parametric timeline of features, a B-rep
|
||||||
kernel, and a desktop UI that agents can drive. Siblings with the same conventions live next to
|
kernel, and a desktop UI that agents can drive. Siblings with the same conventions live next to
|
||||||
it in `storytold/`: `cadcraft` (AutoCAD-style, the closest relative), `photocraft`, `vectorcraft`,
|
it in `storytold/`: `cadcraft` (AutoCAD-style, the closest relative), `photocraft`, `vectorcraft`,
|
||||||
`gridcraft`, `designcraft`, `wordcraft`, `deckcraft`, `soundcraft`. Repos don't share code; when
|
`gridcraft`, `designcraft`, `wordcraft`, `deckcraft`, `soundcraft`. Repos don't share code; when
|
||||||
code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md.
|
code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md.
|
||||||
|
|
||||||
|
## Product name and compatibility
|
||||||
|
SolidCraft is the displayed product name; `solidcraft` is the repository name. Keep existing
|
||||||
|
`solvecraft` crate/binary names, `.solvecraft` files, format markers, environment variables,
|
||||||
|
settings/recovery paths and package IDs compatible. Published SolveCraft release artifacts
|
||||||
|
and attribution records keep their historical names. Public docs/web/Flatpak deployments
|
||||||
|
use project Pages `/solidcraft/`; do not add permanent old-name Pages aliases. See README's
|
||||||
|
compatibility notes.
|
||||||
|
|
||||||
## Start every session here
|
## Start every session here
|
||||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
||||||
and the relevant `plan/architecture.md` section. Behaviour reference: `plan/fusion/`
|
and the relevant `plan/architecture.md` section. Behaviour reference: `plan/fusion/`
|
||||||
@@ -20,7 +28,7 @@ code is adapted from a sibling (MIT OR Apache-2.0) it is noted in ATTRIBUTION.md
|
|||||||
`plan/` is gitignored (local only).
|
`plan/` is gitignored (local only).
|
||||||
|
|
||||||
## Never crash
|
## Never crash
|
||||||
People trust SolveCraft with their designs; a crash loses their work. **This outranks feature work.**
|
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||||
@@ -44,7 +52,7 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
|
|||||||
copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs), SolveSpace,
|
copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs), SolveSpace,
|
||||||
CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. The sketch solver and the blend
|
CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. The sketch solver and the blend
|
||||||
operations are our own.
|
operations are our own.
|
||||||
- **Assets — absolutely essential.** SolveCraft contains **no Autodesk iconography, images,
|
- **Assets — absolutely essential.** SolidCraft contains **no Autodesk iconography, images,
|
||||||
fonts or artwork — ever.** Every icon is drawn in code (`crates/ui-egui/src/icons.rs`). Any
|
fonts or artwork — ever.** Every icon is drawn in code (`crates/ui-egui/src/icons.rs`). Any
|
||||||
file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||||
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
|
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
|
||||||
@@ -64,7 +72,7 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
|
|||||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||||
assets, layers). Commit after every feature arc that builds.
|
assets, layers). Commit after every feature arc that builds.
|
||||||
- **Publishing**: `origin` is github.com/bherbruck/solvecraft (private). After each commit that
|
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. After each commit that
|
||||||
passes `cargo xtask ci`: `git pull --rebase origin main` (re-run ci if it brought changes),
|
passes `cargo xtask ci`: `git pull --rebase origin main` (re-run ci if it brought changes),
|
||||||
then `git push`. Never force-push. Never commit `plan/`, build dirs (`target*/`) or anything
|
then `git push`. Never force-push. Never commit `plan/`, build dirs (`target*/`) or anything
|
||||||
produced by Fusion (oracle STEP/STL/PNG); check `git ls-files` before pushing.
|
produced by Fusion (oracle STEP/STL/PNG); check `git ls-files` before pushing.
|
||||||
|
|||||||
+13
-13
@@ -1,7 +1,7 @@
|
|||||||
# Contributing to SolveCraft
|
# Contributing to SolidCraft
|
||||||
|
|
||||||
SolveCraft is an open-source parametric 3D CAD app written in pure Rust, in the style of
|
SolidCraft is an open-source parametric 3D CAD app written in pure Rust.
|
||||||
Autodesk Fusion. You sketch on a plane, constrain the sketch, turn profiles into solids, and
|
You sketch on a plane, constrain the sketch, turn profiles into solids, and
|
||||||
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
|
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
|
||||||
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
|
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
|
||||||
model with.
|
model with.
|
||||||
@@ -12,11 +12,11 @@ needed most.
|
|||||||
|
|
||||||
## Get it running (about 10 minutes on a fast machine)
|
## Get it running (about 10 minutes on a fast machine)
|
||||||
|
|
||||||
You need stable Rust 1.90 or newer. For the web
|
You need stable Rust 1.95 or newer (required by egui/eframe 0.36). For the web
|
||||||
build you also need `rustup target add wasm32-unknown-unknown` and [`trunk`](https://trunkrs.dev).
|
build you also need `rustup target add wasm32-unknown-unknown` and [`trunk`](https://trunkrs.dev).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/bherbruck/solvecraft && cd solvecraft
|
git clone https://github.com/bherbruck/solidcraft && cd solvecraft
|
||||||
cargo run --release -p solvecraft -- --sample # desktop app with a sample part
|
cargo run --release -p solvecraft -- --sample # desktop app with a sample part
|
||||||
cargo xtask test # unit, engine and UI-scenario tests
|
cargo xtask test # unit, engine and UI-scenario tests
|
||||||
cargo xtask ci # everything a PR must pass (see below)
|
cargo xtask ci # everything a PR must pass (see below)
|
||||||
@@ -97,7 +97,7 @@ The [README](README.md#architecture) has the layer table. The short version:
|
|||||||
tooltips, templates or materials, and we never commit files Fusion produced. Command ids are
|
tooltips, templates or materials, and we never commit files Fusion produced. Command ids are
|
||||||
our own (`solid.fillet`, not Fusion's internal names).
|
our own (`solid.fillet`, not Fusion's internal names).
|
||||||
2. **No GPL/LGPL/AGPL code, not even reading it.** That rules out OpenCASCADE, FreeCAD
|
2. **No GPL/LGPL/AGPL code, not even reading it.** That rules out OpenCASCADE, FreeCAD
|
||||||
(including planegcs), SolveSpace, CadQuery/OCP, LibreCAD and QCAD. SolveCraft is
|
(including planegcs), SolveSpace, CadQuery/OCP, LibreCAD and QCAD. SolidCraft is
|
||||||
MIT OR Apache-2.0, and its solver and blends are written from the maths. Permissive
|
MIT OR Apache-2.0, and its solver and blends are written from the maths. Permissive
|
||||||
dependencies are fine. Note which one you're adding and why in the PR, and run
|
dependencies are fine. Note which one you're adding and why in the PR, and run
|
||||||
`cargo xtask licences` so THIRD-PARTY-LICENSES.txt lists it (CI checks that it is current).
|
`cargo xtask licences` so THIRD-PARTY-LICENSES.txt lists it (CI checks that it is current).
|
||||||
@@ -151,14 +151,14 @@ in the PR. `cargo xtask test` builds the tests with it and then runs every test
|
|||||||
depend on wall-clock timing.
|
depend on wall-clock timing.
|
||||||
|
|
||||||
**The Fusion oracle** is a set of 76 reference parts, each built in Fusion from a recipe.
|
**The Fusion oracle** is a set of 76 reference parts, each built in Fusion from a recipe.
|
||||||
SolveCraft rebuilds the same recipe and compares volume, area and topology counts with Fusion's
|
SolidCraft rebuilds the same recipe and compares volume, area and topology counts with Fusion's
|
||||||
measurements; [docs/oracle.md](docs/oracle.md) has the current results. The recipes and
|
measurements; [docs/oracle.md](docs/oracle.md) has the current results. The recipes and
|
||||||
measurements live outside the repo (`plan/`, gitignored), because they come from a Fusion
|
measurements live outside the repo (`plan/`, gitignored), because they come from a Fusion
|
||||||
install. If you work on geometry and want to check against it, ask in an issue.
|
install. If you work on geometry and want to check against it, ask in an issue.
|
||||||
|
|
||||||
## Sending a change
|
## Sending a change
|
||||||
|
|
||||||
1. Work is tracked in [GitHub issues](https://github.com/bherbruck/solvecraft/issues), labelled
|
1. Work is tracked in [GitHub issues](https://github.com/bherbruck/solidcraft/issues), labelled
|
||||||
by area (`area:kernel`, `area:sketch`, `area:doc`, `area:ui`, `area:infra`). Before starting,
|
by area (`area:kernel`, `area:sketch`, `area:doc`, `area:ui`, `area:infra`). Before starting,
|
||||||
comment on the issue so it gets the `in progress` label and two people don't build the same
|
comment on the issue so it gets the `in progress` label and two people don't build the same
|
||||||
thing. For anything bigger than a bug fix that has no issue yet, open one first.
|
thing. For anything bigger than a bug fix that has no issue yet, open one first.
|
||||||
@@ -189,15 +189,15 @@ CI if it brought changes, and push without force. Report the upstream/PR state y
|
|||||||
any conflicts resolved and validation results. While a task remains active, follow an open
|
any conflicts resolved and validation results. While a task remains active, follow an open
|
||||||
PR through its status changes; at handoff, clearly identify anything still awaiting merge.
|
PR through its status changes; at handoff, clearly identify anything still awaiting merge.
|
||||||
|
|
||||||
Much of SolveCraft so far was written by AI coding agents (Claude) working in parallel, each
|
Much of SolidCraft so far was written by AI coding agents (Claude) working in parallel, each
|
||||||
owning one area, with a human setting direction and reporting bugs. Their working notes are in
|
owning one area, with a human setting direction and reporting bugs. Their working notes are in
|
||||||
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
|
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
|
||||||
long as the change meets the rules above.
|
long as the change meets the rules above.
|
||||||
|
|
||||||
## Where help is wanted most
|
## Where help is wanted most
|
||||||
|
|
||||||
See the [`help wanted`](https://github.com/bherbruck/solvecraft/labels/help%20wanted) and
|
See the [`help wanted`](https://github.com/bherbruck/solidcraft/labels/help%20wanted) and
|
||||||
[`good first issue`](https://github.com/bherbruck/solvecraft/labels/good%20first%20issue) labels.
|
[`good first issue`](https://github.com/bherbruck/solidcraft/labels/good%20first%20issue) labels.
|
||||||
In order of how much they'd help users:
|
In order of how much they'd help users:
|
||||||
|
|
||||||
1. **Fillet and blend robustness.** Fillets are our own code (truck has no fillet operation).
|
1. **Fillet and blend robustness.** Fillets are our own code (truck has no fillet operation).
|
||||||
@@ -206,7 +206,7 @@ In order of how much they'd help users:
|
|||||||
faces, and fillets that consume neighbouring faces.
|
faces, and fillets that consume neighbouring faces.
|
||||||
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
|
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
|
||||||
through without a kernel error. Each failing seed is a reproducible bug.
|
through without a kernel error. Each failing seed is a reproducible bug.
|
||||||
3. **Y-up / Z-up.** SolveCraft is Z-up only, so most STEP files (from SolidWorks, Onshape, or
|
3. **Y-up / Z-up.** SolidCraft is Z-up only, so most STEP files (from SolidWorks, Onshape, or
|
||||||
Fusion's default) open lying on their backs. Fusion solves this with a "default modeling
|
Fusion's default) open lying on their backs. Fusion solves this with a "default modeling
|
||||||
orientation" preference.
|
orientation" preference.
|
||||||
4. **Imported appearances.** Colours from STEP files are lost when a feature changes the
|
4. **Imported appearances.** Colours from STEP files are lost when a feature changes the
|
||||||
@@ -217,4 +217,4 @@ In order of how much they'd help users:
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
|
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
|
||||||
of SolveCraft. SolveCraft isn't affiliated with Autodesk. Fusion is a trademark of Autodesk, Inc.
|
of SolidCraft. SolidCraft isn't affiliated with Autodesk. Fusion is a trademark of Autodesk, Inc.
|
||||||
Generated
+28
-13
@@ -1553,6 +1553,19 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hidapi"
|
||||||
|
version = "2.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "818c0e1d27887aaf76fe737042e27a66b796a7b099e6d2e1a72d106c2dff3fa6"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.65"
|
version = "0.1.65"
|
||||||
@@ -3256,10 +3269,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft"
|
name = "solvecraft"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"hidapi",
|
||||||
"log",
|
"log",
|
||||||
"rfd",
|
"rfd",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3270,7 +3284,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-cli"
|
name = "solvecraft-cli"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"solvecraft-engine",
|
"solvecraft-engine",
|
||||||
@@ -3279,7 +3293,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-doc"
|
name = "solvecraft-doc"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3294,7 +3308,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-engine"
|
name = "solvecraft-engine"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3310,7 +3324,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-geom"
|
name = "solvecraft-geom"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3318,7 +3332,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-io"
|
name = "solvecraft-io"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml 0.38.4",
|
"quick-xml 0.38.4",
|
||||||
"solvecraft-doc",
|
"solvecraft-doc",
|
||||||
@@ -3330,7 +3344,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-kernel"
|
name = "solvecraft-kernel"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3345,7 +3359,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-mcp"
|
name = "solvecraft-mcp"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"solvecraft-engine",
|
"solvecraft-engine",
|
||||||
@@ -3353,16 +3367,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-render"
|
name = "solvecraft-render"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"solvecraft-geom",
|
"solvecraft-geom",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-sketch"
|
name = "solvecraft-sketch"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3373,7 +3388,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-ui-egui"
|
name = "solvecraft-ui-egui"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
@@ -3388,7 +3403,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solvecraft-web"
|
name = "solvecraft-web"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -4760,7 +4775,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|||||||
+4
-4
@@ -5,12 +5,12 @@ members = ["crates/*", "apps/*", "xtask"]
|
|||||||
exclude = ["vendor"]
|
exclude = ["vendor"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.90"
|
rust-version = "1.95"
|
||||||
authors = ["SolveCraft contributors"]
|
authors = ["SolidCraft contributors"]
|
||||||
repository = "https://github.com/bherbruck/solvecraft"
|
repository = "https://github.com/bherbruck/solidcraft"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# internal
|
# internal
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
# SolveCraft
|
# SolidCraft
|
||||||
|
|
||||||
**Parametric 3D CAD; an open-source, [clean-room](CLAUDE.md) reimplementation of Autodesk
|
**An open-source parametric 3D CAD application built in pure Rust.**
|
||||||
Fusion 360, built in pure Rust.**
|
|
||||||
|
|
||||||
Sketch, constrain, and build solids with a parametric feature timeline. Change a dimension
|
Sketch, constrain, and build solids with a parametric feature timeline. Change a dimension
|
||||||
and watch your model rebuild. Run it on your desktop, in your browser, or let AI agents
|
and watch your model rebuild. Run it on your desktop, in your browser, or let AI agents
|
||||||
model through the built-in MCP server. Part of the storytold "craft" family.
|
model through the built-in MCP server. Part of the storytold "craft" family.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
*Start with an empty design and draw the original parametric electronics enclosure in the English desktop UI: rounded corners, a shell, a USB port sketched directly on the wall and a four-boss pattern. Change width from 80 → 100 mm and wall thickness from 2 → 3 mm in Parameters and watch it rebuild. Original animated pointer and click cues; 4× playback. [Full video](https://github.com/bherbruck/solvecraft/raw/refs/heads/main/docs/screenshots/enclosure-demo-0.2.1.mp4), generated with the [demo script](examples/demo/record_ui.sh).*
|
*Start with an empty design and draw the original parametric electronics enclosure in the English desktop UI: rounded corners, a shell, a USB port sketched directly on the wall and a four-boss pattern. Change width from 80 → 100 mm and wall thickness from 2 → 3 mm in Parameters and watch it rebuild. Original animated pointer and click cues; 4× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/enclosure-demo-0.2.1.mp4), generated with the [demo script](examples/demo/record_ui.sh).*
|
||||||
|
|
||||||
**Documentation:** the [SolveCraft book](https://bherbruck.github.io/solvecraft/) (user guide,
|
**Documentation:** the [SolidCraft book](https://bherbruck.github.io/solidcraft/) (user guide,
|
||||||
developer guide and command reference; sources in [docs/book](docs/book)), and the
|
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.
|
[web app](https://bherbruck.github.io/solidcraft/app/) to try it in the browser.
|
||||||
|
|
||||||
**Status: early but usable for simple parts.** 76 of 76 reference parts built in Fusion
|
**Status: early but usable for simple parts.** The historical benchmark report records
|
||||||
rebuild in SolveCraft with the same volume, area and topology ([docs/oracle.md](docs/oracle.md));
|
76 of 76 reference parts matching 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).
|
These checks do not establish complete geometric or workflow equivalence. Feature coverage
|
||||||
Open work and known bugs are in [GitHub issues](https://github.com/bherbruck/solvecraft/issues),
|
is tracked in [docs/parity.md](docs/parity.md) and [ROADMAP.md](ROADMAP.md).
|
||||||
|
Open work and known bugs are in [GitHub issues](https://github.com/bherbruck/solidcraft/issues),
|
||||||
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## Name and compatibility
|
||||||
|
|
||||||
|
SolidCraft was previously called SolveCraft. The product display name and repository are
|
||||||
|
changing; existing identifiers remain compatible: `.solvecraft` designs, `solvecraft-cli`,
|
||||||
|
Cargo package names, settings/recovery locations and package IDs keep their existing names.
|
||||||
|
Published 0.2.0 and 0.2.1 packages and demo recordings retain their original SolveCraft branding
|
||||||
|
and filenames.
|
||||||
|
|
||||||
|
Use the SolidCraft documentation and web-app URLs above. GitHub redirects links to the
|
||||||
|
renamed repository; old-name Pages and Flatpak URLs are not maintained as permanent aliases.
|
||||||
|
|
||||||
## What works
|
## What works
|
||||||
|
|
||||||
- **Sketches** on origin planes, offset planes or planar faces: lines, rectangles, circles,
|
- **Sketches** on origin planes, offset planes or planar faces: lines, rectangles, circles,
|
||||||
@@ -74,7 +85,7 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|||||||
Feature errors and Help can open an editable **Create Issue** draft with relevant context
|
Feature errors and Help can open an editable **Create Issue** draft with relevant context
|
||||||
and an optional reviewed screenshot; opening GitHub does not submit the report.
|
and an optional reviewed screenshot; opening GitHub does not submit the report.
|
||||||
- **Headless CLI** `solvecraft-cli`: run command scripts, measure (volume, area, centre of mass,
|
- **Headless CLI** `solvecraft-cli`: run command scripts, measure (volume, area, centre of mass,
|
||||||
face/edge/vertex counts), export, render PNG snapshots, replay Fusion oracle recipes.
|
face/edge/vertex counts), export, render PNG snapshots and replay reference recipes.
|
||||||
- **MCP server** `solvecraft-cli mcp`: AI agents model, measure and look at parts headless or
|
- **MCP server** `solvecraft-cli mcp`: AI agents model, measure and look at parts headless or
|
||||||
in the running app (see below).
|
in the running app (see below).
|
||||||
|
|
||||||
@@ -92,6 +103,11 @@ cargo xtask ci # fmt, clippy, tests, as
|
|||||||
|
|
||||||
Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solvecraft`.
|
Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solvecraft`.
|
||||||
|
|
||||||
|
On Windows, SolidCraft detects a connected 3Dconnexion SpaceNavigator or SpaceMouse through its
|
||||||
|
multi-axis HID interface. Move the cap to pan and zoom, and tilt or twist it to rotate the 3D view.
|
||||||
|
While editing a sketch, the cap pans and zooms without changing the sketch view. The Navigation
|
||||||
|
page in Preferences has an on/off switch and a speed control for the 3D mouse.
|
||||||
|
|
||||||
In the browser: `cd apps/solvecraft-web && trunk build --release --public-url ./` (WebGPU, or
|
In the browser: `cd apps/solvecraft-web && trunk build --release --public-url ./` (WebGPU, or
|
||||||
WebGL2 with `?webgl`; `?sample` opens the sample part).
|
WebGL2 with `?webgl`; `?sample` opens the sample part).
|
||||||
|
|
||||||
@@ -134,10 +150,19 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
Coming from Autodesk Fusion? SolidCraft uses familiar parametric CAD workflows: constrained
|
||||||
|
sketches, solid features, an editable timeline, parameters and assemblies. See the
|
||||||
|
[roadmap](ROADMAP.md) for supported operations and current limitations.
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md): how to build and test, the code map, the clean-room
|
See [CONTRIBUTING.md](CONTRIBUTING.md): how to build and test, the code map, the clean-room
|
||||||
and licence rules, and where help is wanted most.
|
and licence rules, and where help is wanted most.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
MIT OR Apache-2.0, at your option. SolveCraft is not affiliated with Autodesk; Fusion is a
|
MIT OR Apache-2.0, at your option. Developed under the
|
||||||
trademark of Autodesk, Inc.
|
[clean-room contribution rules](CLAUDE.md); third-party code and assets retain their
|
||||||
|
documented licences and attribution.
|
||||||
|
|
||||||
|
References to other products describe workflow comparisons and benchmark provenance.
|
||||||
|
SolidCraft is not affiliated with, sponsored by or endorsed by Autodesk. Autodesk and
|
||||||
|
Fusion are trademarks of Autodesk, Inc.
|
||||||
+9
-3
@@ -1,6 +1,6 @@
|
|||||||
# SolveCraft roadmap
|
# SolidCraft roadmap
|
||||||
|
|
||||||
Status as of **2026-10-11**. SolveCraft targets Fusion-style parametric modelling (sketch →
|
Status as of **2026-10-11**. SolidCraft supports parametric modelling (sketch →
|
||||||
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
||||||
|
|
||||||
## At a glance
|
## At a glance
|
||||||
@@ -54,7 +54,7 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
|||||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||||
| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
||||||
| M8 | Surface workspace | deferred | — |
|
| M8 | Surface workspace | deferred | — |
|
||||||
| M9 | Components and assemblies (Fusion-style): New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure. Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure. Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
||||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done (oracle 67-76). PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, no Fusion oracle (licence), tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line) | in progress | 36 |
|
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done (oracle 67-76). PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, no Fusion oracle (licence), tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line) | in progress | 36 |
|
||||||
| M11 | 2D drawings | deferred | — |
|
| M11 | 2D drawings | deferred | — |
|
||||||
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
||||||
@@ -72,6 +72,9 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
|||||||
useful reproduction context. The draft is editable, with optional capture of the actual
|
useful reproduction context. The draft is editable, with optional capture of the actual
|
||||||
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
|
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
|
||||||
reporting for unexpected results that did not produce an error.
|
reporting for unexpected results that did not produce an error.
|
||||||
|
- The header exposes **Report a bug**, including an accessible compact control on
|
||||||
|
narrow windows. It opens the same editable report and screenshot review without
|
||||||
|
changing the active model or a pending dimension.
|
||||||
|
|
||||||
## 0.2.0 release batch
|
## 0.2.0 release batch
|
||||||
|
|
||||||
@@ -247,6 +250,9 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
|||||||
record their coordinate frame; legacy wires migrate from known provenance without changing
|
record their coordinate frame; legacy wires migrate from known provenance without changing
|
||||||
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
|
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
|
||||||
See [occurrence workflows](docs/occurrence-workflows.md).
|
See [occurrence workflows](docs/occurrence-workflows.md).
|
||||||
|
- Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel.
|
||||||
|
Parameters opens the user/model variable editor; Configurations opens the configuration table.
|
||||||
|
Configure remains available in the panel dropdown.
|
||||||
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
|
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
|
||||||
restrict values. Preview explains effective values without modifying the document; applying
|
restrict values. Preview explains effective values without modifying the document; applying
|
||||||
rules validates rows atomically. Length expressions retain their authoring units across
|
rules validates rows atomically. Length expressions retain their authoring units across
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Third-party software in SolveCraft
|
Third-party software in SolidCraft
|
||||||
|
|
||||||
SolveCraft (the desktop app, solvecraft-cli and the web app) is built from the 453 crates below.
|
SolidCraft (the desktop app, solvecraft-cli and the web app) is built from the 454 crates below.
|
||||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||||
SolveCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||||
|
|
||||||
accesskit 0.24.1 (MIT OR Apache-2.0)
|
accesskit 0.24.1 (MIT OR Apache-2.0)
|
||||||
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
|
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
|
||||||
@@ -147,6 +147,7 @@ hashbrown 0.16.1 (MIT OR Apache-2.0)
|
|||||||
hashbrown 0.17.1 (MIT OR Apache-2.0)
|
hashbrown 0.17.1 (MIT OR Apache-2.0)
|
||||||
hermit-abi 0.5.3 (MIT OR Apache-2.0)
|
hermit-abi 0.5.3 (MIT OR Apache-2.0)
|
||||||
hex 0.4.3 (MIT OR Apache-2.0)
|
hex 0.4.3 (MIT OR Apache-2.0)
|
||||||
|
hidapi 2.6.7 (MIT)
|
||||||
iana-time-zone 0.1.65 (MIT OR Apache-2.0)
|
iana-time-zone 0.1.65 (MIT OR Apache-2.0)
|
||||||
iana-time-zone-haiku 0.1.2 (MIT OR Apache-2.0)
|
iana-time-zone-haiku 0.1.2 (MIT OR Apache-2.0)
|
||||||
icu_collections 2.3.0 (Unicode-3.0)
|
icu_collections 2.3.0 (Unicode-3.0)
|
||||||
@@ -6019,6 +6020,27 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
hidapi 2.6.7
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Copyright 2017 The hidapi-rs Developers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||||
|
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
iana-time-zone 0.1.65, iana-time-zone-haiku 0.1.2
|
iana-time-zone 0.1.65, iana-time-zone-haiku 0.1.2
|
||||||
================================================================================
|
================================================================================
|
||||||
@@ -13017,7 +13039,7 @@ freely, subject to the following restrictions:
|
|||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
Crates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0
|
Crates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0
|
||||||
texts are as in SolveCraft's own LICENSE-MIT and LICENSE-APACHE)
|
texts are as in SolidCraft's own LICENSE-MIT and LICENSE-APACHE)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-cli"
|
name = "solvecraft-cli"
|
||||||
description = "SolveCraft headless CLI: run command scripts, measure, export, render snapshots, replay oracle recipes"
|
description = "SolidCraft headless CLI: run command scripts, measure, export, render snapshots, replay oracle recipes"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
+114
-22
@@ -53,8 +53,17 @@ fn main() -> ExitCode {
|
|||||||
Some("snapshot") => cmd_snapshot(&args[1..]),
|
Some("snapshot") => cmd_snapshot(&args[1..]),
|
||||||
Some("exec") => cmd_exec(&args[1..]),
|
Some("exec") => cmd_exec(&args[1..]),
|
||||||
Some("commands") => {
|
Some("commands") => {
|
||||||
|
use std::io::Write as _;
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
s.execute("engine.commands", &json!({})).map(|v| println!("{}", pretty(&v))).map_err(|e| e.to_string())
|
s.execute("engine.commands", &json!({})).map_err(|e| e.to_string()).and_then(|v| {
|
||||||
|
// A pipeline consumer may stop early (e.g. `commands | head`).
|
||||||
|
// Unlike println!, writing explicitly lets us handle that without a panic.
|
||||||
|
match writeln!(std::io::stdout().lock(), "{}", pretty(&v)) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
|
||||||
|
Err(e) => Err(format!("stdout: {e}")),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
|
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
|
||||||
let v = read_json(p)?;
|
let v = read_json(p)?;
|
||||||
@@ -219,7 +228,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The app icon: a blue block with a bore, drawn by the CPU renderer on a transparent background
|
/// The app icon: the SolidCraft logo, drawn in code on a transparent background
|
||||||
/// (so packages need no committed image). `.ico` holds the Windows sizes as PNGs.
|
/// (so packages need no committed image). `.ico` holds the Windows sizes as PNGs.
|
||||||
fn cmd_icon(args: &[String]) -> Result<(), String> {
|
fn cmd_icon(args: &[String]) -> Result<(), String> {
|
||||||
let out = flag(args, "--out").ok_or("icon needs --out FILE.png or FILE.ico")?;
|
let out = flag(args, "--out").ok_or("icon needs --out FILE.png or FILE.ico")?;
|
||||||
@@ -236,30 +245,113 @@ fn cmd_icon(args: &[String]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This block is the icon image data
|
||||||
fn icon_png(size: usize) -> Result<Vec<u8>, String> {
|
fn icon_png(size: usize) -> Result<Vec<u8>, String> {
|
||||||
use solvecraft_engine::render::{Rgb, Scene, SceneLine, SceneMesh, png, render_transparent};
|
use solvecraft_engine::render::png;
|
||||||
let mut s = Session::default();
|
type P = (f32, f32);
|
||||||
for (cmd, p) in [
|
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
|
||||||
("solid.box", json!({"length": 20, "width": 20, "height": 20, "center": [0, 0, 0]})),
|
let ss = if size > 512 { 2 } else { 4 };
|
||||||
("solid.cylinder", json!({"diameter": 9, "height": 30, "base": [0, 0, -15], "operation": "cut"})),
|
let n = size * ss;
|
||||||
] {
|
let mut buf = vec![[0u8; 4]; n * n];
|
||||||
s.execute(cmd, &p).map_err(|e| format!("icon: {e}"))?;
|
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
|
||||||
}
|
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
|
||||||
let mut cam = solvecraft_engine::view::home_camera(&s);
|
// Fill a convex polygon given in SVG units.
|
||||||
cam.distance *= 0.95;
|
let mut fill = |pts: &[P], col: [u8; 3]| {
|
||||||
let mut scene = Scene { radius: 60.0, ..Default::default() };
|
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
|
||||||
let px = size as f64 / 256.0;
|
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
|
||||||
for body in &s.world_state().bodies {
|
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||||
let mesh = body.mesh();
|
let cy = y as f32 + 0.5;
|
||||||
scene.meshes.push(SceneMesh { mesh: std::sync::Arc::clone(&mesh), color: Rgb(90, 160, 240), face_colors: Vec::new() });
|
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||||
for (i, e) in mesh.edges.iter().enumerate() {
|
for i in 0..v.len() {
|
||||||
if mesh.shaded_edge_visible(i) {
|
let (a, b) = (v[i], v[(i + 1) % v.len()]);
|
||||||
scene.lines.push(SceneLine { points: e.clone(), color: Rgb(24, 52, 96), width: (3.0 * px).max(0.8), on_top: false });
|
if (a.1 <= cy) != (b.1 <= cy) {
|
||||||
|
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
|
||||||
|
lo = lo.min(x);
|
||||||
|
hi = hi.max(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lo > hi {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||||
|
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
|
||||||
|
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
|
||||||
|
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
|
||||||
|
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
|
||||||
|
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
|
||||||
|
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
|
||||||
|
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
|
||||||
|
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
|
||||||
|
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
|
||||||
|
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
|
||||||
|
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
|
||||||
|
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
|
||||||
|
let white = [255, 255, 255];
|
||||||
|
let mut stroke = |pts: &[P], closed: bool| {
|
||||||
|
let m = pts.len();
|
||||||
|
for i in 0..(if closed { m } else { m - 1 }) {
|
||||||
|
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||||
|
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
|
||||||
|
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
|
||||||
|
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
|
||||||
|
}
|
||||||
|
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
|
||||||
|
let disc: Vec<P> = (0..16)
|
||||||
|
.map(|k| {
|
||||||
|
let t = k as f32 * std::f32::consts::TAU / 16.0;
|
||||||
|
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
fill(&disc, white);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
|
||||||
|
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
|
||||||
|
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
|
||||||
|
stroke(
|
||||||
|
&[
|
||||||
|
(178.5, 5.8),
|
||||||
|
(5.0, 105.9),
|
||||||
|
(5.0, 206.1),
|
||||||
|
(91.7, 256.2),
|
||||||
|
(91.7, 356.4),
|
||||||
|
(178.5, 406.4),
|
||||||
|
(352.0, 306.3),
|
||||||
|
(352.0, 206.1),
|
||||||
|
(265.2, 156.0),
|
||||||
|
(265.2, 55.9),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Average each ss x ss block down to one pixel (straight alpha = covered share).
|
||||||
|
let mut rgba = Vec::with_capacity(size * size * 4);
|
||||||
|
for y in 0..size {
|
||||||
|
for x in 0..size {
|
||||||
|
let (mut sum, mut cnt) = ([0u32; 3], 0u32);
|
||||||
|
for j in 0..ss {
|
||||||
|
for i in 0..ss {
|
||||||
|
let p = buf[(y * ss + j) * n + x * ss + i];
|
||||||
|
if p[3] != 0 {
|
||||||
|
cnt += 1;
|
||||||
|
for k in 0..3 {
|
||||||
|
sum[k] += u32::from(p[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cnt == 0 {
|
||||||
|
rgba.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
} else {
|
||||||
|
let a = 255 * cnt / (ss * ss) as u32;
|
||||||
|
rgba.extend_from_slice(&[(sum[0] / cnt) as u8, (sum[1] / cnt) as u8, (sum[2] / cnt) as u8, a as u8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let c = render_transparent(&mut scene, &cam, size, size);
|
png(size, size, rgba).ok_or_else(|| "icon: PNG encoding failed".to_string())
|
||||||
png(c.w, c.h, c.rgba).ok_or_else(|| "icon: PNG encoding failed".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Windows icon file holding PNG images (Vista and later read these).
|
/// A Windows icon file holding PNG images (Vista and later read these).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub const REL_TOL: f64 = 1e-3;
|
|||||||
const APPROXIMATE: [(&str, f64, &str); 1] = [(
|
const APPROXIMATE: [(&str, f64, &str); 1] = [(
|
||||||
"49-helical-sweep",
|
"49-helical-sweep",
|
||||||
5e-3,
|
5e-3,
|
||||||
"Fusion sweeps along a spline fitted through helix points; SolveCraft builds the true helix (a coil), so the size is compared within 0.5% and the topology not at all",
|
"Fusion sweeps along a spline fitted through helix points; SolidCraft builds the true helix (a coil), so the size is compared within 0.5% and the topology not at all",
|
||||||
)];
|
)];
|
||||||
|
|
||||||
fn check_case(dir: &str) -> Value {
|
fn check_case(dir: &str) -> Value {
|
||||||
@@ -77,7 +77,7 @@ fn check_case(dir: &str) -> Value {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
seam_note = Some(format!(
|
seam_note = Some(format!(
|
||||||
"topology compared seam-normalised: Fusion's {}/{}/{} faces/edges/vertices become {f}/{e}/{v} with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: {})",
|
"topology compared seam-normalised: Fusion's {}/{}/{} faces/edges/vertices become {f}/{e}/{v} with closed-surface patches counted once and seam edges and vertices dropped (raw SolidCraft/Fusion: {})",
|
||||||
want["total"]["faces"],
|
want["total"]["faces"],
|
||||||
want["total"]["edges"],
|
want["total"]["edges"],
|
||||||
want["total"]["vertices"],
|
want["total"]["vertices"],
|
||||||
@@ -120,7 +120,7 @@ fn check_case(dir: &str) -> Value {
|
|||||||
.collect();
|
.collect();
|
||||||
if !effective.is_empty() {
|
if !effective.is_empty() {
|
||||||
out["note"] = json!(format!(
|
out["note"] = json!(format!(
|
||||||
"known Fusion difference: {} replayed with the extent Fusion actually used (its through-all is sized as 1.1 × the farthest vertex, so curved tops stop it short; SolveCraft's through-all goes truly through)",
|
"known Fusion difference: {} replayed with the extent Fusion actually used (its through-all is sized as 1.1 × the farthest vertex, so curved tops stop it short; SolidCraft's through-all goes truly through)",
|
||||||
effective.join(", ")
|
effective.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! The registry remains valid JSON and a closed pipeline reader must not cause a panic.
|
||||||
|
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_output_is_complete_json() {
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("commands").output().unwrap();
|
||||||
|
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let registry: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||||
|
let commands = registry.as_array().unwrap();
|
||||||
|
assert!(commands.iter().any(|command| command["id"] == "solid.box"));
|
||||||
|
assert!(commands.iter().any(|command| command["id"] == "engine.commands"));
|
||||||
|
assert!(output.stdout.ends_with(b"\n"));
|
||||||
|
assert!(output.stderr.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closed_registry_pipe_exits_without_panicking() {
|
||||||
|
let mut child = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("commands").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
||||||
|
// Close our read end before waiting. No sleeps or timing threshold are needed:
|
||||||
|
// the registry is larger than the pipe buffer, so it cannot all be written first.
|
||||||
|
drop(child.stdout.take());
|
||||||
|
let output = child.wait_with_output().unwrap();
|
||||||
|
assert!(output.status.success(), "status: {}; stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
|
||||||
|
assert!(output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-web"
|
name = "solvecraft-web"
|
||||||
description = "SolveCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
description = "SolidCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
<title>SolveCraft</title>
|
<title>SolidCraft</title>
|
||||||
<link data-trunk rel="rust" data-bin="solvecraft-web" data-wasm-opt="z" data-weak-refs />
|
<link data-trunk rel="rust" data-bin="solvecraft-web" data-wasm-opt="z" data-weak-refs />
|
||||||
<style>
|
<style>
|
||||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="solvecraft_canvas"></canvas>
|
<canvas id="solvecraft_canvas"></canvas>
|
||||||
<div id="solvecraft_loading">Loading SolveCraft…</div>
|
<div id="solvecraft_loading">Loading SolidCraft…</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -406,7 +406,7 @@ mod web {
|
|||||||
match result {
|
match result {
|
||||||
Ok(()) => el.remove(),
|
Ok(()) => el.remove(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
el.set_inner_html(&format!("<p>SolveCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>"))
|
el.set_inner_html(&format!("<p>SolidCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft"
|
name = "solvecraft"
|
||||||
description = "SolveCraft desktop app"
|
description = "SolidCraft desktop app"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -19,5 +19,8 @@ serde_json = { workspace = true }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
hidapi = { version = "2.6.7", default-features = false, features = ["windows-native"] }
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||||
+27
-13
@@ -13,36 +13,44 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod control_server;
|
mod control_server;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod space_mouse;
|
||||||
|
|
||||||
use solvecraft_engine::Session;
|
use solvecraft_engine::Session;
|
||||||
use solvecraft_ui_egui::{Services, SolveApp};
|
use solvecraft_ui_egui::{Services, SolveApp};
|
||||||
|
|
||||||
struct App(SolveApp);
|
struct App {
|
||||||
|
solve: SolveApp,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
space_mouse: space_mouse::SpaceMouse,
|
||||||
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
||||||
// designs first; the window closes once they are answered (`quit_requested`).
|
// designs first; the window closes once they are answered (`quit_requested`).
|
||||||
if ctx.input(|i| i.viewport().close_requested()) && !self.0.quit_requested {
|
if ctx.input(|i| i.viewport().close_requested()) && !self.solve.quit_requested {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||||
solvecraft_ui_egui::documents::request_quit(&mut self.0);
|
solvecraft_ui_egui::documents::request_quit(&mut self.solve);
|
||||||
}
|
}
|
||||||
self.0.logic(ctx);
|
self.solve.logic(ctx);
|
||||||
if self.0.quit_requested {
|
#[cfg(target_os = "windows")]
|
||||||
|
self.space_mouse.tick(&mut self.solve, ctx);
|
||||||
|
if self.solve.quit_requested {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||||
self.0.raw_input_hook(raw);
|
self.solve.raw_input_hook(raw);
|
||||||
}
|
}
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
self.0.ui(ui);
|
self.solve.ui(ui);
|
||||||
}
|
}
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
storage.set_string(PREFS_KEY, self.0.prefs());
|
storage.set_string(PREFS_KEY, self.solve.prefs());
|
||||||
}
|
}
|
||||||
fn on_exit(&mut self) {
|
fn on_exit(&mut self) {
|
||||||
solvecraft_ui_egui::recovery_ui::close(&mut self.0);
|
solvecraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +65,7 @@ fn services() -> Services {
|
|||||||
"Designs, STEP, IGES and meshes",
|
"Designs, STEP, IGES and meshes",
|
||||||
&["solvecraft", "step", "stp", "STEP", "STP", "igs", "iges", "IGS", "IGES", "3mf", "3MF", "stl", "STL"],
|
&["solvecraft", "step", "stp", "STEP", "STP", "igs", "iges", "IGS", "IGES", "3mf", "3MF", "stl", "STL"],
|
||||||
)
|
)
|
||||||
.add_filter("SolveCraft design", &["solvecraft"])
|
.add_filter("SolidCraft design", &["solvecraft"])
|
||||||
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
|
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
|
||||||
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
|
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
|
||||||
.add_filter("Mesh (3MF, STL)", &["3mf", "3MF", "stl", "STL"])
|
.add_filter("Mesh (3MF, STL)", &["3mf", "3MF", "stl", "STL"])
|
||||||
@@ -139,7 +147,7 @@ fn main() -> eframe::Result {
|
|||||||
}
|
}
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_title("SolveCraft")
|
.with_title("SolidCraft")
|
||||||
.with_inner_size([1600.0, 1000.0])
|
.with_inner_size([1600.0, 1000.0])
|
||||||
.with_min_inner_size([960.0, 600.0])
|
.with_min_inner_size([960.0, 600.0])
|
||||||
.with_drag_and_drop(true)
|
.with_drag_and_drop(true)
|
||||||
@@ -166,7 +174,7 @@ fn main() -> eframe::Result {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"SolveCraft",
|
"SolidCraft",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
solvecraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
solvecraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||||
@@ -205,7 +213,13 @@ fn main() -> eframe::Result {
|
|||||||
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
||||||
solvecraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
solvecraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||||
}
|
}
|
||||||
Ok(Box::new(App(app)))
|
#[cfg(target_os = "windows")]
|
||||||
|
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||||
|
Ok(Box::new(App {
|
||||||
|
solve: app,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
space_mouse,
|
||||||
|
}))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
//! Windows 3Dconnexion HID navigation. The device reader never touches the document or UI;
|
||||||
|
//! it publishes the latest axes, and the egui thread applies them once per frame.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use egui::Context;
|
||||||
|
use hidapi::{DeviceInfo, HidApi};
|
||||||
|
use solvecraft_engine::render::Camera;
|
||||||
|
use solvecraft_ui_egui::SolveApp;
|
||||||
|
|
||||||
|
const STALE_AFTER: Duration = Duration::from_millis(150);
|
||||||
|
const RETRY_AFTER: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
struct Axes {
|
||||||
|
translation: [i16; 3],
|
||||||
|
rotation: [i16; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Reading {
|
||||||
|
axes: Axes,
|
||||||
|
translation_at: Instant,
|
||||||
|
rotation_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reading {
|
||||||
|
fn new(now: Instant) -> Self {
|
||||||
|
Self { axes: Axes::default(), translation_at: now, rotation_at: now }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(self, now: Instant) -> Axes {
|
||||||
|
Axes {
|
||||||
|
translation: if now.duration_since(self.translation_at) < STALE_AFTER { self.axes.translation } else { [0; 3] },
|
||||||
|
rotation: if now.duration_since(self.rotation_at) < STALE_AFTER { self.axes.rotation } else { [0; 3] },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The old SpaceNavigator uses Logitech's VID; newer 3Dconnexion devices use their own.
|
||||||
|
/// The HID usage excludes ordinary Logitech mice and the keyboard interfaces of SpaceMice.
|
||||||
|
fn is_3d_mouse(info: &DeviceInfo) -> bool {
|
||||||
|
matches!(info.vendor_id(), 0x046d | 0x256f) && info.usage_page() == 0x01 && info.usage() == 0x08
|
||||||
|
}
|
||||||
|
|
||||||
|
fn three_axes(bytes: &[u8]) -> Option<[i16; 3]> {
|
||||||
|
let &[x0, x1, y0, y1, z0, z1] = bytes.get(..6)? else { return None };
|
||||||
|
Some([i16::from_le_bytes([x0, x1]), i16::from_le_bytes([y0, y1]), i16::from_le_bytes([z0, z1])])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SpaceNavigator reports 1 (translation) and 2 (rotation), each with three signed
|
||||||
|
/// little-endian 16-bit values. The HID layer may pad either report to its maximum size.
|
||||||
|
fn decode_report(report: &[u8], reading: &mut Reading, now: Instant) -> bool {
|
||||||
|
match report.first().copied() {
|
||||||
|
Some(1) => {
|
||||||
|
let Some(translation) = report.get(1..).and_then(three_axes) else { return false };
|
||||||
|
reading.axes.translation = translation;
|
||||||
|
reading.translation_at = now;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Some(2) => {
|
||||||
|
let Some(rotation) = report.get(1..).and_then(three_axes) else { return false };
|
||||||
|
reading.axes.rotation = rotation;
|
||||||
|
reading.rotation_at = now;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||||
|
let Ok(mut api) = HidApi::new() else {
|
||||||
|
log::warn!("3D mouse: HID initialization failed");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
if let Err(error) = api.refresh_devices() {
|
||||||
|
log::warn!("3D mouse: device scan failed: {error}");
|
||||||
|
} else if let Some(device) = api.device_list().find(|info| is_3d_mouse(info)).and_then(|info| info.open_device(&api).ok()) {
|
||||||
|
let mut report = [0_u8; 64];
|
||||||
|
loop {
|
||||||
|
match device.read_timeout(&mut report, 100) {
|
||||||
|
Ok(0) => {}
|
||||||
|
Ok(size) => {
|
||||||
|
if let Ok(mut reading) = shared.lock()
|
||||||
|
&& decode_report(&report[..size], &mut reading, Instant::now())
|
||||||
|
{
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::info!("3D mouse disconnected: {error}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(mut reading) = shared.lock() {
|
||||||
|
*reading = Reading::new(Instant::now());
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
std::thread::sleep(RETRY_AFTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpaceMouse {
|
||||||
|
shared: Arc<Mutex<Reading>>,
|
||||||
|
last_frame: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpaceMouse {
|
||||||
|
pub fn start(ctx: &Context) -> Self {
|
||||||
|
let now = Instant::now();
|
||||||
|
let shared = Arc::new(Mutex::new(Reading::new(now)));
|
||||||
|
let worker_state = Arc::clone(&shared);
|
||||||
|
let worker_ctx = ctx.clone();
|
||||||
|
if let Err(error) = std::thread::Builder::new().name("solvecraft-3d-mouse".into()).spawn(move || reader(worker_ctx, worker_state)) {
|
||||||
|
log::warn!("3D mouse: reader could not start: {error}");
|
||||||
|
}
|
||||||
|
Self { shared, last_frame: now }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let seconds = now.duration_since(self.last_frame).as_secs_f64().min(0.05);
|
||||||
|
self.last_frame = now;
|
||||||
|
if !app.preferences.space_mouse_enabled || app.home.open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let axes = match self.shared.lock() {
|
||||||
|
Ok(reading) => reading.current(now),
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if axes == Axes::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let speed = f64::from(app.preferences.space_mouse_speed);
|
||||||
|
if !speed.is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.begin_external_navigation();
|
||||||
|
let height = app.viewport.rect.map_or(600.0, |rect| f64::from(rect.height().max(1.0)));
|
||||||
|
navigate(&mut app.cam, axes, seconds * speed.clamp(0.1, 5.0), height, app.session.active_sketch.is_some());
|
||||||
|
ctx.request_repaint_after(Duration::from_millis(16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A small dead zone avoids drift at rest. A full cap deflection takes roughly one viewport
|
||||||
|
/// per second to pan and about two radians per second to turn, independent of frame rate.
|
||||||
|
fn normalized(value: i16) -> f64 {
|
||||||
|
let v = f64::from(value);
|
||||||
|
if v.abs() <= 20.0 { 0.0 } else { v.signum() * ((v.abs() - 20.0) / 330.0).min(1.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(cam: &mut Camera, axes: Axes, seconds: f64, height: f64, sketch: bool) {
|
||||||
|
if !(seconds > 0.0) || !seconds.is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let [x, y, z] = axes.translation.map(normalized);
|
||||||
|
let [rx, ry, rz] = axes.rotation.map(normalized);
|
||||||
|
cam.pan(x * height * 0.6 * seconds, -y * height * 0.6 * seconds, height);
|
||||||
|
cam.zoom_at((-z * 1.5 * seconds).exp(), None);
|
||||||
|
if !sketch {
|
||||||
|
cam.orbit(ry * 250.0 * seconds, rx * 250.0 * seconds);
|
||||||
|
cam.roll_by(rz * 2.0 * seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_separate_reports_without_stale_motion_or_padding_interference() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut reading = Reading::new(now);
|
||||||
|
assert!(!decode_report(&[1, 1, 2], &mut reading, now));
|
||||||
|
assert_eq!(reading.current(now), Axes::default());
|
||||||
|
assert!(decode_report(&[1, 0x9c, 0xff, 0x2c, 0x01, 0, 0], &mut reading, now));
|
||||||
|
assert_eq!(reading.current(now).translation, [-100, 300, 0]);
|
||||||
|
assert!(decode_report(&[2, 0, 0, 1, 0, 0xff, 0xff], &mut reading, now));
|
||||||
|
assert_eq!(reading.current(now).rotation, [0, 1, -1]);
|
||||||
|
let later = now + STALE_AFTER + Duration::from_millis(1);
|
||||||
|
assert_eq!(reading.current(later), Axes::default());
|
||||||
|
assert!(decode_report(&[2, 4, 0, 5, 0, 6, 0], &mut reading, later));
|
||||||
|
assert!(decode_report(&[1, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0], &mut reading, later));
|
||||||
|
assert_eq!(reading.current(later), Axes { translation: [1, 2, 3], rotation: [4, 5, 6] });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn six_axes_move_the_camera_and_sketches_keep_their_orientation() {
|
||||||
|
let mut cam = Camera::default();
|
||||||
|
let before = cam;
|
||||||
|
navigate(&mut cam, Axes { translation: [350, 350, 350], rotation: [350, 350, 350] }, 0.02, 600.0, false);
|
||||||
|
assert_ne!(cam.target, before.target);
|
||||||
|
assert!(cam.distance < before.distance);
|
||||||
|
assert_ne!(cam.yaw, before.yaw);
|
||||||
|
assert_ne!(cam.pitch, before.pitch);
|
||||||
|
assert_ne!(cam.roll, before.roll);
|
||||||
|
let orientation = (cam.yaw, cam.pitch, cam.roll);
|
||||||
|
navigate(&mut cam, Axes { translation: [350, 0, 0], rotation: [350, 350, 350] }, 0.02, 600.0, true);
|
||||||
|
assert_eq!((cam.yaw, cam.pitch, cam.roll), orientation);
|
||||||
|
assert!(cam.is_valid());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-doc"
|
name = "solvecraft-doc"
|
||||||
description = "SolveCraft document model: parameters and expressions, sketches and features on a parametric timeline, incremental evaluation"
|
description = "SolidCraft document model: parameters and expressions, sketches and features on a parametric timeline, incremental evaluation"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
+11
-11
@@ -132,21 +132,21 @@ fn v2_to_v3(v: &mut Value) -> Result<()> {
|
|||||||
|
|
||||||
/// Read a design file's JSON, upgrading older formats.
|
/// Read a design file's JSON, upgrading older formats.
|
||||||
pub fn read(s: &str) -> Result<Document> {
|
pub fn read(s: &str) -> Result<Document> {
|
||||||
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolveCraft design: {e}")))?;
|
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft design: {e}")))?;
|
||||||
let Some(obj) = v.as_object() else {
|
let Some(obj) = v.as_object() else {
|
||||||
return Err(DocError::Invalid("not a SolveCraft design (expected a JSON object)".into()));
|
return Err(DocError::Invalid("not a SolidCraft design (expected a JSON object)".into()));
|
||||||
};
|
};
|
||||||
let version = match obj.get("format") {
|
let version = match obj.get("format") {
|
||||||
None => 1,
|
None => 1,
|
||||||
Some(Value::String(f)) => version_of(f).ok_or_else(|| DocError::Invalid(format!("not a SolveCraft design (format `{f}`)")))?,
|
Some(Value::String(f)) => version_of(f).ok_or_else(|| DocError::Invalid(format!("not a SolidCraft design (format `{f}`)")))?,
|
||||||
Some(_) => return Err(DocError::Invalid("not a SolveCraft design (format is not a string)".into())),
|
Some(_) => return Err(DocError::Invalid("not a SolidCraft design (format is not a string)".into())),
|
||||||
};
|
};
|
||||||
if version == 0 {
|
if version == 0 {
|
||||||
return Err(DocError::Invalid("not a SolveCraft design (format 0)".into()));
|
return Err(DocError::Invalid("not a SolidCraft design (format 0)".into()));
|
||||||
}
|
}
|
||||||
if version > FORMAT {
|
if version > FORMAT {
|
||||||
return Err(DocError::Invalid(format!(
|
return Err(DocError::Invalid(format!(
|
||||||
"this design was saved by a newer SolveCraft (file format {version}; this version reads formats up to {FORMAT}): update SolveCraft to open it"
|
"this design was saved by a newer SolidCraft (file format {version}; this version reads formats up to {FORMAT}): update SolidCraft to open it"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
check_numbers(&v, 0)?;
|
check_numbers(&v, 0)?;
|
||||||
@@ -950,7 +950,7 @@ mod tests {
|
|||||||
let error = read(&value.to_string()).unwrap_err().to_string();
|
let error = read(&value.to_string()).unwrap_err().to_string();
|
||||||
assert!(error.contains("free-form offset metadata"), "{error}");
|
assert!(error.contains("free-form offset metadata"), "{error}");
|
||||||
value["format"] = Value::from(format_string(FORMAT + 1));
|
value["format"] = Value::from(format_string(FORMAT + 1));
|
||||||
assert!(read(&value.to_string()).unwrap_err().to_string().contains("newer SolveCraft"));
|
assert!(read(&value.to_string()).unwrap_err().to_string().contains("newer SolidCraft"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,7 +975,7 @@ mod tests {
|
|||||||
assert!(error.contains("emboss face"), "{error}");
|
assert!(error.contains("emboss face"), "{error}");
|
||||||
value["format"] = Value::from(format_string(FORMAT + 1));
|
value["format"] = Value::from(format_string(FORMAT + 1));
|
||||||
let error = read(&value.to_string()).unwrap_err().to_string();
|
let error = read(&value.to_string()).unwrap_err().to_string();
|
||||||
assert!(error.contains("newer SolveCraft"), "{error}");
|
assert!(error.contains("newer SolidCraft"), "{error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1038,7 +1038,7 @@ mod tests {
|
|||||||
value["configs"]["columns"] = serde_json::json!([{"unknown_future_column":"Body"}]);
|
value["configs"]["columns"] = serde_json::json!([{"unknown_future_column":"Body"}]);
|
||||||
value["configs"]["rules"] = serde_json::json!({"unknown_future_rule": true});
|
value["configs"]["rules"] = serde_json::json!({"unknown_future_rule": true});
|
||||||
let error = read(&value.to_string()).unwrap_err().to_string();
|
let error = read(&value.to_string()).unwrap_err().to_string();
|
||||||
assert!(error.contains("newer SolveCraft") && error.contains("update SolveCraft"), "{error}");
|
assert!(error.contains("newer SolidCraft") && error.contains("update SolidCraft"), "{error}");
|
||||||
assert!(!error.contains("unknown variant"), "format refusal precedes decoding new variants");
|
assert!(!error.contains("unknown variant"), "format refusal precedes decoding new variants");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,7 +1090,7 @@ mod tests {
|
|||||||
saved["format"] = Value::from(format_string(FORMAT + 1));
|
saved["format"] = Value::from(format_string(FORMAT + 1));
|
||||||
saved["features"][0]["sketch"]["constraints"][0]["type"] = Value::from("future_dimension");
|
saved["features"][0]["sketch"]["constraints"][0]["type"] = Value::from("future_dimension");
|
||||||
let error = read(&saved.to_string()).unwrap_err().to_string();
|
let error = read(&saved.to_string()).unwrap_err().to_string();
|
||||||
assert!(error.contains("newer SolveCraft") && !error.contains("unknown variant"), "{error}");
|
assert!(error.contains("newer SolidCraft") && !error.contains("unknown variant"), "{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1202,7 +1202,7 @@ mod tests {
|
|||||||
assert!(read(&v.to_string()).is_ok(), "a file without a format is format 1");
|
assert!(read(&v.to_string()).is_ok(), "a file without a format is format 1");
|
||||||
v["format"] = Value::from(format_string(FORMAT + 1));
|
v["format"] = Value::from(format_string(FORMAT + 1));
|
||||||
let e = read(&v.to_string()).unwrap_err().to_string();
|
let e = read(&v.to_string()).unwrap_err().to_string();
|
||||||
assert!(e.contains("newer SolveCraft"), "{e}");
|
assert!(e.contains("newer SolidCraft"), "{e}");
|
||||||
for bad in ["solvecraft/0", "acad/3", "solvecraft/-1", "solvecraft/99999999999999999999"] {
|
for bad in ["solvecraft/0", "acad/3", "solvecraft/-1", "solvecraft/99999999999999999999"] {
|
||||||
v["format"] = Value::from(bad);
|
v["format"] = Value::from(bad);
|
||||||
assert!(read(&v.to_string()).is_err(), "{bad}");
|
assert!(read(&v.to_string()).is_err(), "{bad}");
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ mod tests {
|
|||||||
damaged["features"][1]["profiles"]["associations"] = associations;
|
damaged["features"][1]["profiles"]["associations"] = associations;
|
||||||
assert!(validate_raw(&damaged).is_err());
|
assert!(validate_raw(&damaged).is_err());
|
||||||
damaged["format"] = json!(crate::format::format_string(crate::format::FORMAT + 1));
|
damaged["format"] = json!(crate::format::format_string(crate::format::FORMAT + 1));
|
||||||
assert!(crate::format::read(&damaged.to_string()).unwrap_err().to_string().contains("newer SolveCraft"));
|
assert!(crate::format::read(&damaged.to_string()).unwrap_err().to_string().contains("newer SolidCraft"));
|
||||||
}
|
}
|
||||||
// Absent metadata preserves the historical one-to-one chain representation.
|
// Absent metadata preserves the historical one-to-one chain representation.
|
||||||
let mut legacy = fixture();
|
let mut legacy = fixture();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-engine"
|
name = "solvecraft-engine"
|
||||||
description = "SolveCraft engine: the session, the command registry (everything is a command), undo/redo and selection"
|
description = "SolidCraft engine: the session, the command registry (everything is a command), undo/redo and selection"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ pub(super) fn paste(
|
|||||||
let base = entered.as_ref().and_then(|st| st.body(&body.name)).or_else(|| before.as_ref().and_then(|st| st.body(&body.name))).unwrap_or(body);
|
let base = entered.as_ref().and_then(|st| st.body(&body.name)).or_else(|| before.as_ref().and_then(|st| st.body(&body.name))).unwrap_or(body);
|
||||||
let baseline_component = source.model_body_component(base);
|
let baseline_component = source.model_body_component(base);
|
||||||
let geometry = solvecraft_kernel::transform_matrix(&base.body, offset)?;
|
let geometry = solvecraft_kernel::transform_matrix(&base.body, offset)?;
|
||||||
let step = solvecraft_kernel::step_export(&[&geometry], "SolveCraft independent reassigned body")?;
|
let step = solvecraft_kernel::step_export(&[&geometry], "SolidCraft independent reassigned body")?;
|
||||||
let new_name = unique(&body.name, &mut used);
|
let new_name = unique(&body.name, &mut used);
|
||||||
bodies.insert(body.name.clone(), new_name.clone());
|
bodies.insert(body.name.clone(), new_name.clone());
|
||||||
let feature_name = unique(&format!("{} Base", body.name), &mut used);
|
let feature_name = unique(&format!("{} Base", body.name), &mut used);
|
||||||
|
|||||||
@@ -1738,14 +1738,18 @@ fn construction(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let cs: Vec<usize> =
|
let cs: Vec<usize> =
|
||||||
ids.iter().map(|id| sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))).collect::<Result<_>>()?;
|
ids.iter().map(|id| sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))).collect::<Result<_>>()?;
|
||||||
// The selection switches together (a slot or polygon as a whole): to construction
|
// The selection switches together (a slot or polygon as a whole): to construction
|
||||||
// unless all of it already is. Projected curves may switch too.
|
// unless all of it already is. Projected curves may switch too. A centerline that
|
||||||
|
// becomes construction stops being a centerline (one line type at a time).
|
||||||
let to = want.unwrap_or_else(|| !cs.iter().all(|c| sk.curves.get(*c).is_some_and(|c| c.construction)));
|
let to = want.unwrap_or_else(|| !cs.iter().all(|c| sk.curves.get(*c).is_some_and(|c| c.construction)));
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
for c in cs {
|
for c in cs {
|
||||||
if let Some(cu) = sk.curves.get_mut(c)
|
if let Some(cu) = sk.curves.get_mut(c)
|
||||||
&& cu.construction != to
|
&& (cu.construction != to || to && cu.centerline)
|
||||||
{
|
{
|
||||||
cu.construction = to;
|
cu.construction = to;
|
||||||
|
if to {
|
||||||
|
cu.centerline = false;
|
||||||
|
}
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
|||||||
let text = source_text(p, cmd)?;
|
let text = source_text(p, cmd)?;
|
||||||
let geom = if svg { solvecraft_io::read_svg(&text) } else { solvecraft_io::read_dxf(&text) }.map_err(|e| bad(cmd, e.to_string()))?;
|
let geom = if svg { solvecraft_io::read_svg(&text) } else { solvecraft_io::read_dxf(&text) }.map_err(|e| bad(cmd, e.to_string()))?;
|
||||||
if geom.is_empty() {
|
if geom.is_empty() {
|
||||||
return Err(bad(cmd, "the drawing has no geometry SolveCraft reads"));
|
return Err(bad(cmd, "the drawing has no geometry SolidCraft reads"));
|
||||||
}
|
}
|
||||||
let k = num(p, "scale").filter(|k| *k > 1e-9 && *k < 1e6).unwrap_or(1.0);
|
let k = num(p, "scale").filter(|k| *k > 1e-9 && *k < 1e6).unwrap_or(1.0);
|
||||||
let off = p.get("at").and_then(vec2).unwrap_or(Vec2::ZERO);
|
let off = p.get("at").and_then(vec2).unwrap_or(Vec2::ZERO);
|
||||||
|
|||||||
@@ -237,6 +237,21 @@ fn construction_toggle_takes_shapes_out_of_profiles_and_back() {
|
|||||||
assert!(s.execute("sketch.construction", &json!({"curves": ["nope"]})).is_err());
|
assert!(s.execute("sketch.construction", &json!({"curves": ["nope"]})).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn centerline_can_become_construction_directly() {
|
||||||
|
let mut s = new_sketch();
|
||||||
|
let l = curve_ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]})));
|
||||||
|
run(&mut s, "sketch.centerline", json!({ "curves": l, "value": true }));
|
||||||
|
let r = run(&mut s, "sketch.construction", json!({ "curves": l, "value": true }));
|
||||||
|
assert_eq!(r["changed"], 1);
|
||||||
|
let sk = sketch(&s);
|
||||||
|
assert!(sk.curves.iter().filter(|c| l.contains(&c.id)).all(|c| c.construction && !c.centerline));
|
||||||
|
// And back to a centerline.
|
||||||
|
run(&mut s, "sketch.centerline", json!({ "curves": l, "value": true }));
|
||||||
|
let sk = sketch(&s);
|
||||||
|
assert!(sk.curves.iter().filter(|c| l.contains(&c.id)).all(|c| c.centerline && !c.construction));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn projected_geometry_can_become_construction() {
|
fn projected_geometry_can_become_construction() {
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub const THIRD_PARTY: &str = include_str!("../../../THIRD-PARTY-LICENSES.txt");
|
|||||||
pub fn all() -> String {
|
pub fn all() -> String {
|
||||||
let rule = "=".repeat(80);
|
let rule = "=".repeat(80);
|
||||||
format!(
|
format!(
|
||||||
"SolveCraft is licensed under the MIT License or the Apache License, Version 2.0, at your option.\n\n{rule}\nNOTICE\n{rule}\n\n{NOTICE}\n{rule}\nLICENSE-MIT\n{rule}\n\n{LICENSE_MIT}\n{rule}\nLICENSE-APACHE\n{rule}\n\n{LICENSE_APACHE}\n{rule}\n{THIRD_PARTY}"
|
"SolidCraft is licensed under the MIT License or the Apache License, Version 2.0, at your option.\n\n{rule}\nNOTICE\n{rule}\n\n{NOTICE}\n{rule}\nLICENSE-MIT\n{rule}\n\n{LICENSE_MIT}\n{rule}\nLICENSE-APACHE\n{rule}\n\n{LICENSE_APACHE}\n{rule}\n{THIRD_PARTY}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-geom"
|
name = "solvecraft-geom"
|
||||||
description = "SolveCraft geometry foundation: vectors, planes, frames, meshes and their measures"
|
description = "SolidCraft geometry foundation: vectors, planes, frames, meshes and their measures"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-io"
|
name = "solvecraft-io"
|
||||||
description = "SolveCraft file formats: designs (JSON), STL, OBJ, STEP and 3MF"
|
description = "SolidCraft file formats: designs (JSON), STL, OBJ, STEP and 3MF"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ pub fn export_mesh(b: &solvecraft_kernel::Body) -> Result<Mesh> {
|
|||||||
pub fn stl_binary(meshes: &[Mesh], name: &str) -> Vec<u8> {
|
pub fn stl_binary(meshes: &[Mesh], name: &str) -> Vec<u8> {
|
||||||
let n: usize = meshes.iter().map(|m| m.triangles.len()).sum();
|
let n: usize = meshes.iter().map(|m| m.triangles.len()).sum();
|
||||||
let mut out = Vec::with_capacity(84 + n * 50);
|
let mut out = Vec::with_capacity(84 + n * 50);
|
||||||
let mut header = format!("SolveCraft STL {name}").into_bytes();
|
let mut header = format!("SolidCraft STL {name}").into_bytes();
|
||||||
header.resize(80, b' ');
|
header.resize(80, b' ');
|
||||||
out.extend_from_slice(&header);
|
out.extend_from_slice(&header);
|
||||||
out.extend_from_slice(&u32::try_from(n).unwrap_or(u32::MAX).to_le_bytes());
|
out.extend_from_slice(&u32::try_from(n).unwrap_or(u32::MAX).to_le_bytes());
|
||||||
@@ -242,7 +242,7 @@ pub fn stl_ascii(meshes: &[Mesh], name: &str) -> String {
|
|||||||
|
|
||||||
/// Wavefront OBJ, one object per body.
|
/// Wavefront OBJ, one object per body.
|
||||||
pub fn obj(meshes: &[(String, Mesh)]) -> String {
|
pub fn obj(meshes: &[(String, Mesh)]) -> String {
|
||||||
let mut s = String::from("# SolveCraft OBJ export (mm)\n");
|
let mut s = String::from("# SolidCraft OBJ export (mm)\n");
|
||||||
let mut base = 1usize;
|
let mut base = 1usize;
|
||||||
for (name, m) in meshes {
|
for (name, m) in meshes {
|
||||||
s += &format!("o {}\n", name.replace(char::is_whitespace, "_"));
|
s += &format!("o {}\n", name.replace(char::is_whitespace, "_"));
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ pub fn model_xml(objects: &[MeshObject]) -> String {
|
|||||||
let mut x = String::new();
|
let mut x = String::new();
|
||||||
x.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
x.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
let _ = writeln!(x, "<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"{CORE_NS}\">");
|
let _ = writeln!(x, "<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"{CORE_NS}\">");
|
||||||
x.push_str(" <metadata name=\"Application\">SolveCraft</metadata>\n <resources>\n");
|
x.push_str(" <metadata name=\"Application\">SolidCraft</metadata>\n <resources>\n");
|
||||||
let colored = objects.iter().any(|o| o.color.is_some() || o.transparency > 0.0 || !o.looks.is_empty());
|
let colored = objects.iter().any(|o| o.color.is_some() || o.transparency > 0.0 || !o.looks.is_empty());
|
||||||
// Base material group id 1 (each object's own colour, then its faces' colours); objects
|
// Base material group id 1 (each object's own colour, then its faces' colours); objects
|
||||||
// from 2. A see-through colour carries its alpha (#RRGGBBAA).
|
// from 2. A see-through colour carries its alpha (#RRGGBBAA).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-kernel"
|
name = "solvecraft-kernel"
|
||||||
description = "SolveCraft B-rep kernel boundary: solids, extrude/revolve/booleans/fillets, tessellation, measures and STEP, behind our own API (implemented with truck)"
|
description = "SolidCraft B-rep kernel boundary: solids, extrude/revolve/booleans/fillets, tessellation, measures and STEP, behind our own API (implemented with truck)"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1164,7 +1164,7 @@ fn translate(text: &str) -> std::result::Result<(String, Vec<String>), String> {
|
|||||||
t.o.add(format!("STYLED_ITEM('color',(#{psa}),#{item})"));
|
t.o.add(format!("STYLED_ITEM('color',(#{psa}),#{item})"));
|
||||||
}
|
}
|
||||||
let mut s = String::from(
|
let mut s = String::from(
|
||||||
"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('IGES import'),'2;1');\nFILE_NAME('','',(''),(''),'SolveCraft IGES','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\nDATA;\n",
|
"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('IGES import'),'2;1');\nFILE_NAME('','',(''),(''),'SolidCraft IGES','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\nDATA;\n",
|
||||||
);
|
);
|
||||||
for (i, l) in t.o.lines.iter().enumerate() {
|
for (i, l) in t.o.lines.iter().enumerate() {
|
||||||
let _ = writeln!(s, "#{}={l};", i + 1);
|
let _ = writeln!(s, "#{}={l};", i + 1);
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ fn layout(w: &W, header: &StepHeader, max_coord: f64) -> String {
|
|||||||
let line = |out: &mut String, data: &str, sect: char, seq: usize| {
|
let line = |out: &mut String, data: &str, sect: char, seq: usize| {
|
||||||
let _ = writeln!(out, "{data:<72}{sect}{seq:>7}");
|
let _ = writeln!(out, "{data:<72}{sect}{seq:>7}");
|
||||||
};
|
};
|
||||||
line(&mut out, "SolveCraft IGES export", 'S', 1);
|
line(&mut out, "SolidCraft IGES export", 'S', 1);
|
||||||
let stamp = {
|
let stamp = {
|
||||||
let t = header.time_stamp.replace(['-', ':', 'T'], "");
|
let t = header.time_stamp.replace(['-', ':', 'T'], "");
|
||||||
let t: String = t.chars().filter(char::is_ascii_digit).collect();
|
let t: String = t.chars().filter(char::is_ascii_digit).collect();
|
||||||
@@ -618,10 +618,10 @@ fn layout(w: &W, header: &StepHeader, max_coord: f64) -> String {
|
|||||||
let g = [
|
let g = [
|
||||||
"1H,".to_string(),
|
"1H,".to_string(),
|
||||||
"1H;".to_string(),
|
"1H;".to_string(),
|
||||||
holl("SolveCraft"),
|
holl("SolidCraft"),
|
||||||
holl(&header.file_name),
|
holl(&header.file_name),
|
||||||
holl("SolveCraft"),
|
holl("SolidCraft"),
|
||||||
holl("SolveCraft IGES 5.3"),
|
holl("SolidCraft IGES 5.3"),
|
||||||
"32".into(),
|
"32".into(),
|
||||||
"38".into(),
|
"38".into(),
|
||||||
"6".into(),
|
"6".into(),
|
||||||
|
|||||||
@@ -1190,7 +1190,7 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
|||||||
/// The B-rep entities of one body as truck writes them, parsed, and the surfaces of faces
|
/// The B-rep entities of one body as truck writes them, parsed, and the surfaces of faces
|
||||||
/// merged from pieces.
|
/// merged from pieces.
|
||||||
fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V3>, HashMap<u64, FaceLook>)> {
|
fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V3>, HashMap<u64, FaceLook>)> {
|
||||||
let text = crate::step::truck_step(&[b], "SolveCraft")?;
|
let text = crate::step::truck_step(&[b], "SolidCraft")?;
|
||||||
let mut ex = p21::parse(&text).map_err(|e| KernelError::Failed(format!("STEP export: {e}")))?;
|
let mut ex = p21::parse(&text).map_err(|e| KernelError::Failed(format!("STEP export: {e}")))?;
|
||||||
collapse_short_edges(&mut ex);
|
collapse_short_edges(&mut ex);
|
||||||
// Face colours by exchange face: truck writes each shell's faces in the body's face order.
|
// Face colours by exchange face: truck writes each shell's faces in the body's face order.
|
||||||
@@ -1429,10 +1429,10 @@ fn products_text(products: &[ExportProduct], root: usize, header: &StepHeader, m
|
|||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s.push_str("ISO-10303-21;\nHEADER;\n");
|
s.push_str("ISO-10303-21;\nHEADER;\n");
|
||||||
s.push_str("FILE_DESCRIPTION(('SolveCraft model'),'2;1');\n");
|
s.push_str("FILE_DESCRIPTION(('SolidCraft model'),'2;1');\n");
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
s,
|
s,
|
||||||
"FILE_NAME({},{},({}),({}),'SolveCraft','SolveCraft','');",
|
"FILE_NAME({},{},({}),({}),'SolidCraft','SolidCraft','');",
|
||||||
string(&header.file_name),
|
string(&header.file_name),
|
||||||
string(&header.time_stamp),
|
string(&header.time_stamp),
|
||||||
string(&header.author),
|
string(&header.author),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-mcp"
|
name = "solvecraft-mcp"
|
||||||
description = "SolveCraft MCP server: JSON-RPC 2.0 over stdio, driving a headless session or the running app"
|
description = "SolidCraft MCP server: JSON-RPC 2.0 over stdio, driving a headless session or the running app"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl Backend for Remote {
|
|||||||
let reply = match self.roundtrip(&line) {
|
let reply = match self.roundtrip(&line) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
// One retry on a fresh connection (the app may have restarted).
|
// One retry on a fresh connection (the app may have restarted).
|
||||||
Err(_) => self.roundtrip(&line).map_err(|e| format!("SolveCraft at {}: {e}", self.addr))?,
|
Err(_) => self.roundtrip(&line).map_err(|e| format!("SolidCraft at {}: {e}", self.addr))?,
|
||||||
};
|
};
|
||||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||||
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||||
@@ -102,7 +102,7 @@ impl Backend for Remote {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
fn describe(&self) -> String {
|
fn describe(&self) -> String {
|
||||||
format!("connected to the SolveCraft app at {} (the user watches the viewport)", self.addr)
|
format!("connected to the SolidCraft app at {} (the user watches the viewport)", self.addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +168,6 @@ impl Backend for Headless {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn describe(&self) -> String {
|
fn describe(&self) -> String {
|
||||||
"headless in-process SolveCraft session".into()
|
"headless in-process SolidCraft session".into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ const RESOURCE_NOT_FOUND: i64 = -32002;
|
|||||||
/// Most messages in one JSON-RPC batch.
|
/// Most messages in one JSON-RPC batch.
|
||||||
const MAX_BATCH: usize = 1000;
|
const MAX_BATCH: usize = 1000;
|
||||||
|
|
||||||
const INSTRUCTIONS: &str = "SolveCraft is a parametric 3D CAD app in the style of Autodesk Fusion. Units are millimetres \
|
const INSTRUCTIONS: &str = "SolidCraft is a parametric 3D CAD app in the style of Autodesk Fusion. Units are millimetres \
|
||||||
and degrees in expressions (internally radians), Z is up. Model like in Fusion: `sketch.create {plane: XY}`, draw \
|
and degrees in expressions (internally radians), Z is up. Model like in Fusion: `sketch.create {plane: XY}`, draw \
|
||||||
(`sketch.rectangle.two_point {p0, p1}`, `sketch.circle.center {center, radius}`, `sketch.line {points, closed}`), constrain \
|
(`sketch.rectangle.two_point {p0, p1}`, `sketch.circle.center {center, radius}`, `sketch.line {points, closed}`), constrain \
|
||||||
and dimension (`sketch.dimension {entities, value}`; values may name parameters), `sketch.finish`, then features \
|
and dimension (`sketch.dimension {entities, value}`; values may name parameters), `sketch.finish`, then features \
|
||||||
@@ -133,7 +133,7 @@ impl Server {
|
|||||||
Ok(json!({
|
Ok(json!({
|
||||||
"protocolVersion": version,
|
"protocolVersion": version,
|
||||||
"capabilities": {"tools": {"listChanged": false}, "resources": {"listChanged": false}},
|
"capabilities": {"tools": {"listChanged": false}, "resources": {"listChanged": false}},
|
||||||
"serverInfo": {"name": "solvecraft", "title": "SolveCraft", "version": env!("CARGO_PKG_VERSION")},
|
"serverInfo": {"name": "solvecraft", "title": "SolidCraft", "version": env!("CARGO_PKG_VERSION")},
|
||||||
"instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()),
|
"instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-render"
|
name = "solvecraft-render"
|
||||||
description = "SolveCraft view math (orbit camera, projection, picking rays) and a CPU rasterizer for headless snapshots"
|
description = "SolidCraft view math (orbit camera, projection, picking rays) and a CPU rasterizer for headless snapshots"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -12,3 +12,6 @@ workspace = true
|
|||||||
solvecraft-geom = { workspace = true }
|
solvecraft-geom = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = { workspace = true }
|
||||||
+45
-10
@@ -224,6 +224,9 @@ pub struct Camera {
|
|||||||
pub yaw: f64,
|
pub yaw: f64,
|
||||||
/// Elevation above the XY plane, in (−π/2, π/2].
|
/// Elevation above the XY plane, in (−π/2, π/2].
|
||||||
pub pitch: f64,
|
pub pitch: f64,
|
||||||
|
/// Rotation about the viewing direction. Zero keeps the world's up axis vertical.
|
||||||
|
#[serde(default)]
|
||||||
|
pub roll: f64,
|
||||||
/// Distance from target to eye.
|
/// Distance from target to eye.
|
||||||
pub distance: f64,
|
pub distance: f64,
|
||||||
/// Vertical field of view (radians); 0 = orthographic.
|
/// Vertical field of view (radians); 0 = orthographic.
|
||||||
@@ -236,7 +239,7 @@ pub struct Camera {
|
|||||||
impl Default for Camera {
|
impl Default for Camera {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (yaw, pitch) = StandardView::Iso.angles();
|
let (yaw, pitch) = StandardView::Iso.angles();
|
||||||
Camera { target: Vec3::ZERO, yaw, pitch, distance: 200.0, fov: 0.0, up: UpAxis::Z }
|
Camera { target: Vec3::ZERO, yaw, pitch, roll: 0.0, distance: 200.0, fov: 0.0, up: UpAxis::Z }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +269,8 @@ impl Camera {
|
|||||||
let b = self.back();
|
let b = self.back();
|
||||||
let right = self.up.from_frame(Vec3::new(self.yaw.cos(), -self.yaw.sin(), 0.0));
|
let right = self.up.from_frame(Vec3::new(self.yaw.cos(), -self.yaw.sin(), 0.0));
|
||||||
let up = b.cross(right).normalized().unwrap_or(self.up.dir());
|
let up = b.cross(right).normalized().unwrap_or(self.up.dir());
|
||||||
(right, up, b)
|
let (s, c) = self.roll.sin_cos();
|
||||||
|
(right * c + up * s, up * c - right * s, b)
|
||||||
}
|
}
|
||||||
pub fn view(&self) -> Mat4 {
|
pub fn view(&self) -> Mat4 {
|
||||||
let (r, u, b) = self.basis();
|
let (r, u, b) = self.basis();
|
||||||
@@ -349,11 +353,18 @@ impl Camera {
|
|||||||
self.yaw = (self.yaw + dx * 0.008) % std::f64::consts::TAU;
|
self.yaw = (self.yaw + dx * 0.008) % std::f64::consts::TAU;
|
||||||
self.pitch = (self.pitch + dy * 0.008).clamp(-lim, lim);
|
self.pitch = (self.pitch + dy * 0.008).clamp(-lim, lim);
|
||||||
}
|
}
|
||||||
|
/// Rotate the image about the viewing direction.
|
||||||
|
pub fn roll_by(&mut self, radians: f64) {
|
||||||
|
if radians.is_finite() {
|
||||||
|
self.roll = (self.roll + radians) % std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn set_view(&mut self, v: StandardView) {
|
pub fn set_view(&mut self, v: StandardView) {
|
||||||
let lim = std::f64::consts::FRAC_PI_2 - 1e-6;
|
let lim = std::f64::consts::FRAC_PI_2 - 1e-6;
|
||||||
let (y, p) = v.angles();
|
let (y, p) = v.angles();
|
||||||
self.yaw = y;
|
self.yaw = y;
|
||||||
self.pitch = p.clamp(-lim, lim);
|
self.pitch = p.clamp(-lim, lim);
|
||||||
|
self.roll = 0.0;
|
||||||
}
|
}
|
||||||
/// Frame a bounding box.
|
/// Frame a bounding box.
|
||||||
pub fn fit(&mut self, b: &Aabb3) {
|
pub fn fit(&mut self, b: &Aabb3) {
|
||||||
@@ -376,15 +387,14 @@ impl Camera {
|
|||||||
} else {
|
} else {
|
||||||
(-b.x).atan2(-b.y)
|
(-b.x).atan2(-b.y)
|
||||||
};
|
};
|
||||||
Camera { yaw, pitch, ..*self }
|
Camera { yaw, pitch, roll: 0.0, ..*self }
|
||||||
}
|
}
|
||||||
/// Orientation as a unit quaternion (camera → world rotation).
|
/// Orientation as a unit quaternion (camera → world rotation).
|
||||||
pub fn orientation(&self) -> Quat {
|
pub fn orientation(&self) -> Quat {
|
||||||
let (r, u, b) = self.basis();
|
let (r, u, b) = self.basis();
|
||||||
Quat::from_basis(r, u, b)
|
Quat::from_basis(r, u, b)
|
||||||
}
|
}
|
||||||
/// Blend between two cameras at `s` in [0, 1]: heading (the shorter way), elevation,
|
/// Blend between two cameras at `s` in [0, 1]: heading and roll take the shorter way.
|
||||||
/// target, distance and field of view linearly. The ends are returned exactly.
|
|
||||||
pub fn interpolate(a: &Camera, b: &Camera, s: f64) -> Camera {
|
pub fn interpolate(a: &Camera, b: &Camera, s: f64) -> Camera {
|
||||||
if !(s > 0.0) {
|
if !(s > 0.0) {
|
||||||
return *a;
|
return *a;
|
||||||
@@ -392,24 +402,29 @@ impl Camera {
|
|||||||
if s >= 1.0 {
|
if s >= 1.0 {
|
||||||
return *b;
|
return *b;
|
||||||
}
|
}
|
||||||
// The camera has no roll, so it turns in heading and elevation: both change smoothly
|
// Keep heading and elevation smooth near straight up or down. A quaternion slerp
|
||||||
// to the very end (the heading the shorter way round). A slerp of orientations would
|
// swings the heading sharply there when moving to Top.
|
||||||
// need a roll-free heading at every step, and near straight up or down that heading
|
|
||||||
// swings fast, so a move to Top spun in its last frames.
|
|
||||||
let dy = (b.yaw - a.yaw + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
let dy = (b.yaw - a.yaw + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
||||||
let yaw = a.yaw + dy * s;
|
let yaw = a.yaw + dy * s;
|
||||||
|
let dr = (b.roll - a.roll + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
||||||
let pitch = a.pitch + (b.pitch - a.pitch) * s;
|
let pitch = a.pitch + (b.pitch - a.pitch) * s;
|
||||||
Camera {
|
Camera {
|
||||||
target: a.target + (b.target - a.target) * s,
|
target: a.target + (b.target - a.target) * s,
|
||||||
yaw,
|
yaw,
|
||||||
pitch,
|
pitch,
|
||||||
|
roll: a.roll + dr * s,
|
||||||
distance: a.distance + (b.distance - a.distance) * s,
|
distance: a.distance + (b.distance - a.distance) * s,
|
||||||
fov: a.fov + (b.fov - a.fov) * s,
|
fov: a.fov + (b.fov - a.fov) * s,
|
||||||
up: b.up,
|
up: b.up,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool {
|
||||||
self.target.is_finite() && self.yaw.is_finite() && self.pitch.is_finite() && self.distance.is_finite() && self.distance > 0.0
|
self.target.is_finite()
|
||||||
|
&& self.yaw.is_finite()
|
||||||
|
&& self.pitch.is_finite()
|
||||||
|
&& self.roll.is_finite()
|
||||||
|
&& self.distance.is_finite()
|
||||||
|
&& self.distance > 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +459,26 @@ mod tests {
|
|||||||
assert!(x1 > x0, "dragging right must move the near side right: {x0} -> {x1}");
|
assert!(x1 > x0, "dragging right must move the near side right: {x0} -> {x1}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roll_rotates_the_picture_and_standard_views_level_it() {
|
||||||
|
let mut c = Camera::default();
|
||||||
|
c.set_view(StandardView::Front);
|
||||||
|
let (_, before_y, _) = c.to_screen(Vec3::X, 800.0, 600.0, 10.0).unwrap();
|
||||||
|
c.roll_by(std::f64::consts::FRAC_PI_2);
|
||||||
|
let (after_x, after_y, _) = c.to_screen(Vec3::X, 800.0, 600.0, 10.0).unwrap();
|
||||||
|
assert!((before_y - 300.0).abs() < 1e-9);
|
||||||
|
assert!((after_x - 400.0).abs() < 1e-9 && after_y > 300.0);
|
||||||
|
let (r, u, b) = c.basis();
|
||||||
|
assert!(r.dot(u).abs() < 1e-12 && r.dot(b).abs() < 1e-12 && u.dot(b).abs() < 1e-12);
|
||||||
|
c.set_view(StandardView::Front);
|
||||||
|
assert_eq!(c.roll, 0.0);
|
||||||
|
let saved = serde_json::to_value(c).unwrap();
|
||||||
|
let mut old = saved.as_object().unwrap().clone();
|
||||||
|
old.remove("roll");
|
||||||
|
let restored: Camera = serde_json::from_value(serde_json::Value::Object(old)).unwrap();
|
||||||
|
assert_eq!(restored.roll, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn view_animation_interpolates() {
|
fn view_animation_interpolates() {
|
||||||
let mut a = Camera { target: Vec3::new(1.0, 2.0, 3.0), distance: 80.0, ..Default::default() };
|
let mut a = Camera { target: Vec3::new(1.0, 2.0, 3.0), distance: 80.0, ..Default::default() };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-sketch"
|
name = "solvecraft-sketch"
|
||||||
description = "SolveCraft sketches: points, curves, geometric constraints and dimensions, the constraint solver, and profile detection"
|
description = "SolidCraft sketches: points, curves, geometric constraints and dimensions, the constraint solver, and profile detection"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solvecraft-ui-egui"
|
name = "solvecraft-ui-egui"
|
||||||
description = "SolveCraft user interface (egui + wgpu): the swappable front end"
|
description = "SolidCraft user interface (egui + wgpu): the swappable front end"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -97,11 +97,19 @@ impl DimensionDraft {
|
|||||||
|
|
||||||
pub fn cancel() {
|
pub fn cancel() {
|
||||||
EDIT.with(|edit| *edit.borrow_mut() = None);
|
EDIT.with(|edit| *edit.borrow_mut() = None);
|
||||||
|
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Infer the measured value through the same command as scripts, without creating a real
|
/// Infer the measured value through the same command as scripts, without creating a real
|
||||||
/// parameter or an undo entry. Only accepting the editor executes the command on the design.
|
/// parameter or an undo entry. Only accepting the editor executes the command on the design.
|
||||||
pub fn begin_dimension(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Result<(), String> {
|
pub fn begin_dimension(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Result<(), String> {
|
||||||
|
let (param, expr, draft) = dimension_draft(app, entities, at)?;
|
||||||
|
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft) }));
|
||||||
|
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dimension_draft(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Result<(String, String, DimensionDraft), String> {
|
||||||
let sketch = app.session.active_sketch.ok_or("Edit a sketch to add a dimension")?;
|
let sketch = app.session.active_sketch.ok_or("Edit a sketch to add a dimension")?;
|
||||||
if entities.is_empty() || entities.len() > 2 || !at.x.is_finite() || !at.y.is_finite() {
|
if entities.is_empty() || entities.len() > 2 || !at.x.is_finite() || !at.y.is_finite() {
|
||||||
return Err("Pick one edge or two endpoints, then place the dimension".into());
|
return Err("Pick one edge or two endpoints, then place the dimension".into());
|
||||||
@@ -126,8 +134,51 @@ pub fn begin_dimension(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Resul
|
|||||||
if !draft.preview.sketch(sketch).is_some_and(|solved| solved.report.ok()) {
|
if !draft.preview.sketch(sketch).is_some_and(|solved| solved.report.ok()) {
|
||||||
return Err("That dimension conflicts with the sketch constraints".into());
|
return Err("That dimension conflicts with the sketch constraints".into());
|
||||||
}
|
}
|
||||||
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft) }));
|
Ok((param, expr, draft))
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
/// Cache inference at a stationary pointer, including incomplete/unsupported picks.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Placement {
|
||||||
|
baseline: Arc<solvecraft_engine::doc::Document>,
|
||||||
|
sketch: Option<u64>,
|
||||||
|
component: u64,
|
||||||
|
path: Option<Vec<u64>>,
|
||||||
|
pending: std::collections::BTreeMap<u64, solvecraft_engine::doc::Mat>,
|
||||||
|
entities: Vec<String>,
|
||||||
|
at: Vec2,
|
||||||
|
draft: Option<(String, DimensionDraft)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placement_preview(app: &SolveApp) -> Option<(String, DimensionDraft)> {
|
||||||
|
let tool = app.tool.as_ref().filter(|t| t.kind == crate::tools::Kind::Dimension && !t.picks.is_empty())?;
|
||||||
|
let at = tool.hover.as_ref()?.0;
|
||||||
|
PLACEMENT.with(|preview| {
|
||||||
|
let mut preview = preview.borrow_mut();
|
||||||
|
let current = preview.as_ref().is_some_and(|p| {
|
||||||
|
Arc::ptr_eq(&p.baseline, &app.session.doc)
|
||||||
|
&& p.sketch == app.session.active_sketch
|
||||||
|
&& p.component == app.session.active_component
|
||||||
|
&& p.path == app.session.active_occurrence_path
|
||||||
|
&& p.pending == app.session.pending_moves
|
||||||
|
&& p.entities == tool.picks
|
||||||
|
&& p.at == at
|
||||||
|
});
|
||||||
|
if !current {
|
||||||
|
let draft = dimension_draft(app, tool.picks.clone(), at).ok().map(|(param, _, draft)| (param, draft));
|
||||||
|
*preview = Some(Placement {
|
||||||
|
baseline: app.session.doc.clone(),
|
||||||
|
sketch: app.session.active_sketch,
|
||||||
|
component: app.session.active_component,
|
||||||
|
path: app.session.active_occurrence_path.clone(),
|
||||||
|
pending: app.session.pending_moves.clone(),
|
||||||
|
entities: tool.picks.clone(),
|
||||||
|
at,
|
||||||
|
draft,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
preview.as_ref().and_then(|p| p.draft.clone())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -135,6 +186,7 @@ thread_local! {
|
|||||||
/// A text being dragged: dimension id, undo depth at the start, grab offset (sketch).
|
/// A text being dragged: dimension id, undo depth at the start, grab offset (sketch).
|
||||||
static DRAG: RefCell<Option<(String, usize, Vec2)>> = const { RefCell::new(None) };
|
static DRAG: RefCell<Option<(String, usize, Vec2)>> = const { RefCell::new(None) };
|
||||||
static EDIT: RefCell<Option<Edit>> = const { RefCell::new(None) };
|
static EDIT: RefCell<Option<Edit>> = const { RefCell::new(None) };
|
||||||
|
static PLACEMENT: RefCell<Option<Placement>> = const { RefCell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frames the inline editor holds on to the keyboard after opening.
|
/// Frames the inline editor holds on to the keyboard after opening.
|
||||||
@@ -258,7 +310,7 @@ pub fn label(k: &ConstraintKind, expr: Option<&str>, driven: bool) -> String {
|
|||||||
/// Draw the active sketch's dimensions and remember where they are for picking.
|
/// Draw the active sketch's dimensions and remember where they are for picking.
|
||||||
pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &Proj) {
|
pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &Proj) {
|
||||||
let mut drawn = Vec::new();
|
let mut drawn = Vec::new();
|
||||||
let draft = EDIT.with(|edit| {
|
let editor_draft = EDIT.with(|edit| {
|
||||||
let mut edit = edit.borrow_mut();
|
let mut edit = edit.borrow_mut();
|
||||||
if edit.as_ref().and_then(|e| e.draft.as_ref()).is_some_and(|d| !d.valid(app)) {
|
if edit.as_ref().and_then(|e| e.draft.as_ref()).is_some_and(|d| !d.valid(app)) {
|
||||||
*edit = None;
|
*edit = None;
|
||||||
@@ -282,6 +334,8 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let placing = !editing();
|
||||||
|
let draft = editor_draft.or_else(|| placing.then(|| placement_preview(app)).flatten());
|
||||||
let st = app.session.world_state();
|
let st = app.session.world_state();
|
||||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else {
|
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else {
|
||||||
DRAWN.with(|d| d.borrow_mut().clear());
|
DRAWN.with(|d| d.borrow_mut().clear());
|
||||||
@@ -372,13 +426,13 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
|
|||||||
.collect();
|
.collect();
|
||||||
painter.add(Shape::line(pts, stroke));
|
painter.add(Shape::line(pts, stroke));
|
||||||
}
|
}
|
||||||
if is_draft {
|
if is_draft && !placing {
|
||||||
EDIT.with(|edit| {
|
EDIT.with(|edit| {
|
||||||
if let Some(e) = edit.borrow_mut().as_mut() {
|
if let Some(e) = edit.borrow_mut().as_mut() {
|
||||||
e.at = Some(tc);
|
e.at = Some(tc);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else if !is_draft {
|
||||||
drawn.push(Drawn { id: c.id.clone(), param: c.param.clone(), text: r, segs, at: lay.text });
|
drawn.push(Drawn { id: c.id.clone(), param: c.param.clone(), text: r, segs, at: lay.text });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,6 +556,71 @@ pub fn drawn() -> Vec<(String, Pos2)> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placement_previews_radial_and_angular_dimensions_and_invalidates_on_document_change() {
|
||||||
|
cancel();
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.circle.center", json!({"center":[30,30],"radius":5})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[10,10],[20,10]],"infer":false,"connect":false})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[10,10],[10,20]],"infer":false,"connect":false})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
for (picks, at, angular) in [(vec![[35, 30]], [40, 40], false), (vec![[15, 10], [10, 15]], [18, 18], true)] {
|
||||||
|
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||||
|
for pick in picks {
|
||||||
|
h.step(&json!({"click":{"sketch":pick}})).unwrap();
|
||||||
|
}
|
||||||
|
h.step(&json!({"move":{"sketch":at}})).unwrap();
|
||||||
|
let (param, draft) = placement_preview(&h.app).unwrap();
|
||||||
|
let constraint =
|
||||||
|
draft.preview.sketch(draft.sketch).unwrap().sketch.constraints.iter().find(|c| c.param.as_deref() == Some(¶m)).unwrap();
|
||||||
|
assert_eq!(constraint.kind.is_angle(), angular);
|
||||||
|
assert!(!editing());
|
||||||
|
let (_, cached) = placement_preview(&h.app).unwrap();
|
||||||
|
assert!(Arc::ptr_eq(&draft.preview_doc, &cached.preview_doc), "stationary pointer reuses inference");
|
||||||
|
h.app.run("sketch.point", json!({"point":[50,50]})).unwrap();
|
||||||
|
let (_, refreshed) = placement_preview(&h.app).unwrap();
|
||||||
|
assert!(!Arc::ptr_eq(&draft.preview_doc, &refreshed.preview_doc));
|
||||||
|
h.step(&json!({"key":"Escape"})).unwrap();
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placement_follows_real_pointer_without_editing_or_mutating_design() {
|
||||||
|
cancel();
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
let baseline = h.app.session.doc.to_json();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||||
|
h.step(&json!({"click":{"sketch":[20,10]}})).unwrap();
|
||||||
|
for at in [[20., 20.], [20., -5.]] {
|
||||||
|
h.step(&json!({"move":{"sketch":at}})).unwrap();
|
||||||
|
h.settle();
|
||||||
|
let (param, draft) = placement_preview(&h.app).expect("a line has a dimension preview");
|
||||||
|
assert!((draft.at - Vec2::new(at[0], at[1])).len() < 1e-5);
|
||||||
|
let sk = &draft.preview.sketch(draft.sketch).unwrap().sketch;
|
||||||
|
let c = sk.constraints.iter().find(|c| c.param.as_deref() == Some(¶m)).unwrap();
|
||||||
|
assert!(matches!(c.kind, ConstraintKind::Length {value,..} if (value-20.).abs()<1e-6));
|
||||||
|
assert!(!editing());
|
||||||
|
assert!(drawn().is_empty(), "preview must not become a selectable committed dimension");
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), baseline);
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo);
|
||||||
|
}
|
||||||
|
h.step(&json!({"click":{"sketch":[20,-5]}})).unwrap();
|
||||||
|
assert!(editing());
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), baseline);
|
||||||
|
h.step(&json!({"key":"Escape"})).unwrap();
|
||||||
|
h.step(&json!({"key":"Escape"})).unwrap();
|
||||||
|
assert!(placement_preview(&h.app).is_none());
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), baseline);
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo);
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn labels_show_the_value_or_fx() {
|
fn labels_show_the_value_or_fx() {
|
||||||
let k = ConstraintKind::Length { l: 0, value: 25.0 };
|
let k = ConstraintKind::Length { l: 0, value: 25.0 };
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT");
|
pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT");
|
||||||
pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE");
|
pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE");
|
||||||
/// The user and developer guide (docs/book, published by .github/workflows/pages.yml).
|
/// The user and developer guide (docs/book, published by .github/workflows/pages.yml).
|
||||||
pub const DOCS_URL: &str = "https://bherbruck.github.io/solvecraft/";
|
pub const DOCS_URL: &str = "https://bherbruck.github.io/solidcraft/";
|
||||||
const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md");
|
const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md");
|
||||||
use solvecraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
|
use solvecraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
|
||||||
/// Fonts: what this build carries.
|
/// Fonts: what this build carries.
|
||||||
pub const FONTS: &str = "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and \
|
pub const FONTS: &str = "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and \
|
||||||
the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans \
|
the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans \
|
||||||
(SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans \
|
(SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans \
|
||||||
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolveCraft builds.";
|
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.";
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct HelpState {
|
pub struct HelpState {
|
||||||
@@ -32,7 +32,7 @@ pub struct HelpState {
|
|||||||
pub fn diagnostics(app: &SolveApp) -> String {
|
pub fn diagnostics(app: &SolveApp) -> String {
|
||||||
let renderer = if app.viewport.gpu.is_some() { "GPU" } else { "CPU" };
|
let renderer = if app.viewport.gpu.is_some() { "GPU" } else { "CPU" };
|
||||||
format!(
|
format!(
|
||||||
"SolveCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
|
"SolidCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
|
||||||
std::env::consts::OS,
|
std::env::consts::OS,
|
||||||
std::env::consts::ARCH,
|
std::env::consts::ARCH,
|
||||||
app.help.gpu.as_ref().map(|g| format!(", {g}")).unwrap_or_default(),
|
app.help.gpu.as_ref().map(|g| format!(", {g}")).unwrap_or_default(),
|
||||||
@@ -71,7 +71,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
|
|||||||
crate::context_menu::menu_frame(ui, |ui| {
|
crate::context_menu::menu_frame(ui, |ui| {
|
||||||
for (id, label) in [
|
for (id, label) in [
|
||||||
("docs", "Documentation"),
|
("docs", "Documentation"),
|
||||||
("about", "About SolveCraft"),
|
("about", "About SolidCraft"),
|
||||||
("shortcuts", "Keyboard Shortcuts"),
|
("shortcuts", "Keyboard Shortcuts"),
|
||||||
("report", "Report an Issue…"),
|
("report", "Report an Issue…"),
|
||||||
] {
|
] {
|
||||||
@@ -100,12 +100,12 @@ fn about(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
let t = Tokens::get();
|
let t = Tokens::get();
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
crate::frame::window(ctx, "About SolveCraft", crate::frame::Width::Normal)
|
crate::frame::window(ctx, "About SolidCraft", crate::frame::Width::Normal)
|
||||||
.id(egui::Id::new("sc_about"))
|
.id(egui::Id::new("sc_about"))
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.default_size(vec2(440.0, 560.0))
|
.default_size(vec2(440.0, 560.0))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.label(RichText::new("SolveCraft").size(22.0).strong());
|
ui.label(RichText::new("SolidCraft").size(22.0).strong());
|
||||||
ui.label(format!("Version {VERSION} · commit {COMMIT} · built {BUILD_DATE}"));
|
ui.label(format!("Version {VERSION} · commit {COMMIT} · built {BUILD_DATE}"));
|
||||||
ui.label(RichText::new("Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.").color(t.text_dim));
|
ui.label(RichText::new("Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.").color(t.text_dim));
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(36, 24))).show(ui, |ui| {
|
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(36, 24))).show(ui, |ui| {
|
||||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("SolveCraft").size(26.0).strong().color(t.text));
|
ui.label(RichText::new("SolidCraft").size(26.0).strong().color(t.text));
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
ui.label(RichText::new("Start").size(16.0).color(t.text_dim));
|
ui.label(RichText::new("Start").size(16.0).color(t.text_dim));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -192,6 +192,17 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
|||||||
pen.poly(&[(4.0, 7.5), (12.0, 12.0), (12.0, 21.0), (4.0, 16.5)], a, ink);
|
pen.poly(&[(4.0, 7.5), (12.0, 12.0), (12.0, 21.0), (4.0, 16.5)], a, ink);
|
||||||
pen.poly(&[(12.0, 12.0), (20.0, 7.5), (20.0, 16.5), (12.0, 21.0)], Color32::TRANSPARENT, ink);
|
pen.poly(&[(12.0, 12.0), (20.0, 7.5), (20.0, 16.5), (12.0, 21.0)], Color32::TRANSPARENT, ink);
|
||||||
}
|
}
|
||||||
|
// Original bug glyph: rounded body, antennae and three pairs of legs.
|
||||||
|
"bug_report" => {
|
||||||
|
pen.circle(12.0, 13.5, 5.0, Color32::TRANSPARENT, ink);
|
||||||
|
pen.line(&[(12.0, 9.0), (12.0, 18.0)], ink);
|
||||||
|
pen.line(&[(9.5, 8.5), (8.0, 5.0)], ink);
|
||||||
|
pen.line(&[(14.5, 8.5), (16.0, 5.0)], ink);
|
||||||
|
for y in [10.0, 13.5, 17.0] {
|
||||||
|
pen.line(&[(3.0, y), (7.0, y)], ink);
|
||||||
|
pen.line(&[(17.0, y), (21.0, y)], ink);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Help: a question mark in a ring.
|
// Help: a question mark in a ring.
|
||||||
"help" => {
|
"help" => {
|
||||||
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
|
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
|
||||||
@@ -229,6 +240,12 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
|||||||
pen.arrow((12.0, 12.0), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), a);
|
pen.arrow((12.0, 12.0), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"configuration_table" => {
|
||||||
|
pen.poly(&[(3.0, 4.0), (21.0, 4.0), (21.0, 20.0), (3.0, 20.0)], pen.fill, ink);
|
||||||
|
pen.line(&[(3.0, 9.0), (21.0, 9.0)], a);
|
||||||
|
pen.line(&[(3.0, 14.0), (21.0, 14.0)], ink);
|
||||||
|
pen.line(&[(9.0, 4.0), (9.0, 20.0)], ink);
|
||||||
|
}
|
||||||
"params" => {
|
"params" => {
|
||||||
pen.line(&[(8.0, 20.0), (9.0, 8.0), (10.0, 5.0), (13.0, 4.0)], ink);
|
pen.line(&[(8.0, 20.0), (9.0, 8.0), (10.0, 5.0), (13.0, 4.0)], ink);
|
||||||
pen.line(&[(6.0, 11.0), (12.0, 11.0)], ink);
|
pen.line(&[(6.0, 11.0), (12.0, 11.0)], ink);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{SolveApp, dialogs, help};
|
|||||||
|
|
||||||
const MAX_BODY: usize = 24_000;
|
const MAX_BODY: usize = 24_000;
|
||||||
const MAX_IMAGE_PIXELS: usize = 16_000_000;
|
const MAX_IMAGE_PIXELS: usize = 16_000_000;
|
||||||
const ISSUE_URL: &str = "https://github.com/bherbruck/solvecraft/issues/new";
|
const ISSUE_URL: &str = "https://github.com/bherbruck/solidcraft/issues/new";
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static CAD_INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
static CAD_INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||||
@@ -480,7 +480,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
if ui.add_enabled(draft.image.is_some() && app.services.pick_save.is_some(), egui::Button::new("Save screenshot…")).clicked()
|
if ui.add_enabled(draft.image.is_some() && app.services.pick_save.is_some(), egui::Button::new("Save screenshot…")).clicked()
|
||||||
&& let Some(image) = draft.image.as_ref()
|
&& let Some(image) = draft.image.as_ref()
|
||||||
&& let Some(path) =
|
&& let Some(path) =
|
||||||
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solvecraft-issue-{}.png", app.shot_token), &["png"]))
|
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solidcraft-issue-{}.png", app.shot_token), &["png"]))
|
||||||
{
|
{
|
||||||
let result = crate::control::save_screenshot(image, Some(&path));
|
let result = crate::control::save_screenshot(image, Some(&path));
|
||||||
draft.note = if result.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
|
draft.note = if result.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pub fn command_for(app: &SolveApp, key: &str) -> Option<&'static str> {
|
|||||||
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
|
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
|
||||||
let spec = solvecraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
|
let spec = solvecraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
|
||||||
if RESERVED.contains(&key) {
|
if RESERVED.contains(&key) {
|
||||||
return Err(format!("{key} is used by SolveCraft itself"));
|
return Err(format!("{key} is used by SolidCraft itself"));
|
||||||
}
|
}
|
||||||
if !key.is_empty() {
|
if !key.is_empty() {
|
||||||
let others: Vec<&'static str> = commands_for(app, key).into_iter().filter(|o| *o != id).collect();
|
let others: Vec<&'static str> = commands_for(app, key).into_iter().filter(|o| *o != id).collect();
|
||||||
@@ -224,7 +224,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
Some((_, "reset_all")) => reset(app, None),
|
Some((_, "reset_all")) => reset(app, None),
|
||||||
Some((_, "export")) => {
|
Some((_, "export")) => {
|
||||||
let text = crate::toolbar_custom::export(app).to_string();
|
let text = crate::toolbar_custom::export(app).to_string();
|
||||||
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolveCraft shortcuts.json", &["json"])) {
|
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolidCraft shortcuts.json", &["json"])) {
|
||||||
app.keymap.message = std::fs::write(&path, text).err().map(|e| format!("export: {e}"));
|
app.keymap.message = std::fs::write(&path, text).err().map(|e| format!("export: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -547,6 +547,13 @@ impl SolveApp {
|
|||||||
self.last_command = Some((spec.id.to_string(), spec.label.to_string()));
|
self.last_command = Some((spec.id.to_string(), spec.label.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(mut t) = tools::Tool::for_command(id) {
|
if let Some(mut t) = tools::Tool::for_command(id) {
|
||||||
|
if let Some(params) = tools::constraint_preselection(id, &self.session.selection) {
|
||||||
|
if self.run(id, params).is_ok() {
|
||||||
|
let _ = self.session.execute("select.clear", &json!({}));
|
||||||
|
self.set_status("Constraint applied", false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Curve-pair constraints can consume an exact preselection. Leave ambiguous
|
// Curve-pair constraints can consume an exact preselection. Leave ambiguous
|
||||||
// selections to the ordinary pick tool; never silently constrain a subset.
|
// selections to the ordinary pick tool; never silently constrain a subset.
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -856,6 +863,15 @@ impl SolveApp {
|
|||||||
self.cam_anim = None;
|
self.cam_anim = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hand the view to an external navigation device without a pending mouse orbit or spin
|
||||||
|
/// immediately taking it back on the next frame.
|
||||||
|
pub fn begin_external_navigation(&mut self) {
|
||||||
|
self.cancel_view_animation();
|
||||||
|
self.viewport.orbit_snap = None;
|
||||||
|
self.viewport.orbit_free = None;
|
||||||
|
self.viewport.spin = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn step_view_animation(&mut self, ctx: &egui::Context) {
|
fn step_view_animation(&mut self, ctx: &egui::Context) {
|
||||||
let Some(anim) = self.cam_anim else { return };
|
let Some(anim) = self.cam_anim else { return };
|
||||||
let (cam, done) = anim.sample(self.now);
|
let (cam, done) = anim.sample(self.now);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"sketch.make_selection": "選択を{linetype}にする",
|
"sketch.make_selection": "選択を{linetype}にする",
|
||||||
"ui.1_degree_of_freedom": "自由度 1",
|
"ui.1_degree_of_freedom": "自由度 1",
|
||||||
"ui.3d_sketch": "3D スケッチ",
|
"ui.3d_sketch": "3D スケッチ",
|
||||||
"ui.about": "SolveCraft について",
|
"ui.about": "SolidCraft について",
|
||||||
"ui.activate": "アクティブ化",
|
"ui.activate": "アクティブ化",
|
||||||
"ui.active_component": "アクティブなコンポーネント",
|
"ui.active_component": "アクティブなコンポーネント",
|
||||||
"ui.agent_cursor": "AIエージェントカーソル",
|
"ui.agent_cursor": "AIエージェントカーソル",
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ pub struct Prefs {
|
|||||||
pub zoom_reverse: bool,
|
pub zoom_reverse: bool,
|
||||||
/// Orbit around the point under the cursor (else around the view's centre).
|
/// Orbit around the point under the cursor (else around the view's centre).
|
||||||
pub orbit_cursor: bool,
|
pub orbit_cursor: bool,
|
||||||
|
/// Whether a connected 3D mouse moves the view.
|
||||||
|
pub space_mouse_enabled: bool,
|
||||||
|
/// Speed multiplier for 3D mouse navigation.
|
||||||
|
pub space_mouse_speed: f32,
|
||||||
/// Anti-aliasing samples (applies on restart).
|
/// Anti-aliasing samples (applies on restart).
|
||||||
pub msaa: u32,
|
pub msaa: u32,
|
||||||
/// Decimals shown for lengths and angles.
|
/// Decimals shown for lengths and angles.
|
||||||
@@ -68,6 +72,8 @@ impl Default for Prefs {
|
|||||||
nav: "fusion".into(),
|
nav: "fusion".into(),
|
||||||
zoom_reverse: false,
|
zoom_reverse: false,
|
||||||
orbit_cursor: false,
|
orbit_cursor: false,
|
||||||
|
space_mouse_enabled: true,
|
||||||
|
space_mouse_speed: 1.0,
|
||||||
msaa: 4,
|
msaa: 4,
|
||||||
length_decimals: 2,
|
length_decimals: 2,
|
||||||
angle_decimals: 1,
|
angle_decimals: 1,
|
||||||
@@ -175,6 +181,8 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
|||||||
p.zoom_reverse = d.zoom_reverse;
|
p.zoom_reverse = d.zoom_reverse;
|
||||||
p.zoom_to_cursor = d.zoom_to_cursor;
|
p.zoom_to_cursor = d.zoom_to_cursor;
|
||||||
p.orbit_cursor = d.orbit_cursor;
|
p.orbit_cursor = d.orbit_cursor;
|
||||||
|
p.space_mouse_enabled = d.space_mouse_enabled;
|
||||||
|
p.space_mouse_speed = d.space_mouse_speed;
|
||||||
p.look_at_anim = d.look_at_anim;
|
p.look_at_anim = d.look_at_anim;
|
||||||
p.look_at_speed = d.look_at_speed;
|
p.look_at_speed = d.look_at_speed;
|
||||||
}
|
}
|
||||||
@@ -577,6 +585,16 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
egui::Slider::new(&mut app.preferences.look_at_speed, 0.5..=3.0).step_by(0.25).custom_formatter(|v, _| format!("{v:.2}×")),
|
egui::Slider::new(&mut app.preferences.look_at_speed, 0.5..=3.0).step_by(0.25).custom_formatter(|v, _| format!("{v:.2}×")),
|
||||||
);
|
);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
ui.label("3D mouse (Windows)");
|
||||||
|
ui.checkbox(&mut app.preferences.space_mouse_enabled, "Navigate with SpaceNavigator / SpaceMouse");
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("3D mouse speed");
|
||||||
|
ui.add_enabled(
|
||||||
|
app.preferences.space_mouse_enabled,
|
||||||
|
egui::Slider::new(&mut app.preferences.space_mouse_speed, 0.25..=3.0).step_by(0.05).custom_formatter(|v, _| format!("{v:.2}×")),
|
||||||
|
);
|
||||||
|
ui.end_row();
|
||||||
|
note(ui, "Move and rotate the view together; sketches use pan and zoom only.");
|
||||||
}
|
}
|
||||||
PAGE_DESIGN => {
|
PAGE_DESIGN => {
|
||||||
ui.label("Default modelling orientation");
|
ui.label("Default modelling orientation");
|
||||||
@@ -616,7 +634,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
note(ui, "Anti-aliasing applies the next time SolveCraft starts");
|
note(ui, "Anti-aliasing applies the next time SolidCraft starts");
|
||||||
ui.label("Renderer");
|
ui.label("Renderer");
|
||||||
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
|
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
|
||||||
(Some(_), Some(g)) => format!("GPU: {g}"),
|
(Some(_), Some(g)) => format!("GPU: {g}"),
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
.min_width(420.0)
|
.min_width(420.0)
|
||||||
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.label("SolveCraft closed before these designs were saved. Their last autosave can be opened.");
|
ui.label("SolidCraft closed before these designs were saved. Their last autosave can be opened.");
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
for e in app.recoverable.iter().take(20) {
|
for e in app.recoverable.iter().take(20) {
|
||||||
ui.label(RichText::new(&e.name).strong());
|
ui.label(RichText::new(&e.name).strong());
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ fn approx(got: f64, want: &Value, what: &str) -> Result<(), String> {
|
|||||||
/// Checks: `bodies` (count), `volume` / `area` (total, number or [value, rel tol]), `faces`,
|
/// Checks: `bodies` (count), `volume` / `area` (total, number or [value, rel tol]), `faces`,
|
||||||
/// `features` (count), `errors` (timeline features in error, default 0), `dialog` (null: none
|
/// `features` (count), `errors` (timeline features in error, default 0), `dialog` (null: none
|
||||||
/// open, or a word its kind must contain), `tool` (null or command id), `sketching` (bool),
|
/// open, or a word its kind must contain), `tool` (null or command id), `sketching` (bool),
|
||||||
/// `selection` (count), `glyphs` ({constraint id: count}, none overlapping), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
|
/// `selection` (count), `glyphs` ({constraint id: count}, none overlapping one another or a dimension value), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
|
||||||
/// ({command, params?, path, value}: a command's result).
|
/// ({command, params?, path, value}: a command's result).
|
||||||
pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
||||||
let doc = h.call("document.inspect", json!({"measure": true}))["result"].clone();
|
let doc = h.call("document.inspect", json!({"measure": true}))["result"].clone();
|
||||||
@@ -695,6 +695,10 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
|||||||
if let Some((b, _)) = g.iter().skip(i + 1).find(|(_, q)| q.distance(*p) < 14.0) {
|
if let Some((b, _)) = g.iter().skip(i + 1).find(|(_, q)| q.distance(*p) < 14.0) {
|
||||||
return Err(format!("glyphs of {a} and {b} overlap at {p:?}"));
|
return Err(format!("glyphs of {a} and {b} overlap at {p:?}"));
|
||||||
}
|
}
|
||||||
|
let r = egui::Rect::from_center_size(*p, egui::vec2(14.0, 14.0));
|
||||||
|
if let Some((d, _)) = crate::dim_view::text_rects().into_iter().find(|(_, t)| t.intersects(r)) {
|
||||||
|
return Err(format!("glyph of {a} at {p:?} overlaps the value of dimension {d}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"sketching" => {
|
"sketching" => {
|
||||||
|
|||||||
@@ -1000,7 +1000,13 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
|
|||||||
let tk = crate::theme::Tokens::get();
|
let tk = crate::theme::Tokens::get();
|
||||||
let picked = PICKED.with(|p| p.borrow().clone());
|
let picked = PICKED.with(|p| p.borrow().clone());
|
||||||
// Where each glyph goes: beside its entity (to its left, away from the line), every entity
|
// Where each glyph goes: beside its entity (to its left, away from the line), every entity
|
||||||
// of a two-entity constraint getting one; glyphs that would overlap step along.
|
// of a two-entity constraint getting one; glyphs that would overlap step along. A glyph
|
||||||
|
// that would sit on a dimension's value goes to the other side of its line.
|
||||||
|
let dim_texts: Vec<egui::Rect> = crate::dim_view::text_rects().into_iter().map(|(_, r)| r.expand(2.0)).collect();
|
||||||
|
let on_dim = |p: Pos2| {
|
||||||
|
let r = egui::Rect::from_center_size(p, egui::vec2(GLYPH, GLYPH));
|
||||||
|
dim_texts.iter().any(|d| d.intersects(r))
|
||||||
|
};
|
||||||
let mut placed: Vec<(String, Pos2, Pos2)> = Vec::new();
|
let mut placed: Vec<(String, Pos2, Pos2)> = Vec::new();
|
||||||
for c in &ss.sketch.constraints {
|
for c in &ss.sketch.constraints {
|
||||||
if c.kind.is_dimension() {
|
if c.kind.is_dimension() {
|
||||||
@@ -1017,13 +1023,22 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
|
|||||||
None => egui::vec2(0.7, -0.7),
|
None => egui::vec2(0.7, -0.7),
|
||||||
};
|
};
|
||||||
let step = if dir.is_some() { egui::vec2(side.y, -side.x) } else { egui::vec2(1.0, 0.0) };
|
let step = if dir.is_some() { egui::vec2(side.y, -side.x) } else { egui::vec2(1.0, 0.0) };
|
||||||
let mut sp = base + side * (GLYPH * 0.9);
|
let free = |sp: Pos2| !placed.iter().any(|(_, q, _)| q.distance(sp) < GLYPH + 1.0);
|
||||||
for _ in 0..12 {
|
let start = |s: egui::Vec2| base + s * (GLYPH * 0.9);
|
||||||
if !placed.iter().any(|(_, q, _)| q.distance(sp) < GLYPH + 1.0) {
|
let side = if on_dim(start(side)) && !on_dim(start(-side)) { -side } else { side };
|
||||||
break;
|
// Step along until clear of other glyphs, preferring a spot clear of dimension
|
||||||
|
// values too.
|
||||||
|
let settle = |avoid_dims: bool| {
|
||||||
|
let mut sp = start(side);
|
||||||
|
for _ in 0..12 {
|
||||||
|
if free(sp) && !(avoid_dims && on_dim(sp)) {
|
||||||
|
return Some(sp);
|
||||||
|
}
|
||||||
|
sp += step * (GLYPH + 2.0);
|
||||||
}
|
}
|
||||||
sp += step * (GLYPH + 2.0);
|
None
|
||||||
}
|
};
|
||||||
|
let sp = settle(true).or_else(|| settle(false)).unwrap_or_else(|| start(side) + step * (12.0 * (GLYPH + 2.0)));
|
||||||
placed.push((c.id.clone(), sp, base));
|
placed.push((c.id.clone(), sp, base));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+210
-12
@@ -36,6 +36,13 @@ pub fn promoted(tab: &str, panel: &str) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn report_rect(bar: Rect, captions: f32, tabs_start: f32) -> (Rect, bool) {
|
||||||
|
let right = bar.right() - captions - 90.0;
|
||||||
|
let labeled = right - tabs_start >= 340.0;
|
||||||
|
let width = if labeled { 124.0 } else { 28.0 };
|
||||||
|
(Rect::from_min_size(pos2(right - width, bar.top() + 4.0), vec2(width, 26.0)), labeled)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||||
let t = Tokens::get();
|
let t = Tokens::get();
|
||||||
egui::Panel::top("sc_appbar").exact_size(34.0).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
|
egui::Panel::top("sc_appbar").exact_size(34.0).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
|
||||||
@@ -45,14 +52,62 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
crate::titlebar::drag_area(ui, r);
|
crate::titlebar::drag_area(ui, r);
|
||||||
}
|
}
|
||||||
let left = r.left() + if app.integrated_titlebar { crate::titlebar::TRAFFIC_LIGHTS } else { 0.0 };
|
let left = r.left() + if app.integrated_titlebar { crate::titlebar::TRAFFIC_LIGHTS } else { 0.0 };
|
||||||
// Brand mark: a small solid block drawn in code.
|
// Keep the current header controls and product name when merging the new mark.
|
||||||
let logo = Rect::from_center_size(pos2(left + 20.0, r.center().y), vec2(20.0, 20.0));
|
let compact_actions = r.right() - left < 640.0;
|
||||||
icons::paint(ui.painter(), logo, "box", Color32::from_rgb(220, 226, 236), Color32::from_rgb(90, 160, 240), Color32::WHITE);
|
// Brand mark: the SolidCraft logo, drawn in code from the SVG's own coordinates
|
||||||
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolveCraft", FontId::proportional(14.0), t.app_bar_text);
|
// (viewBox 357 x 412). Same 20 px height and centre as the old block.
|
||||||
let mut x = left + 128.0;
|
{
|
||||||
|
let c = pos2(left + 20.0, r.center().y);
|
||||||
|
let s = 20.0 / 412.217;
|
||||||
|
let m =
|
||||||
|
|pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
|
||||||
|
let p = ui.painter();
|
||||||
|
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
|
||||||
|
let face = |col: Color32, pts: &[(f32, f32)]| {
|
||||||
|
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
|
||||||
|
};
|
||||||
|
let (light, mid, dark) = (Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197));
|
||||||
|
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
|
||||||
|
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||||
|
face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]);
|
||||||
|
face(mid, &[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)]);
|
||||||
|
face(mid, &[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)]);
|
||||||
|
face(dark, &[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)]);
|
||||||
|
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||||
|
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
|
||||||
|
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
|
||||||
|
// White edge lines.
|
||||||
|
let edge = Stroke::new(10.0 * s, Color32::WHITE);
|
||||||
|
let line = |pts: &[(f32, f32)]| {
|
||||||
|
p.add(egui::Shape::line(m(pts), edge));
|
||||||
|
};
|
||||||
|
line(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)]);
|
||||||
|
line(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)]);
|
||||||
|
line(&[(177.0, 206.1), (263.8, 156.0)]);
|
||||||
|
p.add(egui::Shape::closed_line(
|
||||||
|
m(&[
|
||||||
|
(178.5, 5.8),
|
||||||
|
(5.0, 105.9),
|
||||||
|
(5.0, 206.1),
|
||||||
|
(91.7, 256.2),
|
||||||
|
(91.7, 356.4),
|
||||||
|
(178.5, 406.4),
|
||||||
|
(352.0, 306.3),
|
||||||
|
(352.0, 206.1),
|
||||||
|
(265.2, 156.0),
|
||||||
|
(265.2, 55.9),
|
||||||
|
]),
|
||||||
|
edge,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !compact_actions {
|
||||||
|
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolidCraft", FontId::proportional(14.0), t.app_bar_text);
|
||||||
|
}
|
||||||
|
let mut x = left + if compact_actions { 60.0 } else { 128.0 };
|
||||||
let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str| -> bool {
|
let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str| -> bool {
|
||||||
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(26.0, 26.0));
|
let width = if compact_actions { 22.0 } else { 26.0 };
|
||||||
x += 30.0;
|
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(width, 26.0));
|
||||||
|
x += if compact_actions { 22.0 } else { 30.0 };
|
||||||
let resp = ui.interact(br, ui.id().with(("ab", icon)), Sense::click());
|
let resp = ui.interact(br, ui.id().with(("ab", icon)), Sense::click());
|
||||||
crate::scenario::publish_handle(&format!("appbar:{icon}"), br.center());
|
crate::scenario::publish_handle(&format!("appbar:{icon}"), br.center());
|
||||||
if resp.hovered() {
|
if resp.hovered() {
|
||||||
@@ -83,7 +138,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
app.home.show(rev);
|
app.home.show(rev);
|
||||||
}
|
}
|
||||||
let theme_tip = if app.ui.dark { app.tr("ui.light") } else { app.tr("ui.dark") };
|
let theme_tip = if app.ui.dark { app.tr("ui.light") } else { app.tr("ui.dark") };
|
||||||
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
if !compact_actions && click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
||||||
app.ui.dark = !app.ui.dark;
|
app.ui.dark = !app.ui.dark;
|
||||||
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
||||||
}
|
}
|
||||||
@@ -109,7 +164,34 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
// Document tabs.
|
// Document tabs.
|
||||||
let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 };
|
let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 };
|
||||||
crate::documents::tabs(app, ui, r, x + 16.0, r.right() - captions - 90.0);
|
let tabs_start = x + 16.0;
|
||||||
|
let (report, report_label) = report_rect(r, captions, tabs_start);
|
||||||
|
let tabs_end = report.left() - 8.0;
|
||||||
|
// Narrow title bars can fit only part of the tab strip. Its hit regions must not
|
||||||
|
// extend into the persistent report and window controls.
|
||||||
|
let tabs_rect = Rect::from_min_max(pos2(tabs_start.min(tabs_end), r.top()), pos2(tabs_end, r.bottom()));
|
||||||
|
let mut tabs_ui = ui.new_child(egui::UiBuilder::new().max_rect(tabs_rect));
|
||||||
|
tabs_ui.set_clip_rect(tabs_rect.intersect(ui.clip_rect()));
|
||||||
|
crate::documents::tabs(app, &mut tabs_ui, r, tabs_start, tabs_end);
|
||||||
|
let response = ui.interact(report, ui.id().with("sc_report_button"), Sense::click());
|
||||||
|
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Report a bug"));
|
||||||
|
crate::scenario::publish_handle("titlebar:report", report.center());
|
||||||
|
ui.painter().rect_filled(report, 4.0, if response.hovered() || response.has_focus() { t.hover } else { t.app_bar });
|
||||||
|
ui.painter().rect_stroke(report, 4.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||||
|
let icon = Rect::from_center_size(pos2(report.left() + 14.0, report.center().y), vec2(18.0, 18.0));
|
||||||
|
icons::paint(ui.painter(), icon, "bug_report", t.app_bar_text, t.app_bar_text, t.app_bar);
|
||||||
|
if report_label {
|
||||||
|
ui.painter().text(
|
||||||
|
pos2(report.left() + 29.0, report.center().y),
|
||||||
|
Align2::LEFT_CENTER,
|
||||||
|
"Report a bug",
|
||||||
|
FontId::proportional(13.0),
|
||||||
|
t.app_bar_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if response.on_hover_text("Report a bug — review a screenshot and open a GitHub draft").clicked() {
|
||||||
|
crate::help::run(app, &ui.ctx().clone(), "report");
|
||||||
|
}
|
||||||
// Preferences, at the top right like Fusion's profile menu (also Ctrl+,).
|
// Preferences, at the top right like Fusion's profile menu (also Ctrl+,).
|
||||||
let gear = Rect::from_center_size(pos2(r.right() - captions - 70.0, r.center().y), vec2(24.0, 24.0));
|
let gear = Rect::from_center_size(pos2(r.right() - captions - 70.0, r.center().y), vec2(24.0, 24.0));
|
||||||
let gresp = ui.interact(gear, ui.id().with("sc_prefs_button"), Sense::click());
|
let gresp = ui.interact(gear, ui.id().with("sc_prefs_button"), Sense::click());
|
||||||
@@ -332,6 +414,14 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
// Panels.
|
// Panels.
|
||||||
let tab = app.ui.tab.clone();
|
let tab = app.ui.tab.clone();
|
||||||
|
// The command whose tool or dialog is open right now, shown pressed in the toolbar.
|
||||||
|
let running: Option<String> = match (&app.tool, &app.dialog) {
|
||||||
|
(Some(tool), _) => Some(tool.cmd.to_string()),
|
||||||
|
// Create Sketch never lands in last_command (it is not repeatable).
|
||||||
|
(None, Some(d)) if matches!(d.kind, crate::dialogs::Kind::Sketch) => Some("sketch.create".to_string()),
|
||||||
|
(None, Some(_)) => app.last_command.as_ref().map(|(id, _)| id.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
let mut px = r.left() + 112.0;
|
let mut px = r.left() + 112.0;
|
||||||
let top = r.top() + 26.0;
|
let top = r.top() + 26.0;
|
||||||
let mut drop: Option<(&'static str, &'static str, Option<&'static str>)> = None;
|
let mut drop: Option<(&'static str, &'static str, Option<&'static str>)> = None;
|
||||||
@@ -381,7 +471,9 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
if resp.secondary_clicked() {
|
if resp.secondary_clicked() {
|
||||||
menu = Some((resp.interact_pointer_pos().unwrap_or(br.left_bottom()), c.id));
|
menu = Some((resp.interact_pointer_pos().unwrap_or(br.left_bottom()), c.id));
|
||||||
}
|
}
|
||||||
if resp.hovered() && info.enabled {
|
if running.as_deref() == Some(c.id) {
|
||||||
|
painter.rect_filled(br, 4.0, t.border.gamma_multiply(0.7));
|
||||||
|
} else if resp.hovered() && info.enabled {
|
||||||
painter.rect_filled(br, 4.0, t.hover);
|
painter.rect_filled(br, 4.0, t.hover);
|
||||||
}
|
}
|
||||||
let (ink, fill, acc) = if info.enabled {
|
let (ink, fill, acc) = if info.enabled {
|
||||||
@@ -389,9 +481,19 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
} else {
|
} else {
|
||||||
(t.border, t.panel_header, t.border)
|
(t.border, t.panel_header, t.border)
|
||||||
};
|
};
|
||||||
let icon = if c.id == "sketch.finish" { "finish" } else { c.icon };
|
let icon = match c.id {
|
||||||
|
"sketch.finish" => "finish",
|
||||||
|
"config.table" => "configuration_table",
|
||||||
|
_ => c.icon,
|
||||||
|
};
|
||||||
icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc);
|
icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc);
|
||||||
let tr_label = app.tr_label(c.label);
|
let label = match c.id {
|
||||||
|
"parameters.change" if *panel == "CONFIGURE" => "Parameters",
|
||||||
|
"config.table" if *panel == "CONFIGURE" => "Configurations",
|
||||||
|
_ => c.label,
|
||||||
|
};
|
||||||
|
let tr_label = app.tr_label(label);
|
||||||
|
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled() && info.enabled, tr_label));
|
||||||
let tip = match &info.disabled_reason {
|
let tip = match &info.disabled_reason {
|
||||||
Some(why) => format!("{}\n{why}", tr_label),
|
Some(why) => format!("{}\n{why}", tr_label),
|
||||||
None => format!("{}{}", tr_label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
|
None => format!("{}{}", tr_label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
|
||||||
@@ -612,3 +714,99 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod header_report_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn captured_frame(h: &mut crate::scenario::Harness) {
|
||||||
|
let token = h.app.shot_token;
|
||||||
|
crate::issue_report::receive(&mut h.app, token, std::sync::Arc::new(egui::ColorImage::new([2, 2], vec![Color32::GREEN; 4])));
|
||||||
|
h.frames(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_report_reserves_controls_at_normal_and_double_scale_widths() {
|
||||||
|
for physical_width in [900.0, 1400.0] {
|
||||||
|
for scale in [1.0, 2.0] {
|
||||||
|
let width = physical_width / scale;
|
||||||
|
let bar = Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 34.0));
|
||||||
|
for captions in [0.0, crate::titlebar::WIDTH] {
|
||||||
|
let (report, labeled) = report_rect(bar, captions, 354.0);
|
||||||
|
assert!(bar.contains_rect(report));
|
||||||
|
let prefs = Rect::from_center_size(pos2(width - captions - 70.0, 17.0), vec2(24.0, 24.0));
|
||||||
|
assert!(!report.intersects(prefs));
|
||||||
|
assert!(report.right() < width - captions - 82.0);
|
||||||
|
assert_eq!(labeled, width - captions - 90.0 - 354.0 >= 340.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_report_actual_click_opens_capture_without_changing_healthy_design() {
|
||||||
|
for width in [450.0, 700.0, 900.0, 1400.0] {
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
h.app.home.open = false;
|
||||||
|
h.app.custom_titlebar = true;
|
||||||
|
h.app.run("solid.box", json!({"length":35,"width":12,"height":4})).unwrap();
|
||||||
|
h.call("ui.resize", json!({"width":width,"height":720}));
|
||||||
|
h.frames(3);
|
||||||
|
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
|
||||||
|
let point = h.widget("Report a bug").expect("compact and full buttons retain an accessible name");
|
||||||
|
h.step(&json!({"click":point})).unwrap();
|
||||||
|
assert!(h.app.issue_report.draft.is_some(), "width {width}: header click must reach the report");
|
||||||
|
assert!(crate::issue_report::pending(&h.app));
|
||||||
|
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||||
|
captured_frame(&mut h);
|
||||||
|
// Existing review content scrolls vertically when the window is narrow.
|
||||||
|
if width < 700.0 {
|
||||||
|
let [x, y] = h.widget("Close draft").unwrap();
|
||||||
|
h.call("ui.scroll", json!({"x":x,"y":y - 60.0,"delta":-1200}));
|
||||||
|
// Let the scroll animation finish before locating the click target.
|
||||||
|
h.frames(60);
|
||||||
|
}
|
||||||
|
h.step(&json!({"widget":"Close draft"})).unwrap();
|
||||||
|
assert!(h.app.issue_report.draft.is_none(), "width {width}: review closes through its actual button");
|
||||||
|
h.step(&json!({"click":{"handle":"appbar:home"}})).unwrap();
|
||||||
|
assert!(h.app.home.open, "width {width}: Home remains reachable beside Report");
|
||||||
|
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_report_preserves_new_dimension_draft_and_single_commit() {
|
||||||
|
crate::dim_view::cancel();
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
h.app.home.open = false;
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[0,0],[20,0]]})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
h.app.cam.distance = 80.0;
|
||||||
|
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
|
||||||
|
h.settle();
|
||||||
|
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||||
|
for point in [[0, 0, 0], [20, 0, 0], [10, 5, 0]] {
|
||||||
|
h.step(&json!({"click":{"world":point}})).unwrap();
|
||||||
|
}
|
||||||
|
h.step(&json!({"text":"30 mm"})).unwrap();
|
||||||
|
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
|
||||||
|
h.step(&json!({"widget":"Report a bug"})).unwrap();
|
||||||
|
h.frames(3);
|
||||||
|
assert!(h.app.issue_report.draft.is_some() && crate::dim_view::editing());
|
||||||
|
captured_frame(&mut h);
|
||||||
|
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
|
||||||
|
h.step(&json!({"text":"Describe this dimension problem."})).unwrap();
|
||||||
|
h.step(&json!({"key":"Enter"})).unwrap();
|
||||||
|
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||||
|
h.step(&json!({"widget":"Close draft"})).unwrap();
|
||||||
|
h.frames(2);
|
||||||
|
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
|
||||||
|
h.step(&json!({"key":"Enter"})).unwrap();
|
||||||
|
assert!(!crate::dim_view::editing());
|
||||||
|
assert_eq!(h.app.session.undo.len(), before.1 + 1);
|
||||||
|
assert_eq!(h.app.session.doc.param("d1").unwrap().expr, "30 mm");
|
||||||
|
h.app.run("edit.undo", json!({})).unwrap();
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), before.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,7 +177,7 @@ pub fn export(app: &SolveApp) -> Value {
|
|||||||
|
|
||||||
pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
|
pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
|
||||||
if v.get("format").and_then(Value::as_str) != Some("solvecraft-shortcuts/1") {
|
if v.get("format").and_then(Value::as_str) != Some("solvecraft-shortcuts/1") {
|
||||||
return Err("not a SolveCraft shortcuts file".into());
|
return Err("not a SolidCraft shortcuts file".into());
|
||||||
}
|
}
|
||||||
if let Some(s) = v.get("shortcuts") {
|
if let Some(s) = v.get("shortcuts") {
|
||||||
app.keymap.load(s);
|
app.keymap.load(s);
|
||||||
|
|||||||
@@ -323,6 +323,43 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result<Vec<String>, String> {
|
|||||||
Ok(result["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
|
Ok(result["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Only consume a complete, unambiguous selection; command validation owns geometry.
|
||||||
|
pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_engine::Sel]) -> Option<Value> {
|
||||||
|
use solvecraft_engine::Sel::{SketchCurve, SketchPoint};
|
||||||
|
match (command, selection) {
|
||||||
|
("sketch.constraint.horizontal_vertical", [SketchCurve { id }]) => Some(json!({"line":id})),
|
||||||
|
("sketch.constraint.horizontal_vertical", [SketchPoint { id: a }, SketchPoint { id: b }]) if a != b => Some(json!({"points":[a,b]})),
|
||||||
|
("sketch.constraint.coincident", [a, b]) => {
|
||||||
|
let entity = |s: &solvecraft_engine::Sel| match s {
|
||||||
|
SketchPoint { id } | SketchCurve { id } => Some(id.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let (a, b) = (entity(a)?, entity(b)?);
|
||||||
|
(a != b).then(|| json!({"a":a,"b":b}))
|
||||||
|
}
|
||||||
|
(
|
||||||
|
"sketch.constraint.midpoint",
|
||||||
|
[SketchPoint { id: point }, SketchCurve { id: line }] | [SketchCurve { id: line }, SketchPoint { id: point }],
|
||||||
|
) => Some(json!({"point":point,"line":line})),
|
||||||
|
("sketch.constraint.symmetry", [a, b, c]) => {
|
||||||
|
let mut points = Vec::new();
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for entity in [a, b, c] {
|
||||||
|
match entity {
|
||||||
|
SketchPoint { id } => points.push(id),
|
||||||
|
SketchCurve { id } => lines.push(id),
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match (points.as_slice(), lines.as_slice()) {
|
||||||
|
([a, b], [line]) if a != b => Some(json!({"a":a,"b":b,"line":line})),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||||
let k = &t.picks;
|
let k = &t.picks;
|
||||||
let params = match t.cmd {
|
let params = match t.cmd {
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
|||||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||||
pos.x.to_bits().hash(&mut h);
|
pos.x.to_bits().hash(&mut h);
|
||||||
pos.y.to_bits().hash(&mut h);
|
pos.y.to_bits().hash(&mut h);
|
||||||
for v in [app.cam.yaw, app.cam.pitch, app.cam.distance, app.cam.target.x, app.cam.target.y, app.cam.target.z] {
|
for v in [app.cam.yaw, app.cam.pitch, app.cam.roll, app.cam.distance, app.cam.target.x, app.cam.target.y, app.cam.target.z] {
|
||||||
v.to_bits().hash(&mut h);
|
v.to_bits().hash(&mut h);
|
||||||
}
|
}
|
||||||
app.session.revision.hash(&mut h);
|
app.session.revision.hash(&mut h);
|
||||||
@@ -2241,7 +2241,7 @@ fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter,
|
|||||||
}
|
}
|
||||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||||
sketch_scene_key(app, proj).hash(&mut h);
|
sketch_scene_key(app, proj).hash(&mut h);
|
||||||
for v in [proj.cam.yaw, proj.cam.pitch, proj.cam.distance, proj.cam.target.x, proj.cam.target.y, proj.cam.target.z] {
|
for v in [proj.cam.yaw, proj.cam.pitch, proj.cam.roll, proj.cam.distance, proj.cam.target.x, proj.cam.target.y, proj.cam.target.z] {
|
||||||
v.to_bits().hash(&mut h);
|
v.to_bits().hash(&mut h);
|
||||||
}
|
}
|
||||||
(rect.width() as u32, rect.height() as u32).hash(&mut h);
|
(rect.width() as u32, rect.height() as u32).hash(&mut h);
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ pub fn panels(tab: &str) -> Option<&'static [&'static str]> {
|
|||||||
/// to the registry.
|
/// to the registry.
|
||||||
pub fn layout<'a>(tab: &str, panel: &str, specs: &[&'a CommandSpec]) -> Option<(Vec<&'a CommandSpec>, usize)> {
|
pub fn layout<'a>(tab: &str, panel: &str, specs: &[&'a CommandSpec]) -> Option<(Vec<&'a CommandSpec>, usize)> {
|
||||||
let (ids, promoted, own_tab): (&[&str], usize, &str) = match (tab, panel) {
|
let (ids, promoted, own_tab): (&[&str], usize, &str) = match (tab, panel) {
|
||||||
|
("SOLID", "CONFIGURE") => (&["parameters.change", "config.table"], 2, tab),
|
||||||
("SOLID" | "SHEET METAL" | "PLASTIC", "ASSEMBLE") => (ASSEMBLE, 3, tab),
|
("SOLID" | "SHEET METAL" | "PLASTIC", "ASSEMBLE") => (ASSEMBLE, 3, tab),
|
||||||
("SHEET METAL", "CREATE") => (SHEET_CREATE, 3, tab),
|
("SHEET METAL", "CREATE") => (SHEET_CREATE, 3, tab),
|
||||||
("SHEET METAL", "MODIFY") => (SHEET_MODIFY, 2, tab),
|
("SHEET METAL", "MODIFY") => (SHEET_MODIFY, 2, tab),
|
||||||
@@ -146,6 +147,38 @@ mod tests {
|
|||||||
layout(tab, panel, &specs).map(|(v, _)| v.iter().map(|c| c.id).collect()).unwrap_or_default()
|
layout(tab, panel, &specs).map(|(v, _)| v.iter().map(|c| c.id).collect()).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configure_header_clicks_open_parameters_and_configurations_separately() {
|
||||||
|
use serde_json::json;
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
let commands = ids("SOLID", "CONFIGURE");
|
||||||
|
assert_eq!(&commands[..2], &["parameters.change", "config.table"]);
|
||||||
|
assert!(commands.contains(&"config.configure"), "Configure remains available in the dropdown");
|
||||||
|
h.step(&json!({"widget":"Parameters"})).unwrap();
|
||||||
|
assert!(h.app.params.is_some(), "the ordinary Parameters button opens the editor");
|
||||||
|
assert!(h.app.dialog.is_none());
|
||||||
|
assert!(h.widget("User Parameters").is_some());
|
||||||
|
assert!(h.widget("Model Parameters").is_some());
|
||||||
|
// Exercise the editor's Add button, not a script-only parameter command.
|
||||||
|
let parameters = h.app.params.as_mut().unwrap();
|
||||||
|
parameters.new_name = "width".into();
|
||||||
|
parameters.new_expr = "25 mm".into();
|
||||||
|
h.frames(3);
|
||||||
|
h.step(&json!({"widget":"Add"})).unwrap();
|
||||||
|
let values = h.app.session.execute("parameters.list", &json!({})).unwrap();
|
||||||
|
assert!(values["parameters"].as_array().unwrap().iter().any(|p| p["name"] == "width" && p["value"] == 25.0));
|
||||||
|
h.step(&json!({"click":{"handle":"params:close"}})).unwrap();
|
||||||
|
assert!(h.app.params.is_none());
|
||||||
|
h.step(&json!({"widget":"Configurations"})).unwrap();
|
||||||
|
assert!(h.app.params.is_none());
|
||||||
|
assert!(matches!(
|
||||||
|
h.app.dialog.as_ref().map(|d| &d.kind),
|
||||||
|
Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::Configurations { .. }))
|
||||||
|
));
|
||||||
|
assert!(h.widget("Add Column").is_some());
|
||||||
|
assert!(h.widget("Add Row").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
/// Fusion's layout: joints on every tab's ASSEMBLE panel, sheet metal and plastic tabs
|
/// Fusion's layout: joints on every tab's ASSEMBLE panel, sheet metal and plastic tabs
|
||||||
/// reach the shared solid commands, the plastic rules sit on SETUP.
|
/// reach the shared solid commands, the plastic rules sit on SETUP.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,6 +4,97 @@ use serde_json::json;
|
|||||||
use solvecraft_engine::sketch::ConstraintKind;
|
use solvecraft_engine::sketch::ConstraintKind;
|
||||||
use solvecraft_ui_egui::scenario::Harness;
|
use solvecraft_ui_egui::scenario::Harness;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_vertical_uses_preselected_line_and_preserves_rejection() {
|
||||||
|
for fixed in [false, true] {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
h.run(&json!([
|
||||||
|
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||||
|
{"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[10,10],[40,13]],"infer":false}}},
|
||||||
|
{"call":"ui.view","params":{"view":"top"}}
|
||||||
|
]))
|
||||||
|
.unwrap();
|
||||||
|
if fixed {
|
||||||
|
h.app.session.execute("sketch.constraint.fix", &json!({"entity":"l1"})).unwrap();
|
||||||
|
}
|
||||||
|
h.run(&json!([{"click":{"sketch":[25,11.5]}}])).unwrap();
|
||||||
|
assert_eq!(h.app.session.selection.len(), 1);
|
||||||
|
let selection = h.app.session.selection.clone();
|
||||||
|
let before = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||||
|
assert_eq!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error), fixed);
|
||||||
|
if fixed {
|
||||||
|
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), before);
|
||||||
|
assert_eq!(h.app.session.selection, selection);
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo);
|
||||||
|
} else {
|
||||||
|
let sk = &h.app.session.model.state().sketches[0].sketch;
|
||||||
|
let a = sk.point(sk.resolve_point("l1.start").unwrap()).unwrap();
|
||||||
|
let b = sk.point(sk.resolve_point("l1.end").unwrap()).unwrap();
|
||||||
|
assert!((a.y - b.y).abs() < 1e-6);
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||||
|
assert!(h.app.session.selection.is_empty());
|
||||||
|
}
|
||||||
|
assert!(h.app.tool.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_vertical_leaves_ambiguous_preselection_to_pick_tool() {
|
||||||
|
use solvecraft_engine::Sel;
|
||||||
|
for selection in [
|
||||||
|
vec![Sel::SketchPoint { id: "origin".into() }],
|
||||||
|
vec![Sel::SketchPoint { id: "origin".into() }, Sel::SketchCurve { id: "c1".into() }],
|
||||||
|
vec![Sel::SketchPoint { id: "origin".into() }, Sel::SketchPoint { id: "c1.center".into() }, Sel::SketchCurve { id: "c1".into() }],
|
||||||
|
] {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
h.app.session.execute("sketch.create", &json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.session.execute("sketch.circle.center", &json!({"center":[20,3],"radius":5})).unwrap();
|
||||||
|
h.app.session.selection = selection.clone();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||||
|
assert!(h.app.tool.as_ref().unwrap().picks.is_empty());
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo);
|
||||||
|
assert_eq!(h.app.session.selection, selection);
|
||||||
|
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_vertical_uses_preselected_points() {
|
||||||
|
for center in [[20.0, 3.0], [3.0, 20.0]] {
|
||||||
|
for reverse in [false, true] {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
let (first, second) = if reverse { (center, [0.0, 0.0]) } else { ([0.0, 0.0], center) };
|
||||||
|
h.run(&json!([
|
||||||
|
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||||
|
{"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":center,"radius":5}}},
|
||||||
|
{"call":"ui.view","params":{"view":"top"}},
|
||||||
|
{"click":{"sketch":first}},
|
||||||
|
{"click":{"sketch":second},"shift":true}
|
||||||
|
]))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(h.app.session.selection.len(), 2);
|
||||||
|
let before = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||||
|
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||||
|
let sk = &h.app.session.model.state().sketches[0].sketch;
|
||||||
|
let p = sk.point(sk.resolve_point("c1.center").unwrap()).unwrap();
|
||||||
|
assert!((if center[0] > center[1] { p.y } else { p.x }).abs() < 1e-6);
|
||||||
|
assert!(h.app.tool.is_none());
|
||||||
|
assert!(h.app.session.selection.is_empty());
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||||
|
let after = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||||
|
h.app.session.execute("edit.undo", &json!({})).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), before);
|
||||||
|
h.app.session.execute("edit.redo", &json!({})).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn horizontal_vertical_aligns_origin_and_circle_center() {
|
fn horizontal_vertical_aligns_origin_and_circle_center() {
|
||||||
for (center, horizontal) in [([20.0, 3.0], true), ([3.0, 20.0], false)] {
|
for (center, horizontal) in [([20.0, 3.0], true), ([3.0, 20.0], false)] {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use serde_json::json;
|
||||||
|
use solvecraft_engine::{Sel, sketch::ConstraintKind};
|
||||||
|
use solvecraft_ui_egui::scenario::Harness;
|
||||||
|
|
||||||
|
fn setup() -> Harness {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false,"connect":false})).unwrap();
|
||||||
|
h.app.run("sketch.point", json!({"point":[20,20],"id":"a"})).unwrap();
|
||||||
|
h.app.run("sketch.point", json!({"point":[25,30],"id":"b"})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clicked_point_constraint_preselection_applies_and_undoes_once() {
|
||||||
|
for (command, picks) in [
|
||||||
|
("coincident", vec![[20, 20], [25, 30]]),
|
||||||
|
("coincident", vec![[20, 20], [20, 10]]),
|
||||||
|
("coincident", vec![[20, 10], [20, 20]]),
|
||||||
|
("midpoint", vec![[20, 20], [20, 10]]),
|
||||||
|
("midpoint", vec![[20, 10], [20, 20]]),
|
||||||
|
("symmetry", vec![[20, 10], [20, 20], [25, 30]]),
|
||||||
|
("symmetry", vec![[20, 20], [20, 10], [25, 30]]),
|
||||||
|
] {
|
||||||
|
let mut h = setup();
|
||||||
|
for (index, pick) in picks.iter().enumerate() {
|
||||||
|
h.step(&json!({"click":{"sketch":pick},"shift":index>0})).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(h.app.session.selection.len(), picks.len(), "{command}");
|
||||||
|
let before = h.app.session.doc.to_json();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.step(&json!({"start":format!("sketch.constraint.{command}")})).unwrap();
|
||||||
|
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{command}: {:?}", h.app.status);
|
||||||
|
assert!(h.app.tool.is_none());
|
||||||
|
assert!(h.app.session.selection.is_empty());
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||||
|
let state = h.app.session.world_state();
|
||||||
|
let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
|
||||||
|
assert!(sk.constraints.iter().any(|c| match command {
|
||||||
|
"coincident" => matches!(c.kind, ConstraintKind::Coincident { .. } | ConstraintKind::PointOnCurve { .. }),
|
||||||
|
"midpoint" => matches!(c.kind, ConstraintKind::Midpoint { .. }),
|
||||||
|
_ => matches!(c.kind, ConstraintKind::Symmetric { .. }),
|
||||||
|
}));
|
||||||
|
let point = |id| sk.point(sk.resolve_point(id).unwrap()).unwrap();
|
||||||
|
let a = point("a");
|
||||||
|
let b = point("b");
|
||||||
|
let start = point("l1.start");
|
||||||
|
let end = point("l1.end");
|
||||||
|
let axis = (end - start).normalized().unwrap();
|
||||||
|
match command {
|
||||||
|
"midpoint" => assert!(a.dist((start + end) * 0.5) < 1e-6),
|
||||||
|
"symmetry" => {
|
||||||
|
assert!(((a + b) * 0.5 - start).cross(axis).abs() < 1e-6);
|
||||||
|
assert!((b - a).dot(axis).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
_ if picks.contains(&[20, 10]) => assert!((a - start).cross(axis).abs() < 1e-6),
|
||||||
|
_ => assert!(a.dist(b) < 1e-6),
|
||||||
|
}
|
||||||
|
let after = h.app.session.doc.to_json();
|
||||||
|
h.app.run("edit.undo", json!({})).unwrap();
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), before);
|
||||||
|
h.app.run("edit.redo", json!({})).unwrap();
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejected_and_ambiguous_preselection_preserve_document_and_selection() {
|
||||||
|
let mut h = setup();
|
||||||
|
h.app.run("sketch.constraint.fix", json!({"entities":["a","b"]})).unwrap();
|
||||||
|
h.app.session.selection = vec![Sel::SketchPoint { id: "a".into() }, Sel::SketchPoint { id: "b".into() }];
|
||||||
|
let selection = h.app.session.selection.clone();
|
||||||
|
let before = h.app.session.doc.to_json();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
h.step(&json!({"start":"sketch.constraint.coincident"})).unwrap();
|
||||||
|
assert!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||||
|
assert_eq!(h.app.session.selection, selection);
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), before);
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo);
|
||||||
|
for command in ["coincident", "midpoint", "symmetry"] {
|
||||||
|
h.app.session.selection = vec![Sel::SketchPoint { id: "a".into() }];
|
||||||
|
h.step(&json!({"start":format!("sketch.constraint.{command}")})).unwrap();
|
||||||
|
assert!(h.app.tool.as_ref().unwrap().picks.is_empty());
|
||||||
|
assert_eq!(h.app.session.selection.len(), 1);
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), before);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
||||||
|
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [10, 10]}}},
|
||||||
|
{"call": "engine.execute", "params": {"command": "sketch.dimension", "params": {"entities": ["l1"], "value": 10, "text_at": [5, -0.3]}}},
|
||||||
|
{"call": "engine.execute", "params": {"command": "sketch.dimension", "params": {"entities": ["l2"], "value": 10, "text_at": [10.3, 5]}}},
|
||||||
|
{"call": "ui.view", "params": {"view": "top"}},
|
||||||
|
{"call": "ui.view", "params": {"view": "fit"}},
|
||||||
|
{"expect": {"glyphs": {"k1": 1, "k2": 1, "k3": 1, "k4": 1}}}
|
||||||
|
]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
[
|
[
|
||||||
{"note": "Help ▸ About ▸ Licences shows SolveCraft's licences and every third-party crate's, built into the program."},
|
{"note": "Help ▸ About ▸ Licences shows SolidCraft's licences and every third-party crate's, built into the program."},
|
||||||
{"call": "ui.help", "params": {"item": "about"}},
|
{"call": "ui.help", "params": {"item": "about"}},
|
||||||
{"widget": "Licences"},
|
{"widget": "Licences"},
|
||||||
{"expect": {"shown": "Third-party licences"}},
|
{"expect": {"shown": "Third-party licences"}},
|
||||||
{"widget": "Third-party licences"},
|
{"widget": "Third-party licences"},
|
||||||
{"expect": {"shown": "Copy all licences"}},
|
{"expect": {"shown": "Copy all licences"}},
|
||||||
{"expect": {"shown": "Third-party software in SolveCraft"}}
|
{"expect": {"shown": "Third-party software in SolidCraft"}}
|
||||||
]
|
]
|
||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
[book]
|
[book]
|
||||||
title = "SolveCraft"
|
title = "SolidCraft"
|
||||||
description = "An open-source parametric 3D CAD application in pure Rust: user guide and developer guide."
|
description = "An open-source parametric 3D CAD application in pure Rust: user guide and developer guide."
|
||||||
authors = ["SolveCraft contributors"]
|
authors = ["SolidCraft contributors"]
|
||||||
language = "en"
|
language = "en"
|
||||||
src = "src"
|
src = "src"
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@ build-dir = "../../target/book"
|
|||||||
create-missing = false
|
create-missing = false
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
git-repository-url = "https://github.com/bherbruck/solvecraft"
|
git-repository-url = "https://github.com/bherbruck/solidcraft"
|
||||||
edit-url-template = "https://github.com/bherbruck/solvecraft/edit/main/docs/book/{path}"
|
edit-url-template = "https://github.com/bherbruck/solidcraft/edit/main/docs/book/{path}"
|
||||||
site-url = "/solvecraft/"
|
site-url = "/solidcraft/"
|
||||||
no-section-label = false
|
no-section-label = false
|
||||||
|
|
||||||
[output.html.search]
|
[output.html.search]
|
||||||
@@ -23,7 +23,7 @@ enable = true
|
|||||||
# links; these send them to the right place.
|
# links; these send them to the right place.
|
||||||
"/dev/mcp.html" = "../guide/mcp.html"
|
"/dev/mcp.html" = "../guide/mcp.html"
|
||||||
"/dev/docs/oracle.html" = "../oracle.html"
|
"/dev/docs/oracle.html" = "../oracle.html"
|
||||||
"/dev/README.html" = "https://github.com/bherbruck/solvecraft/blob/main/README.md"
|
"/dev/README.html" = "https://github.com/bherbruck/solidcraft/blob/main/README.md"
|
||||||
"/dev/ATTRIBUTION.html" = "https://github.com/bherbruck/solvecraft/blob/main/ATTRIBUTION.md"
|
"/dev/ATTRIBUTION.html" = "https://github.com/bherbruck/solidcraft/blob/main/ATTRIBUTION.md"
|
||||||
"/dev/AGENTS.html" = "https://github.com/bherbruck/solvecraft/blob/main/AGENTS.md"
|
"/dev/AGENTS.html" = "https://github.com/bherbruck/solidcraft/blob/main/AGENTS.md"
|
||||||
"/dev/ROADMAP.html" = "https://github.com/bherbruck/solvecraft/blob/main/ROADMAP.md"
|
"/dev/ROADMAP.html" = "https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
- [Plastic parts](guide/plastic.md)
|
- [Plastic parts](guide/plastic.md)
|
||||||
- [Import and export](guide/files.md)
|
- [Import and export](guide/files.md)
|
||||||
- [Keyboard shortcuts](guide/shortcuts.md)
|
- [Keyboard shortcuts](guide/shortcuts.md)
|
||||||
- [Driving SolveCraft with an AI agent (MCP)](guide/mcp.md)
|
- [Driving SolidCraft with an AI agent (MCP)](guide/mcp.md)
|
||||||
|
|
||||||
# Developer guide
|
# Developer guide
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
SolveCraft is a Cargo workspace in layers. A crate only depends on crates in lower layers, and
|
SolidCraft is a Cargo workspace in layers. A crate only depends on crates in lower layers, and
|
||||||
`cargo xtask layers` fails the build otherwise.
|
`cargo xtask layers` fails the build otherwise.
|
||||||
|
|
||||||
{{#include ../../../../README.md:architecture}}
|
{{#include ../../../../README.md:architecture}}
|
||||||
@@ -11,7 +11,7 @@ The web app, `apps/solvecraft-web`, is the desktop front end compiled to wasm wi
|
|||||||
|
|
||||||
A few rules follow from the layering:
|
A few rules follow from the layering:
|
||||||
|
|
||||||
- **Only `crates/kernel` touches truck.** Everything above it sees SolveCraft's own types:
|
- **Only `crates/kernel` touches truck.** Everything above it sees SolidCraft's own types:
|
||||||
bodies, faces, edges, meshes and measures. See [The kernel boundary](kernel.md).
|
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 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.
|
the feature timeline and the component tree, and evaluates them into a model.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# The kernel boundary and truck
|
# The kernel boundary and truck
|
||||||
|
|
||||||
`crates/kernel` is SolveCraft's B-rep kernel. It wraps
|
`crates/kernel` is SolidCraft's B-rep kernel. It wraps
|
||||||
[truck](https://github.com/ricosjp/truck) (Apache-2.0) for topology, NURBS geometry, booleans,
|
[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
|
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
|
SolidCraft types (`Body`, face and edge indices, meshes, measures) and a
|
||||||
`KernelError` instead of truck types and panics.
|
`KernelError` instead of truck types and panics.
|
||||||
|
|
||||||
## `guard`
|
## `guard`
|
||||||
@@ -20,7 +20,7 @@ A failing feature shows an error on the timeline; it never takes the app down.
|
|||||||
|
|
||||||
## What is our own
|
## What is our own
|
||||||
|
|
||||||
truck has no fillets, chamfers, shells or face edits, so these are SolveCraft code in the
|
truck has no fillets, chamfers, shells or face edits, so these are SolidCraft code in the
|
||||||
kernel: constant, chord and variable-radius fillets with corner blends, chamfers, shell and
|
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
|
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
|
truck's parser, and face provenance (which input face each output face came from, for
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A fillet stores the edges it rounds, and a sketch stores the face it sits on. If those were face
|
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.
|
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`):
|
SolidCraft 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
|
- 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:
|
feature's input keeps that face's name; a face new to the feature is named by its role:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ faded.
|
|||||||
|
|
||||||
A component is placed by its **occurrences**. Paste creates another instance of the same
|
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
|
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
|
triad, then **Capture Position** to keep it or **Revert** to put it back. SolidCraft asks before
|
||||||
the next command if a move wasn't captured. **Ground** fixes an occurrence in place.
|
the next command if a move wasn't captured. **Ground** fixes an occurrence in place.
|
||||||
|
|
||||||
Paste New copies nested component definitions, sketches, feature histories and generated
|
Paste New copies nested component definitions, sketches, feature histories and generated
|
||||||
@@ -16,7 +16,7 @@ dimension parameters. Repeated child instances remain shared within the new copy
|
|||||||
appearances and wholly internal joints/motion records receive copied references. Design-wide
|
appearances and wholly internal joints/motion records receive copied references. Design-wide
|
||||||
user parameters remain shared; retained external links and omitted cross-boundary motion
|
user parameters remain shared; retained external links and omitted cross-boundary motion
|
||||||
records are reported. Unsupported destructive links refuse the entire copy. See
|
records are reported. Unsupported destructive links refuse the entire copy. See
|
||||||
[individual-instance and copy details](https://github.com/bherbruck/solvecraft/blob/main/docs/occurrence-workflows.md).
|
[individual-instance and copy details](https://github.com/bherbruck/solidcraft/blob/main/docs/occurrence-workflows.md).
|
||||||
|
|
||||||
**Joints** (`joint.create`, J) connect two components by joint origins that snap to faces, edges,
|
**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
|
circles and vertices: rigid, revolute, slider, cylindrical, pin-slot, planar and ball, with
|
||||||
@@ -35,4 +35,4 @@ uses its first placement; `occurrence.properties` selects a specific instance. R
|
|||||||
measures the complete assembly. Volumes are summed, including overlapping bodies, rather than
|
measures the complete assembly. Volumes are summed, including overlapping bodies, rather than
|
||||||
measuring their union. The same workflow is available through `component.properties` and
|
measuring their union. The same workflow is available through `component.properties` and
|
||||||
`occurrence.properties`; see
|
`occurrence.properties`; see
|
||||||
[placement and measurement details](https://github.com/bherbruck/solvecraft/blob/main/docs/assembly-properties.md).
|
[placement and measurement details](https://github.com/bherbruck/solidcraft/blob/main/docs/assembly-properties.md).
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
|
SolidCraft keeps its compatibility names: the desktop executable is `solvecraft`, the CLI
|
||||||
|
is `solvecraft-cli`, and saved designs remain `.solvecraft`. Published 0.2.0 and 0.2.1 packages
|
||||||
|
were released under the SolveCraft name.
|
||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
Download the latest build from the
|
Download the latest build from the
|
||||||
[releases page](https://github.com/bherbruck/solvecraft/releases), or build it yourself with
|
[releases page](https://github.com/bherbruck/solidcraft/releases), or build it yourself with
|
||||||
stable Rust (1.90 or newer):
|
stable Rust (1.95 or newer):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/bherbruck/solvecraft && cd solvecraft
|
git clone https://github.com/bherbruck/solidcraft && cd solidcraft
|
||||||
cargo run --release -p solvecraft -- --sample # opens with a sample part
|
cargo run --release -p solvecraft -- --sample # opens with a sample part
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -17,7 +21,7 @@ cargo run --release -p solvecraft -- --sample # opens with a sample part
|
|||||||
|
|
||||||
## Web
|
## Web
|
||||||
|
|
||||||
The same app runs in the browser: [open SolveCraft on the web](../app/) (`?sample` opens the sample
|
The same app runs in the browser: [open SolidCraft on the web](../app/) (`?sample` opens the sample
|
||||||
part, `?webgl` forces WebGL2 where WebGPU is missing). Designs are saved in the browser's private
|
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:
|
file storage, which needs HTTPS or localhost. To build it yourself:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ are ones you add yourself, with a unit, an expression and an optional comment. C
|
|||||||
**Change Parameters** (`parameters.change`). The timeline rebuilds from the first feature that
|
**Change Parameters** (`parameters.change`). The timeline rebuilds from the first feature that
|
||||||
uses them.
|
uses them.
|
||||||
|
|
||||||
|
In the SOLID toolbar's CONFIGURE panel, click **Parameters** to open the user and model
|
||||||
|
parameter editor. The separate **Configurations** button opens the configuration table;
|
||||||
|
**Configure** remains in the dropdown for configuring columns.
|
||||||
|
|
||||||
Expressions are unit-aware: `2 * width + 5 mm`, `angle / 2`, `sqrt(area)`, `1 in + 3 mm`, using
|
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
|
`+ - * / ^`, 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
|
trunc` and unit conversion. A length where an angle is expected (or the other way round) is an
|
||||||
@@ -22,4 +26,4 @@ suppressed features, physical materials and body appearances, switched with `con
|
|||||||
The table's Library picker selects material or appearance values. Choose **Follow material**
|
The table's Library picker selects material or appearance values. Choose **Follow material**
|
||||||
to clear an appearance override. Custom looks retain their colour and opacity across
|
to clear an appearance override. Custom looks retain their colour and opacity across
|
||||||
activation, undo and save/reopen. See
|
activation, undo and save/reopen. See
|
||||||
[configuration commands and examples](https://github.com/bherbruck/solvecraft/blob/main/docs/configurations.md).
|
[configuration commands and examples](https://github.com/bherbruck/solidcraft/blob/main/docs/configurations.md).
|
||||||
@@ -85,7 +85,7 @@ fresh link once the source is available; their stored point values are preserved
|
|||||||
including the origin, endpoints and circle centres. For two points, it chooses horizontal
|
including the origin, endpoints and circle centres. For two points, it chooses horizontal
|
||||||
or vertical alignment from their current positions. Esc cancels a pending first point.
|
or vertical alignment from their current positions. Esc cancels a pending first point.
|
||||||
|
|
||||||
The solver is SolveCraft's own (damped Gauss–Newton with degree-of-freedom analysis). Constraints:
|
The solver is SolidCraft's own (damped Gauss–Newton with degree-of-freedom analysis). Constraints:
|
||||||
coincident, horizontal/vertical, parallel, perpendicular, tangent, equal, concentric, collinear,
|
coincident, horizontal/vertical, parallel, perpendicular, tangent, equal, concentric, collinear,
|
||||||
midpoint, symmetry, fix, curvature (G2) and polygon. **Sketch Dimension** (D) adds linear,
|
midpoint, symmetry, fix, curvature (G2) and polygon. **Sketch Dimension** (D) adds linear,
|
||||||
aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a parameter
|
aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a parameter
|
||||||
@@ -96,10 +96,18 @@ distinct straight lines. Two lines join their closest pair of endpoints; the fir
|
|||||||
picked line stays in place when the existing constraints allow it. Pick endpoints
|
picked line stays in place when the existing constraints allow it. Pick endpoints
|
||||||
explicitly when you want to connect a different pair.
|
explicitly when you want to connect a different pair.
|
||||||
|
|
||||||
|
Horizontal/Vertical also applies immediately to one selected line or two distinct selected
|
||||||
|
points. Shift-click to add the second point before choosing the constraint.
|
||||||
|
|
||||||
For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two
|
For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two
|
||||||
sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately.
|
sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately.
|
||||||
You can also start the constraint first and then pick its curves. An incomplete or mixed
|
You can also start the constraint first and then pick its curves. An incomplete or mixed
|
||||||
selection starts that pick workflow; a rejected constraint keeps the selection for correction.
|
selection leaves the tool ready for ordinary picking.
|
||||||
|
|
||||||
|
Coincident accepts an exact selection of two points or a point and a curve. Midpoint
|
||||||
|
accepts one point and one line, in either selection order. Symmetry accepts two distinct
|
||||||
|
points and one symmetry line, regardless of where the line occurs in the selection.
|
||||||
|
Successful constraints clear the selection; rejected constraints preserve it.
|
||||||
|
|
||||||
Under-constrained geometry is blue and fully constrained geometry turns dark, and the status
|
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
|
shows the remaining degrees of freedom. A constraint or dimension that would over-constrain the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# A tour of the window
|
# A tour of the window
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- **Title bar.** On Windows and Linux the application bar is the window's title bar: File, save,
|
- **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 and redo, the start page, the theme, Help, and one tab per open design (each with its own
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
wheel zooms at the cursor, the middle button pans and right-drag orbits freely (the mouse
|
wheel zooms at the cursor, the middle button pans and right-drag orbits freely (the mouse
|
||||||
preset in Preferences switches to SolidWorks or Inventor style). Hold Shift during an orbit
|
preset in Preferences switches to SolidWorks or Inventor style). Hold Shift during an orbit
|
||||||
to snap to the nearest standard or diagonal view; the default Shift+middle gesture also snaps.
|
to snap to the nearest standard or diagonal view; the default Shift+middle gesture also snaps.
|
||||||
The axis view shows the world's X/Y/Z directions in SolveCraft's colors. Click a signed
|
The axis view shows the world's X/Y/Z directions in SolidCraft's colors. Click a signed
|
||||||
axis tip to look along that axis; click the aligned tip again to look from the opposite side.
|
axis tip to look along that axis; click the aligned tip again to look from the opposite side.
|
||||||
Drag the axis view to orbit around the view centre, with Shift for snapping. Top and bottom
|
Drag the axis view to orbit around the view centre, with Shift for snapping. Top and bottom
|
||||||
keep the nearest quarter-turn heading. The house and navigation bar's Home button go home.
|
keep the nearest quarter-turn heading. The house and navigation bar's Home button go home.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# SolveCraft
|
# SolidCraft
|
||||||
|
|
||||||
SolveCraft is an open-source parametric 3D CAD application written in pure Rust. It works the way
|
SolidCraft is an open-source parametric 3D CAD application written in pure Rust.
|
||||||
Fusion-style modellers do: sketch on a plane, constrain and dimension the sketch, turn profiles
|
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
|
into solids with features on a timeline, and change a parameter to watch the whole timeline
|
||||||
rebuild.
|
rebuild.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
It runs as:
|
It runs as:
|
||||||
|
|
||||||
@@ -18,15 +18,17 @@ Everything the user can do is a *command* with JSON parameters. The toolbar, the
|
|||||||
the CLI, the control channel and MCP all run the same commands, so anything you can click you can
|
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.
|
also script, test or hand to an agent.
|
||||||
|
|
||||||
SolveCraft is early. Sketching, solid modelling, parameters, components with joints, sheet metal
|
SolidCraft 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
|
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
|
[roadmap](https://github.com/bherbruck/solidcraft/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
|
[command parity](dev/parity.md) and the [Fusion oracle](dev/oracle.md) measure how far along it
|
||||||
is.
|
is.
|
||||||
|
|
||||||
This book has two halves: the **user guide** for people modelling with SolveCraft, and the
|
Existing `.solvecraft` designs and `solvecraft-cli` commands keep their names and behavior.
|
||||||
|
|
||||||
|
This book has two halves: the **user guide** for people modelling with SolidCraft, and the
|
||||||
**developer guide** for people working on it. The [command reference](reference/commands.md) is
|
**developer guide** for people working on it. The [command reference](reference/commands.md) is
|
||||||
generated from the code on every build.
|
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
|
SolidCraft 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.
|
with Autodesk; Fusion is a trademark of Autodesk, Inc.
|
||||||
@@ -149,5 +149,5 @@ the current rule scope.
|
|||||||
Newly saved designs use file format 12, including optional durable face attachments and
|
Newly saved designs use file format 12, including optional durable face attachments and
|
||||||
analytic offset boundary identities and qualified adaptive whole-profile references, including
|
analytic offset boundary identities and qualified adaptive whole-profile references, including
|
||||||
holes. Older designs upgrade on opening without
|
holes. Older designs upgrade on opening without
|
||||||
changing their existing configuration values. Older SolveCraft versions must be updated to
|
changing their existing configuration values. Older SolidCraft versions must be updated to
|
||||||
open newly saved designs.
|
open newly saved designs.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# IGES import (Fusion corpus)
|
# IGES import (Fusion corpus)
|
||||||
|
|
||||||
Generated by `cargo xtask step-corpus --iges`. Each case is a reference part exported from Fusion as IGES 5.3 (kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.igs` exactly as File → Open does and compares with Fusion's measurements of the part it exported: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file. How IGES is read: see [STEP import](step-import.md#iges). 50-sweep-tight-bend is written by Fusion as five loose, partly degenerate surfaces (seams running into slits, a ring given twice); Fusion cannot read it back as a solid either.
|
Generated by `cargo xtask step-corpus --iges`. Each case is a reference part exported from Fusion as IGES 5.3 (kept locally in `plan/fusion/oracle/`, never committed). SolidCraft opens `part.igs` exactly as File → Open does and compares with Fusion's measurements of the part it exported: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file. How IGES is read: see [STEP import](step-import.md#iges). 50-sweep-tight-bend is written by Fusion as five loose, partly degenerate surfaces (seams running into slits, a ring given twice); Fusion cannot read it back as a solid either.
|
||||||
|
|
||||||
**75 / 76 files pass.**
|
**75 / 76 files pass.**
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# UI localization
|
# UI localization
|
||||||
|
|
||||||
SolveCraft embeds its translations and works offline. English and Japanese are currently
|
SolidCraft embeds its translations and works offline. English and Japanese are currently
|
||||||
registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an embedded catalog and
|
registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an embedded catalog and
|
||||||
one registration there; the preferences selector and validation use the same registry.
|
one registration there; the preferences selector and validation use the same registry.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# SolveCraft and AI agents (MCP)
|
# SolidCraft and AI agents (MCP)
|
||||||
|
|
||||||
SolveCraft speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio
|
SolidCraft speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio
|
||||||
(JSON-RPC 2.0, one message per line; protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05).
|
(JSON-RPC 2.0, one message per line; protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05).
|
||||||
An agent models exactly like a person does: every tool runs engine commands, the same ones the
|
An agent models exactly like a person does: every tool runs engine commands, the same ones the
|
||||||
toolbar, palette, scripts and control channel use, and never opens a dialog.
|
toolbar, palette, scripts and control channel use, and never opens a dialog.
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
# Fusion oracle
|
# Fusion oracle
|
||||||
|
|
||||||
Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe and measurements kept locally in `plan/fusion/oracle/`, not committed). SolveCraft replays the recipe through its command engine and compares body count, volume and area (relative tolerance 1e-3) and face/edge/vertex counts (exact). SolveCraft counts each closed-surface patch once and ignores seam edges and vertices; when body count, volume and area pass but the raw counts differ, Fusion's counts are normalised the same way from its face, edge and vertex lists (faces cut from one cylinder, cone, sphere or torus and separated only by seams count once; an edge on that surface and no other is a seam and is dropped; a vertex left with two edges joins them) and compared exactly; the note gives both sides' raw counts.
|
Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe and measurements kept locally in `plan/fusion/oracle/`, not committed). SolidCraft replays the recipe through its command engine and compares body count, volume and area (relative tolerance 1e-3) and face/edge/vertex counts (exact). SolidCraft counts each closed-surface patch once and ignores seam edges and vertices; when body count, volume and area pass but the raw counts differ, Fusion's counts are normalised the same way from its face, edge and vertex lists (faces cut from one cylinder, cone, sphere or torus and separated only by seams count once; an edge on that surface and no other is a seam and is dropped; a vertex left with two edges joins them) and compared exactly; the note gives both sides' raw counts.
|
||||||
|
|
||||||
**76 / 76 cases pass** (0 need features not implemented yet).
|
**76 / 76 cases pass** (0 need features not implemented yet).
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe a
|
|||||||
| 32-boss-plate-fillets | pass | |
|
| 32-boss-plate-fillets | pass | |
|
||||||
| 33-fillet-tangent-chain | pass | |
|
| 33-fillet-tangent-chain | pass | |
|
||||||
| 34-chamfer-then-fillet | pass | |
|
| 34-chamfer-then-fillet | pass | |
|
||||||
| 35-fillet-curved-curved | pass | topology compared seam-normalised: Fusion's 6/6/5 faces/edges/vertices become 6/5/5 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 9/6, edges 17/6, vertices 10/5) |
|
| 35-fillet-curved-curved | pass | topology compared seam-normalised: Fusion's 6/6/5 faces/edges/vertices become 6/5/5 with closed-surface patches counted once and seam edges and vertices dropped (raw SolidCraft/Fusion: faces 9/6, edges 17/6, vertices 10/5) |
|
||||||
| 36-corner-blend-two-radii | pass | |
|
| 36-corner-blend-two-radii | pass | |
|
||||||
| 37-pyramid-apex-blend | pass | |
|
| 37-pyramid-apex-blend | pass | |
|
||||||
| 38-coincident-cylinders | pass | |
|
| 38-coincident-cylinders | pass | |
|
||||||
@@ -54,21 +54,21 @@ Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe a
|
|||||||
| 46-shell-two-open-faces | pass | |
|
| 46-shell-two-open-faces | pass | |
|
||||||
| 47-draft-curved-pocket | pass | |
|
| 47-draft-curved-pocket | pass | |
|
||||||
| 48-pulley | pass | |
|
| 48-pulley | pass | |
|
||||||
| 49-helical-sweep | pass | Fusion sweeps along a spline fitted through helix points; SolveCraft builds the true helix (a coil), so the size is compared within 0.5% and the topology not at all |
|
| 49-helical-sweep | pass | Fusion sweeps along a spline fitted through helix points; SolidCraft builds the true helix (a coil), so the size is compared within 0.5% and the topology not at all |
|
||||||
| 50-sweep-tight-bend | pass | area not compared: Fusion reports this tight sweep's horn torus face at three times its area (π²·R·r quarter-turn value; 740 mm² against 246.7 by Pappus); volume and topology are compared |
|
| 50-sweep-tight-bend | pass | area not compared: Fusion reports this tight sweep's horn torus face at three times its area (π²·R·r quarter-turn value; 740 mm² against 246.7 by Pappus); volume and topology are compared |
|
||||||
| 51-loft-rect-circle-offset | pass | |
|
| 51-loft-rect-circle-offset | pass | |
|
||||||
| 52-circular-pattern-fillet | pass | |
|
| 52-circular-pattern-fillet | pass | |
|
||||||
| 53-path-pattern | pass | |
|
| 53-path-pattern | pass | |
|
||||||
| 54-mirror-body-fillets | pass | |
|
| 54-mirror-body-fillets | pass | |
|
||||||
| 55-tapped-hole-cosmetic | pass | |
|
| 55-tapped-hole-cosmetic | pass | |
|
||||||
| 56-holes-on-curved-face | pass | topology compared seam-normalised: Fusion's 7/10/10 faces/edges/vertices become 7/8/8 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 16/7, edges 32/10, vertices 19/10) |
|
| 56-holes-on-curved-face | pass | topology compared seam-normalised: Fusion's 7/10/10 faces/edges/vertices become 7/8/8 with closed-surface patches counted once and seam edges and vertices dropped (raw SolidCraft/Fusion: faces 16/7, edges 32/10, vertices 19/10) |
|
||||||
| 57-hole-pattern-sketch-points | pass | |
|
| 57-hole-pattern-sketch-points | pass | |
|
||||||
| 58-phone-stand | pass | |
|
| 58-phone-stand | pass | |
|
||||||
| 59-enclosure-base | pass | |
|
| 59-enclosure-base | pass | |
|
||||||
| 60-pipe-flange | pass | |
|
| 60-pipe-flange | pass | |
|
||||||
| 61-shaft-keyway | pass | topology compared seam-normalised: Fusion's 14/22/18 faces/edges/vertices become 14/20/16 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 24/14, edges 54/22, vertices 34/18) |
|
| 61-shaft-keyway | pass | topology compared seam-normalised: Fusion's 14/22/18 faces/edges/vertices become 14/20/16 with closed-surface patches counted once and seam edges and vertices dropped (raw SolidCraft/Fusion: faces 24/14, edges 54/22, vertices 34/18) |
|
||||||
| 62-heat-sink | pass | |
|
| 62-heat-sink | pass | |
|
||||||
| 63-hinge-knuckle | pass | known Fusion difference: Gap replayed with the extent Fusion actually used (its through-all is sized as 1.1 × the farthest vertex, so curved tops stop it short; SolveCraft's through-all goes truly through) |
|
| 63-hinge-knuckle | pass | known Fusion difference: Gap replayed with the extent Fusion actually used (its through-all is sized as 1.1 × the farthest vertex, so curved tops stop it short; SolidCraft's through-all goes truly through) |
|
||||||
| 64-sketch-tangent-arc-chain | pass | |
|
| 64-sketch-tangent-arc-chain | pass | |
|
||||||
| 65-sketch-gear-tooth | pass | |
|
| 65-sketch-gear-tooth | pass | |
|
||||||
| 66-sketch-linkage | pass | |
|
| 66-sketch-linkage | pass | |
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Fusion command parity
|
# Fusion command parity
|
||||||
|
|
||||||
Generated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolveCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.
|
Generated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolidCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.
|
||||||
|
|
||||||
**In scope (SOLID, SKETCH, ASSEMBLY, SHEET METAL, PLASTIC enclosure subset): 335 / 541 commands live (62%).** SOLID + SKETCH: 188 / 291 (65%). Registered commands: 311.
|
**In scope (SOLID, SKETCH, ASSEMBLY, SHEET METAL, PLASTIC enclosure subset): 335 / 541 commands live (62%).** SOLID + SKETCH: 188 / 291 (65%). Registered commands: 311.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ Pi case in 15.2 s and 10.5 s. During those jobs, median `ui.inspect` round trips
|
|||||||
and 8.3 ms; the slowest were 33.7 ms and 28.6 ms. This fixes the window freeze, not the
|
and 8.3 ms; the slowest were 33.7 ms and 28.6 ms. This fixes the window freeze, not the
|
||||||
remaining cost of cold geometry evaluation.
|
remaining cost of cold geometry evaluation.
|
||||||
|
|
||||||
Recent thumbnails are 240 × 150 PNG files in `SolveCraft/thumbnails` under the platform
|
Recent thumbnails are 240 × 150 PNG files in `SolveCraft/thumbnails` under the platform
|
||||||
cache directory (`LOCALAPPDATA` on Windows). Their keys include the source path, file size
|
cache directory (`LOCALAPPDATA` on Windows). Their keys include the source path, file size
|
||||||
and modification time. Open and Save render from the model already in memory. Cached
|
and modification time. Open and Save render from the model already in memory. Cached
|
||||||
recents decode the PNG without reading or rebuilding a document, and run before cold
|
recents decode the PNG without reading or rebuilding a document, and run before cold
|
||||||
|
|||||||
+15
-3
@@ -1,4 +1,4 @@
|
|||||||
# Releasing SolveCraft
|
# Releasing SolidCraft
|
||||||
|
|
||||||
Releases are deliberate operations that an agent can complete end to end. Development lands
|
Releases are deliberate operations that an agent can complete end to end. Development lands
|
||||||
on `main`; pushing the selected commit to `release` builds all packages and creates a **draft**
|
on `main`; pushing the selected commit to `release` builds all packages and creates a **draft**
|
||||||
@@ -6,6 +6,18 @@ GitHub Release. Publishing that verified draft is a separate operation. Pushing
|
|||||||
pushing a tag does not start the release workflow. There is no automatic commit-message-based
|
pushing a tag does not start the release workflow. There is no automatic commit-message-based
|
||||||
version bump and no patch release for every merge.
|
version bump and no patch release for every merge.
|
||||||
|
|
||||||
|
## Compatibility names and deployment
|
||||||
|
|
||||||
|
SolidCraft is the product display name. Keep existing release asset filenames, including
|
||||||
|
`SolveCraft-{version}-windows-x64.exe`, `SolveCraft.app` and `solvecraft-cli` archives, together
|
||||||
|
with installed package IDs and settings paths. Do not rewrite published 0.2.0 or 0.2.1 assets,
|
||||||
|
checksums, tags or historical release notes.
|
||||||
|
|
||||||
|
Project Pages publishes the documentation, browser app and published-release Flatpak
|
||||||
|
repository under `/solidcraft/`. Do not add a separate owner-site publisher or permanent
|
||||||
|
old-name Pages/Flatpak aliases. GitHub repository redirects are separate from Pages hosting;
|
||||||
|
verify the new project URLs after deployment.
|
||||||
|
|
||||||
## Nightly prereleases
|
## Nightly prereleases
|
||||||
|
|
||||||
`release.yml` also runs at midnight in `America/New_York` (Eastern time, following daylight
|
`release.yml` also runs at midnight in `America/New_York` (Eastern time, following daylight
|
||||||
@@ -42,7 +54,7 @@ deployment. The stable Flatpak repository is not automatically updated with nigh
|
|||||||
2. Update `[workspace.package] version` in root `Cargo.toml`. Then run
|
2. Update `[workspace.package] version` in root `Cargo.toml`. Then run
|
||||||
`cargo metadata --format-version 1 --no-deps > /dev/null` to refresh workspace package
|
`cargo metadata --format-version 1 --no-deps > /dev/null` to refresh workspace package
|
||||||
versions in `Cargo.lock`. Inspect the diff and keep unrelated dependency changes out.
|
versions in `Cargo.lock`. Inspect the diff and keep unrelated dependency changes out.
|
||||||
SolveCraft does not currently implement the siblings' `cargo xtask version set` command.
|
SolidCraft does not currently implement the siblings' `cargo xtask version set` command.
|
||||||
3. Run `cargo xtask ci` and the packaging checks in `.github/workflows/packaging-lint.yml`.
|
3. Run `cargo xtask ci` and the packaging checks in `.github/workflows/packaging-lint.yml`.
|
||||||
Commit the version files through the normal review flow and land them on `main`.
|
Commit the version files through the normal review flow and land them on `main`.
|
||||||
Do not use `[skip ci]`. Record the exact commit SHA to release. If it changes, revalidate it.
|
Do not use `[skip ci]`. Record the exact commit SHA to release. If it changes, revalidate it.
|
||||||
@@ -68,7 +80,7 @@ fast-forward fails, inspect and resolve the divergence through the normal review
|
|||||||
Find the `release.yml` Actions run for the recorded SHA and `release` branch, and monitor that
|
Find the `release.yml` Actions run for the recorded SHA and `release` branch, and monitor that
|
||||||
specific run. The workflow builds Linux (both architectures, including Flatpak), macOS,
|
specific run. The workflow builds Linux (both architectures, including Flatpak), macOS,
|
||||||
Windows, FreeBSD, and web packages. Once every required job succeeds it creates a draft
|
Windows, FreeBSD, and web packages. Once every required job succeeds it creates a draft
|
||||||
`SolveCraft v<version>` with the artifacts, `SHA256SUMS.txt`, install notes, and generated changes.
|
`SolidCraft v<version>` with the artifacts, `SHA256SUMS.txt`, install notes, and generated changes.
|
||||||
|
|
||||||
A second push with the same untagged version can rebuild and replace that draft's complete
|
A second push with the same untagged version can rebuild and replace that draft's complete
|
||||||
asset set. Existing release notes are preserved on rebuild; refresh their changes section if
|
asset set. Existing release notes are preserved on rebuild; refresh their changes section if
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# STEP import
|
# STEP import
|
||||||
|
|
||||||
SolveCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its own reader in `crates/kernel/src/step_in` (written from the public standards; no third-party STEP code). A STEP file becomes an **Import** base feature on the timeline: it holds the file's B-rep (the STEP text is kept in the design, so the design reopens without the file) and later features — fillets, holes, cuts sketched on its faces, shells — build on its bodies.
|
SolidCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its own reader in `crates/kernel/src/step_in` (written from the public standards; no third-party STEP code). A STEP file becomes an **Import** base feature on the timeline: it holds the file's B-rep (the STEP text is kept in the design, so the design reopens without the file) and later features — fillets, holes, cuts sketched on its faces, shells — build on its bodies.
|
||||||
|
|
||||||
- **Open**: File → Open (`doc.open`) accepts `.step`/`.stp` in any case and starts a new design; `solvecraft part.step`; `solvecraft-cli eval part.step`, `solvecraft-cli run script.json --in part.step`; the MCP `open` tool.
|
- **Open**: File → Open (`doc.open`) accepts `.step`/`.stp` in any case and starts a new design; `solvecraft part.step`; `solvecraft-cli eval part.step`, `solvecraft-cli run script.json --in part.step`; the MCP `open` tool.
|
||||||
- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`file.insert_step {path}`).
|
- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`file.insert_step {path}`).
|
||||||
@@ -14,7 +14,7 @@ IGES 5.3 files (`.igs`/`.iges`) open, insert and drop the same way: `crates/kern
|
|||||||
|
|
||||||
## Fusion corpus
|
## Fusion corpus
|
||||||
|
|
||||||
Generated by `cargo xtask step-corpus`. Each case is a part exported from Fusion as STEP (AP214; kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.step` exactly as File → Open does (an Import base feature) and compares with Fusion's own measurements: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file.
|
Generated by `cargo xtask step-corpus`. Each case is a part exported from Fusion as STEP (AP214; kept locally in `plan/fusion/oracle/`, never committed). SolidCraft opens `part.step` exactly as File → Open does (an Import base feature) and compares with Fusion's own measurements: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file.
|
||||||
|
|
||||||
**76 / 76 files pass.**
|
**76 / 76 files pass.**
|
||||||
|
|
||||||
|
|||||||
@@ -49,20 +49,20 @@ for arch in x86_64 aarch64; do
|
|||||||
ostree --repo="$validation_repo" refs | grep -Fxq "app/$app_id/$arch/stable"
|
ostree --repo="$validation_repo" refs | grep -Fxq "app/$app_id/$arch/stable"
|
||||||
flatpak build-import-bundle "$repo_dir" "$bundles/$asset"
|
flatpak build-import-bundle "$repo_dir" "$bundles/$asset"
|
||||||
done
|
done
|
||||||
flatpak build-update-repo --title="SolveCraft" --comment='Official SolveCraft releases' \
|
flatpak build-update-repo --title="SolidCraft" --comment='Official SolidCraft releases' \
|
||||||
--description='Official Flatpak repository for SolveCraft' \
|
--description='Official Flatpak repository for SolidCraft' \
|
||||||
--homepage="https://github.com/$GITHUB_REPOSITORY" --default-branch=stable "$repo_dir"
|
--homepage="https://github.com/$GITHUB_REPOSITORY" --default-branch=stable "$repo_dir"
|
||||||
test -s "$repo_dir/summary"
|
test -s "$repo_dir/summary"
|
||||||
cat >"$site/flatpak/solvecraft.flatpakrepo" <<META
|
cat >"$site/flatpak/solvecraft.flatpakrepo" <<META
|
||||||
[Flatpak Repo]
|
[Flatpak Repo]
|
||||||
Title=SolveCraft
|
Title=SolidCraft
|
||||||
Comment=Official SolveCraft releases
|
Comment=Official SolidCraft releases
|
||||||
Description=Official Flatpak repository for SolveCraft
|
Description=Official Flatpak repository for SolidCraft
|
||||||
Url=$base_url/flatpak/repo/
|
Url=$base_url/flatpak/repo/
|
||||||
Homepage=https://github.com/$GITHUB_REPOSITORY
|
Homepage=https://github.com/$GITHUB_REPOSITORY
|
||||||
META
|
META
|
||||||
cat >"$site/flatpak/index.html" <<HTML
|
cat >"$site/flatpak/index.html" <<HTML
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SolveCraft Flatpak repository</title></head><body><main><h1>SolveCraft Flatpak repository</h1><p>Latest published release: <strong>$tag</strong></p><p><a href="solvecraft.flatpakrepo">Add the SolveCraft repository</a></p><pre>flatpak remote-add --if-not-exists solvecraft $base_url/flatpak/solvecraft.flatpakrepo
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SolidCraft Flatpak repository</title></head><body><main><h1>SolidCraft Flatpak repository</h1><p>Latest published release: <strong>$tag</strong></p><p><a href="solvecraft.flatpakrepo">Add the SolidCraft repository</a></p><pre>flatpak remote-add --if-not-exists solvecraft $base_url/flatpak/solvecraft.flatpakrepo
|
||||||
flatpak install solvecraft $app_id</pre></main></body></html>
|
flatpak install solvecraft $app_id</pre></main></body></html>
|
||||||
HTML
|
HTML
|
||||||
touch "$site/.nojekyll"
|
touch "$site/.nojekyll"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build and package SolveCraft for FreeBSD:
|
# Build and package SolidCraft for FreeBSD:
|
||||||
#
|
#
|
||||||
# $DIST/solvecraft-<version>-freebsd-x86_64.tar.gz a /usr/local-style tree:
|
# $DIST/solvecraft-<version>-freebsd-x86_64.tar.gz a /usr/local-style tree:
|
||||||
# solvecraft-<version>-freebsd-x86_64/{bin, share/applications, share/icons, share/mime,
|
# solvecraft-<version>-freebsd-x86_64/{bin, share/applications, share/icons, share/mime,
|
||||||
@@ -32,7 +32,7 @@ case "$(uname -m)" in
|
|||||||
*) echo "unsupported architecture $(uname -m)" >&2; exit 2 ;;
|
*) echo "unsupported architecture $(uname -m)" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
BASENAME="solvecraft-$VERSION-freebsd-$ARCH"
|
BASENAME="solvecraft-$VERSION-freebsd-$ARCH"
|
||||||
echo "==> SolveCraft $VERSION for FreeBSD $ARCH"
|
echo "==> SolidCraft $VERSION for FreeBSD $ARCH"
|
||||||
|
|
||||||
# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory.
|
# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory.
|
||||||
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}"
|
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SolveCraft AppImage entry point (packaging/linux/AppRun; installed by package.sh).
|
# SolidCraft AppImage entry point (packaging/linux/AppRun; installed by package.sh).
|
||||||
# Adapted from PhotoCraft's AppRun (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
# Adapted from PhotoCraft's AppRun (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
||||||
#
|
#
|
||||||
# On Wayland the compositor resolves a window's dock icon by taking the window's
|
# On Wayland the compositor resolves a window's dock icon by taking the window's
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=SolveCraft
|
Name=SolidCraft
|
||||||
GenericName=3D CAD
|
GenericName=3D CAD
|
||||||
Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild
|
Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild
|
||||||
Exec=solvecraft %f
|
Exec=solvecraft %f
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<id>ai.storyteller.solvecraft</id>
|
<id>ai.storyteller.solvecraft</id>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>MIT OR Apache-2.0</project_license>
|
<project_license>MIT OR Apache-2.0</project_license>
|
||||||
<name>SolveCraft</name>
|
<name>SolidCraft</name>
|
||||||
<summary>Parametric 3D CAD in the style of Fusion</summary>
|
<summary>Parametric 3D CAD in the style of Fusion</summary>
|
||||||
<developer id="io.github.bherbruck">
|
<developer id="io.github.bherbruck">
|
||||||
<name>SolveCraft contributors</name>
|
<name>SolidCraft contributors</name>
|
||||||
</developer>
|
</developer>
|
||||||
<description>
|
<description>
|
||||||
<p>
|
<p>
|
||||||
SolveCraft is an open-source parametric 3D CAD application written in Rust. Sketch on a
|
SolidCraft is an open-source parametric 3D CAD application written in Rust. Sketch on a
|
||||||
plane, constrain and dimension the sketch, turn profiles into solids on a feature
|
plane, constrain and dimension the sketch, turn profiles into solids on a feature
|
||||||
timeline, and change a parameter to watch the timeline rebuild.
|
timeline, and change a parameter to watch the timeline rebuild.
|
||||||
</p>
|
</p>
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
</description>
|
</description>
|
||||||
<launchable type="desktop-id">ai.storyteller.solvecraft.desktop</launchable>
|
<launchable type="desktop-id">ai.storyteller.solvecraft.desktop</launchable>
|
||||||
<icon type="stock">ai.storyteller.solvecraft</icon>
|
<icon type="stock">ai.storyteller.solvecraft</icon>
|
||||||
<url type="homepage">https://bherbruck.github.io/solvecraft/</url>
|
<url type="homepage">https://bherbruck.github.io/solidcraft/</url>
|
||||||
<url type="bugtracker">https://github.com/bherbruck/solvecraft/issues</url>
|
<url type="bugtracker">https://github.com/bherbruck/solidcraft/issues</url>
|
||||||
<url type="help">https://bherbruck.github.io/solvecraft/</url>
|
<url type="help">https://bherbruck.github.io/solidcraft/</url>
|
||||||
<categories>
|
<categories>
|
||||||
<category>Graphics</category>
|
<category>Graphics</category>
|
||||||
<category>3DGraphics</category>
|
<category>3DGraphics</category>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<caption>A sample part with its sketches and feature timeline</caption>
|
<caption>A sample part with its sketches and feature timeline</caption>
|
||||||
<image>https://raw.githubusercontent.com/bherbruck/solvecraft/main/docs/screenshots/sample-plate.png</image>
|
<image>https://raw.githubusercontent.com/bherbruck/solidcraft/main/docs/screenshots/sample-plate.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<requires>
|
<requires>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Installed to /usr/share/mime/packages/ (update-mime-database runs on install). -->
|
Installed to /usr/share/mime/packages/ (update-mime-database runs on install). -->
|
||||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
<mime-type type="application/x-solvecraft">
|
<mime-type type="application/x-solvecraft">
|
||||||
<comment>SolveCraft design</comment>
|
<comment>SolidCraft design</comment>
|
||||||
<icon name="ai.storyteller.solvecraft"/>
|
<icon name="ai.storyteller.solvecraft"/>
|
||||||
<sub-class-of type="application/json"/>
|
<sub-class-of type="application/json"/>
|
||||||
<glob pattern="*.solvecraft"/>
|
<glob pattern="*.solvecraft"/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Repackage a SolveCraft Linux tarball as a single-file Flatpak bundle:
|
# Repackage a SolidCraft Linux tarball as a single-file Flatpak bundle:
|
||||||
#
|
#
|
||||||
# $DIST/solvecraft-<version>-linux-<arch>.flatpak
|
# $DIST/solvecraft-<version>-linux-<arch>.flatpak
|
||||||
#
|
#
|
||||||
@@ -43,7 +43,7 @@ for tool in flatpak flatpak-builder; do
|
|||||||
command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; }
|
command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "==> SolveCraft $VERSION Flatpak bundle for $ARCH from $(basename "$TARBALL")"
|
echo "==> SolidCraft $VERSION Flatpak bundle for $ARCH from $(basename "$TARBALL")"
|
||||||
|
|
||||||
WORK="$CARGO_TARGET_DIR/flatpak-bundle"
|
WORK="$CARGO_TARGET_DIR/flatpak-bundle"
|
||||||
rm -rf "$WORK"
|
rm -rf "$WORK"
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ release: 1
|
|||||||
section: graphics
|
section: graphics
|
||||||
priority: optional
|
priority: optional
|
||||||
maintainer: ${SOLVECRAFT_MAINTAINER}
|
maintainer: ${SOLVECRAFT_MAINTAINER}
|
||||||
vendor: SolveCraft contributors
|
vendor: SolidCraft contributors
|
||||||
homepage: https://bherbruck.github.io/solvecraft/
|
homepage: https://bherbruck.github.io/solidcraft/
|
||||||
license: MIT OR Apache-2.0
|
license: MIT OR Apache-2.0
|
||||||
description: |-
|
description: |-
|
||||||
Open-source parametric 3D CAD in the style of Fusion
|
Open-source parametric 3D CAD in the style of Fusion
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build and package SolveCraft for Linux (<arch> is x86_64 or aarch64):
|
# Build and package SolidCraft for Linux (<arch> is x86_64 or aarch64):
|
||||||
#
|
#
|
||||||
# $DIST/solvecraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
|
# $DIST/solvecraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
|
||||||
# $DIST/solvecraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
|
# $DIST/solvecraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
|
||||||
@@ -37,10 +37,10 @@ case "$ARCH" in
|
|||||||
aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;;
|
aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;;
|
||||||
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
|
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
export SOLVECRAFT_MAINTAINER="${SOLVECRAFT_MAINTAINER:-SolveCraft contributors <https://github.com/bherbruck/solvecraft>}"
|
export SOLVECRAFT_MAINTAINER="${SOLVECRAFT_MAINTAINER:-SolidCraft contributors <https://github.com/bherbruck/solidcraft>}"
|
||||||
BASENAME="solvecraft-$VERSION-linux-$ARCH"
|
BASENAME="solvecraft-$VERSION-linux-$ARCH"
|
||||||
|
|
||||||
echo "==> SolveCraft $VERSION for Linux $ARCH ($FORMATS)"
|
echo "==> SolidCraft $VERSION for Linux $ARCH ($FORMATS)"
|
||||||
|
|
||||||
if [ "$SKIP_BUILD" = 0 ]; then
|
if [ "$SKIP_BUILD" = 0 ]; then
|
||||||
if [ -n "${CROSS_TARGET:-}" ]; then
|
if [ -n "${CROSS_TARGET:-}" ]; then
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>SolveCraft</string>
|
<string>SolidCraft</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>SolveCraft</string>
|
<string>SolidCraft</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>SolveCraft</string>
|
<string>SolveCraft</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeName</key>
|
<key>CFBundleTypeName</key>
|
||||||
<string>SolveCraft design</string>
|
<string>SolidCraft design</string>
|
||||||
<key>CFBundleTypeExtensions</key>
|
<key>CFBundleTypeExtensions</key>
|
||||||
<array><string>solvecraft</string></array>
|
<array><string>solvecraft</string></array>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build and package SolveCraft for macOS:
|
# Build and package SolidCraft for macOS:
|
||||||
#
|
#
|
||||||
# $DIST/solvecraft-<version>-macos-<arch>.dmg SolveCraft.app on a drag-to-Applications DMG
|
# $DIST/solvecraft-<version>-macos-<arch>.dmg SolveCraft.app on a drag-to-Applications DMG
|
||||||
# $DIST/solvecraft-cli-<version>-macos-<arch>.zip the headless CLI with the licence files
|
# $DIST/solvecraft-cli-<version>-macos-<arch>.zip the headless CLI with the licence files
|
||||||
@@ -38,7 +38,7 @@ APP="$WORK/SolveCraft.app"
|
|||||||
DMG="$DIST/solvecraft-$VERSION-macos-$ARCH.dmg"
|
DMG="$DIST/solvecraft-$VERSION-macos-$ARCH.dmg"
|
||||||
CLI_ZIP="$DIST/solvecraft-cli-$VERSION-macos-$ARCH.zip"
|
CLI_ZIP="$DIST/solvecraft-cli-$VERSION-macos-$ARCH.zip"
|
||||||
|
|
||||||
echo "==> SolveCraft $VERSION for macOS ($ARCH)"
|
echo "==> SolidCraft $VERSION for macOS ($ARCH)"
|
||||||
if [ "$SKIP_BUILD" = 0 ]; then
|
if [ "$SKIP_BUILD" = 0 ]; then
|
||||||
args=()
|
args=()
|
||||||
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
||||||
@@ -84,7 +84,7 @@ ln -s /Applications "$STAGE/Applications"
|
|||||||
rm -f "$DMG" "$WORK/raw.dmg"
|
rm -f "$DMG" "$WORK/raw.dmg"
|
||||||
# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,
|
# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,
|
||||||
# which is flaky on CI runners ("Resource busy").
|
# which is flaky on CI runners ("Resource busy").
|
||||||
hdiutil makehybrid -hfs -hfs-volume-name "SolveCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"
|
hdiutil makehybrid -hfs -hfs-volume-name "SolidCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"
|
||||||
hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
||||||
rm -f "$WORK/raw.dmg"
|
rm -f "$WORK/raw.dmg"
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ $Dist = if ($env:DIST) { $env:DIST } else { Join-Path $Root 'dist\release' }
|
|||||||
$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' }
|
$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' }
|
||||||
New-Item -ItemType Directory -Force -Path $Dist | Out-Null
|
New-Item -ItemType Directory -Force -Path $Dist | Out-Null
|
||||||
|
|
||||||
Write-Output "SolveCraft $Version for Windows $Arch ($Target)"
|
Write-Output "SolidCraft $Version for Windows $Arch ($Target)"
|
||||||
|
|
||||||
if (-not $SkipBuild) {
|
if (-not $SkipBuild) {
|
||||||
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
|
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
SolveCraft MSI (WiX Toolset v5, plus the UI and Util extensions). Built by package.ps1:
|
SolidCraft MSI (WiX Toolset v5, plus the UI and Util extensions). Built by package.ps1:
|
||||||
|
|
||||||
wix build solvecraft.wxs -arch x64 -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext
|
wix build solvecraft.wxs -arch x64 -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext
|
||||||
-culture en-US -loc solvecraft.en-us.wxl -d Version=1.2.3 -d DisplayVersion=1.2.3-rc.1
|
-culture en-US -loc solvecraft.en-us.wxl -d Version=1.2.3 -d DisplayVersion=1.2.3-rc.1
|
||||||
@@ -8,33 +8,33 @@
|
|||||||
|
|
||||||
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolveCraft by
|
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolveCraft by
|
||||||
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
|
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
|
||||||
SolveCraft" box. Installs a Start Menu shortcut, solvecraft-cli, the licence files (including
|
SolidCraft" box. Installs a Start Menu shortcut, solvecraft-cli, the licence files (including
|
||||||
THIRD-PARTY-LICENSES.txt), makes SolveCraft the owner of `.solvecraft` and offers it under
|
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.solvecraft` and offers it under
|
||||||
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
|
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
|
||||||
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
||||||
-->
|
-->
|
||||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||||
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
||||||
<Package
|
<Package
|
||||||
Name="SolveCraft"
|
Name="SolidCraft"
|
||||||
Manufacturer="SolveCraft contributors"
|
Manufacturer="SolidCraft contributors"
|
||||||
Version="$(var.Version)"
|
Version="$(var.Version)"
|
||||||
UpgradeCode="6A145FD6-D285-428D-AE84-A69570CDF4DB"
|
UpgradeCode="6A145FD6-D285-428D-AE84-A69570CDF4DB"
|
||||||
Scope="perMachine"
|
Scope="perMachine"
|
||||||
Compressed="yes"
|
Compressed="yes"
|
||||||
Language="1033">
|
Language="1033">
|
||||||
<SummaryInformation Description="SolveCraft parametric 3D CAD" Manufacturer="SolveCraft contributors" />
|
<SummaryInformation Description="SolidCraft parametric 3D CAD" Manufacturer="SolidCraft contributors" />
|
||||||
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other. -->
|
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other. -->
|
||||||
<MajorUpgrade AllowSameVersionUpgrades="yes"
|
<MajorUpgrade AllowSameVersionUpgrades="yes"
|
||||||
DowngradeErrorMessage="A newer version of SolveCraft is already installed." />
|
DowngradeErrorMessage="A newer version of SolidCraft is already installed." />
|
||||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||||
|
|
||||||
<!-- Advertised shortcuts to an .exe need an .exe icon identifier (ICE50). -->
|
<!-- Advertised shortcuts to an .exe need an .exe icon identifier (ICE50). -->
|
||||||
<Icon Id="SolvecraftIcon.exe" SourceFile="$(var.IconPath)" />
|
<Icon Id="SolvecraftIcon.exe" SourceFile="$(var.IconPath)" />
|
||||||
<Property Id="ARPPRODUCTICON" Value="SolvecraftIcon.exe" />
|
<Property Id="ARPPRODUCTICON" Value="SolvecraftIcon.exe" />
|
||||||
<Property Id="ARPURLINFOABOUT" Value="https://bherbruck.github.io/solvecraft/" />
|
<Property Id="ARPURLINFOABOUT" Value="https://bherbruck.github.io/solidcraft/" />
|
||||||
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/bherbruck/solvecraft/releases" />
|
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/bherbruck/solidcraft/releases" />
|
||||||
<Property Id="ARPHELPLINK" Value="https://github.com/bherbruck/solvecraft/issues" />
|
<Property Id="ARPHELPLINK" Value="https://github.com/bherbruck/solidcraft/issues" />
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||||
<Directory Id="INSTALLFOLDER" Name="SolveCraft" />
|
<Directory Id="INSTALLFOLDER" Name="SolveCraft" />
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Key="Software\SolveCraft" Name="InstallDir" Type="raw" />
|
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Key="Software\SolveCraft" Name="InstallDir" Type="raw" />
|
||||||
</Property>
|
</Property>
|
||||||
|
|
||||||
<Feature Id="Main" Title="SolveCraft" Level="1">
|
<Feature Id="Main" Title="SolidCraft" Level="1">
|
||||||
<ComponentGroupRef Id="SolvecraftFiles" />
|
<ComponentGroupRef Id="SolvecraftFiles" />
|
||||||
</Feature>
|
</Feature>
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
</UI>
|
</UI>
|
||||||
|
|
||||||
<Property Id="SolvecraftVersion" Value="$(var.DisplayVersion)" />
|
<Property Id="SolvecraftVersion" Value="$(var.DisplayVersion)" />
|
||||||
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="SolveCraft $(var.DisplayVersion) is installed. You can start it any time from the Start menu." />
|
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="SolidCraft $(var.DisplayVersion) is installed. You can start it any time from the Start menu." />
|
||||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch SolveCraft" />
|
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch SolidCraft" />
|
||||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
|
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
|
||||||
<Property Id="WixShellExecTarget" Value="[#SolvecraftExe]" />
|
<Property Id="WixShellExecTarget" Value="[#SolvecraftExe]" />
|
||||||
<!-- Runs in the wizard's own (unelevated) process, so the app starts as the user. -->
|
<!-- Runs in the wizard's own (unelevated) process, so the app starts as the user. -->
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
<ComponentGroup Id="SolvecraftFiles" Directory="INSTALLFOLDER">
|
<ComponentGroup Id="SolvecraftFiles" Directory="INSTALLFOLDER">
|
||||||
<Component Id="SolvecraftApp" Guid="290BEF27-C200-4069-AA43-7DE6A39CDB0A">
|
<Component Id="SolvecraftApp" Guid="290BEF27-C200-4069-AA43-7DE6A39CDB0A">
|
||||||
<File Id="SolvecraftExe" Source="$(var.BinDir)\solvecraft.exe" KeyPath="yes">
|
<File Id="SolvecraftExe" Source="$(var.BinDir)\solvecraft.exe" KeyPath="yes">
|
||||||
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="SolveCraft"
|
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="SolidCraft"
|
||||||
Description="SolveCraft parametric 3D CAD" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
|
Description="SolidCraft parametric 3D CAD" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
|
||||||
Icon="SolvecraftIcon.exe" IconIndex="0" />
|
Icon="SolvecraftIcon.exe" IconIndex="0" />
|
||||||
</File>
|
</File>
|
||||||
|
|
||||||
<!-- .solvecraft: SolveCraft's own format; SolveCraft is its default app. -->
|
<!-- .solvecraft: SolidCraft's own format; SolidCraft is its default app. -->
|
||||||
<ProgId Id="Solvecraft.Design" Description="SolveCraft Design" Icon="SolvecraftExe" IconIndex="0">
|
<ProgId Id="Solvecraft.Design" Description="SolidCraft Design" Icon="SolvecraftExe" IconIndex="0">
|
||||||
<Extension Id="solvecraft" ContentType="application/x-solvecraft">
|
<Extension Id="solvecraft" ContentType="application/x-solvecraft">
|
||||||
<Verb Id="open" Command="Open" TargetFile="SolvecraftExe" Argument=""%1"" />
|
<Verb Id="open" Command="Open" TargetFile="SolvecraftExe" Argument=""%1"" />
|
||||||
</Extension>
|
</Extension>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<RegistryValue Root="HKLM" Key="Software\Classes\.stl\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
|
<RegistryValue Root="HKLM" Key="Software\Classes\.stl\OpenWithProgids" Name="Solvecraft.Model" Value="" Type="string" />
|
||||||
|
|
||||||
<!-- "Open with" app list and the Run dialog (Win+R "solvecraft"). -->
|
<!-- "Open with" app list and the Run dialog (Win+R "solvecraft"). -->
|
||||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe" Name="FriendlyAppName" Value="SolveCraft" Type="string" />
|
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe" Name="FriendlyAppName" Value="SolidCraft" Type="string" />
|
||||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\shell\open\command" Value=""[#SolvecraftExe]" "%1"" Type="string" />
|
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\shell\open\command" Value=""[#SolvecraftExe]" "%1"" Type="string" />
|
||||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".solvecraft" Value="" Type="string" />
|
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".solvecraft" Value="" Type="string" />
|
||||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".step" Value="" Type="string" />
|
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solvecraft.exe\SupportedTypes" Name=".step" Value="" Type="string" />
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ fn licence_files(dir: &Path, p: &Value) -> Vec<(String, String)> {
|
|||||||
fn render(crates: &[Crate]) -> String {
|
fn render(crates: &[Crate]) -> String {
|
||||||
let rule = "=".repeat(80);
|
let rule = "=".repeat(80);
|
||||||
let mut out = format!(
|
let mut out = format!(
|
||||||
"Third-party software in SolveCraft\n\nSolveCraft (the desktop app, solvecraft-cli and the web app) is built from the {} crates below.\n\
|
"Third-party software in SolidCraft\n\nSolidCraft (the desktop app, solvecraft-cli and the web app) is built from the {} crates below.\n\
|
||||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.\n\
|
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.\n\
|
||||||
SolveCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.\n\n",
|
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.\n\n",
|
||||||
crates.len()
|
crates.len()
|
||||||
);
|
);
|
||||||
for c in crates {
|
for c in crates {
|
||||||
@@ -146,7 +146,7 @@ fn render(crates: &[Crate]) -> String {
|
|||||||
}
|
}
|
||||||
if !without.is_empty() {
|
if !without.is_empty() {
|
||||||
out += &format!(
|
out += &format!(
|
||||||
"\n{rule}\nCrates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0\ntexts are as in SolveCraft's own LICENSE-MIT and LICENSE-APACHE)\n{rule}\n\n"
|
"\n{rule}\nCrates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0\ntexts are as in SolidCraft's own LICENSE-MIT and LICENSE-APACHE)\n{rule}\n\n"
|
||||||
);
|
);
|
||||||
for c in without {
|
for c in without {
|
||||||
out += &format!(
|
out += &format!(
|
||||||
|
|||||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user