Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29b0d2f13 | ||
|
|
480fd8d517 | ||
|
|
4c09fa5355 | ||
|
|
626fe6fdee | ||
|
|
01f2c78cd1 | ||
|
|
9ca7d307a4 | ||
|
|
2f91338d3d | ||
|
|
dfbf7dd6a8 | ||
|
|
cf9d7772f1 | ||
|
|
6e84c03946 | ||
|
|
3669d4b3d0 | ||
|
|
345c0882cd | ||
|
|
18347b3d0d | ||
|
|
ac84491638 | ||
|
|
6a67cc8f1e | ||
|
|
6cb3025b72 | ||
|
|
b34d45233d | ||
|
|
47a7f29485 | ||
|
|
9c51f9d433 | ||
|
|
4687e3b073 | ||
|
|
1ff16cc72b | ||
|
|
813bee8914 | ||
|
|
482abb4562 | ||
|
|
7bed28752e | ||
|
|
ea23401710 | ||
|
|
9f76cca8b4 | ||
|
|
d5ff5ea1a5 | ||
|
|
ce902be66c | ||
|
|
63095fedb3 | ||
|
|
00fdcf3e72 | ||
|
|
774a8b1d9a | ||
|
|
97ff7578c2 | ||
|
|
19bf854dc9 | ||
|
|
f204042bf2 | ||
|
|
4bea4c7785 | ||
|
|
09b5da3e9f | ||
|
|
a2c7396160 | ||
|
|
45f92277cb | ||
|
|
587cf865b8 | ||
|
|
c1c813e572 | ||
|
|
7498e3b367 | ||
|
|
f037c92370 | ||
|
|
2b70ed7b72 | ||
|
|
15b5b51bf5 | ||
|
|
abddd67d22 | ||
|
|
78d9aa0153 | ||
|
|
17db109c5d | ||
|
|
d2e409f40c | ||
|
|
8746376ee9 | ||
|
|
5808bcc0f0 | ||
|
|
d23593d3eb | ||
|
|
897b7d93b5 | ||
|
|
7c85970bfc | ||
|
|
8fb5742706 | ||
|
|
aba57f1ca2 | ||
|
|
57d738eac4 | ||
|
|
3f1c023563 | ||
|
|
7da7bbb335 | ||
|
|
2eb292dd4a | ||
|
|
b41ad23e3f | ||
|
|
2dcfea8df7 | ||
|
|
7135407268 | ||
|
|
5f7ce80a93 | ||
|
|
fdaf57daa3 | ||
|
|
7eac1de9ee | ||
|
|
fe07f79387 | ||
|
|
b52d4390a0 | ||
|
|
c616f50bd0 | ||
|
|
6c61442bb2 | ||
|
|
c333889fc0 | ||
|
|
648f0ff768 | ||
|
|
eaab205302 | ||
|
|
eea49922bd | ||
|
|
dd7790a43f | ||
|
|
fe3717f2e4 | ||
|
|
d9237d004e | ||
|
|
d8d54ded0d | ||
|
|
95d394dcab | ||
|
|
7ee351dc91 | ||
|
|
9297f81501 | ||
|
|
2afc10fbd5 | ||
|
|
5c98c1e24a | ||
|
|
0040ce6bf2 | ||
|
|
1f7f37d1ef | ||
|
|
96f6424aec | ||
|
|
3d3f9b626f | ||
|
|
c3ebe13991 | ||
|
|
b69cd2d0b7 | ||
|
|
753b0a2eab | ||
|
|
b702f0efde | ||
|
|
152418b624 | ||
|
|
e99b3bd4e8 | ||
|
|
8319c35a32 | ||
|
|
c3e0ea498a | ||
|
|
b6e3a6c366 | ||
|
|
4d8c80a86d | ||
|
|
ec5d7f9789 | ||
|
|
69050c81d9 | ||
|
|
53238fba5f | ||
|
|
5d661fa2af | ||
|
|
ff7a347013 | ||
|
|
534393ed5a |
No files matched your search
@@ -10,6 +10,7 @@ not (
|
||||
(package(solidcraft-engine) & (
|
||||
test(=tests::hostile_params_never_panic)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(/^tests::robustness_goldens::/)
|
||||
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
|
||||
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
|
||||
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
|
||||
|
||||
@@ -21,8 +21,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
# Every push to main gets its full run; a new PR push replaces the old run.
|
||||
# A new PR push replaces the old run. Only one run per group may wait, so a busy main would
|
||||
# cancel the waiting nightly (or a Run workflow) before it starts: they get groups of their own.
|
||||
group: ci-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
@@ -47,6 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -59,12 +61,14 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
native=true
|
||||
else
|
||||
# Compare both old and new paths of renames; unknown paths run native checks.
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
case "$path" in
|
||||
.github/workflows/ci.yml) native=true ;;
|
||||
*.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;;
|
||||
@@ -74,6 +78,7 @@ jobs:
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
fi
|
||||
echo "native=$native" >> "$GITHUB_OUTPUT"
|
||||
echo "kernel=$kernel" >> "$GITHUB_OUTPUT"
|
||||
|
||||
checks:
|
||||
name: Format and repository checks
|
||||
@@ -95,6 +100,8 @@ jobs:
|
||||
run: cargo xtask licences --check
|
||||
- name: Dependency layers
|
||||
run: cargo xtask layers
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
run: cargo xtask web-offline-test
|
||||
|
||||
clippy:
|
||||
name: Native Linux Clippy
|
||||
@@ -150,7 +157,7 @@ jobs:
|
||||
|
||||
tests:
|
||||
name: Native Linux tests (${{ matrix.shard }}/4)
|
||||
needs: build-tests
|
||||
needs: [changes, build-tests]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -177,6 +184,16 @@ jobs:
|
||||
# the build job's path, which is the same checkout path here.
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --partition "slice:$SHARD/4" "${all[@]}"
|
||||
- name: Robustness goldens sample (pull requests that touch the kernel)
|
||||
# Every 8th seed of tests/robustness/goldens.json, a quarter per shard; pushes to main and
|
||||
# the nightly run every seed in the step above.
|
||||
if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true'
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --ignore-default-filter \
|
||||
-E "package(solidcraft-engine) & test(=tests::robustness_goldens::sample_$((SHARD - 1)))"
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
@@ -269,9 +286,31 @@ jobs:
|
||||
name: oracle-report
|
||||
path: docs/oracle.md
|
||||
|
||||
# D3D12's older shader compiler (FXC, used where dxcompiler.dll is absent) is stricter than
|
||||
# DXC, Vulkan and Metal: a shader it cannot compile once crashed the app at startup. Build the
|
||||
# GPU tessellation pipeline with FXC on the runner's WARP software adapter and compare its
|
||||
# results with the CPU.
|
||||
windows-fxc:
|
||||
name: Windows D3D12 FXC shaders
|
||||
needs: changes
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
WGPU_BACKEND: dx12
|
||||
WGPU_DX12_COMPILER: fxc
|
||||
SOLIDCRAFT_REQUIRE_GPU: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-fxc
|
||||
- run: cargo test -p solidcraft-gpu --profile ci --locked
|
||||
|
||||
# Preserve the existing check name for branch protection, including docs-only PRs.
|
||||
ci:
|
||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle]
|
||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@@ -285,6 +324,7 @@ jobs:
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||
run: |
|
||||
test "$CHANGES" = success
|
||||
test "$CHECKS" = success
|
||||
@@ -294,6 +334,7 @@ jobs:
|
||||
test "$TESTS" = success
|
||||
test "$DOCS" = success
|
||||
test "$ORACLE" = success
|
||||
test "$WINDOWS_FXC" = success
|
||||
fi
|
||||
|
||||
wasm:
|
||||
|
||||
@@ -6,9 +6,9 @@ name: Packaging lint
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: [".github/workflows/**", "packaging/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
pull_request:
|
||||
paths: [".github/workflows/**", "packaging/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
}
|
||||
- name: XML, desktop entry, AppStream, Flatpak manifest
|
||||
run: |
|
||||
xmllint --noout packaging/windows/solidcraft.wxs packaging/windows/solidcraft.en-us.wxl packaging/macos/*.plist* packaging/linux/*.xml*
|
||||
desktop-file-validate packaging/linux/ai.storyteller.solidcraft.desktop
|
||||
xmllint --noout assets/platforms/windows/solidcraft.wxs assets/platforms/windows/solidcraft.en-us.wxl assets/platforms/macos/*.plist* assets/platforms/linux/*.xml*
|
||||
desktop-file-validate assets/platforms/linux/ai.storyteller.solidcraft.desktop
|
||||
# The window's X11 class is its app id; the old id stays only as the rename shims.
|
||||
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' packaging/linux/ai.storyteller.solidcraft.desktop
|
||||
grep -q '<id>ai.storyteller.solvecraft</id>' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in
|
||||
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' assets/platforms/linux/ai.storyteller.solidcraft.desktop
|
||||
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in
|
||||
grep -qF -e '--end-of-life-rebase=' packaging/flatpak-site.sh
|
||||
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solidcraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solidcraft' and m['command'] == 'solidcraft'"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
|
||||
|
||||
@@ -50,6 +50,8 @@ jobs:
|
||||
- name: Build the web app
|
||||
working-directory: apps/solidcraft-web
|
||||
run: trunk build --release --public-url /solidcraft/app/
|
||||
- name: Test the web app offline and across an update (headless Chrome)
|
||||
run: cargo xtask web-offline-test --site dist/web
|
||||
- name: Assemble the site
|
||||
run: |
|
||||
mkdir -p site
|
||||
|
||||
@@ -59,8 +59,8 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
|
||||
operations are our own.
|
||||
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
|
||||
artwork from commercial CAD products — 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**
|
||||
artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code
|
||||
(`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
|
||||
Breaking this rule is the most serious mistake you can make in this repo.
|
||||
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
|
||||
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.start(id)`. Colours come from `theme::Tokens`.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
|
||||
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
|
||||
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
|
||||
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
|
||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||
assets, layers). Commit after every feature arc that builds.
|
||||
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
|
||||
@@ -122,7 +125,10 @@ before the draft is verified.
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
|
||||
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
|
||||
request on a connection carries the launch's token as `"token"`: read it from
|
||||
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
|
||||
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
|
||||
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
|
||||
|
||||
@@ -8,47 +8,44 @@ file is missing from this table.
|
||||
templates, materials or help text.** Every asset is original work by SolidCraft contributors or
|
||||
third-party material under an open licence. Screenshots of Autodesk software are never committed.
|
||||
|
||||
Generated-in-code assets are original and have no file to list:
|
||||
- the UI icon set (`crates/ui-egui/src/icons.rs`);
|
||||
- the axis view, navigation bar and timeline glyphs (`crates/ui-egui/src/viewport.rs`,
|
||||
`crates/ui-egui/src/timeline.rs`).
|
||||
Editable drawing code, native projects and shipped exports are collected under `assets/`.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
|
||||
| assets/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
|
||||
| assets/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/Assets.car | SolidCraft contributors | Apple actool compilation of assets/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
|
||||
| assets/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
|
||||
| assets/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/icon.svg from VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/SolidCraft.icon composition | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/archive/icons/macos-v1/exports/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/archive/icons/macos-v1/exports/icon.svg from VectorCraft | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
|
||||
| docs/book/src/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
|
||||
| assets/branding/web/source/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/sample-plate.png | SolidCraft contributors | copy of docs/screenshots/sample-plate.png made by `cargo xtask book` (gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/bracket.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot examples/bracket.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/enclosure.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/enclosure.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/revolve.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/revolve.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/sheet-metal.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/sheet-metal.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| crates/sketch/fonts/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in crates/sketch/fonts/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
|
||||
| apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in apps/solidcraft-web/fonts/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
|
||||
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
|
||||
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
|
||||
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
@@ -70,6 +67,45 @@ Generated-in-code assets are original and have no file to list:
|
||||
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
|
||||
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/icons/app/linux/exports/hicolor/128x128/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/16x16/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/24x24/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/glyphs.rs | SolidCraft / VectorCraft contributors | Original glyphs; caption geometry adapted from VectorCraft titlebar.rs (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/icons.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/markers.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
|
||||
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
| SolidCraft file | Adapted from | Licence |
|
||||
@@ -86,6 +122,16 @@ Generated-in-code assets are original and have no file to list:
|
||||
| LICENSE-MIT, LICENSE-APACHE (licence text) | CADCraft | — |
|
||||
| .github/workflows/release.yml, .github/workflows/packaging-lint.yml and packaging/ (release pipeline: Linux AppImage/deb/rpm/Flatpak/tar.gz, FreeBSD, macOS dmg, Windows MSI/portable zip, web zip; AppRun, nfpm, Flatpak, WiX and Info.plist templates) | PhotoCraft (https://github.com/storytold/photocraft) `.github/workflows/{release,packaging-lint}.yml`, `packaging/` | Apache-2.0 |
|
||||
|
||||
## Incremental HDR codec adapter
|
||||
|
||||
`crates/render/src/lighting/hdr.rs` adapts the scanline/component/old-RLE decoding
|
||||
state machines from the Radiance decoder in `image` 0.25.10
|
||||
([source](https://docs.rs/crate/image/0.25.10/source/src/codecs/hdr/decoder.rs)),
|
||||
MIT OR Apache-2.0. Metadata parsing still uses that library. The adapter makes row
|
||||
work cooperative and adds checked run lengths, zero-run rejection, width validation,
|
||||
header/input/radiance bounds and cancellation. Original licence texts are retained
|
||||
in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
|
||||
|
||||
## Recovered contributions
|
||||
|
||||
| SolidCraft file | Original contribution | Licence |
|
||||
@@ -96,7 +142,7 @@ Generated-in-code assets are original and have no file to list:
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
|
||||
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro (vendor/README.md) |
|
||||
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
|
||||
|
||||
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.start(id)`. Colours come from `theme::Tokens`.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
|
||||
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
|
||||
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
|
||||
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
|
||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||
assets, layers). Commit after every feature arc that builds.
|
||||
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
|
||||
@@ -99,7 +102,10 @@ before the draft is verified.
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
|
||||
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
|
||||
request on a connection carries the launch's token as `"token"`: read it from
|
||||
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
|
||||
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
|
||||
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
|
||||
|
||||
@@ -161,7 +161,8 @@ Pull requests skip a few heavy tests (one to four minutes each), which run on ev
|
||||
`main` and nightly; check the `main` run after merging. They are listed in
|
||||
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
`tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`
|
||||
|
||||
@@ -3281,11 +3281,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"embed-resource",
|
||||
"getrandom 0.3.4",
|
||||
"hidapi",
|
||||
"log",
|
||||
"quick-xml",
|
||||
@@ -3298,7 +3299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3307,7 +3308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3322,7 +3323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3338,16 +3339,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
"pollster 0.4.0",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"thiserror 2.0.21",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@@ -3362,7 +3376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rayon",
|
||||
@@ -3380,7 +3394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3388,7 +3402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -3398,7 +3412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3409,22 +3423,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"image",
|
||||
"log",
|
||||
"pollster 0.4.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"ttf-parser",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4847,9 +4863,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -18,6 +18,7 @@ solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
solidcraft-engine = { path = "crates/engine" }
|
||||
@@ -50,7 +51,7 @@ quick-xml = "0.41"
|
||||
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
|
||||
|
||||
# images
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
|
||||
|
||||
# ui
|
||||
egui = "0.36"
|
||||
@@ -58,6 +59,10 @@ eframe = { version = "0.36", default-features = false, features = ["persistence"
|
||||
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
|
||||
web-time = "1"
|
||||
egui-wgpu = "0.36"
|
||||
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
|
||||
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
|
||||
naga = { version = "30", default-features = false }
|
||||
pollster = "0.4"
|
||||
rfd = "0.17"
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -19,6 +19,11 @@ licensed MIT OR Apache-2.0; ATTRIBUTION.md lists the files.
|
||||
The release packaging (.github/workflows/release.yml, packaging/) is adapted from PhotoCraft
|
||||
(https://github.com/storytold/photocraft), licensed under the Apache License 2.0.
|
||||
|
||||
The incremental Radiance RGBE row adapter in crates/render/src/lighting/hdr.rs adapts
|
||||
decoding state machines from image 0.25.10 (https://github.com/image-rs/image),
|
||||
MIT OR Apache-2.0. ATTRIBUTION.md describes the bounded/cancellable changes; the
|
||||
original licence texts are included in THIRD-PARTY-LICENSES.txt.
|
||||
|
||||
Most bundled assets are drawn in code or contributed originally; third-party fonts (Liberation Sans
|
||||
for sketch text, BIZ UDPGothic for web Japanese display) are licensed under SIL OFL 1.1. The
|
||||
per-file list is in ATTRIBUTION.md.
|
||||
|
||||
@@ -125,7 +125,8 @@ rebuild with no errors, replayed here at 2× speed.
|
||||
provisional picture from warm geometry caches while rebuilding before editing; caches never
|
||||
replace the parametric design. Export to STEP (AP242 with body names, colours and
|
||||
components as assemblies; revolved faces as native planes, cylinders, cones, spheres and tori),
|
||||
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ;
|
||||
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ, and
|
||||
assemblies with joints as URDF for robot simulators (docs/urdf.md);
|
||||
3MF, STL and OBJ import as mesh bodies (they render, measure, move and export; solid features
|
||||
need B-rep bodies). Convert Mesh reconstructs supported closed constant-section parts
|
||||
as parametric solids with planar and circular cylindrical faces;
|
||||
@@ -199,7 +200,9 @@ stdio. Headless it runs its own design session; with `--connect 127.0.0.1:PORT`
|
||||
started with `solidcraft --control PORT`, so you can watch the agent model. Tools:
|
||||
`list_commands`, `execute`, `batch`, `inspect_design`, `measure`, `body_topology`,
|
||||
`set_parameter`, `screenshot` (returns the image), `export`, `undo`, `redo`, `new_design`, `open`,
|
||||
`save`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
|
||||
`save`. Tool calls reach files only in the open design's folder (when it lies inside your home
|
||||
folder, never home itself or a hidden folder), SolidCraft's recovery folder and folders given with
|
||||
`--allow-path DIR`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
|
||||
configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -47,20 +47,41 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
|
||||
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
|
||||
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks | in progress | 30 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
|
||||
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
|
||||
| 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, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), 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 |
|
||||
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis. Proposed, in order: surface Extrude/Revolve/Sweep/Loft from open and closed curves (#757), Offset surface (#758), Unstitch and Reverse Normal (#766), Delete Face without healing (#768); Extend curved faces and edges (#760), mutual trim and trim by projected curves (#767), Untrim (#761), Thicken every surface kind (#765); Ruled (#762), Boundary surface and curve networks with G0/G1/G2 (#763), Patch with G1/G2 and general loops (#759), Blend surface (#764), sweep rails, twist and scale (#771, loft rails #201), fillet between surface bodies (#769); continuity analysis, surface curvature combs and isocurves (#770) | deferred | — |
|
||||
| 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 |
|
||||
| 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; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). 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. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, 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); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| 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 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
|
||||
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
|
||||
|
||||
## Physical material data
|
||||
|
||||
- Physical material card validation (#923/#922) provides typed SI mechanical and
|
||||
thermal data, per-property sources/conditions and isotropic/orthotropic stability
|
||||
checks through read-only `material.properties.validate`. It does not assign or
|
||||
persist properties, provide a solver or enable rendering; see
|
||||
[the contract and staged follow-ups](docs/physical-material-cards.md).
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Proposed phases cover offline bundled/project/user packs, physical materials and
|
||||
appearances, editable wood mapping, and parameterized component/assembly families.
|
||||
Stable identity/revision, copied design definitions, provenance and resource limits
|
||||
apply from the first implementation. Existing material, appearance and standard-part
|
||||
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
|
||||
for source/license review and acceptance gates. This is planning only: pack loading,
|
||||
texture rendering and expanded component families are not implemented by this entry.
|
||||
|
||||
## Render foundation
|
||||
|
||||
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
|
||||
it does not yet enable HDRI/PBR rendering or a document import command. See
|
||||
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
|
||||
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
|
||||
bounded image/scratch admission checks, exposed by the read-only `render.backends`
|
||||
command. It describes the current CPU raster snapshot path without evaluating the model.
|
||||
@@ -194,6 +215,9 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
- Associative shell-face sketches require a unique certified planar source. Nested shell
|
||||
attachments and ambiguous coincident sources remain unsupported.
|
||||
- G2 fillets: exact quintic blends of straight edges between planar faces, with one perpendicular
|
||||
planar end face at each end. The UI and `solid.fillet` expose `curvature` (alias `g2`); the size
|
||||
is an equivalent circular setback. Curved faces/edges, sloped ends and corner junctions remain unsupported.
|
||||
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
|
||||
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
|
||||
meet (a sphere for equal radii); straight edges (convex or concave) between planar faces with perpendicular
|
||||
@@ -211,7 +235,14 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
|
||||
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
|
||||
between two overlapping extruded arcs).
|
||||
between two overlapping extruded arcs); knife edges, where the two faces have opposite
|
||||
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
|
||||
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
|
||||
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
|
||||
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
|
||||
other, except a round after a round of the edge between the wall and a flat face that it
|
||||
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
|
||||
piece of sphere.
|
||||
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
|
||||
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
|
||||
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
|
||||
@@ -247,8 +278,12 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
|
||||
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
|
||||
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
|
||||
certificate and refine within bounded node limits while retaining existing IDs. Singular,
|
||||
self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
certificate and refine within bounded node limits while retaining existing IDs.
|
||||
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
|
||||
speeds without changing stored nodes or relaxing the distance tolerance (#971).
|
||||
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
|
||||
editable source updates the derived boundary live. Break Association restores point editing.
|
||||
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
|
||||
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
|
||||
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
|
||||
|
||||
@@ -13349,11 +13349,11 @@ truck-topology 0.6.0: Apache-2.0 (https://github.com/ricosjp/truck)
|
||||
Bundled fonts (SIL Open Font License, Version 1.1)
|
||||
================================================================================
|
||||
|
||||
- LiberationSans-Regular.ttf (crates/sketch/fonts/LiberationSans-Regular.ttf)
|
||||
- LiberationSans-Regular.ttf (assets/fonts/liberation-sans/LiberationSans-Regular.ttf)
|
||||
Copyright 2012 Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google)
|
||||
Source: https://github.com/liberationfonts
|
||||
|
||||
- BIZUDPGothic-Regular.ttf (apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf)
|
||||
- BIZUDPGothic-Regular.ttf (assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf)
|
||||
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)
|
||||
Source: https://github.com/googlefonts/morisawa-biz-ud-gothic
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! solidcraft-cli commands the command registry as JSON
|
||||
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
|
||||
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
|
||||
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT] MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
|
||||
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
|
||||
//! ```
|
||||
@@ -25,7 +26,7 @@ mod spatial;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
#[path = "../../app_icon.rs"]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
|
||||
use solidcraft_engine::Session;
|
||||
@@ -43,7 +44,7 @@ const USAGE: &str = "usage:
|
||||
solidcraft-cli recipe <recipe.json>
|
||||
solidcraft-cli oracle <case-dir>... [--json]
|
||||
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
|
||||
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT]
|
||||
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||
solidcraft-cli licences
|
||||
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
|
||||
@@ -312,11 +313,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
|
||||
/// a running app's control port. stdout carries only protocol messages.
|
||||
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
|
||||
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
|
||||
fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
|
||||
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
|
||||
if let Some(s) = &scope {
|
||||
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
|
||||
eprintln!(
|
||||
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
|
||||
roots.join(", ")
|
||||
);
|
||||
let design_dir = flag(args, "--in")
|
||||
.and_then(|p| std::path::Path::new(p).parent())
|
||||
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
|
||||
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
|
||||
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
}
|
||||
}
|
||||
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
|
||||
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
|
||||
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect(addr).map_err(|e| {
|
||||
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect_with_token(addr, flag(args, "--token")).map_err(|e| {
|
||||
let port = addr.rsplit(':').next().filter(|p| p.parse::<u16>().is_ok()).unwrap_or("PORT");
|
||||
format!(
|
||||
"cannot connect to {addr}: {e}\nno SolidCraft app is listening there; start it with \
|
||||
@@ -328,7 +345,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
};
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
solidcraft_mcp::Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
|
||||
@@ -393,10 +410,13 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let read_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
println!("{}", json!({"step": "read", "ms": read_ms, "bytes": bytes.len()}));
|
||||
let t = std::time::Instant::now();
|
||||
// The file as read, held until the timeline has evaluated it (as the import command does).
|
||||
let mut read = None;
|
||||
let doc = if step {
|
||||
let imported = solidcraft_engine::io::step_import_feature(&bytes, path).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::doc::Document::new(&imported.name);
|
||||
doc.add_feature(imported.kind, Some(&imported.name)).map_err(|e| e.to_string())?;
|
||||
read = Some(imported.import);
|
||||
doc
|
||||
} else {
|
||||
solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?
|
||||
@@ -408,12 +428,15 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::new(doc);
|
||||
drop(read);
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0}));
|
||||
for r in &s.model.results {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let st = s.world_state();
|
||||
// As the desktop app prepares a design it opened.
|
||||
st.warm_meshes(|| true);
|
||||
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
|
||||
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
|
||||
Ok(())
|
||||
|
||||
@@ -639,7 +639,14 @@ fn action_matrix_worker() {
|
||||
}
|
||||
let list = actions(&s);
|
||||
let edit = driving_parameter(&s);
|
||||
// Local runs only: `ACTION_MATRIX_ACTIONS=edge.fillet,edge.chamfer` tries just those actions
|
||||
// (don't bless such a run), `ACTION_MATRIX_VALIDITY=1` adds each passing action's body check.
|
||||
let only: Option<Vec<String>> = std::env::var("ACTION_MATRIX_ACTIONS").ok().map(|v| v.split(',').map(str::to_string).collect());
|
||||
let validity = std::env::var_os("ACTION_MATRIX_VALIDITY").is_some();
|
||||
for (i, a) in list.iter().enumerate().skip(from) {
|
||||
if only.as_ref().is_some_and(|o| !o.iter().any(|x| x == a.action)) {
|
||||
continue;
|
||||
}
|
||||
// Whole families of pieces are sampled together (see [`family`]).
|
||||
let k = key(&design, &family(&a.entity), a.action);
|
||||
if !stable_hash(&k).is_multiple_of(sample) {
|
||||
@@ -650,12 +657,37 @@ fn action_matrix_worker() {
|
||||
let mut scratch = s.scratch();
|
||||
let r = perform(&mut scratch, &a.act, &before);
|
||||
let mut line = outcome(&design, &a.entity, a.action, &r, t.elapsed().as_millis());
|
||||
if validity && r.is_ok() {
|
||||
let st = scratch.model.state();
|
||||
let bad: Vec<String> = st.bodies.iter().flat_map(|b| b.body.validity().into_iter().map(|v| format!("{}: {v}", b.name))).collect();
|
||||
line["validity"] = json!(bad);
|
||||
}
|
||||
// What was sent, for reproducing a failure (kept in the results, not the baseline).
|
||||
line["command"] = match &a.act {
|
||||
Act::Run(id, p) => json!({"command": id, "params": p}),
|
||||
Act::Project { plane, edge } => json!({"command": "sketch.project", "params": {"refs": [edge]}, "sketch_plane": plane}),
|
||||
};
|
||||
emit(line);
|
||||
// With ACTION_MATRIX_UNFOLD, sheet metal must still unfold and fold back after the action.
|
||||
if r.is_ok() && std::env::var_os("ACTION_MATRIX_UNFOLD").is_some() {
|
||||
let t = Instant::now();
|
||||
let sheets: Vec<String> = scratch.model.state().sheets.iter().map(|sh| sh.body.clone()).collect();
|
||||
let mut r = Ok(());
|
||||
for body in &sheets {
|
||||
for id in ["sheet.unfold", "sheet.refold"] {
|
||||
if r.is_ok() {
|
||||
r = scratch
|
||||
.execute(id, &json!({"body": body}))
|
||||
.map(|_| ())
|
||||
.map_err(|e| (command_kind(&e).to_string(), format!("{body}: {e}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sheets.is_empty() {
|
||||
let unfold: &'static str = Box::leak(format!("{}+unfold", a.action).into_boxed_str());
|
||||
emit(outcome(&design, &a.entity, unfold, &r, t.elapsed().as_millis()));
|
||||
}
|
||||
}
|
||||
// The edit round: a driving parameter changes; the new feature still resolves.
|
||||
if r.is_ok()
|
||||
&& let Some((name, expr)) = &edit
|
||||
@@ -1051,6 +1083,18 @@ fn a_through_hole_into_a_shelled_box_rim() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #559: a 6 mm hole drilled at an angle through a 920 mm prism, its through-all tool nearly
|
||||
/// 1.9 m long beside a thin bar it misses. The bar's cut came out empty: the tool filled too
|
||||
/// little of its own box for the sampled shortcut, and the boolean's tolerances (from the
|
||||
/// tool's length) were wider than the hole.
|
||||
#[test]
|
||||
fn an_oblique_through_hole_beside_a_thin_bar() {
|
||||
reproduces(
|
||||
Some("examples/regressions/oblique_cut_of_cylinder.json"),
|
||||
json!([{"command": "solid.hole", "params": {"diameter": 6.0, "position": [296.66666666666663, -458.66666666666663, 848.7633333333333]}}]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hole_through_a_sheet_metal_base() {
|
||||
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||
@@ -1061,6 +1105,29 @@ fn a_hole_through_a_sheet_metal_base() {
|
||||
reproduces(None, Value::Array(c));
|
||||
}
|
||||
|
||||
/// `face.hole` on a rim of the sheet metal corpus (#560): drilled along the panel, the hole is a
|
||||
/// bore of that panel, and the part still unfolds and folds back.
|
||||
#[test]
|
||||
fn a_hole_drilled_into_a_sheet_metal_rim() {
|
||||
// What the matrix sends: the face's point, the hole square into it.
|
||||
for (design, at, diameter) in [
|
||||
// The base's end rim of the L bracket, through to the other end.
|
||||
("typical/sheet-metal-bracket", [60.0, 7.777777777777779, 0.8333333333333333], 0.667),
|
||||
// The end rim of a tray's wall, beside the corner.
|
||||
("oracle/69-sm-tray", [99.58333333333333, 4.267766952966369, 15.0], 0.333),
|
||||
] {
|
||||
let Some((_, path)) = designs().into_iter().find(|(n, _)| n == design) else { panic!("no design {design}") };
|
||||
let mut s = load(&path).unwrap();
|
||||
let before = errors(&s);
|
||||
let start = s.doc.features.len();
|
||||
s.execute("solid.hole", &json!({"position": at, "diameter": diameter})).unwrap_or_else(|e| panic!("{design}: {e}"));
|
||||
check(&s, start, &before).unwrap_or_else(|(kind, e)| panic!("{design}: {kind}: {e}"));
|
||||
for id in ["sheet.unfold", "sheet.refold"] {
|
||||
s.execute(id, &json!({})).unwrap_or_else(|e| panic!("{design} {id}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketches_on_loft_and_sweep_faces() {
|
||||
let mut loft: Vec<Value> = rectangle(json!("XY"), [-20., -20.], [20., 20.]).into();
|
||||
|
||||
@@ -8,3 +8,9 @@ filehash = true
|
||||
[serve]
|
||||
addresses = ["127.0.0.1"]
|
||||
port = 8781
|
||||
|
||||
# Offline use and updates: release builds get sw.js listing this build's files (xtask/src/web_offline.rs).
|
||||
[[hooks]]
|
||||
stage = "post_build"
|
||||
command = "cargo"
|
||||
command_arguments = ["xtask", "web-offline"]
|
||||
@@ -20,17 +20,18 @@
|
||||
<meta name="twitter:image" content="https://bherbruck.github.io/solidcraft/og.png">
|
||||
<meta name="twitter:image:alt" content="SolidCraft: open-source parametric CAD built in Rust">
|
||||
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<link data-trunk rel="copy-file" href="fonts/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="fonts/OFL.txt" data-target-path="fonts" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
||||
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
</style>
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
|
||||
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="solidcraft_canvas"></canvas>
|
||||
<div id="solidcraft_loading">Loading SolidCraft…</div>
|
||||
<script>
|
||||
// Offline use and updates (sw.js; written into release builds by `cargo xtask web-offline`).
|
||||
if ("serviceWorker" in navigator) {
|
||||
addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -331,22 +331,34 @@ mod web {
|
||||
};
|
||||
if installed {
|
||||
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
|
||||
} else {
|
||||
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
|
||||
log::warn!("CJK font unavailable; showing English for this session");
|
||||
}
|
||||
self.font.finished(installed, &mut self.app.preferences.language);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
if self.font.tick(&mut self.app.preferences.language) {
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
|
||||
/// fetch the font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_font(&self, ctx: &egui::Context) {
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Shell {
|
||||
@@ -365,6 +377,7 @@ mod web {
|
||||
let ctx = ui.ctx().clone();
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
self.watch_text_for_font(&ctx);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
// Persist the user's own language, not a session-only English fallback.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// SolidCraft's service worker: the web app keeps working offline and updates itself.
|
||||
//
|
||||
// `cargo xtask web-offline` (Trunk's post_build hook) writes the build into the line below: the
|
||||
// version (workspace version + a hash of every built file) and the files to keep, each with its
|
||||
// SHA-256, and the same as `build.json` next to it. Files the app only fetches when it needs
|
||||
// them (the CJK font) aren't kept: they come from the network. Every download is checked
|
||||
// against its SHA-256: a deployment that lands mid-download (another build's `index.html`) fails
|
||||
// the download instead of mixing into the kept build. A new build changes this file, so the
|
||||
// browser installs the new worker, which keeps the whole new build before taking over.
|
||||
//
|
||||
// A launch only ever starts a build whose every file is kept, so a half-downloaded update can't
|
||||
// break it. It asks for `build.json` (waiting at most NAVIGATE_TIMEOUT_MS): offline, slow or
|
||||
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
|
||||
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
|
||||
// started. When the site has no `build.json` (the app was removed), launches go to the network,
|
||||
// the kept build answering when that fails. Each page then gets its files from the build it
|
||||
// started (remembered in PAGES), never from another build with a file of the same name.
|
||||
//
|
||||
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
|
||||
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
|
||||
// an open page uses the build it started. Then unfinished downloads are dropped, and so are
|
||||
// builds replaced by a newer worker (RETIRED). The origin is shared with other apps (github.io)
|
||||
// and other copies of this one (the release zip works from any path): only caches named after
|
||||
// this worker's path (CACHE_PREFIX…, PAGES) are ever touched.
|
||||
//
|
||||
// Unbuilt (VERSION "dev", as debug builds get it), the worker retires: it drops what a release
|
||||
// build kept and unregisters, so `trunk serve` after `trunk serve --release --no-autoreload`
|
||||
// shows the development build. Deploying this file unbuilt switches the worker off for everyone.
|
||||
/* @build */ const VERSION = "dev", FILES = [];
|
||||
const RETIRE = VERSION === "dev";
|
||||
|
||||
const PATH = new URL(self.registration.scope).pathname;
|
||||
const CACHE_PREFIX = `solidcraft-app ${PATH} `;
|
||||
const CACHE = CACHE_PREFIX + VERSION;
|
||||
/** Which build each open page started: page id → cache name (or NETWORK). */
|
||||
const PAGES = `solidcraft-app-pages ${PATH}`;
|
||||
/** A page that came from the network gets its files from there too. */
|
||||
const NETWORK = "network";
|
||||
const NAVIGATE_TIMEOUT_MS = 4000;
|
||||
const UPDATE_TIMEOUT_MS = 20000;
|
||||
/** The entry that marks a cache as holding its whole build. */
|
||||
const COMPLETE = "build-complete";
|
||||
/** The entry that marks a build replaced by a newer worker. */
|
||||
const RETIRED = "build-retired";
|
||||
/** A page remembered this recently may still be loading (not yet listed among the clients). */
|
||||
const PAGE_LOADING_MS = 30 * 1000;
|
||||
|
||||
const url = (file) => new URL(file, self.registration.scope).href;
|
||||
const has = (name, entry) => caches.match(url(entry), { cacheName: name }).then(Boolean);
|
||||
|
||||
/** Run `fn` while using the cache `name`: it isn't deleted meanwhile. */
|
||||
const using = (name, fn) => navigator.locks.request(name, { mode: "shared" }, fn);
|
||||
|
||||
/** Delete the cache `name` if nothing is using it and `unwanted()` (asked once no one can start to) agrees. */
|
||||
const drop = (name, unwanted) =>
|
||||
navigator.locks.request(name, { ifAvailable: true }, async (free) => free && (await unwanted()) && caches.delete(name));
|
||||
|
||||
const hex = (buffer) => Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
/**
|
||||
* Keep every file of a build (an earlier partial download is resumed); resolves to its cache's
|
||||
* name. The first file that fails cancels the others.
|
||||
*/
|
||||
function keep(version, files) {
|
||||
const name = CACHE_PREFIX + version;
|
||||
return using(name, async () => {
|
||||
const cache = await caches.open(name);
|
||||
await cache.delete(url(RETIRED)); // Back in use (a rollback).
|
||||
if (await has(name, COMPLETE)) return name;
|
||||
const cancel = new AbortController();
|
||||
const results = await Promise.allSettled(
|
||||
files.map(async ({ file, sha256 }) => {
|
||||
try {
|
||||
if (await cache.match(url(file))) return;
|
||||
// `no-cache`: revalidate with the server, so a stale HTTP-cached page never mixes builds.
|
||||
const response = await fetch(new Request(url(file), { cache: "no-cache" }), { signal: cancel.signal });
|
||||
if (!response.ok) throw new Error(`${file}: HTTP ${response.status}`);
|
||||
const body = await response.arrayBuffer();
|
||||
if (hex(await crypto.subtle.digest("SHA-256", body)) !== sha256) throw new Error(`${file}: not this build's`);
|
||||
await cache.put(url(file), new Response(body, { headers: response.headers }));
|
||||
} catch (error) {
|
||||
cancel.abort();
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const failed = results.find((r) => r.status === "rejected");
|
||||
if (failed) throw failed.reason;
|
||||
await cache.put(url(COMPLETE), new Response(version));
|
||||
return name;
|
||||
});
|
||||
}
|
||||
|
||||
const pageEntry = (id) => url(`~page/${encodeURIComponent(id)}`);
|
||||
|
||||
/** Remember which build a page started. */
|
||||
async function started(id, build) {
|
||||
if (!id) return;
|
||||
const record = JSON.stringify({ build, at: Date.now() });
|
||||
await (await caches.open(PAGES)).put(pageEntry(id), new Response(record));
|
||||
}
|
||||
|
||||
/** The build a page started; a page started before it was remembered gets this worker's. */
|
||||
async function startedBy(id) {
|
||||
const response = id ? await caches.match(pageEntry(id), { cacheName: PAGES }) : null;
|
||||
return response ? (await response.json()).build : CACHE;
|
||||
}
|
||||
|
||||
/** The builds open pages use (pages closed since are forgotten). */
|
||||
async function inUse() {
|
||||
const open = new Set((await self.clients.matchAll({ includeUncontrolled: true })).map((c) => c.id));
|
||||
const pages = await caches.open(PAGES);
|
||||
const builds = new Set();
|
||||
for (const request of await pages.keys()) {
|
||||
const id = decodeURIComponent(request.url.split("~page/")[1] ?? "");
|
||||
const record = await pages.match(request).then((r) => r?.json()).catch(() => null);
|
||||
if (record && (open.has(id) || Date.now() - record.at < PAGE_LOADING_MS)) builds.add(record.build);
|
||||
else await pages.delete(request);
|
||||
}
|
||||
return builds;
|
||||
}
|
||||
|
||||
/** Drop unfinished downloads nothing is using, and replaced builds no open page uses. */
|
||||
async function sweep() {
|
||||
for (const name of await caches.keys()) {
|
||||
if (!name.startsWith(CACHE_PREFIX) || name === CACHE) continue;
|
||||
await drop(name, async () => !(await has(name, COMPLETE)) || ((await has(name, RETIRED)) && !(await inUse()).has(name)));
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
// Clean up first: a page open for days sees updates (and failed ones) without any launch.
|
||||
const install = () => sweep().catch(() => {}).then(() => keep(VERSION, FILES));
|
||||
event.waitUntil(RETIRE ? self.skipWaiting() : install().then(() => self.skipWaiting()));
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
if (RETIRE) {
|
||||
for (const name of await caches.keys()) if (name.startsWith(CACHE_PREFIX) || name === PAGES) await caches.delete(name);
|
||||
return self.registration.unregister();
|
||||
}
|
||||
for (const name of await caches.keys()) {
|
||||
if (name.startsWith(CACHE_PREFIX) && name !== CACHE) await (await caches.open(name)).put(url(RETIRED), new Response(""));
|
||||
}
|
||||
await sweep();
|
||||
await self.clients.claim();
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
/** The promise's value, or null when it fails or takes longer than `ms`. */
|
||||
function within(ms, promise) {
|
||||
return Promise.race([promise.catch(() => null), new Promise((resolve) => setTimeout(resolve, ms, null))]);
|
||||
}
|
||||
|
||||
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
|
||||
const GONE = "gone";
|
||||
|
||||
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
|
||||
async function deployed() {
|
||||
const response = await fetch(url("build.json"), { cache: "no-cache" });
|
||||
if (response.status === 404 || response.status === 410) return GONE;
|
||||
const build = response.ok ? await response.json() : null;
|
||||
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
|
||||
return valid && build.files.every((f) => typeof f.file === "string" && typeof f.sha256 === "string") ? build : null;
|
||||
}
|
||||
|
||||
/** Keep the worker alive for `promise` (allowed while the launch is being answered). */
|
||||
function extend(event, promise) {
|
||||
try {
|
||||
event.waitUntil(promise.catch(() => {}));
|
||||
} catch {
|
||||
// The event has ended; the work goes on while the worker runs.
|
||||
}
|
||||
}
|
||||
|
||||
/** A launch: the start page of a complete build (whatever query the URL has: `?sample`), and that build. */
|
||||
async function launch(event) {
|
||||
const kept = await caches.match(url("index.html"), { cacheName: CACHE });
|
||||
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
|
||||
extend(event, sweep());
|
||||
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
|
||||
if (build === GONE) {
|
||||
const page = await fetch(event.request).catch(() => null);
|
||||
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
|
||||
}
|
||||
if (!build || build.version === VERSION) return [kept, CACHE];
|
||||
const download = keep(build.version, build.files);
|
||||
extend(event, download);
|
||||
const name = await within(UPDATE_TIMEOUT_MS, download);
|
||||
const page = name && (await caches.match(url("index.html"), { cacheName: name }));
|
||||
return page ? [page, name] : [kept, CACHE];
|
||||
}
|
||||
|
||||
/**
|
||||
* A launch that fails unexpectedly still starts the kept build (or asks the network). A newer
|
||||
* worker only takes over once the launch has ended, by when its page is remembered (in use).
|
||||
*/
|
||||
async function launchOrKept(event) {
|
||||
let page, build;
|
||||
try {
|
||||
[page, build] = await launch(event);
|
||||
} catch {
|
||||
page = await caches.match(url("index.html"), { cacheName: CACHE }).catch(() => null);
|
||||
build = page ? CACHE : NETWORK;
|
||||
page = page || (await fetch(event.request));
|
||||
}
|
||||
await started(event.resultingClientId, build).catch(() => {});
|
||||
return page;
|
||||
}
|
||||
|
||||
/** A page's files come from the build it started, else the network. */
|
||||
async function asset(event) {
|
||||
const build = await startedBy(event.clientId).catch(() => CACHE);
|
||||
const kept = build === NETWORK ? null : await caches.match(event.request, { cacheName: build, ignoreSearch: true });
|
||||
return kept || fetch(event.request);
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const request = event.request;
|
||||
if (RETIRE || request.method !== "GET" || new URL(request.url).origin !== self.location.origin) return;
|
||||
const path = request.url.split(/[?#]/)[0];
|
||||
const start = request.mode === "navigate" && (path === url("") || path === url("index.html"));
|
||||
event.respondWith(start ? launchOrKept(event) : asset(event));
|
||||
});
|
||||
@@ -18,6 +18,7 @@ egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
getrandom = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
quick-xml = { workspace = true }
|
||||
|
||||
@@ -1,61 +1,17 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#[path = "../app_icon.rs"]
|
||||
mod app_icon;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed=../app_icon.rs");
|
||||
let icon = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons/app/windows/exports/solidcraft-executable.ico");
|
||||
println!("cargo:rerun-if-changed={}", icon.display());
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
|
||||
return Ok(());
|
||||
}
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or("missing OUT_DIR")?);
|
||||
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
|
||||
let mut images = Vec::new();
|
||||
for size in sizes {
|
||||
let rgba = app_icon::rgba(size)?;
|
||||
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
|
||||
let mask_stride = size.div_ceil(32) * 4;
|
||||
let mut dib = Vec::new();
|
||||
dib.extend_from_slice(&40u32.to_le_bytes());
|
||||
dib.extend_from_slice(&(size as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&1u16.to_le_bytes());
|
||||
dib.extend_from_slice(&32u16.to_le_bytes());
|
||||
dib.extend_from_slice(&[0; 24]);
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
for p in row.as_chunks::<4>().0.iter() {
|
||||
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||||
}
|
||||
}
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
let mut mask = vec![0u8; mask_stride];
|
||||
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
if p[3] == 0 {
|
||||
mask[x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
dib.extend_from_slice(&mask);
|
||||
}
|
||||
images.push((size, dib));
|
||||
}
|
||||
let mut ico = vec![0, 0, 1, 0];
|
||||
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
|
||||
let mut offset = 6 + 16 * images.len();
|
||||
for (size, data) in &images {
|
||||
let dim = if *size == 256 { 0 } else { *size as u8 };
|
||||
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
|
||||
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&(offset as u32).to_le_bytes());
|
||||
offset += data.len();
|
||||
}
|
||||
for (_, data) in images {
|
||||
ico.extend_from_slice(&data);
|
||||
}
|
||||
let icon = out.join("solidcraft.ico");
|
||||
std::fs::write(&icon, ico)?;
|
||||
let copy = out.join("solidcraft.ico");
|
||||
std::fs::copy(icon, ©)?;
|
||||
let rc = out.join("solidcraft.rc");
|
||||
std::fs::write(&rc, format!("1 ICON \"{}\"\n", icon.display().to_string().replace('\\', "/")))?;
|
||||
// Missing resource tools are a build error, rather than silently shipping a generic icon.
|
||||
std::fs::write(&rc, format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/")))?;
|
||||
embed_resource::compile_for(&rc, ["solidcraft"], embed_resource::NONE).manifest_required()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,18 +9,39 @@ use std::time::Duration;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::MAX_LINE;
|
||||
use solidcraft_engine::control_auth;
|
||||
use solidcraft_ui_egui::ControlRequest;
|
||||
|
||||
/// Connections served at once. The channel has no authentication beyond being bound to
|
||||
/// loopback, so a flood must not be able to spawn threads without end.
|
||||
/// Connections served at once, so a flood must not be able to spawn threads without end.
|
||||
const MAX_CONNECTIONS: usize = 16;
|
||||
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
|
||||
const MAX_BAD_LINES: usize = 8;
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token file this launch wrote, removed when the app ends.
|
||||
static TOKEN_FILE: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||
|
||||
/// A new random token: 32 bytes from the operating system, as hex.
|
||||
fn new_token() -> Option<String> {
|
||||
let mut bytes = [0u8; 32];
|
||||
getrandom::fill(&mut bytes).ok()?;
|
||||
Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
|
||||
}
|
||||
|
||||
/// Remove this launch's token file (at exit).
|
||||
pub fn remove_token_file() {
|
||||
if let Some(p) = TOKEN_FILE.get() {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// Every connection must show the token before anything runs (docs/control-protocol.md).
|
||||
let Some(token) = control_auth::env_token().or_else(new_token) else {
|
||||
eprintln!("solidcraft: control server not started: no random token available");
|
||||
return rx;
|
||||
};
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
@@ -28,7 +49,23 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
return rx;
|
||||
}
|
||||
};
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}");
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
let _ = TOKEN_FILE.set(path);
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
|
||||
clients need SOLIDCRAFT_CONTROL_TOKEN",
|
||||
path.display()
|
||||
),
|
||||
},
|
||||
None => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
|
||||
}
|
||||
}
|
||||
let token = std::sync::Arc::new(token);
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||
@@ -39,8 +76,9 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
}
|
||||
let tx = tx.clone();
|
||||
let ctx = ctx.clone();
|
||||
let token = token.clone();
|
||||
std::thread::spawn(move || {
|
||||
serve(stream, tx, ctx);
|
||||
serve(stream, &token, tx, ctx);
|
||||
OPEN.fetch_sub(1, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
@@ -80,11 +118,28 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
/// The reply to a connection that didn't show the token; the connection is then closed.
|
||||
fn unauthorized() -> Value {
|
||||
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
|
||||
}
|
||||
|
||||
/// Refuse the connection: the reply, then the end of what we send, then what the client had
|
||||
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
use std::io::Read;
|
||||
let _ = writeln!(out, "{}", unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
let mut out = stream;
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
loop {
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
@@ -99,20 +154,36 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
}
|
||||
let reply = match serde_json::from_str::<Value>(&line) {
|
||||
Ok(msg) => {
|
||||
// The first request must carry the token; later ones on the same connection may.
|
||||
if !authenticated {
|
||||
match msg.get("token").and_then(Value::as_str) {
|
||||
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(json!({}));
|
||||
// A batch replaces many short requests: the whole of it may take long (#743).
|
||||
let wait = Duration::from_secs(if method == "engine.batch" { 3600 } else { 60 });
|
||||
let (req, rrx) = ControlRequest::new(method, params);
|
||||
if tx.send(req).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||
let mut r = rrx.recv_timeout(wait).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("id".into(), id);
|
||||
}
|
||||
r
|
||||
}
|
||||
Err(_) if !authenticated => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
bad += 1;
|
||||
json!({"ok": false, "error": format!("bad JSON: {e}")})
|
||||
@@ -131,6 +202,62 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
|
||||
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
|
||||
/// need not be repeated on that connection.
|
||||
#[test]
|
||||
fn requests_run_only_after_the_token() {
|
||||
use std::io::Read;
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
let ran = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let seen = ran.clone();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
seen.lock().unwrap().push(req.method.clone());
|
||||
let _ = req.reply.send(json!({"ok": true, "result": null}));
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve(stream, token, tx, egui::Context::default()));
|
||||
}
|
||||
});
|
||||
let session = |lines: &[String]| {
|
||||
let mut s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
for l in lines {
|
||||
let _ = writeln!(s, "{l}");
|
||||
}
|
||||
let _ = s.shutdown(std::net::Shutdown::Write);
|
||||
let mut text = String::new();
|
||||
let _ = s.read_to_string(&mut text);
|
||||
text.lines().map(|l| serde_json::from_str::<Value>(l).unwrap()).collect::<Vec<_>>()
|
||||
};
|
||||
for first in [
|
||||
json!({"id": 1, "method": "app.quit", "params": {"force": true}}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": "0123456789abcdef0123456789abcdee"}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": "0123"}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": 7}).to_string(),
|
||||
"not json".to_string(),
|
||||
] {
|
||||
let replies = session(&[first.clone(), json!({"id": 2, "method": "app.quit", "token": token}).to_string()]);
|
||||
assert_eq!(replies.len(), 1, "{first}: closed after refusing: {replies:?}");
|
||||
assert!(replies[0]["error"].as_str().is_some_and(|e| e.starts_with("unauthorized")), "{first}: {replies:?}");
|
||||
}
|
||||
assert!(ran.lock().unwrap().is_empty(), "nothing ran without the token");
|
||||
let replies = session(&[
|
||||
json!({"id": 1, "method": "ui.inspect", "token": token}).to_string(),
|
||||
json!({"id": 2, "method": "engine.commands"}).to_string(),
|
||||
]);
|
||||
assert_eq!(replies.len(), 2, "{replies:?}");
|
||||
assert!(replies.iter().all(|r| r["ok"] == true), "{replies:?}");
|
||||
assert_eq!(*ran.lock().unwrap(), ["ui.inspect", "engine.commands"]);
|
||||
}
|
||||
|
||||
/// A request longer than the cap is refused instead of allocated.
|
||||
#[test]
|
||||
fn lines_past_the_cap_are_refused() {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[path = "../../app_icon.rs"]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
mod control_server;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
@@ -286,7 +286,7 @@ fn main() -> eframe::Result {
|
||||
b.with_x11();
|
||||
}));
|
||||
}
|
||||
eframe::run_native(
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
@@ -344,7 +344,9 @@ fn main() -> eframe::Result {
|
||||
space_mouse,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
result
|
||||
}
|
||||
|
||||
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
|
||||
|
||||
@@ -1,41 +1,98 @@
|
||||
# SolidCraft macOS icon
|
||||
# Application resources
|
||||
|
||||
`SolidCraft.icon/` is the approved Icon Composer source, containing separate
|
||||
grid and solid SVG layers and the owner's appearance settings. The saturated
|
||||
blue solid has transparent geometry cutouts instead of painted edge lines.
|
||||
The solid and grid are both scaled to 90%, with separate Default and Dark grid artwork.
|
||||
Icon Composer supplies the system backdrop and Mono material effects.
|
||||
Artwork is original and attributed in `ATTRIBUTION.md`.
|
||||
`assets/` is the design resource home for SolidCraft.
|
||||
[ATTRIBUTION.md](../ATTRIBUTION.md) is the file inventory and provenance record.
|
||||
Keep one editable master per resource and place shipped derivatives in `exports/`.
|
||||
|
||||
`Assets.car` is compiled by Apple's `actool` and supplies the layered icon on
|
||||
macOS 26 and later. It is checked in so the existing release workflow can still
|
||||
package on older Xcode runners. After editing and saving `SolidCraft.icon`,
|
||||
regenerate the catalog with Xcode 26 or later from the repository root:
|
||||
|
||||
```sh
|
||||
mkdir -p target/macos-icon
|
||||
xcrun actool assets/SolidCraft.icon \
|
||||
--compile target/macos-icon --platform macosx \
|
||||
--minimum-deployment-target 11.0 --app-icon SolidCraft \
|
||||
--output-partial-info-plist target/macos-icon/icon-info.plist \
|
||||
--output-format human-readable-text
|
||||
cp target/macos-icon/Assets.car assets/Assets.car
|
||||
cp target/macos-icon/SolidCraft.icns assets/icon.icns
|
||||
```text
|
||||
assets/
|
||||
icons/
|
||||
app/
|
||||
shared/source/ # original Rust mark and executable ICO encoder
|
||||
macos/source/SolidCraft.icon/ # approved Composer project and linked SVGs
|
||||
macos/exports/ # paired Assets.car and icon.icns
|
||||
windows/exports/ # executable DIB ICO and installer PNG ICO
|
||||
linux/exports/hicolor/ # Linux/FreeBSD package images, 16–512 px
|
||||
ui/source/ # original drawing library and other UI glyphs
|
||||
branding/web/source/og.png # shared social preview card
|
||||
styles/ui/palettes.rs # light/dark defaults
|
||||
styles/web/loading.css # web loading/canvas styles
|
||||
appearances/library.rs # built-in visual presets
|
||||
materials/library.rs # physical defaults and colours
|
||||
shaders/viewport.wgsl # original viewport shader
|
||||
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
|
||||
locales/{en,de,ja,ko}.json # shared offline catalogs
|
||||
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
|
||||
archive/icons/macos-v1/
|
||||
source/icon.vectorcraft # earlier editable work, retained
|
||||
exports/ # all historical SVG, PNG and iconset exports
|
||||
```
|
||||
|
||||
`icon.icns` is the older-macOS fallback generated by the same Apple compiler
|
||||
from the approved composition, retaining its layer sizes and positions.
|
||||
Regenerate both compiled resources together with the commands above.
|
||||
`icon.vectorcraft`, `icon.svg`, `icon.png` and `icon.iconset/` retain the earlier
|
||||
VectorCraft artwork; packaging uses the Icon Composer compilation instead.
|
||||
## Editing and regeneration
|
||||
|
||||
Only `packaging/macos/package.sh` consumes these resources. Its existing signed
|
||||
app/DMG workflow copies the catalog and ICNS into `Contents/Resources`, and
|
||||
`Info.plist` references `SolidCraft` through `CFBundleIconName` and
|
||||
`CFBundleIconFile`. The macOS viewport opts out of eframe's runtime bitmap so
|
||||
Finder, the Dock and the application switcher use the bundle icon. Other
|
||||
platforms retain the existing code-drawn icon and packaging.
|
||||
[UI artwork](icons/ui/README.md) identifies the drawing masters and their consumers.
|
||||
Edit geometry and IDs together in the original Rust source and review changes in
|
||||
both application themes. Palettes and model presets are also editable Rust data.
|
||||
|
||||
Design references: [Apple app-icon HIG](https://developer.apple.com/design/human-interface-guidelines/app-icons)
|
||||
and [Icon Composer documentation](https://developer.apple.com/documentation/xcode/creating-your-app-icon-using-icon-composer).
|
||||
No Apple artwork is shipped.
|
||||
The current macOS master is `icons/app/macos/source/SolidCraft.icon/`, including
|
||||
its linked SVG layers and appearance settings. The earlier VectorCraft document
|
||||
is preserved in the archive. No other native design projects were available for
|
||||
the current UI set or preview card. Keep any future `.vectorcraft`, `.ai` or other
|
||||
native work and linked inputs under that resource's `source/work/`, with an
|
||||
attribution row; retain the editable document alongside its exports.
|
||||
|
||||
Regenerate all exports of the shared Windows/Linux/FreeBSD mark together:
|
||||
|
||||
```sh
|
||||
cargo xtask app-icons
|
||||
```
|
||||
|
||||
For macOS, use Xcode with Icon Composer support:
|
||||
|
||||
```sh
|
||||
mkdir -p target/resources/macos-icon
|
||||
xcrun actool assets/icons/app/macos/source/SolidCraft.icon \
|
||||
--compile target/resources/macos-icon --platform macosx \
|
||||
--minimum-deployment-target 11.0 --app-icon SolidCraft \
|
||||
--output-partial-info-plist target/resources/macos-icon/icon-info.plist \
|
||||
--output-format human-readable-text
|
||||
cp target/resources/macos-icon/Assets.car assets/icons/app/macos/exports/Assets.car
|
||||
cp target/resources/macos-icon/SolidCraft.icns assets/icons/app/macos/exports/icon.icns
|
||||
```
|
||||
|
||||
Review and commit source changes and required exports together. Temporary renders,
|
||||
tool caches and package staging belong under ignored `target/` or `dist/`.
|
||||
Normal builds use the committed exports and require no design applications.
|
||||
The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
|
||||
|
||||
## Platform bindings
|
||||
|
||||
| Platform | Resources and output paths |
|
||||
| --- | --- |
|
||||
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
|
||||
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
|
||||
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
|
||||
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
|
||||
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
|
||||
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
|
||||
| Web | HTML/Trunk stays in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/` and uses the collected CSS; no favicon/PWA icon currently exists |
|
||||
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
|
||||
|
||||
## Code and provenance
|
||||
|
||||
Resources remain compiled by their owning layer through existing APIs:
|
||||
`ui-egui::icons::paint` and `Tokens` at L5, model presets in L2 `doc`, and sketch
|
||||
fonts at L1. State, projection, interaction, commands and GPU bindings stay in
|
||||
their current modules. Embedded data uses `CARGO_MANIFEST_DIR`; application code
|
||||
needs no runtime resource registry or repository lookup.
|
||||
|
||||
`cargo xtask assets` requires an exact attribution row for every collected file
|
||||
except Markdown documentation, and continues checking file assets elsewhere in
|
||||
the repository. This includes native work, Rust masters, catalogs, shaders and
|
||||
platform templates. Packaging selects explicit current files and fails when
|
||||
required inputs are absent. Archived artwork has no production consumers.
|
||||
|
||||
Dependency-owned egui fonts and vendored renderer shaders stay with their
|
||||
libraries; their licences remain in `THIRD-PARTY-LICENSES.txt` and `ATTRIBUTION.md`.
|
||||
System fonts, optional packs, user fonts/canvases/decals/materials and native OS
|
||||
controls retain external ownership. Root licence/notice texts stay canonical.
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Built-in model appearances: names, sRGB colours and metalness.
|
||||
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
|
||||
("Aluminum - Satin", [204, 208, 214], 1.0),
|
||||
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
|
||||
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
|
||||
("Steel - Satin", [158, 164, 172], 1.0),
|
||||
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
|
||||
("Cast Iron", [104, 104, 110], 1.0),
|
||||
("Brass - Polished", [208, 172, 88], 1.0),
|
||||
("Copper", [204, 122, 80], 1.0),
|
||||
("Titanium", [150, 148, 158], 1.0),
|
||||
("ABS - White", [236, 236, 230], 1.0),
|
||||
("ABS - Black", [36, 36, 38], 1.0),
|
||||
("ABS - Red", [196, 40, 40], 1.0),
|
||||
("ABS - Blue", [40, 90, 186], 1.0),
|
||||
("ABS - Yellow", [236, 196, 40], 1.0),
|
||||
("ABS - Green", [52, 150, 80], 1.0),
|
||||
("Rubber - Black", [28, 28, 28], 1.0),
|
||||
("Glass - Clear", [190, 220, 228], 0.25),
|
||||
("Glass - Smoked", [90, 96, 104], 0.45),
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,48 @@
|
||||
//! Original executable ICO encoder: DIB images and transparency masks.
|
||||
use super::app_icon;
|
||||
|
||||
pub fn encode() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
|
||||
let mut images = Vec::new();
|
||||
for size in sizes {
|
||||
let rgba = app_icon::rgba(size)?;
|
||||
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
|
||||
let mask_stride = size.div_ceil(32) * 4;
|
||||
let mut dib = Vec::new();
|
||||
dib.extend_from_slice(&40u32.to_le_bytes());
|
||||
dib.extend_from_slice(&(size as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&1u16.to_le_bytes());
|
||||
dib.extend_from_slice(&32u16.to_le_bytes());
|
||||
dib.extend_from_slice(&[0; 24]);
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
for p in row.as_chunks::<4>().0.iter() {
|
||||
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||||
}
|
||||
}
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
let mut mask = vec![0u8; mask_stride];
|
||||
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
if p[3] == 0 {
|
||||
mask[x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
dib.extend_from_slice(&mask);
|
||||
}
|
||||
images.push((size, dib));
|
||||
}
|
||||
let mut ico = vec![0, 0, 1, 0];
|
||||
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
|
||||
let mut offset = 6 + 16 * images.len();
|
||||
for (size, data) in &images {
|
||||
let dim = if *size == 256 { 0 } else { *size as u8 };
|
||||
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
|
||||
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&(offset as u32).to_le_bytes());
|
||||
offset += data.len();
|
||||
}
|
||||
for (_, data) in images {
|
||||
ico.extend_from_slice(&data);
|
||||
}
|
||||
Ok(ico)
|
||||
}
|
||||
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,22 @@
|
||||
# UI artwork
|
||||
|
||||
Edit the Rust drawing masters in `source/`; the UI crate compiles them through
|
||||
`crates/ui-egui/src/icons.rs`. Use the 24 × 24 grid and semantic ink/fill/accent
|
||||
colours. Icon IDs and their geometry live together in `source/icons.rs`;
|
||||
unknown IDs retain the existing fallback. Add new artwork there without
|
||||
maintaining a separate ID registry.
|
||||
|
||||
| Source | Artwork | Consumers |
|
||||
| --- | --- | --- |
|
||||
| `icons.rs` | Toolbar, browser, menu and constraint icons | `icons::paint` |
|
||||
| `glyphs.rs` | Brand mark, captions, transport, badges, pin, caret, disclosure, radial centre and removal cross | toolbar, titlebar, timeline, browser, menus |
|
||||
| `navigation.rs` | Signed axes, cube faces/labels and arrows | viewport/navigation cube |
|
||||
| `markers.rs` | Constraint mappings/letters, snap markers and dimension arrowheads | sketch, assembly and dimensions |
|
||||
| `pointer.rs` | Agent pointer and tag | agent cursor |
|
||||
|
||||
Widget state, layout, projection, picking and commands stay with their widgets.
|
||||
Review drawing changes in the actual application in both light and dark themes.
|
||||
No separate native design projects were available for this set. Preserve any
|
||||
future native work and linked inputs under `source/work/`, with attribution.
|
||||
Original artwork is MIT OR Apache-2.0; caption geometry retains VectorCraft
|
||||
attribution in the root `ATTRIBUTION.md`.
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Collected glyph artwork. Caption geometry adapted from VectorCraft (MIT OR Apache-2.0).
|
||||
use crate::titlebar::Caption;
|
||||
use egui::{Color32, Pos2, Rect, Stroke, Ui, pos2, vec2};
|
||||
|
||||
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
let tri = |dir: f32, dx: f32| {
|
||||
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
|
||||
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
|
||||
};
|
||||
let bar = |dx: f32| p.line_segment([pos2(c.x + dx, c.y - 5.0), pos2(c.x + dx, c.y + 5.0)], Stroke::new(1.6, col));
|
||||
match what {
|
||||
"first" => {
|
||||
bar(-5.0);
|
||||
tri(-1.0, 1.0);
|
||||
}
|
||||
"back" => tri(-1.0, 0.0),
|
||||
"forward" => tri(1.0, 0.0),
|
||||
_ => {
|
||||
tri(1.0, -1.0);
|
||||
bar(5.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
|
||||
let p = ui.painter();
|
||||
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
|
||||
match c {
|
||||
Caption::Minimize => {
|
||||
p.line_segment([g.left_center(), g.right_center()], s);
|
||||
}
|
||||
// Restore: a front square with the corner of the one behind it.
|
||||
Caption::Maximize if maximized => {
|
||||
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
|
||||
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
|
||||
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
|
||||
}
|
||||
Caption::Maximize => {
|
||||
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
|
||||
}
|
||||
Caption::Close => {
|
||||
p.line_segment([g.left_top(), g.right_bottom()], s);
|
||||
p.line_segment([g.right_top(), g.left_bottom()], s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn badge(p: &egui::Painter, r: Rect, name: &str, c: Color32) {
|
||||
let s = Stroke::new(1.3, c);
|
||||
match name {
|
||||
"lock" => {
|
||||
let body = Rect::from_min_max(pos2(r.left() + 1.5, r.center().y - 0.5), pos2(r.right() - 1.5, r.bottom() - 0.5));
|
||||
p.rect_filled(body, 1.5, c);
|
||||
let top = pos2(r.center().x, r.top() + 4.0);
|
||||
p.add(egui::Shape::line(
|
||||
(0..=8)
|
||||
.map(|k| {
|
||||
let a = std::f32::consts::PI * (k as f32 / 8.0);
|
||||
pos2(top.x - 3.2 * a.cos(), top.y - 3.2 * a.sin() + 1.0)
|
||||
})
|
||||
.chain([pos2(top.x + 3.2, body.top())])
|
||||
.collect(),
|
||||
s,
|
||||
));
|
||||
p.line_segment([pos2(top.x - 3.2, top.y + 1.0), pos2(top.x - 3.2, body.top())], s);
|
||||
}
|
||||
"pin" => {
|
||||
let head = pos2(r.center().x + 2.0, r.top() + 4.0);
|
||||
p.circle_filled(head, 3.2, c);
|
||||
p.line_segment([head, pos2(r.left() + 1.5, r.bottom() - 1.0)], Stroke::new(1.6, c));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
|
||||
let c = r.center();
|
||||
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
|
||||
// Local coordinates: the pin points down, its head on top.
|
||||
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
|
||||
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
|
||||
let stroke = Stroke::new(1.4, col);
|
||||
if on {
|
||||
p.add(egui::Shape::convex_polygon(head, col, stroke));
|
||||
} else {
|
||||
p.add(egui::Shape::closed_line(head, stroke));
|
||||
}
|
||||
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
|
||||
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
|
||||
}
|
||||
|
||||
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
|
||||
}
|
||||
|
||||
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
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() };
|
||||
// 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,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn cross(p: &egui::Painter, r: Rect, s: Stroke) {
|
||||
p.line_segment([r.left_top(), r.right_bottom()], s);
|
||||
p.line_segment([r.right_top(), r.left_bottom()], s);
|
||||
}
|
||||
pub(crate) fn radial_centre(p: &egui::Painter, c: Pos2, direction: Option<(egui::Vec2, bool)>, t: crate::theme::Tokens) {
|
||||
p.circle(c, 15.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, t.border));
|
||||
if let Some((d, enabled)) = direction {
|
||||
p.line_segment([c + d * 6.0, c + d * 15.0], Stroke::new(3.0, if enabled { t.accent } else { t.border }));
|
||||
} else {
|
||||
p.circle_filled(c, 3.0, t.text_dim);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn disclosure(p: &egui::Painter, ar: Rect, open: bool, ink: Color32) {
|
||||
let pts = if open {
|
||||
vec![pos2(ar.left() + 2.0, ar.top() + 4.0), pos2(ar.right() - 2.0, ar.top() + 4.0), pos2(ar.center().x, ar.bottom() - 3.0)]
|
||||
} else {
|
||||
vec![pos2(ar.left() + 4.0, ar.top() + 2.0), pos2(ar.right() - 3.0, ar.center().y), pos2(ar.left() + 4.0, ar.bottom() - 2.0)]
|
||||
};
|
||||
p.add(egui::Shape::convex_polygon(pts, ink, Stroke::NONE));
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Constraint, assembly snap and dimension marker artwork.
|
||||
use crate::dialogs_assembly::SnapKind;
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
|
||||
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
|
||||
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
|
||||
let halo = Stroke::new(4.5, Color32::from_black_alpha(150));
|
||||
let white = Stroke::new(1.5, Color32::WHITE);
|
||||
if let Some(tip) = tip {
|
||||
let d = (tip - c).normalized();
|
||||
let n = vec2(-d.y, d.x);
|
||||
let head = vec![tip + d * 2.0, tip - d * 9.0 + n * 5.0, tip - d * 9.0 - n * 5.0];
|
||||
painter.line_segment([c, tip], halo);
|
||||
painter.add(egui::Shape::convex_polygon(head.clone(), Color32::TRANSPARENT, halo));
|
||||
painter.line_segment([c, tip], Stroke::new(2.0, col));
|
||||
painter.add(egui::Shape::convex_polygon(head, col, Stroke::NONE));
|
||||
}
|
||||
let shape = |fill: Color32, stroke: Stroke, grow: f32| -> egui::Shape {
|
||||
match kind {
|
||||
SnapKind::FaceCenter => egui::Shape::rect_filled(egui::Rect::from_center_size(c, vec2(12.0 + grow, 12.0 + grow)), 1.5, fill),
|
||||
SnapKind::CircleCenter => egui::Shape::circle_filled(c, 7.0 + grow / 2.0, fill),
|
||||
SnapKind::EdgeMid => {
|
||||
let r = 8.0 + grow / 2.0;
|
||||
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r * 0.93, r * 0.7), c + vec2(-r * 0.93, r * 0.7)], fill, stroke)
|
||||
}
|
||||
SnapKind::Vertex => {
|
||||
let r = 7.5 + grow / 2.0;
|
||||
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)], fill, stroke)
|
||||
}
|
||||
}
|
||||
};
|
||||
painter.add(shape(Color32::from_black_alpha(150), Stroke::NONE, 4.0));
|
||||
painter.add(shape(col, Stroke::NONE, 0.0));
|
||||
match kind {
|
||||
SnapKind::FaceCenter => {
|
||||
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(12.0, 12.0)), 1.5, white, egui::StrokeKind::Middle);
|
||||
}
|
||||
SnapKind::CircleCenter => {
|
||||
painter.circle_stroke(c, 7.0, white);
|
||||
painter.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], Stroke::new(1.2, Color32::WHITE));
|
||||
painter.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], Stroke::new(1.2, Color32::WHITE));
|
||||
}
|
||||
SnapKind::EdgeMid | SnapKind::Vertex => {
|
||||
painter.add(shape(Color32::TRANSPARENT, white, 0.0));
|
||||
}
|
||||
}
|
||||
// What it snaps to (hovered), or which component it is (picked): a small label.
|
||||
if let Some(tag) = tag {
|
||||
let t = Tokens::get();
|
||||
let font = egui::FontId::proportional(11.5);
|
||||
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
|
||||
let at = c + vec2(12.0, 10.0);
|
||||
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
|
||||
painter.rect(r, 3.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, col), egui::StrokeKind::Inside);
|
||||
painter.galley(at, g, t.text);
|
||||
}
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
"Parallel" => "c_parallel",
|
||||
"Perpendicular" => "c_perpendicular",
|
||||
"Collinear" => "c_collinear",
|
||||
"Tangent" => "c_tangent",
|
||||
"Smooth" => "c_smooth",
|
||||
"Equal" => "c_equal",
|
||||
"Concentric" => "c_concentric",
|
||||
"MidPoint" => "c_midpoint",
|
||||
"Symmetry" => "c_symmetry",
|
||||
"Fix" => "c_fix",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
pub(crate) fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
pub(crate) fn dimension_arrow(painter: &egui::Painter, t: Pos2, back: Pos2, col: Color32) {
|
||||
let d = (t - back).normalized();
|
||||
let n = vec2(-d.y, d.x);
|
||||
painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE));
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Align2, Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
pub(crate) struct Facet {
|
||||
pub(crate) direction: Vec3,
|
||||
pub(crate) points: Vec<Pos2>,
|
||||
pub(crate) outer_edges: Vec<bool>,
|
||||
pub(crate) centre: Pos2,
|
||||
pub(crate) label: String,
|
||||
pub(crate) text_axes: Option<(egui::Vec2, egui::Vec2)>,
|
||||
pub(crate) depth: f64,
|
||||
}
|
||||
// Private marker for label meshes with atlas-pixel UVs. The output hook replaces
|
||||
// it with the font texture before any renderer sees the frame.
|
||||
pub(crate) const LABEL_PIXELS: egui::TextureId = egui::TextureId::User(u64::from_le_bytes(*b"cubetext"));
|
||||
|
||||
struct LabelAtlas;
|
||||
|
||||
impl egui::plugin::Plugin for LabelAtlas {
|
||||
fn debug_name(&self) -> &'static str {
|
||||
"navigation cube label atlas"
|
||||
}
|
||||
|
||||
fn output_hook(&mut self, ctx: &egui::Context, output: &mut egui::FullOutput) {
|
||||
let size = ctx.fonts(|f| f.font_image_size());
|
||||
let normalize = vec2(1.0 / size[0].max(1) as f32, 1.0 / size[1].max(1) as f32);
|
||||
for shape in &mut output.shapes {
|
||||
if let Shape::Mesh(mesh) = &mut shape.shape
|
||||
&& mesh.texture_id == LABEL_PIXELS
|
||||
{
|
||||
let mesh = std::sync::Arc::make_mut(mesh);
|
||||
mesh.texture_id = egui::TextureId::default();
|
||||
for v in &mut mesh.vertices {
|
||||
v.uv = (v.uv.to_vec2() * normalize).to_pos2();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warp the font mesh into the projected face, so labels turn with the model.
|
||||
pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, font_size: f32) {
|
||||
let Some((r, down)) = face.text_axes else { return };
|
||||
let opacity = ((face.depth - 0.18) / 0.17).clamp(0.0, 1.0) as f32;
|
||||
if opacity <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ink = ink.gamma_multiply(opacity);
|
||||
let galley = painter.layout_no_wrap(face.label.clone(), FontId::proportional(font_size), ink);
|
||||
let centre = galley.rect.center();
|
||||
// Keep atlas-pixel UVs until the frame is complete: another label or widget
|
||||
// can grow the atlas after this mesh is created, invalidating early UVs.
|
||||
painter.ctx().add_plugin(LabelAtlas);
|
||||
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
|
||||
let atlas = painter.ctx().fonts(|f| f.font_image_size());
|
||||
let primitives =
|
||||
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
|
||||
for primitive in primitives {
|
||||
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
|
||||
mesh.texture_id = LABEL_PIXELS;
|
||||
for v in &mut mesh.vertices {
|
||||
v.uv = pos2(v.uv.x * atlas[0] as f32, v.uv.y * atlas[1] as f32);
|
||||
let local = v.pos - centre;
|
||||
v.pos = face.centre + r * local.x + down * (local.y * 1.35);
|
||||
}
|
||||
painter.add(Shape::mesh(mesh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint_cube(painter: &egui::Painter, faces: &[Facet], hovered: Option<usize>, t: Tokens, labels: bool, scale: f32) {
|
||||
// A shared, opaque triangle mesh has no antialiasing fringes between facets.
|
||||
// Per-polygon fringes and miter joins overlap as a face becomes edge-on.
|
||||
let mut fill_mesh = egui::epaint::Mesh::default();
|
||||
let mut edges: Vec<(Pos2, Pos2, usize, bool)> = Vec::new();
|
||||
let hovered_direction = hovered.and_then(|i| faces.get(i)).map(|f| f.direction);
|
||||
for face in faces {
|
||||
let base = t.cube_face;
|
||||
let shade = (0.72 + 0.28 * face.depth.min(1.0)) as f32;
|
||||
let highlighted = hovered_direction == Some(face.direction);
|
||||
let fill = if highlighted {
|
||||
t.cube_hover
|
||||
} else {
|
||||
Color32::from_rgb((f32::from(base.r()) * shade) as u8, (f32::from(base.g()) * shade) as u8, (f32::from(base.b()) * shade) as u8)
|
||||
};
|
||||
let centre = fill_mesh.vertices.len() as u32;
|
||||
fill_mesh.colored_vertex(face.centre, fill);
|
||||
for &p in &face.points {
|
||||
fill_mesh.colored_vertex(p, fill);
|
||||
}
|
||||
for (j, (&a, &b)) in face.points.iter().zip(face.points.iter().cycle().skip(1)).take(face.points.len()).enumerate() {
|
||||
fill_mesh.add_triangle(centre, centre + 1 + j as u32, centre + 1 + ((j + 1) % face.points.len()) as u32);
|
||||
// Coplanar picking regions have no visible seams. Only actual cube
|
||||
// edges and the hovered target's boundary receive an outline.
|
||||
let outer = face.outer_edges.get(j).copied().unwrap_or(false);
|
||||
if !highlighted && !outer {
|
||||
continue;
|
||||
}
|
||||
if let Some(edge) = edges
|
||||
.iter_mut()
|
||||
.find(|(p, q, _, _)| (p.distance_sq(a) < 1e-6 && q.distance_sq(b) < 1e-6) || (p.distance_sq(b) < 1e-6 && q.distance_sq(a) < 1e-6))
|
||||
{
|
||||
edge.2 += usize::from(highlighted);
|
||||
edge.3 |= outer;
|
||||
} else {
|
||||
edges.push((a, b, usize::from(highlighted), outer));
|
||||
}
|
||||
}
|
||||
}
|
||||
painter.add(Shape::mesh(fill_mesh));
|
||||
for (a, b, highlights, outer) in edges {
|
||||
// Two highlighted coplanar patches share an internal subdivision, not
|
||||
// a target boundary. Actual cube edges remain visible across faces.
|
||||
if !outer && highlights != 1 {
|
||||
continue;
|
||||
}
|
||||
let highlighted = highlights > 0;
|
||||
painter.line_segment([a, b], Stroke::new(1.0, if highlighted { t.accent } else { t.cube_border }));
|
||||
}
|
||||
if labels {
|
||||
for face in faces {
|
||||
face_text(painter, face, t.cube_text, 14.0 * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn arrow(painter: &egui::Painter, at: Pos2, direction: egui::Vec2, hover: bool, t: Tokens) {
|
||||
let side = vec2(-direction.y, direction.x);
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![at + direction * 7.0, at - direction * 5.0 + side * 7.0, at - direction * 5.0 - side * 7.0],
|
||||
if hover { t.accent_soft } else { t.cube_face },
|
||||
Stroke::new(1.0, if hover { t.accent } else { t.cube_border }),
|
||||
));
|
||||
}
|
||||
pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, hover: bool, t: Tokens, scale: f32) {
|
||||
let (start, end) =
|
||||
if clockwise { (-std::f32::consts::FRAC_PI_4 + 0.12, 0.0) } else { (-std::f32::consts::FRAC_PI_4 - 0.12, -std::f32::consts::FRAC_PI_2) };
|
||||
let points: Vec<_> = (0..=16)
|
||||
.map(|i| {
|
||||
let angle = egui::lerp(start..=end, f32::from(i as u8) / 16.0);
|
||||
at + vec2(angle.cos(), angle.sin()) * (32.0 * scale)
|
||||
})
|
||||
.collect();
|
||||
painter.add(Shape::line(points.clone(), Stroke::new(5.0 * scale, if hover { t.accent } else { t.cube_border })));
|
||||
painter.add(Shape::line(points.clone(), Stroke::new(3.0 * scale, if hover { t.accent_soft } else { t.cube_face })));
|
||||
if let Some(tip) = points.last() {
|
||||
let sign = if clockwise { 1.0 } else { -1.0 };
|
||||
arrow(painter, *tip, vec2(-end.sin(), end.cos()) * (sign * scale), hover, t);
|
||||
}
|
||||
}
|
||||
pub(crate) const AXIS_RADIUS: f32 = 11.0;
|
||||
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
|
||||
let col = if d.x != 0.0 {
|
||||
solidcraft_engine::view::colors::AXIS_X
|
||||
} else if d.y != 0.0 {
|
||||
solidcraft_engine::view::colors::AXIS_Y
|
||||
} else {
|
||||
solidcraft_engine::view::colors::AXIS_Z
|
||||
};
|
||||
let col = Color32::from_rgb(col.0, col.1, col.2);
|
||||
let positive = !name.starts_with('-');
|
||||
let ink = if depth < -0.01 { col.gamma_multiply(0.55) } else { col };
|
||||
painter.line_segment([c, at], Stroke::new(if positive { 2.0 } else { 1.0 }, ink));
|
||||
painter.circle_filled(at, AXIS_RADIUS, if hovered { t.accent_soft } else { t.overlay });
|
||||
painter.circle_stroke(at, AXIS_RADIUS, Stroke::new(if hovered { 2.0 } else { 1.0 }, if hovered { t.accent } else { ink }));
|
||||
painter.text(at, Align2::CENTER_CENTER, name.to_uppercase(), FontId::proportional(11.0), ink);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Original agent pointer and label. Animation stays with the UI controller.
|
||||
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
|
||||
pub(crate) const AGENT: Color32 = Color32::from_rgb(168, 85, 247);
|
||||
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
|
||||
// An arrow pointer, tip at `p`.
|
||||
let pts: Vec<Pos2> =
|
||||
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
|
||||
painter.add(Shape::convex_polygon(pts.clone(), AGENT, Stroke::NONE));
|
||||
painter.add(Shape::closed_line(pts, Stroke::new(1.5, Color32::WHITE)));
|
||||
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(11.0), Color32::WHITE);
|
||||
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
|
||||
painter.rect_filled(r, 4.0, AGENT);
|
||||
painter.galley(r.min + vec2(5.0, 2.0), label, Color32::WHITE);
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
{
|
||||
"browser.body_name": "Körper {number}",
|
||||
"browser.component_name": "Komponente {number}",
|
||||
"browser.parameters": "Parameter ({count})",
|
||||
"browser.precision": "Genauigkeit: {precision}",
|
||||
"browser.sketch_name": "Skizze {number}",
|
||||
"browser.units": "Einheiten: {units}",
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.select": "↖ Auswählen",
|
||||
"dialog.selected_count": "↖ {count} ausgewählt",
|
||||
"dialog.step": "Schritt",
|
||||
"dialog.selection_step": "{step} von {total}: {input} auswählen",
|
||||
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
|
||||
"document.save_changes": "Änderungen in {name} speichern?",
|
||||
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
|
||||
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
|
||||
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
|
||||
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
|
||||
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
|
||||
"settings.reset_title": "Einstellungen wurden geändert",
|
||||
"sketch.degrees_of_freedom": "{count} Freiheitsgrade",
|
||||
"sketch.draw_curves": "Als {linetype} zeichnen",
|
||||
"sketch.make_selection": "Auswahl als {linetype} markieren",
|
||||
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
|
||||
"ui.3d_sketch": "3D-Skizze",
|
||||
"ui.about": "Über",
|
||||
"ui.activate": "Aktivieren",
|
||||
"ui.active_component": "Aktive Komponente",
|
||||
"ui.agent_cursor": "Agent-Cursor",
|
||||
"ui.agent_cursor_speed": "Agent-Cursor-Geschwindigkeit",
|
||||
"ui.all": "Alle",
|
||||
"ui.analysis": "Analyse",
|
||||
"ui.angle": "Winkel",
|
||||
"ui.angle_decimals": "Winkel-Dezimalstellen",
|
||||
"ui.animate_view_changes": "Ansichtsänderungen animieren",
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animationsgeschwindigkeit",
|
||||
"ui.anti_aliasing": "Kantenglättung",
|
||||
"ui.apply": "Anwenden",
|
||||
"ui.arc": "Kreisbogen",
|
||||
"ui.assemble": "Zusammenbau",
|
||||
"ui.autosave_every": "Autospeichern alle",
|
||||
"ui.axes": "Achsen",
|
||||
"ui.axis": "Achse",
|
||||
"ui.base_point": "Basispunkt",
|
||||
"ui.bodies": "Körper",
|
||||
"ui.box": "Block",
|
||||
"ui.break": "Unterbrechen",
|
||||
"ui.cancel": "Abbrechen",
|
||||
"ui.canvases": "Kanvas",
|
||||
"ui.center": "Mittelpunkt",
|
||||
"ui.centerline": "Mittellinie",
|
||||
"ui.chain": "Kette",
|
||||
"ui.chamfer": "Abschrägung",
|
||||
"ui.chamfer_type": "Fasentyp",
|
||||
"ui.change_workspace": "Arbeitsbereich wechseln",
|
||||
"ui.chord_length": "Sehnenlänge",
|
||||
"ui.curvature_g2": "Krümmungsstetig (G2)",
|
||||
"ui.equivalent_radius": "Äquivalenter Radius",
|
||||
"ui.circle": "Kreis",
|
||||
"ui.circular_pattern": "Radiales Muster",
|
||||
"ui.clear_recent": "Zuletzt verwendet leeren",
|
||||
"ui.clear_the_selection": "Auswahl aufheben",
|
||||
"ui.close": "Schließen",
|
||||
"ui.coil": "Schraube",
|
||||
"ui.coincident": "Koinzident",
|
||||
"ui.collapse": "Einklappen",
|
||||
"ui.collinear": "Kollinear",
|
||||
"ui.combine": "Kombinieren",
|
||||
"ui.coming_later": "bald verfügbar",
|
||||
"ui.concentric": "Konzentrisch",
|
||||
"ui.configurations": "Konfigurationen",
|
||||
"ui.conic_curve": "Kegelschnitt",
|
||||
"ui.constant": "Konstant",
|
||||
"ui.constraints_conflict": "Bedingungen konfliktreich",
|
||||
"ui.construct": "Konstruktion",
|
||||
"ui.construction": "Konstruktionslinie",
|
||||
"ui.copy": "Kopieren",
|
||||
"ui.corners": "Ecken",
|
||||
"ui.corner": "Ecke",
|
||||
"ui.counterbore": "Tiefenbohrung",
|
||||
"ui.countersink": "Senkung",
|
||||
"ui.create": "Erstellen",
|
||||
"ui.create_copy": "Kopie erstellen",
|
||||
"ui.create_sketch": "Skizze erstellen",
|
||||
"ui.current": "aktuell",
|
||||
"ui.curvature": "Krümmung",
|
||||
"ui.curves": "Kurven",
|
||||
"ui.cut": "Ausschneiden",
|
||||
"ui.cylinder": "Zylinder",
|
||||
"ui.dark": "Dunkel",
|
||||
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
|
||||
"ui.degrees_of_freedom": "Freiheitsgrade",
|
||||
"ui.delete": "Löschen",
|
||||
"ui.depth": "Tiefe",
|
||||
"ui.design": "Design",
|
||||
"ui.diameter": "Durchmesser",
|
||||
"ui.dimension": "Maß",
|
||||
"ui.direction": "Richtung",
|
||||
"ui.distance": "Abstand",
|
||||
"ui.distance_1": "Abstand 1",
|
||||
"ui.distance_2": "Abstand 2",
|
||||
"ui.document_settings": "Dokumenteneinstellungen",
|
||||
"ui.don_t_save": "Nicht speichern",
|
||||
"ui.draft": "Abziehen",
|
||||
"ui.drawing": "Zeichnung",
|
||||
"ui.drill_point": "Bohrpunkt",
|
||||
"ui.edit": "Bearbeiten",
|
||||
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
|
||||
"ui.ellipse": "Ellipse",
|
||||
"ui.end_radius": "Endradius",
|
||||
"ui.entities": "Elemente",
|
||||
"ui.equal": "Gleich",
|
||||
"ui.expand": "Aufklappen",
|
||||
"ui.export_ellipsis": "Exportieren...",
|
||||
"ui.extend": "Verlängern",
|
||||
"ui.extent_type": "Ausdehnungstyp",
|
||||
"ui.extrude": "Extrudieren",
|
||||
"ui.factor": "Faktor",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
"ui.feature_options": "Funktionsoptionen",
|
||||
"ui.features": "Funktionen",
|
||||
"ui.file": "Datei",
|
||||
"ui.fillet": "Vollrundung",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
"ui.flat": "Flach",
|
||||
"ui.flip": "Umkehren",
|
||||
"ui.front": "Vorne",
|
||||
"ui.fully_constrained": "Vollständig bestimmt",
|
||||
"ui.general": "Allgemein",
|
||||
"ui.graphics": "Grafik",
|
||||
"ui.heading_assemble": "ZUSAMMENBAU",
|
||||
"ui.heading_box": "BLOCK",
|
||||
"ui.heading_chamfer": "ABSCHRÄGUNG",
|
||||
"ui.heading_circular_pattern": "RADIALES MUSTER",
|
||||
"ui.heading_coil": "SCHRAUBE",
|
||||
"ui.heading_combine": "KOMBINIEREN",
|
||||
"ui.heading_construct": "KONSTRUKTION",
|
||||
"ui.heading_create": "ERSTELLEN",
|
||||
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
|
||||
"ui.heading_cylinder": "ZYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "ABZIEHEN",
|
||||
"ui.heading_edit": "BEARBEITEN",
|
||||
"ui.heading_extrude": "EXTRUDIEREN",
|
||||
"ui.heading_fillet": "VOLLRUNDUNG",
|
||||
"ui.heading_finish_sketch": "SKIZZE BEENDEN",
|
||||
"ui.heading_front": "VORNE",
|
||||
"ui.heading_hole": "BOHRUNG",
|
||||
"ui.heading_home": "START",
|
||||
"ui.heading_insert": "EINFÜGEN",
|
||||
"ui.heading_inspect": "PRÜFEN",
|
||||
"ui.heading_loft": "LOFT",
|
||||
"ui.heading_measure": "Messen",
|
||||
"ui.heading_mesh": "MESH",
|
||||
"ui.heading_mirror": "SPIEGELN",
|
||||
"ui.heading_modify": "ÄNDERN",
|
||||
"ui.heading_move": "VERSCHIEBEN",
|
||||
"ui.heading_offset": "ABSTAND",
|
||||
"ui.heading_move_copy": "VERSCHIEBEN/KOPIEREN",
|
||||
"ui.heading_offset_plane": "ABSTANDSEBENE",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMETER",
|
||||
"ui.heading_pattern_on_path": "PFADMUSTER",
|
||||
"ui.heading_pipe": "ROHR",
|
||||
"ui.heading_plane_at_angle": "EBENE IM WINKEL",
|
||||
"ui.heading_plastic": "PLASTIK",
|
||||
"ui.heading_rectangular_pattern": "RECHTECKMUSTER",
|
||||
"ui.heading_revolve": "DREHEN",
|
||||
"ui.heading_right": "RECHTS",
|
||||
"ui.heading_scale": "SKALIEREN",
|
||||
"ui.heading_section_analysis": "QUERSCHNITTSANALYSE",
|
||||
"ui.heading_select": "AUSWÄHLEN",
|
||||
"ui.heading_sheet_metal": "BLECH",
|
||||
"ui.heading_shell": "HÜLLE",
|
||||
"ui.heading_sketch": "SKIZZE",
|
||||
"ui.heading_sketch_palette": "SKIZZENPALETTE",
|
||||
"ui.heading_sketch_scale": "SKIZZE SKALIEREN",
|
||||
"ui.heading_solid": "KÖRPER",
|
||||
"ui.heading_sphere": "SPHÄRE",
|
||||
"ui.heading_split_body": "KÖRPER TRENNEN",
|
||||
"ui.heading_surface": "FLÄCHE",
|
||||
"ui.heading_sweep": "FÜHREN",
|
||||
"ui.heading_thread": "GEWINDE",
|
||||
"ui.heading_top": "OBEN",
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.help": "Hilfe",
|
||||
"ui.hide": "Ausblenden",
|
||||
"ui.hole": "Bohrung",
|
||||
"ui.hole_tap_type": "Gewindetyp",
|
||||
"ui.hole_type": "Bohrungstyp",
|
||||
"ui.home": "Start",
|
||||
"ui.horizontal_vertical": "Horizontal/Vertikal",
|
||||
"ui.insert": "Einfügen",
|
||||
"ui.inspect": "Prüfen",
|
||||
"ui.intersect": "Schnitt",
|
||||
"ui.join": "Verbinden",
|
||||
"ui.joints": "Gelenke",
|
||||
"ui.keyboard_shortcuts": "Tastenkürzel",
|
||||
"ui.language": "Sprache",
|
||||
"ui.length_decimals": "Längen-Dezimalstellen",
|
||||
"ui.light": "Hell",
|
||||
"ui.line": "Linie",
|
||||
"ui.linetype": "Linientyp",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Ansehen",
|
||||
"ui.manufacture": "Fertigung",
|
||||
"ui.measure": "Messen",
|
||||
"ui.midpoint": "Mittelpunkt",
|
||||
"ui.mirror": "Spiegeln",
|
||||
"ui.mirror_line": "Spiegelachse",
|
||||
"ui.modify": "Ändern",
|
||||
"ui.move": "Verschieben",
|
||||
"ui.multiple": "Mehrfach",
|
||||
"ui.named_views": "Benannte Ansichten",
|
||||
"ui.navigation": "Navigation",
|
||||
"ui.needs_design": "{command}: Öffnen Sie zuerst ein Design",
|
||||
"ui.new_body": "Neuer Körper",
|
||||
"ui.new_design": "Neues Design",
|
||||
"ui.normal": "Normal",
|
||||
"ui.object_type": "Objekttyp",
|
||||
"ui.objects": "Objekte",
|
||||
"ui.offer_to_stand_up_y_up_files": "Y-hoch-Dateien aufrecht anbieten",
|
||||
"ui.offset": "Abstand",
|
||||
"ui.offset_plane": "Abstandsebene",
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ellipsis": "Öffnen...",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
"ui.options": "Optionen",
|
||||
"ui.orbit_around": "Umkreisen um",
|
||||
"ui.origin": "Ursprung",
|
||||
"ui.palette_points": "Punkte",
|
||||
"ui.palette_profile": "Profil",
|
||||
"ui.pan_zoom_orbit": "Verschieben, zoomen, umkreisen",
|
||||
"ui.parallel": "Parallel",
|
||||
"ui.parameters": "Parameter",
|
||||
"ui.paste": "Einfügen",
|
||||
"ui.path": "Pfad",
|
||||
"ui.pattern_on_path": "Pfadmuster",
|
||||
"ui.perpendicular": "Senkrecht",
|
||||
"ui.pipe": "Rohr",
|
||||
"ui.placement": "Platzierung",
|
||||
"ui.plane": "Ebene",
|
||||
"ui.plane_at_angle": "Ebene im Winkel",
|
||||
"ui.point": "Punkt",
|
||||
"ui.point_under_the_cursor": "Punkt unter dem Cursor",
|
||||
"ui.points": "Punkte",
|
||||
"ui.polygon": "Polygon",
|
||||
"ui.preferences": "Einstellungen",
|
||||
"ui.profile": "Profil",
|
||||
"ui.profile_plane": "Profilebene",
|
||||
"ui.profiles": "Profile",
|
||||
"ui.project": "Projizieren",
|
||||
"ui.quantity_2": "Anzahl 2",
|
||||
"ui.quantity": "Anzahl",
|
||||
"ui.quit": "Beenden",
|
||||
"ui.radius": "Radius",
|
||||
"ui.radius_type": "Radius-Typ",
|
||||
"ui.rectangle": "Rechteck",
|
||||
"ui.rectangular_pattern": "Rechteckmuster",
|
||||
"ui.redo": "Wiederholen",
|
||||
"ui.render": "Rendern",
|
||||
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
|
||||
"ui.restore_defaults": "Standardeinstellungen wiederherstellen",
|
||||
"ui.reverse_zoom_direction": "Zoomrichtung umkehren",
|
||||
"ui.revolve": "Drehen",
|
||||
"ui.right": "Rechts",
|
||||
"ui.save": "Speichern",
|
||||
"ui.save_as": "Speichern unter",
|
||||
"ui.save_as_ellipsis": "Speichern unter...",
|
||||
"ui.scale": "Skalieren",
|
||||
"ui.section_analysis": "Querschnittsanalyse",
|
||||
"ui.select": "Auswählen",
|
||||
"ui.selection": "Auswahl",
|
||||
"ui.shell": "Hülle",
|
||||
"ui.show": "Einblenden",
|
||||
"ui.show_constraints": "Bedingungen",
|
||||
"ui.show_dimensions": "Maße",
|
||||
"ui.show_points": "Punkte anzeigen",
|
||||
"ui.show_profile": "Profil anzeigen",
|
||||
"ui.show_projected_geometries": "Projizierte Geometrien",
|
||||
"ui.simple": "Einfach",
|
||||
"ui.simulation": "Simulation",
|
||||
"ui.single": "Einzel",
|
||||
"ui.sketch": "Skizze",
|
||||
"ui.sketch_dimension": "Skizzenmaß",
|
||||
"ui.sketch_grid": "Skizzenraster",
|
||||
"ui.sketch_scale": "Skizzenskalierung",
|
||||
"ui.sketch_status": "Skizzenstatus",
|
||||
"ui.sketches": "Skizzen",
|
||||
"ui.slice": "Scheibe",
|
||||
"ui.slot": "Langloch",
|
||||
"ui.smooth": "Glatt",
|
||||
"ui.snap": "Einrasten",
|
||||
"ui.spacing_2": "Abstand 2",
|
||||
"ui.spacing": "Abstand",
|
||||
"ui.sphere": "Sphäre",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Körper trennen",
|
||||
"ui.start": "Start",
|
||||
"ui.start_radius": "Startradius",
|
||||
"ui.sweep": "Führen",
|
||||
"ui.symmetric": "Symmetrisch",
|
||||
"ui.symmetry": "Symmetrie",
|
||||
"ui.system": "System",
|
||||
"ui.tangent": "Tangential",
|
||||
"ui.taper_angle": "Kegelwinkel",
|
||||
"ui.tapped": "Gewindebohrung",
|
||||
"ui.text": "Text",
|
||||
"ui.theme": "Design",
|
||||
"ui.thickness": "Dicke",
|
||||
"ui.thread": "Gewinde",
|
||||
"ui.timeline_bottom": "Timeline unten",
|
||||
"ui.timeline_left_note": "Im linken Layout wird die Rücksetzmarke über dem Teile- und Szenenbrowser eingeblendet.",
|
||||
"ui.timeline_left_sidebar": "Linke Seitenleiste",
|
||||
"ui.tip_angle": "Spitzenwinkel",
|
||||
"ui.to_object": "Bis Objekt",
|
||||
"ui.top": "Oben",
|
||||
"ui.torus": "Torus",
|
||||
"ui.trim": "Kürzen",
|
||||
"ui.two_sides": "Beidseitig",
|
||||
"ui.ui_scale": "UI-Skalierung",
|
||||
"ui.undo": "Rückgängig",
|
||||
"ui.units": "Einheiten",
|
||||
"ui.units_of_new_designs": "Einheiten für neue Designs",
|
||||
"ui.untitled": "Ohne Titel",
|
||||
"ui.variable": "Variabel",
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.visual_style": "Ansichtsstil",
|
||||
"ui.workspace": "Arbeitsbereich",
|
||||
"ui.y_distance": "Y-Abstand",
|
||||
"ui.x_distance": "X-Abstand",
|
||||
"ui.zoom_to_the_cursor": "Zum Cursor zoomen",
|
||||
"ui.animateJoint": "Gelenk animieren",
|
||||
"ui.calibrate": "Kalibrieren",
|
||||
"ui.customizeToolbar": "Symbolleiste anpassen…",
|
||||
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
|
||||
"ui.exportDxf": "Als DXF exportieren…",
|
||||
"ui.findInTimeline": "In der Zeitleiste suchen",
|
||||
"ui.findInWindow": "Im Fenster suchen",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.groupSelected": "Auswahl gruppieren",
|
||||
"ui.hideAxes": "Achsen ausblenden",
|
||||
"ui.hideDimension": "Bemaßung ausblenden",
|
||||
"ui.hideOrigin": "Ursprung ausblenden",
|
||||
"ui.hidePlanes": "Ebenen ausblenden",
|
||||
"ui.hideProfile": "Profil ausblenden",
|
||||
"ui.homeView": "Startansicht",
|
||||
"ui.isolate": "Isolieren",
|
||||
"ui.lock": "Sperren",
|
||||
"ui.newSketchGroup": "Neue Skizzengruppe",
|
||||
"ui.normalConstruction": "Normal / Konstruktion",
|
||||
"ui.orbit": "Drehen",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.pan": "Verschieben",
|
||||
"ui.pinToShortcuts": "An Verknüpfungen anheften",
|
||||
"ui.pinToToolbar": "An Symbolleiste anheften",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.removeSection": "Schnitt entfernen",
|
||||
"ui.removeFromToolbar": "Aus der Symbolleiste entfernen",
|
||||
"ui.repeat": "Wiederholen",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.restore": "Wiederherstellen",
|
||||
"ui.heading_setup": "EINRICHTEN",
|
||||
"ui.selectOther": "Andere auswählen…",
|
||||
"ui.selectTangentChain": "Tangentialkette auswählen",
|
||||
"ui.showAll": "Alle einblenden",
|
||||
"ui.showAllBodies": "Alle Volumenkörper einblenden",
|
||||
"ui.showAxes": "Achsen einblenden",
|
||||
"ui.showDimension": "Bemaßung einblenden",
|
||||
"ui.showOrigin": "Ursprung einblenden",
|
||||
"ui.showPlanes": "Ebenen einblenden",
|
||||
"ui.suppress": "Unterdrücken",
|
||||
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
|
||||
"ui.unground": "Fixierung lösen",
|
||||
"ui.unisolate": "Isolierung aufheben",
|
||||
"ui.unlock": "Entsperren",
|
||||
"ui.unsuppress": "Unterdrückung aufheben",
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.zoom": "Zoomen",
|
||||
"ui.command_done": "{command} fertig",
|
||||
"cmd.appearance_assign": "Erscheinungsbild",
|
||||
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
|
||||
"cmd.appearance_list": "Erscheinungsbilder auflisten",
|
||||
"cmd.body_rename": "Körper umbenennen",
|
||||
"cmd.browser_group": "Neue Gruppe",
|
||||
"cmd.browser_move": "Browser-Elemente verschieben",
|
||||
"cmd.browser_order": "Browser-Elemente ordnen",
|
||||
"cmd.browser_rename_group": "Gruppe umbenennen",
|
||||
"cmd.browser_ungroup": "Gruppierung aufheben",
|
||||
"cmd.browser_visibility": "Ein-/Ausblenden",
|
||||
"cmd.canvas_calibrate": "Zeichenfläche kalibrieren",
|
||||
"cmd.canvas_decal": "Dekal",
|
||||
"cmd.canvas_delete": "Zeichenfläche löschen",
|
||||
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
|
||||
"cmd.canvas_insert": "Zeichenfläche",
|
||||
"cmd.component_activate": "Komponente aktivieren",
|
||||
"cmd.component_break_link": "Verknüpfung trennen",
|
||||
"cmd.component_capture_position": "Position erfassen",
|
||||
"cmd.component_create": "Neue Komponente",
|
||||
"cmd.component_delete": "Komponente löschen",
|
||||
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
|
||||
"cmd.component_insert_design": "Konstruktion einfügen",
|
||||
"cmd.component_links": "Verknüpfungen auflisten",
|
||||
"cmd.component_list": "Komponenten auflisten",
|
||||
"cmd.component_move_bodies": "Körper in Komponente verschieben",
|
||||
"cmd.component_move_sketches": "Skizzen in Komponente verschieben",
|
||||
"cmd.component_paste_new": "Neu einfügen",
|
||||
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
|
||||
"cmd.component_rename": "Komponente umbenennen",
|
||||
"cmd.component_revert_position": "Position zurücksetzen",
|
||||
"cmd.component_update_links": "Verknüpfungen aktualisieren",
|
||||
"cmd.config_activate": "Konfiguration aktivieren",
|
||||
"cmd.config_column": "Konfigurationsspalte",
|
||||
"cmd.config_configure": "Konfigurieren",
|
||||
"cmd.config_preview": "Konfiguration vorschauen",
|
||||
"cmd.config_row": "Konfigurationszeile",
|
||||
"cmd.config_rule": "Konfigurationsregel",
|
||||
"cmd.config_rules": "Konfigurationsregeln",
|
||||
"cmd.config_table": "Konfigurationstabelle anzeigen",
|
||||
"cmd.construct_axis_edge": "Achse durch Kante",
|
||||
"cmd.construct_axis_normal_to_face": "Achse senkrecht zur Fläche",
|
||||
"cmd.construct_axis_through_cylinder": "Achse durch Zylinder/Kegel/Torus",
|
||||
"cmd.construct_axis_two_planes": "Achse durch zwei Ebenen",
|
||||
"cmd.construct_axis_two_points": "Achse durch zwei Punkte",
|
||||
"cmd.construct_plane_along_path": "Ebene entlang Pfad",
|
||||
"cmd.construct_plane_midplane": "Mittenebene",
|
||||
"cmd.construct_plane_perpendicular": "Rechtebene",
|
||||
"cmd.construct_plane_tangent": "Tangentialebene",
|
||||
"cmd.construct_plane_three_points": "Ebene durch drei Punkte",
|
||||
"cmd.construct_plane_two_edges": "Ebene durch zwei Kanten",
|
||||
"cmd.construct_point_along_path": "Punkt entlang Pfad",
|
||||
"cmd.construct_point_center": "Punkt im Mittelpunkt von Kreis/Kugel/Torus",
|
||||
"cmd.construct_point_edge_plane": "Punkt an Kante und Ebene",
|
||||
"cmd.construct_point_three_planes": "Punkt aus drei Ebenen",
|
||||
"cmd.construct_point_two_edges": "Punkt aus zwei Kanten",
|
||||
"cmd.construct_point_vertex": "Punkt an Scheitelpunkt",
|
||||
"cmd.contact_check": "Kontakt prüfen",
|
||||
"cmd.contact_create": "Neuer Kontaktsatz",
|
||||
"cmd.contact_disable_all": "Kontakt deaktivieren",
|
||||
"cmd.contact_edit": "Kontaktsatz bearbeiten",
|
||||
"cmd.contact_enable_all": "Alle Kontakte aktivieren",
|
||||
"cmd.contact_enable_sets": "Kontaktsätze aktivieren",
|
||||
"cmd.contact_list": "Kontaktsätze auflisten",
|
||||
"cmd.diagnostics_crash_report": "Absturzbericht",
|
||||
"cmd.diagnostics_crash_reports": "Absturzberichte",
|
||||
"cmd.diagnostics_crash_reports_clear": "Absturzberichte löschen",
|
||||
"cmd.diagnostics_crash_test": "Absturztest",
|
||||
"cmd.doc_recover": "Konstruktion wiederherstellen",
|
||||
"cmd.doc_recovery_discard": "Wiederhergestellte Konstruktion verwerfen",
|
||||
"cmd.doc_recovery_list": "Wiederherstellbare Konstruktionen",
|
||||
"cmd.document_inspect": "Konstruktion prüfen",
|
||||
"cmd.document_units": "Einheiten der Konstruktion ändern",
|
||||
"cmd.edit_checkpoint": "Prüfpunkt",
|
||||
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
|
||||
"cmd.engine_commands": "Befehle auflisten",
|
||||
"cmd.explode_create": "Explosionsdarstellung",
|
||||
"cmd.explode_delete": "Explosionsdarstellung löschen",
|
||||
"cmd.explode_list": "Explosionsdarstellungen auflisten",
|
||||
"cmd.explode_show": "Explosionsdarstellung anzeigen",
|
||||
"cmd.expr_evaluate": "Ausdruck auswerten",
|
||||
"cmd.face_delete": "Fläche löschen",
|
||||
"cmd.file_export": "Exportieren",
|
||||
"cmd.file_insert_mesh": "Netz einfügen",
|
||||
"cmd.file_insert_step": "STEP einfügen",
|
||||
"cmd.file_save_mesh": "Als Netz speichern",
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
|
||||
"cmd.inspect_curvature_map": "Krümmungskartenanalyse",
|
||||
"cmd.inspect_draft": "Auswinkelanalyse",
|
||||
"cmd.inspect_environment_map": "Umgebungskartenanalyse",
|
||||
"cmd.inspect_interference": "Eingriff",
|
||||
"cmd.inspect_isocurve": "Isokurvenanalyse",
|
||||
"cmd.inspect_minimum_radius": "Mindestradiusanalyse",
|
||||
"cmd.inspect_zebra": "Zebra-Analyse",
|
||||
"cmd.joint_as_built": "Fertigungsgelenk",
|
||||
"cmd.joint_create": "Gelenk",
|
||||
"cmd.joint_delete": "Gelenk löschen",
|
||||
"cmd.joint_drive": "Gelenke antreiben",
|
||||
"cmd.joint_edit": "Gelenk bearbeiten",
|
||||
"cmd.joint_limits": "Gelenkgrenzen",
|
||||
"cmd.joint_list": "Gelenke auflisten",
|
||||
"cmd.joint_motion_link": "Bewegungsverknüpfung",
|
||||
"cmd.joint_origin": "Gelenkursprung",
|
||||
"cmd.joint_rigid_group": "Starrgruppe",
|
||||
"cmd.joint_solve": "Gelenke lösen",
|
||||
"cmd.material_assign": "Physikalisches Material",
|
||||
"cmd.material_create": "Neues physikalisches Material",
|
||||
"cmd.material_delete": "Physisches Material löschen",
|
||||
"cmd.material_edit": "Physisches Material bearbeiten",
|
||||
"cmd.material_list": "Materialien auflisten",
|
||||
"cmd.mesh_convert": "Netz konvertieren",
|
||||
"cmd.model_edges": "Kanten auflisten",
|
||||
"cmd.model_faces": "Flächen auflisten",
|
||||
"cmd.model_threads": "Gewinde auflisten",
|
||||
"cmd.motion_delete": "Bewegungsuntersuchung löschen",
|
||||
"cmd.motion_export": "Bewegungsuntersuchung exportieren",
|
||||
"cmd.motion_list": "Bewegungsuntersuchungen auflisten",
|
||||
"cmd.motion_play": "Bewegungsuntersuchung abspielen",
|
||||
"cmd.motion_study": "Bewegungsuntersuchung",
|
||||
"cmd.occurrence_copy": "Einfügen (Instanz)",
|
||||
"cmd.occurrence_ground": "Festsetzen",
|
||||
"cmd.occurrence_move": "Instanz verschieben",
|
||||
"cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz",
|
||||
"cmd.parameters_add": "Benutzerparameter hinzufügen",
|
||||
"cmd.parameters_change": "Parameter ändern",
|
||||
"cmd.parameters_comment": "Parameter kommentieren",
|
||||
"cmd.parameters_complete": "Ausdruck vervollständigen",
|
||||
"cmd.parameters_delete": "Parameter löschen",
|
||||
"cmd.parameters_export": "Parameter exportieren",
|
||||
"cmd.parameters_favorite": "Favorisierter Parameter",
|
||||
"cmd.parameters_graph": "Parametergraph",
|
||||
"cmd.parameters_import": "Parameter importieren",
|
||||
"cmd.parameters_list": "Parameter auflisten",
|
||||
"cmd.parameters_rename": "Parameter umbenennen",
|
||||
"cmd.parameters_users": "Parameterverwendungen",
|
||||
"cmd.parts_fastener": "Verbindungselement einfügen",
|
||||
"cmd.parts_insert": "Bauteil einfügen",
|
||||
"cmd.parts_library": "Standardbauteile",
|
||||
"cmd.parts_resize": "Bauteilgröße ändern",
|
||||
"cmd.plastic_assign_rule": "Kunststoffregel zuweisen",
|
||||
"cmd.plastic_boss": "Erhöhung",
|
||||
"cmd.plastic_lip": "Lippe",
|
||||
"cmd.plastic_manage_rules": "Kunststoffregeln verwalten",
|
||||
"cmd.plastic_rest": "Auflage",
|
||||
"cmd.plastic_rules": "Kunststoffregeln auflisten",
|
||||
"cmd.plastic_snap_fit": "Rastverbindung",
|
||||
"cmd.select_clear": "Auswahl aufheben",
|
||||
"cmd.sheet_convert": "In Blech umwandeln",
|
||||
"cmd.sheet_export_bend_table": "Biegungstabelle exportieren",
|
||||
"cmd.sheet_export_dxf": "Flachmuster als DXF exportieren",
|
||||
"cmd.sheet_flange": "Flansch",
|
||||
"cmd.sheet_flat_pattern": "Flachmuster erstellen",
|
||||
"cmd.sheet_fold": "Falten",
|
||||
"cmd.sheet_hem": "Umkantung",
|
||||
"cmd.sheet_manage_rules": "Blechregeln",
|
||||
"cmd.sheet_refold": "Wieder falten",
|
||||
"cmd.sheet_rules": "Blechregeln auflisten",
|
||||
"cmd.sheet_unfold": "Entfalten",
|
||||
"cmd.sketch_arc_center_point": "Mittelpunkt-Kreisbogen",
|
||||
"cmd.sketch_arc_tangent": "Tangentialbogen",
|
||||
"cmd.sketch_arc_three_point": "3-Punkt-Kreisbogen",
|
||||
"cmd.sketch_auto_constrain": "Auto-Bedingung",
|
||||
"cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug",
|
||||
"cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren",
|
||||
"cmd.sketch_blend_curve": "Übergangskurve",
|
||||
"cmd.sketch_chain": "Kette",
|
||||
"cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel",
|
||||
"cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand",
|
||||
"cmd.sketch_chamfer_two_distance": "Abschrägung mit zwei Abständen",
|
||||
"cmd.sketch_circle_center": "Kreis aus Mittelpunkt und Durchmesser",
|
||||
"cmd.sketch_circle_three_point": "3-Punkt-Kreis",
|
||||
"cmd.sketch_circle_three_tangent": "3-Tangenten-Kreis",
|
||||
"cmd.sketch_circle_two_point": "2-Punkt-Kreis",
|
||||
"cmd.sketch_circle_two_tangent": "2-Tangenten-Kreis",
|
||||
"cmd.sketch_constrainer": "Skizzen-Bedingung",
|
||||
"cmd.sketch_constraint_fix": "Fixieren/Lösen",
|
||||
"cmd.sketch_constraint_midpoint": "Mittelpunkt",
|
||||
"cmd.sketch_construction": "Normal/Konstruktion",
|
||||
"cmd.sketch_delete": "Skizzenentitäten löschen",
|
||||
"cmd.sketch_dimension_text": "Maßtext verschieben",
|
||||
"cmd.sketch_drag": "Skizzengeometrie ziehen",
|
||||
"cmd.sketch_drag_end": "Skizzen-Ziehen beenden",
|
||||
"cmd.sketch_draw_dimensioned": "Skizzengeometrie mit Maßen zeichnen",
|
||||
"cmd.sketch_edit": "Skizze bearbeiten",
|
||||
"cmd.sketch_edit_text": "Text bearbeiten",
|
||||
"cmd.sketch_export_dxf": "Als DXF speichern",
|
||||
"cmd.sketch_finish_auto_constrain": "Mit Auto-Bedingung fertigstellen",
|
||||
"cmd.sketch_fit_curves_to_section": "Kurven an Netzquerschnitt anpassen",
|
||||
"cmd.sketch_glyphs": "Bedingungssymbole",
|
||||
"cmd.sketch_include_3d": "3D-Geometrie einbeziehen",
|
||||
"cmd.sketch_insert_dxf": "DXF einfügen",
|
||||
"cmd.sketch_insert_svg": "SVG einfügen",
|
||||
"cmd.sketch_inspect": "Skizze prüfen",
|
||||
"cmd.sketch_intersection_curve": "Schnittkurve",
|
||||
"cmd.sketch_isoparametric_curve": "Isoparametrische Kurve",
|
||||
"cmd.sketch_line3d": "3D-Linie",
|
||||
"cmd.sketch_line_midpoint": "Linie über den Mittelpunkt",
|
||||
"cmd.sketch_move": "Skizzenentitäten verschieben/kopieren",
|
||||
"cmd.sketch_move3d": "3D-Punkt verschieben",
|
||||
"cmd.sketch_move_point": "Skizzenpunkt ziehen",
|
||||
"cmd.sketch_offset_break": "Versatzverknüpfung trennen",
|
||||
"cmd.sketch_options": "Skizzenpalettenoptionen",
|
||||
"cmd.sketch_point3d": "3D-Punkt",
|
||||
"cmd.sketch_polygon_circumscribed": "Umgeschriebenes Polygon",
|
||||
"cmd.sketch_polygon_edge": "Polygon über eine Kante",
|
||||
"cmd.sketch_polygon_inscribed": "Eingeschriebenes Polygon",
|
||||
"cmd.sketch_project_to_surface": "Auf Fläche projizieren",
|
||||
"cmd.sketch_rectangle_center": "Rechteck vom Mittelpunkt",
|
||||
"cmd.sketch_rectangle_three_point": "3-Punkt-Rechteck",
|
||||
"cmd.sketch_rectangle_two_point": "2-Punkt-Rechteck",
|
||||
"cmd.sketch_redefine": "Skizzierebene neu definieren",
|
||||
"cmd.sketch_slot_arc_center": "Mittelpunkt-Bogenschlitz",
|
||||
"cmd.sketch_slot_arc_three_point": "3-Punkt-Bogenschlitz",
|
||||
"cmd.sketch_slot_center_point": "Mittelpunktschlitz",
|
||||
"cmd.sketch_slot_center_to_center": "Schlitz von Mittelpunkt zu Mittelpunkt",
|
||||
"cmd.sketch_slot_overall": "Gesamtschlitz",
|
||||
"cmd.sketch_snap": "Einrastinferenz",
|
||||
"cmd.sketch_solve": "Skizze lösen",
|
||||
"cmd.sketch_spline3d": "3D-Spline",
|
||||
"cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten",
|
||||
"cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)",
|
||||
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
|
||||
"cmd.sketch_spun_profile": "Gedrehtes Profil",
|
||||
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
|
||||
"cmd.solid_align": "Ausrichten",
|
||||
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
|
||||
"cmd.solid_bounding_solid": "Begrenzender Körper",
|
||||
"cmd.solid_emboss": "Prägen",
|
||||
"cmd.solid_move": "Verschieben/Kopieren",
|
||||
"cmd.solid_offset_face": "Fläche versetzen",
|
||||
"cmd.solid_press_pull": "Drücken/Ziehen",
|
||||
"cmd.solid_remove": "Entfernen",
|
||||
"cmd.solid_replace_face": "Fläche ersetzen",
|
||||
"cmd.solid_rib": "Rippe",
|
||||
"cmd.solid_split_face": "Fläche teilen",
|
||||
"cmd.solid_web": "Gewebe",
|
||||
"cmd.surface_patch": "Patch",
|
||||
"cmd.surface_stitch": "Nähen",
|
||||
"cmd.surface_thicken": "Dicke vergeben",
|
||||
"cmd.surface_extrude": "Flächen-Extrusion",
|
||||
"cmd.surface_revolve": "Flächen-Rotation",
|
||||
"cmd.surface_sweep": "Flächen-Sweep",
|
||||
"cmd.surface_loft": "Flächen-Loft",
|
||||
"cmd.surface_ruled": "Regelfläche",
|
||||
"cmd.surface_boundary": "Begrenzungsfläche",
|
||||
"cmd.surface_offset": "Offsetfläche",
|
||||
"cmd.surface_blend": "Verbindungsfläche",
|
||||
"cmd.surface_fillet": "Flächenverrundung",
|
||||
"cmd.surface_untrim": "Trimmen aufheben",
|
||||
"cmd.surface_unstitch": "Verbund aufheben",
|
||||
"cmd.surface_reverse_normal": "Normale umkehren",
|
||||
"cmd.inspect_continuity": "Kontinuitätsprüfung",
|
||||
"cmd.timeline_compute_all": "Alles berechnen",
|
||||
"cmd.timeline_copy": "Merkmale kopieren",
|
||||
"cmd.timeline_dependents": "Merkmalsabhängigkeiten",
|
||||
"cmd.timeline_inspect": "Merkmal prüfen",
|
||||
"cmd.timeline_paste": "Merkmale einfügen",
|
||||
"cmd.timeline_redefine": "Merkmal bearbeiten",
|
||||
"cmd.timeline_rename": "Umbenennen",
|
||||
"cmd.timeline_reorder": "Merkmal neu ordnen",
|
||||
"cmd.timeline_roll_to": "Verlaufsmarkierung verschieben",
|
||||
"cmd.timeline_rollback": "Zeitleistenmarkierung verschieben",
|
||||
"cmd.timeline_suppress": "Merkmal unterdrücken",
|
||||
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
|
||||
"cmd.view_delete": "Benannte Ansicht löschen",
|
||||
"cmd.view_list": "Benannte Ansichten auflisten",
|
||||
"cmd.view_rename": "Benannte Ansicht umbenennen",
|
||||
"cmd.view_save": "Neue benannte Ansicht",
|
||||
"ui.export_ellipsis_keymap": "Exportieren…",
|
||||
"ui.heading_configure": "KONFIGURIEREN",
|
||||
"ui.heading_constraints": "RANDBEDINGUNGEN",
|
||||
"ui.heading_utilities": "WERKZEUGE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Drive Joint": "Gelenk antreiben",
|
||||
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
|
||||
"Find in Browser": "Im Browser suchen",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
|
||||
"Reset Toolbar": "Symbolleiste zurücksetzen",
|
||||
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
|
||||
"Selection filter": "Auswahlfilter",
|
||||
"Revert Position": "Position zurücksetzen",
|
||||
"Construction": "Konstruktionslinie",
|
||||
"Faces": "Flächen",
|
||||
"Edges": "Kanten",
|
||||
"Vertices": "Scheitelpunkte",
|
||||
"Sketch geometry and profiles": "Skizzengeometrie und Profile",
|
||||
"Sketch curves": "Skizzenkurven",
|
||||
"Sketch points": "Skizzenpunkte",
|
||||
"Sketch dimensions": "Skizzenmaße",
|
||||
"Sketch constraints": "Skizzenbedingungen",
|
||||
"Flat": "Flach",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.view_navigate": "Ansicht navigieren",
|
||||
"timeline.group": "Neuen Merkmalsordner anlegen",
|
||||
"timeline.ungroup": "Merkmalsordner entfernen",
|
||||
"timeline.rename_group": "Merkmalsordner umbenennen",
|
||||
"timeline.move_to_group": "Merkmale in Ordner verschieben",
|
||||
"timeline.move_many": "Zeitleistenelemente verschieben",
|
||||
"timeline.reorder_many": "Merkmale neu ordnen",
|
||||
"ui.show_construction_geometries": "Konstruktionsgeometrien",
|
||||
"ui.total_angle": "Gesamtwinkel",
|
||||
"sketch_text.builtin_font": "{family} (integriert)",
|
||||
"sketch_text.font_search": "Schriftarten suchen",
|
||||
"sketch_text.loading_fonts": "Installierte Schriftarten werden gesucht…",
|
||||
"sketch_text.use_font_file": "Schriftdatei {path} verwenden",
|
||||
"sketch_text.bold": "Fett",
|
||||
"sketch_text.italic": "Kursiv",
|
||||
"sketch_text.align_left": "Links",
|
||||
"sketch_text.align_center": "Mitte",
|
||||
"sketch_text.align_right": "Rechts",
|
||||
"sketch_text.path_hint": "Eine Skizzenkurve, der der Text folgt: hier klicken, dann die Kurve",
|
||||
"sketch_text.path_clear": "Text von der Kurve lösen",
|
||||
"sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage"
|
||||
}
|
||||
@@ -214,6 +214,7 @@
|
||||
"cmd.sketch_polygon_inscribed": "Inscribed Polygon",
|
||||
"cmd.sketch_project_to_surface": "Project To Surface",
|
||||
"cmd.sketch_rectangle_center": "Center Rectangle",
|
||||
"cmd.sketch_rectangle_rounded": "Rounded Rectangle",
|
||||
"cmd.sketch_rectangle_three_point": "3-Point Rectangle",
|
||||
"cmd.sketch_rectangle_two_point": "2-Point Rectangle",
|
||||
"cmd.sketch_redefine": "Redefine Sketch Plane",
|
||||
@@ -245,6 +246,19 @@
|
||||
"cmd.surface_patch": "Patch",
|
||||
"cmd.surface_stitch": "Stitch",
|
||||
"cmd.surface_thicken": "Thicken",
|
||||
"cmd.surface_extrude": "Surface Extrude",
|
||||
"cmd.surface_revolve": "Surface Revolve",
|
||||
"cmd.surface_sweep": "Surface Sweep",
|
||||
"cmd.surface_loft": "Surface Loft",
|
||||
"cmd.surface_ruled": "Ruled Surface",
|
||||
"cmd.surface_boundary": "Boundary Surface",
|
||||
"cmd.surface_offset": "Offset Surface",
|
||||
"cmd.surface_blend": "Blend Surface",
|
||||
"cmd.surface_fillet": "Surface Fillet",
|
||||
"cmd.surface_untrim": "Untrim",
|
||||
"cmd.surface_unstitch": "Unstitch",
|
||||
"cmd.surface_reverse_normal": "Reverse Normal",
|
||||
"cmd.inspect_continuity": "Continuity Analysis",
|
||||
"cmd.timeline_compute_all": "Compute All",
|
||||
"cmd.timeline_copy": "Copy Features",
|
||||
"cmd.timeline_dependents": "Feature Dependents",
|
||||
@@ -265,6 +279,9 @@
|
||||
"dialog.edit_title": "EDIT {title}",
|
||||
"dialog.select": "↖ Select",
|
||||
"dialog.selected_count": "↖ {count} selected",
|
||||
"dialog.step": "Step",
|
||||
"dialog.selection_step": "{step} of {total}: Select {input}",
|
||||
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
|
||||
"document.save_changes": "Save changes to {name}?",
|
||||
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
|
||||
@@ -285,6 +302,7 @@
|
||||
"ui.agent_cursor": "Agent cursor",
|
||||
"ui.agent_cursor_speed": "Agent cursor speed",
|
||||
"ui.all": "All",
|
||||
"ui.analysis": "Analysis",
|
||||
"ui.angle": "Angle",
|
||||
"ui.angle_decimals": "Angle decimals",
|
||||
"ui.animate_view_changes": "Animate view changes",
|
||||
@@ -310,6 +328,8 @@
|
||||
"ui.chamfer_type": "Chamfer Type",
|
||||
"ui.change_workspace": "Change workspace",
|
||||
"ui.chord_length": "Chord Length",
|
||||
"ui.curvature_g2": "Curvature (G2)",
|
||||
"ui.equivalent_radius": "Equivalent Radius",
|
||||
"ui.circle": "Circle",
|
||||
"ui.circular_pattern": "Circular Pattern",
|
||||
"ui.clear": "Clear",
|
||||
@@ -325,6 +345,7 @@
|
||||
"ui.combine": "Combine",
|
||||
"ui.coming_later": "coming later",
|
||||
"ui.concentric": "Concentric",
|
||||
"ui.configurations": "Configurations",
|
||||
"ui.conic_curve": "Conic Curve",
|
||||
"ui.constant": "Constant",
|
||||
"ui.constraints_conflict": "Constraints conflict",
|
||||
@@ -362,6 +383,7 @@
|
||||
"ui.drawing": "Drawing",
|
||||
"ui.drill_point": "Drill Point",
|
||||
"ui.edit": "Edit",
|
||||
"ui.edit_section_analysis": "Edit Section Analysis",
|
||||
"ui.ellipse": "Ellipse",
|
||||
"ui.end_radius": "End Radius",
|
||||
"ui.entities": "Entities",
|
||||
@@ -374,6 +396,7 @@
|
||||
"ui.extent_type": "Extent Type",
|
||||
"ui.extrude": "Extrude",
|
||||
"ui.factor": "Factor",
|
||||
"ui.feature_history": "Feature history",
|
||||
"ui.feature_options": "Feature Options",
|
||||
"ui.features": "Features",
|
||||
"ui.file": "File",
|
||||
@@ -554,7 +577,6 @@
|
||||
"ui.shell": "Shell",
|
||||
"ui.show": "Show",
|
||||
"ui.show_constraints": "Constraints",
|
||||
"ui.show_construction_geometries": "Construction Geometries",
|
||||
"ui.show_dimensions": "Dimensions",
|
||||
"ui.show_points": "Show Points",
|
||||
"ui.show_profile": "Show Profile",
|
||||
@@ -595,6 +617,9 @@
|
||||
"ui.theme": "Theme",
|
||||
"ui.thickness": "Thickness",
|
||||
"ui.thread": "Thread",
|
||||
"ui.timeline_bottom": "Bottom timeline",
|
||||
"ui.timeline_left_note": "The left layout shows the rollback bar above the parts and scene browser.",
|
||||
"ui.timeline_left_sidebar": "Left sidebar",
|
||||
"ui.tip_angle": "Tip Angle",
|
||||
"ui.to_object": "To Object",
|
||||
"ui.top": "Top",
|
||||
@@ -858,5 +883,79 @@
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).",
|
||||
"cmd.render_backends": "Render Backends",
|
||||
"cmd.view_navigate": "Navigate View"
|
||||
}
|
||||
"cmd.view_navigate": "Navigate View",
|
||||
"ui.animateJoint": "Animate Joint",
|
||||
"ui.calibrate": "Calibrate",
|
||||
"ui.customizeToolbar": "Customize Toolbar…",
|
||||
"ui.editJointLimits": "Edit Joint Limits",
|
||||
"ui.exportDxf": "Export DXF…",
|
||||
"ui.findInTimeline": "Find in Timeline",
|
||||
"ui.findInWindow": "Find in Window",
|
||||
"ui.fit": "Fit",
|
||||
"ui.groupSelected": "Group Selected",
|
||||
"ui.hideAxes": "Hide Axes",
|
||||
"ui.hideDimension": "Hide Dimension",
|
||||
"ui.hideOrigin": "Hide Origin",
|
||||
"ui.hidePlanes": "Hide Planes",
|
||||
"ui.hideProfile": "Hide Profile",
|
||||
"ui.homeView": "Home View",
|
||||
"ui.isolate": "Isolate",
|
||||
"ui.lock": "Lock",
|
||||
"ui.newSketchGroup": "New Sketch Group",
|
||||
"ui.normalConstruction": "Normal / Construction",
|
||||
"ui.orbit": "Orbit",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.pan": "Pan",
|
||||
"ui.pinToShortcuts": "Pin to Shortcuts",
|
||||
"ui.pinToToolbar": "Pin to Toolbar",
|
||||
"ui.properties": "Properties",
|
||||
"ui.removeSection": "Remove Section",
|
||||
"ui.removeFromToolbar": "Remove from Toolbar",
|
||||
"ui.repeat": "Repeat",
|
||||
"ui.report_a_bug": "Report a bug",
|
||||
"ui.restore": "Restore",
|
||||
"ui.heading_setup": "SETUP",
|
||||
"ui.selectOther": "Select Other…",
|
||||
"ui.selectTangentChain": "Select Tangent Chain",
|
||||
"ui.showAll": "Show All",
|
||||
"ui.showAllBodies": "Show All Bodies",
|
||||
"ui.showAxes": "Show Axes",
|
||||
"ui.showDimension": "Show Dimension",
|
||||
"ui.showOrigin": "Show Origin",
|
||||
"ui.showPlanes": "Show Planes",
|
||||
"ui.suppress": "Suppress",
|
||||
"ui.command_palette_hint": "Type a command, e.g. {example} — or press S to search",
|
||||
"ui.unground": "Unground",
|
||||
"ui.unisolate": "Unisolate",
|
||||
"ui.unlock": "Unlock",
|
||||
"ui.unsuppress": "Unsuppress",
|
||||
"ui.updateToCurrentView": "Update to Current View",
|
||||
"ui.zoom": "Zoom",
|
||||
"ui.command_done": "{command} done",
|
||||
"timeline.group": "New Feature Folder",
|
||||
"timeline.ungroup": "Remove Feature Folders",
|
||||
"timeline.rename_group": "Rename Feature Folder",
|
||||
"timeline.move_to_group": "Move Features to Folder",
|
||||
"timeline.move_many": "Move Timeline Items",
|
||||
"timeline.reorder_many": "Reorder Features",
|
||||
"ui.show_construction_geometries": "Construction Geometries",
|
||||
"ui.total_angle": "Total angle",
|
||||
"sketch_text.builtin_font": "{family} (built-in)",
|
||||
"sketch_text.font_search": "Search fonts",
|
||||
"sketch_text.loading_fonts": "Looking for installed fonts…",
|
||||
"sketch_text.use_font_file": "Use the font file {path}",
|
||||
"sketch_text.bold": "Bold",
|
||||
"sketch_text.italic": "Italic",
|
||||
"sketch_text.align_left": "Left",
|
||||
"sketch_text.align_center": "Center",
|
||||
"sketch_text.align_right": "Right",
|
||||
"sketch_text.path_hint": "A curve of the sketch the text follows: click here, then the curve",
|
||||
"sketch_text.path_clear": "Take the text off its curve",
|
||||
"Style": "Style",
|
||||
"Alignment": "Alignment",
|
||||
"Letter Spacing": "Letter Spacing",
|
||||
"Line Spacing": "Line Spacing",
|
||||
"Path": "Path",
|
||||
"sketch_text.on_path": "The text follows its path: the curve places it",
|
||||
"cmd.material_properties_validate":"Validate Physical Material Data"
|
||||
}
|
||||
@@ -214,6 +214,7 @@
|
||||
"cmd.sketch_polygon_inscribed": "内接ポリゴン",
|
||||
"cmd.sketch_project_to_surface": "サーフェスに投影",
|
||||
"cmd.sketch_rectangle_center": "中心指定の長方形",
|
||||
"cmd.sketch_rectangle_rounded": "角丸長方形",
|
||||
"cmd.sketch_rectangle_three_point": "3点指定長方形",
|
||||
"cmd.sketch_rectangle_two_point": "2点指定長方形",
|
||||
"cmd.sketch_redefine": "スケッチ平面を再定義",
|
||||
@@ -245,6 +246,19 @@
|
||||
"cmd.surface_patch": "パッチ",
|
||||
"cmd.surface_stitch": "ステッチ",
|
||||
"cmd.surface_thicken": "厚み付け",
|
||||
"cmd.surface_extrude": "サーフェス押し出し",
|
||||
"cmd.surface_revolve": "サーフェス回転",
|
||||
"cmd.surface_sweep": "サーフェススイープ",
|
||||
"cmd.surface_loft": "サーフェスロフト",
|
||||
"cmd.surface_ruled": "ルールドサーフェス",
|
||||
"cmd.surface_boundary": "境界サーフェス",
|
||||
"cmd.surface_offset": "オフセットサーフェス",
|
||||
"cmd.surface_blend": "ブレンドサーフェス",
|
||||
"cmd.surface_fillet": "サーフェスフィレット",
|
||||
"cmd.surface_untrim": "トリム解除",
|
||||
"cmd.surface_unstitch": "縫い合わせ解除",
|
||||
"cmd.surface_reverse_normal": "法線を反転",
|
||||
"cmd.inspect_continuity": "連続性解析",
|
||||
"cmd.timeline_compute_all": "すべて再計算",
|
||||
"cmd.timeline_copy": "フィーチャーをコピー",
|
||||
"cmd.timeline_dependents": "従属フィーチャー",
|
||||
@@ -265,6 +279,9 @@
|
||||
"dialog.edit_title": "{title}を編集",
|
||||
"dialog.select": "↖ 選択",
|
||||
"dialog.selected_count": "↖ {count} 個選択中",
|
||||
"dialog.step": "ステップ",
|
||||
"dialog.selection_step": "{total} ステップ中 {step}: {input} を選択",
|
||||
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
|
||||
"document.save_changes": "{name} への変更を保存しますか?",
|
||||
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
|
||||
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
|
||||
@@ -285,6 +302,7 @@
|
||||
"ui.agent_cursor": "エージェントカーソル",
|
||||
"ui.agent_cursor_speed": "エージェントカーソル速度",
|
||||
"ui.all": "すべて",
|
||||
"ui.analysis": "解析",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度の小数点以下の桁数",
|
||||
"ui.animate_view_changes": "視点変更をアニメーション表示",
|
||||
@@ -310,6 +328,8 @@
|
||||
"ui.chamfer_type": "面取りタイプ",
|
||||
"ui.change_workspace": "ワークスペースの変更",
|
||||
"ui.chord_length": "弦長",
|
||||
"ui.curvature_g2": "曲率連続 (G2)",
|
||||
"ui.equivalent_radius": "等価半径",
|
||||
"ui.circle": "円",
|
||||
"ui.circular_pattern": "円形パターン",
|
||||
"ui.clear": "クリア",
|
||||
@@ -325,6 +345,7 @@
|
||||
"ui.combine": "結合",
|
||||
"ui.coming_later": "準備中",
|
||||
"ui.concentric": "同心",
|
||||
"ui.configurations": "構成",
|
||||
"ui.conic_curve": "円錐曲線",
|
||||
"ui.constant": "一定",
|
||||
"ui.constraints_conflict": "拘束が競合しています",
|
||||
@@ -362,6 +383,7 @@
|
||||
"ui.drawing": "図面",
|
||||
"ui.drill_point": "ドリル先端",
|
||||
"ui.edit": "編集",
|
||||
"ui.edit_section_analysis": "断面解析を編集",
|
||||
"ui.ellipse": "楕円",
|
||||
"ui.end_radius": "終了半径",
|
||||
"ui.entities": "エンティティ",
|
||||
@@ -374,6 +396,7 @@
|
||||
"ui.extent_type": "範囲タイプ",
|
||||
"ui.extrude": "押し出し",
|
||||
"ui.factor": "倍率",
|
||||
"ui.feature_history": "フィーチャー履歴",
|
||||
"ui.feature_options": "フィーチャーオプション",
|
||||
"ui.features": "フィーチャー",
|
||||
"ui.file": "ファイル",
|
||||
@@ -554,7 +577,6 @@
|
||||
"ui.shell": "シェル",
|
||||
"ui.show": "表示",
|
||||
"ui.show_constraints": "拘束",
|
||||
"ui.show_construction_geometries": "構築ジオメトリ",
|
||||
"ui.show_dimensions": "寸法",
|
||||
"ui.show_points": "点を表示",
|
||||
"ui.show_profile": "プロファイルを表示",
|
||||
@@ -595,6 +617,9 @@
|
||||
"ui.theme": "テーマ",
|
||||
"ui.thickness": "厚さ",
|
||||
"ui.thread": "ねじ",
|
||||
"ui.timeline_bottom": "下部タイムライン",
|
||||
"ui.timeline_left_note": "左レイアウトでは、ロールバックバーがパーツとシーンブラウザーの上に表示されます。",
|
||||
"ui.timeline_left_sidebar": "左サイドバー",
|
||||
"ui.tip_angle": "先端角度",
|
||||
"ui.to_object": "オブジェクトまで",
|
||||
"ui.top": "上面",
|
||||
@@ -858,5 +883,79 @@
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "ビュー上で中ボタンをダブルクリックした時の動作: ホーム(全体を収めたホームビュー)、全体表示(向きを維持して全体を収める)、または無効",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。中ボタンをダブルクリックするとホームビューに戻ります(環境設定 › ナビゲーション)。",
|
||||
"cmd.render_backends": "レンダリング バックエンド",
|
||||
"cmd.view_navigate": "ビューナビゲーション"
|
||||
}
|
||||
"cmd.view_navigate": "ビューナビゲーション",
|
||||
"ui.animateJoint": "ジョイントをアニメート",
|
||||
"ui.calibrate": "校正",
|
||||
"ui.customizeToolbar": "ツールバーをカスタマイズ…",
|
||||
"ui.editJointLimits": "ジョイント制限を編集",
|
||||
"ui.exportDxf": "DXF をエクスポート…",
|
||||
"ui.findInTimeline": "タイムラインで検索",
|
||||
"ui.findInWindow": "ウィンドウで検索",
|
||||
"ui.fit": "フィット",
|
||||
"ui.groupSelected": "選択をグループ化",
|
||||
"ui.hideAxes": "軸を非表示",
|
||||
"ui.hideDimension": "寸法を非表示",
|
||||
"ui.hideOrigin": "原点を非表示",
|
||||
"ui.hidePlanes": "平面を非表示",
|
||||
"ui.hideProfile": "プロファイルを非表示",
|
||||
"ui.homeView": "ホームビュー",
|
||||
"ui.isolate": "分離表示",
|
||||
"ui.lock": "ロック",
|
||||
"ui.newSketchGroup": "新しいスケッチグループ",
|
||||
"ui.normalConstruction": "通常 / コンストラクション",
|
||||
"ui.orbit": "回転",
|
||||
"ui.heading_position": "配置",
|
||||
"ui.pan": "パン",
|
||||
"ui.pinToShortcuts": "ショートカットに固定",
|
||||
"ui.pinToToolbar": "ツールバーに固定",
|
||||
"ui.properties": "プロパティ",
|
||||
"ui.removeSection": "セクションを解除",
|
||||
"ui.removeFromToolbar": "ツールバーから削除",
|
||||
"ui.repeat": "繰り返し",
|
||||
"ui.report_a_bug": "不具合を報告",
|
||||
"ui.restore": "復元",
|
||||
"ui.heading_setup": "セットアップ",
|
||||
"ui.selectOther": "その他を選択…",
|
||||
"ui.selectTangentChain": "接線チェーンを選択",
|
||||
"ui.showAll": "すべて表示",
|
||||
"ui.showAllBodies": "すべてのボディを表示",
|
||||
"ui.showAxes": "軸を表示",
|
||||
"ui.showDimension": "寸法を表示",
|
||||
"ui.showOrigin": "原点を表示",
|
||||
"ui.showPlanes": "平面を表示",
|
||||
"ui.suppress": "抑制",
|
||||
"ui.command_palette_hint": "コマンドを入力(例: {example})— または S キーで検索",
|
||||
"ui.unground": "固定を解除",
|
||||
"ui.unisolate": "分離表示を解除",
|
||||
"ui.unlock": "ロック解除",
|
||||
"ui.unsuppress": "抑制を解除",
|
||||
"ui.updateToCurrentView": "現在のビューに更新",
|
||||
"ui.zoom": "ズーム",
|
||||
"ui.command_done": "{command} 完了",
|
||||
"timeline.group": "フィーチャーフォルダを新規作成",
|
||||
"timeline.ungroup": "フィーチャーフォルダを削除",
|
||||
"timeline.rename_group": "フィーチャーフォルダの名前を変更",
|
||||
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
|
||||
"timeline.move_many": "タイムライン項目を移動",
|
||||
"timeline.reorder_many": "フィーチャーの順序を変更",
|
||||
"ui.show_construction_geometries": "構築ジオメトリ",
|
||||
"ui.total_angle": "合計角度",
|
||||
"sketch_text.builtin_font": "{family}(内蔵)",
|
||||
"sketch_text.font_search": "フォントを検索",
|
||||
"sketch_text.loading_fonts": "インストール済みフォントを検索中…",
|
||||
"sketch_text.use_font_file": "フォント ファイル {path} を使用",
|
||||
"sketch_text.bold": "太字",
|
||||
"sketch_text.italic": "斜体",
|
||||
"sketch_text.align_left": "左",
|
||||
"sketch_text.align_center": "中央",
|
||||
"sketch_text.align_right": "右",
|
||||
"sketch_text.path_hint": "文字が沿うスケッチの曲線: ここをクリックしてから曲線をクリック",
|
||||
"sketch_text.path_clear": "文字を曲線から外す",
|
||||
"Style": "スタイル",
|
||||
"Alignment": "配置",
|
||||
"Letter Spacing": "文字間隔",
|
||||
"Line Spacing": "行間",
|
||||
"Path": "パス",
|
||||
"sketch_text.on_path": "文字はパスに沿います: 位置は曲線で決まります",
|
||||
"cmd.material_properties_validate":"物理材料データを検証"
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
{
|
||||
"browser.body_name": "바디{number}",
|
||||
"browser.component_name": "컴포넌트{number}",
|
||||
"browser.parameters": "매개변수 ({count})",
|
||||
"browser.precision": "정밀도: {precision}",
|
||||
"browser.sketch_name": "스케치{number}",
|
||||
"browser.units": "단위: {units}",
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.select": "↖ 선택",
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"dialog.step": "단계",
|
||||
"dialog.selection_step": "{step}/{total} 단계: {input} 선택",
|
||||
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
|
||||
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
|
||||
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
|
||||
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
|
||||
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
|
||||
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
|
||||
"settings.reset_intro": "다음 설정은 이 컴퓨터에서 작동하지 않아 SolidCraft를 시작할 수 없게 하므로 변경되었습니다:",
|
||||
"settings.reset_title": "설정이 변경되었습니다",
|
||||
"sketch.degrees_of_freedom": "자유도 {count}개",
|
||||
"sketch.draw_curves": "{linetype}(으)로 그리기",
|
||||
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
|
||||
"ui.1_degree_of_freedom": "자유도 1개",
|
||||
"ui.3d_sketch": "3D 스케치",
|
||||
"ui.about": "정보",
|
||||
"ui.activate": "활성화",
|
||||
"ui.active_component": "활성 컴포넌트",
|
||||
"ui.agent_cursor": "에이전트 커서",
|
||||
"ui.agent_cursor_speed": "에이전트 커서 속도",
|
||||
"ui.all": "모두",
|
||||
"ui.analysis": "분석",
|
||||
"ui.angle": "각도",
|
||||
"ui.angle_decimals": "각도 소수점 자릿수",
|
||||
"ui.animate_view_changes": "뷰 변경 애니메이션",
|
||||
"ui.animation": "애니메이션",
|
||||
"ui.animation_speed": "애니메이션 속도",
|
||||
"ui.anti_aliasing": "안티앨리어싱",
|
||||
"ui.apply": "적용",
|
||||
"ui.arc": "호",
|
||||
"ui.assemble": "조립",
|
||||
"ui.autosave_every": "자동 저장 간격",
|
||||
"ui.axes": "축",
|
||||
"ui.axis": "축",
|
||||
"ui.base_point": "기준점",
|
||||
"ui.bodies": "바디",
|
||||
"ui.box": "박스",
|
||||
"ui.break": "끊기",
|
||||
"ui.cancel": "취소",
|
||||
"ui.canvases": "캔버스",
|
||||
"ui.center": "중심",
|
||||
"ui.centerline": "중심선",
|
||||
"ui.chain": "체인",
|
||||
"ui.chamfer": "모따기",
|
||||
"ui.chamfer_type": "모따기 유형",
|
||||
"ui.change_workspace": "작업 공간 변경",
|
||||
"ui.chord_length": "현 길이",
|
||||
"ui.curvature_g2": "곡률 연속 (G2)",
|
||||
"ui.equivalent_radius": "등가 반지름",
|
||||
"ui.circle": "원",
|
||||
"ui.circular_pattern": "원형 패턴",
|
||||
"ui.clear_recent": "최근 항목 지우기",
|
||||
"ui.clear_the_selection": "선택 해제",
|
||||
"ui.close": "닫기",
|
||||
"ui.coil": "코일",
|
||||
"ui.coincident": "일치",
|
||||
"ui.collapse": "접기",
|
||||
"ui.collinear": "동일 선상",
|
||||
"ui.combine": "결합",
|
||||
"ui.coming_later": "추후 지원 예정",
|
||||
"ui.concentric": "동심",
|
||||
"ui.configurations": "구성",
|
||||
"ui.conic_curve": "원뿔 곡선",
|
||||
"ui.constant": "상수",
|
||||
"ui.constraints_conflict": "구속 조건 충돌",
|
||||
"ui.construct": "구성",
|
||||
"ui.construction": "구성선",
|
||||
"ui.copy": "복사",
|
||||
"ui.corners": "코너",
|
||||
"ui.corner": "코너",
|
||||
"ui.counterbore": "카운터보어",
|
||||
"ui.countersink": "카운터싱크",
|
||||
"ui.create": "만들기",
|
||||
"ui.create_copy": "복사본 만들기",
|
||||
"ui.create_sketch": "스케치 만들기",
|
||||
"ui.current": "현재",
|
||||
"ui.curvature": "곡률",
|
||||
"ui.curves": "곡선",
|
||||
"ui.cut": "잘라내기",
|
||||
"ui.cylinder": "원통",
|
||||
"ui.dark": "어둡게",
|
||||
"ui.default_modelling_orientation": "기본 모델링 방향",
|
||||
"ui.degrees_of_freedom": "자유도",
|
||||
"ui.delete": "삭제",
|
||||
"ui.depth": "깊이",
|
||||
"ui.design": "디자인",
|
||||
"ui.diameter": "지름",
|
||||
"ui.dimension": "치수",
|
||||
"ui.direction": "방향",
|
||||
"ui.distance": "거리",
|
||||
"ui.distance_1": "거리 1",
|
||||
"ui.distance_2": "거리 2",
|
||||
"ui.document_settings": "문서 설정",
|
||||
"ui.don_t_save": "저장 안 함",
|
||||
"ui.draft": "구배",
|
||||
"ui.drawing": "도면",
|
||||
"ui.drill_point": "드릴 선단",
|
||||
"ui.edit": "편집",
|
||||
"ui.edit_section_analysis": "단면 분석 편집",
|
||||
"ui.ellipse": "타원",
|
||||
"ui.end_radius": "끝 반지름",
|
||||
"ui.entities": "요소",
|
||||
"ui.equal": "동일",
|
||||
"ui.expand": "펼치기",
|
||||
"ui.export_ellipsis": "내보내기...",
|
||||
"ui.extend": "연장",
|
||||
"ui.extent_type": "범위 유형",
|
||||
"ui.extrude": "돌출",
|
||||
"ui.factor": "배율",
|
||||
"ui.feature_history": "피처 기록",
|
||||
"ui.feature_options": "피처 옵션",
|
||||
"ui.features": "피처",
|
||||
"ui.file": "파일",
|
||||
"ui.fillet": "필렛",
|
||||
"ui.finish_sketch": "스케치 종료",
|
||||
"ui.fix_unfix": "고정/고정 해제",
|
||||
"ui.flat": "플랫",
|
||||
"ui.flip": "뒤집기",
|
||||
"ui.front": "정면",
|
||||
"ui.fully_constrained": "완전 구속",
|
||||
"ui.general": "일반",
|
||||
"ui.graphics": "그래픽",
|
||||
"ui.heading_assemble": "조립",
|
||||
"ui.heading_box": "박스",
|
||||
"ui.heading_chamfer": "모따기",
|
||||
"ui.heading_circular_pattern": "원형 패턴",
|
||||
"ui.heading_coil": "코일",
|
||||
"ui.heading_combine": "결합",
|
||||
"ui.heading_construct": "구성",
|
||||
"ui.heading_create": "만들기",
|
||||
"ui.heading_create_sketch": "스케치 만들기",
|
||||
"ui.heading_cylinder": "원통",
|
||||
"ui.heading_design": "디자인",
|
||||
"ui.heading_draft": "구배",
|
||||
"ui.heading_edit": "편집",
|
||||
"ui.heading_extrude": "돌출",
|
||||
"ui.heading_fillet": "필렛",
|
||||
"ui.heading_finish_sketch": "스케치 종료",
|
||||
"ui.heading_front": "정면",
|
||||
"ui.heading_hole": "구멍",
|
||||
"ui.heading_home": "홈",
|
||||
"ui.heading_insert": "삽입",
|
||||
"ui.heading_inspect": "검사",
|
||||
"ui.heading_loft": "로프트",
|
||||
"ui.heading_measure": "측정",
|
||||
"ui.heading_mesh": "메시",
|
||||
"ui.heading_mirror": "대칭 복사",
|
||||
"ui.heading_modify": "수정",
|
||||
"ui.heading_move": "이동",
|
||||
"ui.heading_offset": "오프셋",
|
||||
"ui.heading_move_copy": "이동/복사",
|
||||
"ui.heading_offset_plane": "오프셋 평면",
|
||||
"ui.heading_ok": "확인",
|
||||
"ui.heading_parameters": "매개변수",
|
||||
"ui.heading_pattern_on_path": "경로 패턴",
|
||||
"ui.heading_pipe": "파이프",
|
||||
"ui.heading_plane_at_angle": "각도 평면",
|
||||
"ui.heading_plastic": "플라스틱",
|
||||
"ui.heading_rectangular_pattern": "직사각형 패턴",
|
||||
"ui.heading_revolve": "회전",
|
||||
"ui.heading_right": "우측면",
|
||||
"ui.heading_scale": "축척",
|
||||
"ui.heading_section_analysis": "단면 분석",
|
||||
"ui.heading_select": "선택",
|
||||
"ui.heading_sheet_metal": "판금",
|
||||
"ui.heading_shell": "쉘",
|
||||
"ui.heading_sketch": "스케치",
|
||||
"ui.heading_sketch_palette": "스케치 팔레트",
|
||||
"ui.heading_sketch_scale": "스케치 배율",
|
||||
"ui.heading_solid": "솔리드",
|
||||
"ui.heading_sphere": "구",
|
||||
"ui.heading_split_body": "바디 분할",
|
||||
"ui.heading_surface": "서피스",
|
||||
"ui.heading_sweep": "스윕",
|
||||
"ui.heading_thread": "나사산",
|
||||
"ui.heading_top": "상면",
|
||||
"ui.heading_torus": "토러스",
|
||||
"ui.help": "도움말",
|
||||
"ui.hide": "숨기기",
|
||||
"ui.hole": "구멍",
|
||||
"ui.hole_tap_type": "탭 유형",
|
||||
"ui.hole_type": "구멍 유형",
|
||||
"ui.home": "홈",
|
||||
"ui.horizontal_vertical": "수평/수직",
|
||||
"ui.insert": "삽입",
|
||||
"ui.inspect": "검사",
|
||||
"ui.intersect": "교차",
|
||||
"ui.join": "접합",
|
||||
"ui.joints": "조인트",
|
||||
"ui.keyboard_shortcuts": "키보드 단축키",
|
||||
"ui.language": "언어",
|
||||
"ui.length_decimals": "길이 소수점 자릿수",
|
||||
"ui.light": "밝게",
|
||||
"ui.line": "선",
|
||||
"ui.linetype": "선 종류",
|
||||
"ui.loft": "로프트",
|
||||
"ui.look_at": "바라보기",
|
||||
"ui.manufacture": "제조",
|
||||
"ui.measure": "측정",
|
||||
"ui.midpoint": "중간점",
|
||||
"ui.mirror": "대칭 복사",
|
||||
"ui.mirror_line": "대칭선",
|
||||
"ui.modify": "수정",
|
||||
"ui.move": "이동",
|
||||
"ui.multiple": "다중",
|
||||
"ui.named_views": "명명된 뷰",
|
||||
"ui.navigation": "탐색",
|
||||
"ui.needs_design": "{command}: 먼저 디자인을 열어 주세요",
|
||||
"ui.new_body": "새 바디",
|
||||
"ui.new_design": "새 디자인",
|
||||
"ui.normal": "일반",
|
||||
"ui.object_type": "객체 유형",
|
||||
"ui.objects": "객체",
|
||||
"ui.offer_to_stand_up_y_up_files": "Y-up 파일을 Z-up으로 세우도록 제안",
|
||||
"ui.offset": "오프셋",
|
||||
"ui.offset_plane": "오프셋 평면",
|
||||
"ui.one_side": "한 방향",
|
||||
"ui.open": "열기",
|
||||
"ui.open_ellipsis": "열기...",
|
||||
"ui.open_preferences": "환경 설정 열기",
|
||||
"ui.open_recent": "최근 항목 열기",
|
||||
"ui.operation": "작업",
|
||||
"ui.options": "옵션",
|
||||
"ui.orbit_around": "궤도 회전 기준",
|
||||
"ui.origin": "원점",
|
||||
"ui.palette_points": "점",
|
||||
"ui.palette_profile": "프로파일",
|
||||
"ui.pan_zoom_orbit": "이동, 확대/축소, 궤도 회전",
|
||||
"ui.parallel": "평행",
|
||||
"ui.parameters": "매개변수",
|
||||
"ui.paste": "붙여넣기",
|
||||
"ui.path": "경로",
|
||||
"ui.pattern_on_path": "경로 패턴",
|
||||
"ui.perpendicular": "수직",
|
||||
"ui.pipe": "파이프",
|
||||
"ui.placement": "배치",
|
||||
"ui.plane": "평면",
|
||||
"ui.plane_at_angle": "각도 평면",
|
||||
"ui.point": "점",
|
||||
"ui.point_under_the_cursor": "커서 아래의 점",
|
||||
"ui.points": "점",
|
||||
"ui.polygon": "다각형",
|
||||
"ui.preferences": "환경 설정",
|
||||
"ui.profile": "프로파일",
|
||||
"ui.profile_plane": "프로파일 평면",
|
||||
"ui.profiles": "프로파일",
|
||||
"ui.project": "투영",
|
||||
"ui.quantity_2": "수량 2",
|
||||
"ui.quantity": "수량",
|
||||
"ui.quit": "종료",
|
||||
"ui.radius": "반지름",
|
||||
"ui.radius_type": "반지름 유형",
|
||||
"ui.rectangle": "직사각형",
|
||||
"ui.rectangular_pattern": "직사각형 패턴",
|
||||
"ui.redo": "다시 실행",
|
||||
"ui.render": "렌더",
|
||||
"ui.reset_the_settings_on_this_page": "이 페이지의 설정 초기화",
|
||||
"ui.restore_defaults": "기본값 복원",
|
||||
"ui.reverse_zoom_direction": "확대/축소 방향 반전",
|
||||
"ui.revolve": "회전",
|
||||
"ui.right": "우측면",
|
||||
"ui.save": "저장",
|
||||
"ui.save_as": "다른 이름으로 저장",
|
||||
"ui.save_as_ellipsis": "다른 이름으로 저장...",
|
||||
"ui.scale": "축척",
|
||||
"ui.section_analysis": "단면 분석",
|
||||
"ui.select": "선택",
|
||||
"ui.selection": "선택",
|
||||
"ui.shell": "쉘",
|
||||
"ui.show": "표시",
|
||||
"ui.show_constraints": "구속 조건",
|
||||
"ui.show_dimensions": "치수",
|
||||
"ui.show_points": "점 표시",
|
||||
"ui.show_profile": "프로파일 표시",
|
||||
"ui.show_projected_geometries": "투영된 형상",
|
||||
"ui.simple": "단순",
|
||||
"ui.simulation": "시뮬레이션",
|
||||
"ui.single": "단일",
|
||||
"ui.sketch": "스케치",
|
||||
"ui.sketch_dimension": "스케치 치수",
|
||||
"ui.sketch_grid": "스케치 격자",
|
||||
"ui.sketch_scale": "스케치 축척",
|
||||
"ui.sketch_status": "스케치 상태",
|
||||
"ui.sketches": "스케치",
|
||||
"ui.slice": "슬라이스",
|
||||
"ui.slot": "슬롯",
|
||||
"ui.smooth": "부드럽게",
|
||||
"ui.snap": "스냅",
|
||||
"ui.spacing_2": "간격 2",
|
||||
"ui.spacing": "간격",
|
||||
"ui.sphere": "구",
|
||||
"ui.spline": "스플라인",
|
||||
"ui.split_body": "바디 분할",
|
||||
"ui.start": "시작",
|
||||
"ui.start_radius": "시작 반지름",
|
||||
"ui.sweep": "스윕",
|
||||
"ui.symmetric": "대칭",
|
||||
"ui.symmetry": "대칭",
|
||||
"ui.system": "시스템",
|
||||
"ui.tangent": "접선",
|
||||
"ui.taper_angle": "테이퍼 각도",
|
||||
"ui.tapped": "탭",
|
||||
"ui.text": "텍스트",
|
||||
"ui.theme": "테마",
|
||||
"ui.thickness": "두께",
|
||||
"ui.thread": "나사산",
|
||||
"ui.timeline_bottom": "하단 타임라인",
|
||||
"ui.timeline_left_note": "왼쪽 레이아웃에서는 롤백 바가 부품 및 장면 브라우저 위에 표시됩니다.",
|
||||
"ui.timeline_left_sidebar": "왼쪽 사이드바",
|
||||
"ui.tip_angle": "선단 각도",
|
||||
"ui.to_object": "객체까지",
|
||||
"ui.top": "상면",
|
||||
"ui.torus": "토러스",
|
||||
"ui.trim": "트림",
|
||||
"ui.two_sides": "양방향",
|
||||
"ui.ui_scale": "UI 배율",
|
||||
"ui.undo": "실행 취소",
|
||||
"ui.units": "단위",
|
||||
"ui.units_of_new_designs": "새 디자인의 단위",
|
||||
"ui.untitled": "제목 없음",
|
||||
"ui.variable": "가변",
|
||||
"ui.view_centre": "뷰 중심",
|
||||
"ui.visual_style": "시각 스타일",
|
||||
"ui.workspace": "작업 공간",
|
||||
"ui.y_distance": "Y 거리",
|
||||
"ui.x_distance": "X 거리",
|
||||
"ui.zoom_to_the_cursor": "커서 위치로 확대/축소",
|
||||
"ui.animateJoint": "조인트 애니메이션",
|
||||
"ui.calibrate": "캘리브레이션",
|
||||
"ui.customizeToolbar": "도구 모음 사용자 지정…",
|
||||
"ui.editJointLimits": "조인트 제한 편집",
|
||||
"ui.exportDxf": "DXF 내보내기…",
|
||||
"ui.findInTimeline": "타임라인에서 찾기",
|
||||
"ui.findInWindow": "창에서 찾기",
|
||||
"ui.fit": "맞춤",
|
||||
"ui.groupSelected": "선택 항목 그룹화",
|
||||
"ui.hideAxes": "축 숨기기",
|
||||
"ui.hideDimension": "치수 숨기기",
|
||||
"ui.hideOrigin": "원점 숨기기",
|
||||
"ui.hidePlanes": "평면 숨기기",
|
||||
"ui.hideProfile": "윤곽 숨기기",
|
||||
"ui.homeView": "홈 보기",
|
||||
"ui.isolate": "분리 표시",
|
||||
"ui.lock": "잠금",
|
||||
"ui.newSketchGroup": "새 스케치 그룹",
|
||||
"ui.normalConstruction": "일반 / 생성",
|
||||
"ui.orbit": "회전",
|
||||
"ui.heading_position": "배치",
|
||||
"ui.pan": "이동",
|
||||
"ui.pinToShortcuts": "바로 가기에 고정",
|
||||
"ui.pinToToolbar": "도구 모음에 고정",
|
||||
"ui.properties": "속성",
|
||||
"ui.removeSection": "단면 제거",
|
||||
"ui.removeFromToolbar": "도구 모음에서 제거",
|
||||
"ui.repeat": "반복",
|
||||
"ui.report_a_bug": "버그 신고",
|
||||
"ui.restore": "복원",
|
||||
"ui.heading_setup": "설정",
|
||||
"ui.selectOther": "기타 선택…",
|
||||
"ui.selectTangentChain": "접선 체인 선택",
|
||||
"ui.showAll": "모두 표시",
|
||||
"ui.showAllBodies": "모두 표시",
|
||||
"ui.showAxes": "축 표시",
|
||||
"ui.showDimension": "치수 표시",
|
||||
"ui.showOrigin": "원점 표시",
|
||||
"ui.showPlanes": "평면 표시",
|
||||
"ui.suppress": "비활성화",
|
||||
"ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
|
||||
"ui.unground": "고정 해제",
|
||||
"ui.unisolate": "분리 표시 해제",
|
||||
"ui.unlock": "잠금 해제",
|
||||
"ui.unsuppress": "비활성화 해제",
|
||||
"ui.updateToCurrentView": "현재 보기로 업데이트",
|
||||
"ui.zoom": "확대/축소",
|
||||
"ui.command_done": "{command} 완료",
|
||||
"cmd.appearance_assign": "외관",
|
||||
"cmd.appearance_library": "외관 라이브러리",
|
||||
"cmd.appearance_list": "외관 목록",
|
||||
"cmd.body_rename": "바디 이름 변경",
|
||||
"cmd.browser_group": "새 그룹",
|
||||
"cmd.browser_move": "브라우저 항목 이동",
|
||||
"cmd.browser_order": "브라우저 항목 정렬",
|
||||
"cmd.browser_rename_group": "그룹 이름 변경",
|
||||
"cmd.browser_ungroup": "그룹 해제",
|
||||
"cmd.browser_visibility": "표시/숨기기",
|
||||
"cmd.canvas_calibrate": "캔버스 보정",
|
||||
"cmd.canvas_decal": "데칼",
|
||||
"cmd.canvas_delete": "캔버스 삭제",
|
||||
"cmd.canvas_edit": "캔버스 편집",
|
||||
"cmd.canvas_insert": "캔버스",
|
||||
"cmd.component_activate": "구성 요소 활성화",
|
||||
"cmd.component_break_link": "링크 끊기",
|
||||
"cmd.component_capture_position": "위치 캡처",
|
||||
"cmd.component_create": "새 구성 요소",
|
||||
"cmd.component_delete": "구성 요소 삭제",
|
||||
"cmd.component_from_bodies": "바디로 구성 요소 만들기",
|
||||
"cmd.component_insert_design": "설계 삽입",
|
||||
"cmd.component_links": "링크 목록",
|
||||
"cmd.component_list": "구성 요소 목록",
|
||||
"cmd.component_move_bodies": "바디를 구성 요소로 이동",
|
||||
"cmd.component_move_sketches": "스케치를 구성 요소로 이동",
|
||||
"cmd.component_paste_new": "새로 붙여넣기",
|
||||
"cmd.component_properties": "구성 요소 물리 속성",
|
||||
"cmd.component_rename": "구성 요소 이름 변경",
|
||||
"cmd.component_revert_position": "위치 복원",
|
||||
"cmd.component_update_links": "링크 업데이트",
|
||||
"cmd.config_activate": "구성 활성화",
|
||||
"cmd.config_column": "구성 열",
|
||||
"cmd.config_configure": "구성",
|
||||
"cmd.config_preview": "구성 미리 보기",
|
||||
"cmd.config_row": "구성 행",
|
||||
"cmd.config_rule": "구성 규칙",
|
||||
"cmd.config_rules": "구성 규칙",
|
||||
"cmd.config_table": "구성 표시",
|
||||
"cmd.construct_axis_edge": "모서리를 통과하는 축",
|
||||
"cmd.construct_axis_normal_to_face": "면에 수직인 축",
|
||||
"cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축",
|
||||
"cmd.construct_axis_two_planes": "두 평면을 통과하는 축",
|
||||
"cmd.construct_axis_two_points": "두 점을 통과하는 축",
|
||||
"cmd.construct_plane_along_path": "경로를 따르는 평면",
|
||||
"cmd.construct_plane_midplane": "중간 평면",
|
||||
"cmd.construct_plane_perpendicular": "수직 평면",
|
||||
"cmd.construct_plane_tangent": "접선 평면",
|
||||
"cmd.construct_plane_three_points": "세 점을 통과하는 평면",
|
||||
"cmd.construct_plane_two_edges": "두 모서리를 통과하는 평면",
|
||||
"cmd.construct_point_along_path": "경로를 따르는 점",
|
||||
"cmd.construct_point_center": "원/구/토러스 중심의 점",
|
||||
"cmd.construct_point_edge_plane": "모서리와 평면의 점",
|
||||
"cmd.construct_point_three_planes": "세 평면이 만나는 점",
|
||||
"cmd.construct_point_two_edges": "두 모서리가 만나는 점",
|
||||
"cmd.construct_point_vertex": "꼭짓점의 점",
|
||||
"cmd.contact_check": "접촉 확인",
|
||||
"cmd.contact_create": "새 접촉 집합",
|
||||
"cmd.contact_disable_all": "접촉 비활성화",
|
||||
"cmd.contact_edit": "접촉 집합 편집",
|
||||
"cmd.contact_enable_all": "모든 접촉 활성화",
|
||||
"cmd.contact_enable_sets": "접촉 집합 활성화",
|
||||
"cmd.contact_list": "접촉 집합 목록",
|
||||
"cmd.diagnostics_crash_report": "충돌 보고서",
|
||||
"cmd.diagnostics_crash_reports": "충돌 보고서",
|
||||
"cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제",
|
||||
"cmd.diagnostics_crash_test": "충돌 테스트",
|
||||
"cmd.doc_recover": "설계 복구",
|
||||
"cmd.doc_recovery_discard": "복구된 설계 폐기",
|
||||
"cmd.doc_recovery_list": "복구 가능한 설계",
|
||||
"cmd.document_inspect": "설계 검사",
|
||||
"cmd.document_units": "설계 단위 변경",
|
||||
"cmd.edit_checkpoint": "체크포인트",
|
||||
"cmd.edit_restore_checkpoint": "체크포인트 복원",
|
||||
"cmd.engine_commands": "명령 목록",
|
||||
"cmd.explode_create": "분해 보기",
|
||||
"cmd.explode_delete": "분해 보기 삭제",
|
||||
"cmd.explode_list": "분해 보기 목록",
|
||||
"cmd.explode_show": "분해 보기 표시",
|
||||
"cmd.expr_evaluate": "식 평가",
|
||||
"cmd.face_delete": "면 삭제",
|
||||
"cmd.file_export": "내보내기",
|
||||
"cmd.file_insert_mesh": "메쉬 삽입",
|
||||
"cmd.file_insert_step": "STEP 삽입",
|
||||
"cmd.file_save_mesh": "메쉬로 저장",
|
||||
"cmd.inspect_accessibility": "접근성 분석",
|
||||
"cmd.inspect_center_of_mass": "무게중심",
|
||||
"cmd.inspect_curvature_comb": "곡률빗 분석",
|
||||
"cmd.inspect_curvature_map": "곡률맵 분석",
|
||||
"cmd.inspect_draft": "블록각 분석",
|
||||
"cmd.inspect_environment_map": "환경 맵 분석",
|
||||
"cmd.inspect_interference": "간섭",
|
||||
"cmd.inspect_isocurve": "등가곡선 분석",
|
||||
"cmd.inspect_minimum_radius": "최소 반경 분석",
|
||||
"cmd.inspect_zebra": "얼룩말줄무늬 분석",
|
||||
"cmd.joint_as_built": "조립 상태 관절",
|
||||
"cmd.joint_create": "관절",
|
||||
"cmd.joint_delete": "관절 삭제",
|
||||
"cmd.joint_drive": "관절 구동",
|
||||
"cmd.joint_edit": "관절 편집",
|
||||
"cmd.joint_limits": "관절 제한",
|
||||
"cmd.joint_list": "관절 목록",
|
||||
"cmd.joint_motion_link": "모션 링크",
|
||||
"cmd.joint_origin": "관절 원점",
|
||||
"cmd.joint_rigid_group": "강체 그룹",
|
||||
"cmd.joint_solve": "관절 해석",
|
||||
"cmd.material_assign": "물리 재질",
|
||||
"cmd.material_create": "새 물리 재질",
|
||||
"cmd.material_delete": "물리 재질 삭제",
|
||||
"cmd.material_edit": "물리 재질 편집",
|
||||
"cmd.material_list": "재질 목록",
|
||||
"cmd.mesh_convert": "메쉬 변환",
|
||||
"cmd.model_edges": "모서리 목록",
|
||||
"cmd.model_faces": "면 목록",
|
||||
"cmd.model_threads": "나사산 목록",
|
||||
"cmd.motion_delete": "운동 연구 삭제",
|
||||
"cmd.motion_export": "운동 연구 내보내기",
|
||||
"cmd.motion_list": "운동 연구 목록",
|
||||
"cmd.motion_play": "운동 연구 재생",
|
||||
"cmd.motion_study": "운동 연구",
|
||||
"cmd.occurrence_copy": "붙여넣기(인스턴스)",
|
||||
"cmd.occurrence_ground": "고정",
|
||||
"cmd.occurrence_move": "인스턴스 이동",
|
||||
"cmd.occurrence_properties": "인스턴스 물리 속성",
|
||||
"cmd.parameters_add": "사용자 매개 변수 추가",
|
||||
"cmd.parameters_change": "매개 변수 변경",
|
||||
"cmd.parameters_comment": "매개 변수에 설명 추가",
|
||||
"cmd.parameters_complete": "식 완성",
|
||||
"cmd.parameters_delete": "매개 변수 삭제",
|
||||
"cmd.parameters_export": "매개 변수 내보내기",
|
||||
"cmd.parameters_favorite": "즐겨찾는 매개 변수",
|
||||
"cmd.parameters_graph": "매개 변수 그래프",
|
||||
"cmd.parameters_import": "매개 변수 가져오기",
|
||||
"cmd.parameters_list": "매개 변수 목록",
|
||||
"cmd.parameters_rename": "매개 변수 이름 변경",
|
||||
"cmd.parameters_users": "매개 변수 사용처",
|
||||
"cmd.parts_fastener": "체결 부품 삽입",
|
||||
"cmd.parts_insert": "부품 삽입",
|
||||
"cmd.parts_library": "표준 부품",
|
||||
"cmd.parts_resize": "부품 크기 변경",
|
||||
"cmd.plastic_assign_rule": "플라스틱 규칙 지정",
|
||||
"cmd.plastic_boss": "보스",
|
||||
"cmd.plastic_lip": "리프",
|
||||
"cmd.plastic_manage_rules": "플라스틱 규칙 관리",
|
||||
"cmd.plastic_rest": "레스트",
|
||||
"cmd.plastic_rules": "플라스틱 규칙 목록",
|
||||
"cmd.plastic_snap_fit": "스냅핏",
|
||||
"cmd.select_clear": "선택 지우기",
|
||||
"cmd.sheet_convert": "판금으로 변환",
|
||||
"cmd.sheet_export_bend_table": "벤딩 표 내보내기",
|
||||
"cmd.sheet_export_dxf": "전개도를 DXF로 내보내기",
|
||||
"cmd.sheet_flange": "플랜지",
|
||||
"cmd.sheet_flat_pattern": "전개도 만들기",
|
||||
"cmd.sheet_fold": "접기",
|
||||
"cmd.sheet_hem": "헴",
|
||||
"cmd.sheet_manage_rules": "판금 규칙",
|
||||
"cmd.sheet_refold": "다시 접기",
|
||||
"cmd.sheet_rules": "판금 규칙 목록",
|
||||
"cmd.sheet_unfold": "펼치기",
|
||||
"cmd.sketch_arc_center_point": "중심점 호",
|
||||
"cmd.sketch_arc_tangent": "접선 호",
|
||||
"cmd.sketch_arc_three_point": "3점 호",
|
||||
"cmd.sketch_auto_constrain": "자동 구속",
|
||||
"cmd.sketch_auto_constrain_from_datum": "기준으로 자동 구속",
|
||||
"cmd.sketch_auto_project": "참조에 모서리 자동 투영",
|
||||
"cmd.sketch_blend_curve": "블렌드 곡선",
|
||||
"cmd.sketch_chain": "연속",
|
||||
"cmd.sketch_chamfer_distance_angle": "거리와 각도 모따기",
|
||||
"cmd.sketch_chamfer_equal_distance": "등거리 모따기",
|
||||
"cmd.sketch_chamfer_two_distance": "두 거리 모따기",
|
||||
"cmd.sketch_circle_center": "중심 지름 원",
|
||||
"cmd.sketch_circle_three_point": "3점 원",
|
||||
"cmd.sketch_circle_three_tangent": "3접선 원",
|
||||
"cmd.sketch_circle_two_point": "2점 원",
|
||||
"cmd.sketch_circle_two_tangent": "2접선 원",
|
||||
"cmd.sketch_constrainer": "스케치 구속기",
|
||||
"cmd.sketch_constraint_fix": "고정/고정 해제",
|
||||
"cmd.sketch_constraint_midpoint": "중간점",
|
||||
"cmd.sketch_construction": "일반/구성선",
|
||||
"cmd.sketch_delete": "스케치 개체 삭제",
|
||||
"cmd.sketch_dimension_text": "치수 텍스트 이동",
|
||||
"cmd.sketch_drag": "스케치 기하 드래그",
|
||||
"cmd.sketch_drag_end": "스케치 드래그 종료",
|
||||
"cmd.sketch_draw_dimensioned": "치수가 있는 스케치 개체 그리기",
|
||||
"cmd.sketch_edit": "스케치 편집",
|
||||
"cmd.sketch_edit_text": "텍스트 편집",
|
||||
"cmd.sketch_export_dxf": "DXF로 저장",
|
||||
"cmd.sketch_finish_auto_constrain": "자동 구속으로 완료",
|
||||
"cmd.sketch_fit_curves_to_section": "메시 단면에 곡선 맞추기",
|
||||
"cmd.sketch_glyphs": "구속 기호",
|
||||
"cmd.sketch_include_3d": "3D 기하 포함",
|
||||
"cmd.sketch_insert_dxf": "DXF 삽입",
|
||||
"cmd.sketch_insert_svg": "SVG 삽입",
|
||||
"cmd.sketch_inspect": "스케치 검사",
|
||||
"cmd.sketch_intersection_curve": "교차 곡선",
|
||||
"cmd.sketch_isoparametric_curve": "등매개 곡선",
|
||||
"cmd.sketch_line3d": "3D 선",
|
||||
"cmd.sketch_line_midpoint": "중점 선",
|
||||
"cmd.sketch_move": "스케치 개체 이동/복사",
|
||||
"cmd.sketch_move3d": "3D 점 이동",
|
||||
"cmd.sketch_move_point": "스케치 점 드래그",
|
||||
"cmd.sketch_offset_break": "오프셋 연결 끊기",
|
||||
"cmd.sketch_options": "스케치 팔레트 옵션",
|
||||
"cmd.sketch_point3d": "3D 점",
|
||||
"cmd.sketch_polygon_circumscribed": "외접 다각형",
|
||||
"cmd.sketch_polygon_edge": "변 다각형",
|
||||
"cmd.sketch_polygon_inscribed": "내접 다각형",
|
||||
"cmd.sketch_project_to_surface": "곡면으로 투영",
|
||||
"cmd.sketch_rectangle_center": "중심 사각형",
|
||||
"cmd.sketch_rectangle_three_point": "3점 사각형",
|
||||
"cmd.sketch_rectangle_two_point": "2점 사각형",
|
||||
"cmd.sketch_redefine": "스케치 평면 재정의",
|
||||
"cmd.sketch_slot_arc_center": "중심점 호 슬롯",
|
||||
"cmd.sketch_slot_arc_three_point": "3점 호 슬롯",
|
||||
"cmd.sketch_slot_center_point": "중심점 슬롯",
|
||||
"cmd.sketch_slot_center_to_center": "중심에서 중심 슬롯",
|
||||
"cmd.sketch_slot_overall": "전장 슬롯",
|
||||
"cmd.sketch_snap": "스냅 추론",
|
||||
"cmd.sketch_solve": "스케치 해석",
|
||||
"cmd.sketch_spline3d": "3D 스플라인",
|
||||
"cmd.sketch_spline_control_point": "제어점 스플라인",
|
||||
"cmd.sketch_spline_control_point_5": "제어점 스플라인(5차)",
|
||||
"cmd.sketch_spline_fit_point": "맞춤점 스플라인",
|
||||
"cmd.sketch_spun_profile": "회전 프로파일",
|
||||
"cmd.sketch_toggle_driven": "종속 치수 전환",
|
||||
"cmd.solid_align": "정렬",
|
||||
"cmd.solid_boundary_fill": "경계 채우기",
|
||||
"cmd.solid_bounding_solid": "둘러싸는 솔리드",
|
||||
"cmd.solid_emboss": "엠보스",
|
||||
"cmd.solid_move": "이동/복사",
|
||||
"cmd.solid_offset_face": "면 오프셋",
|
||||
"cmd.solid_press_pull": "밀기/당기기",
|
||||
"cmd.solid_remove": "제거",
|
||||
"cmd.solid_replace_face": "면 교체",
|
||||
"cmd.solid_rib": "리브",
|
||||
"cmd.solid_split_face": "면 분할",
|
||||
"cmd.solid_web": "웹",
|
||||
"cmd.surface_patch": "패치",
|
||||
"cmd.surface_stitch": "재봉",
|
||||
"cmd.surface_thicken": "두께 주기",
|
||||
"cmd.surface_extrude": "서피스 돌출",
|
||||
"cmd.surface_revolve": "서피스 회전",
|
||||
"cmd.surface_sweep": "서피스 스윕",
|
||||
"cmd.surface_loft": "서피스 로프트",
|
||||
"cmd.surface_ruled": "룰드 서피스",
|
||||
"cmd.surface_boundary": "바운더리 서피스",
|
||||
"cmd.surface_offset": "오프셋 서피스",
|
||||
"cmd.surface_blend": "블렌드 서피스",
|
||||
"cmd.surface_fillet": "서피스 필렛",
|
||||
"cmd.surface_untrim": "트림 해제",
|
||||
"cmd.surface_unstitch": "스티치 해제",
|
||||
"cmd.surface_reverse_normal": "법선 반전",
|
||||
"cmd.inspect_continuity": "연속성 분석",
|
||||
"cmd.timeline_compute_all": "모두 다시 계산",
|
||||
"cmd.timeline_copy": "특성 복사",
|
||||
"cmd.timeline_dependents": "특성 종속 항목",
|
||||
"cmd.timeline_inspect": "특성 검사",
|
||||
"cmd.timeline_paste": "특성 붙여넣기",
|
||||
"cmd.timeline_redefine": "특성 편집",
|
||||
"cmd.timeline_rename": "이름 변경",
|
||||
"cmd.timeline_reorder": "특성 순서 변경",
|
||||
"cmd.timeline_roll_to": "이력 마커 이동",
|
||||
"cmd.timeline_rollback": "타임라인 마커 이동",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.view_apply": "이름 있는 보기 복원",
|
||||
"cmd.view_delete": "이름 있는 보기 삭제",
|
||||
"cmd.view_list": "이름 있는 보기 목록",
|
||||
"cmd.view_rename": "이름 있는 보기 이름 변경",
|
||||
"cmd.view_save": "새 이름 있는 보기",
|
||||
"ui.export_ellipsis_keymap": "내보내기…",
|
||||
"ui.heading_configure": "구성",
|
||||
"ui.heading_constraints": "구속 조건",
|
||||
"ui.heading_utilities": "유틸리티",
|
||||
"BROWSER": "브라우저",
|
||||
"Drive Joint": "조인트 구동",
|
||||
"Edit Profile Sketch": "윤곽 스케치 편집",
|
||||
"Find in Browser": "브라우저에서 찾기",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기",
|
||||
"Reset Toolbar": "도구 모음 재설정",
|
||||
"Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)",
|
||||
"Selection filter": "선택 필터",
|
||||
"Revert Position": "위치 복원",
|
||||
"Construction": "구성선",
|
||||
"Faces": "면",
|
||||
"Edges": "모서리",
|
||||
"Vertices": "꼭짓점",
|
||||
"Sketch geometry and profiles": "스케치 기하 및 프로파일",
|
||||
"Sketch curves": "스케치 곡선",
|
||||
"Sketch points": "스케치 점",
|
||||
"Sketch dimensions": "스케치 치수",
|
||||
"Sketch constraints": "스케치 구속 조건",
|
||||
"Flat": "플랫",
|
||||
"cmd.render_backends": "렌더링 백엔드",
|
||||
"cmd.view_navigate": "보기 탐색",
|
||||
"timeline.group": "새 특성 폴더",
|
||||
"timeline.ungroup": "특성 폴더 제거",
|
||||
"timeline.rename_group": "특성 폴더 이름 변경",
|
||||
"timeline.move_to_group": "특성을 폴더로 이동",
|
||||
"timeline.move_many": "타임라인 항목 이동",
|
||||
"timeline.reorder_many": "특성 순서 변경",
|
||||
"ui.show_construction_geometries": "구성 형상",
|
||||
"ui.total_angle": "전진 각도",
|
||||
"sketch_text.builtin_font": "{family}(내장)",
|
||||
"sketch_text.font_search": "글꼴 검색",
|
||||
"sketch_text.loading_fonts": "설치된 글꼴을 찾는 중…",
|
||||
"sketch_text.use_font_file": "글꼴 파일 {path} 사용",
|
||||
"sketch_text.bold": "굵게",
|
||||
"sketch_text.italic": "기울임꼴",
|
||||
"sketch_text.align_left": "왼쪽",
|
||||
"sketch_text.align_center": "가운데",
|
||||
"sketch_text.align_right": "오른쪽",
|
||||
"sketch_text.path_hint": "텍스트가 따라갈 스케치 곡선: 여기를 클릭한 다음 곡선을 클릭",
|
||||
"sketch_text.path_clear": "곡선에서 텍스트 떼기",
|
||||
"sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다"
|
||||
}
|
||||
@@ -0,0 +1,961 @@
|
||||
{
|
||||
"browser.body_name": "实体{number}",
|
||||
"browser.component_name": "组件{number}",
|
||||
"browser.parameters": "参数 ({count})",
|
||||
"browser.precision": "精度: {precision}",
|
||||
"browser.sketch_name": "草图{number}",
|
||||
"browser.units": "单位: {units}",
|
||||
"cmd.appearance_assign": "外观",
|
||||
"cmd.appearance_library": "外观库",
|
||||
"cmd.appearance_list": "列出外观",
|
||||
"cmd.body_rename": "重命名实体",
|
||||
"cmd.browser_group": "新建组",
|
||||
"cmd.browser_move": "移动浏览器项目",
|
||||
"cmd.browser_order": "排列浏览器项目",
|
||||
"cmd.browser_rename_group": "重命名组",
|
||||
"cmd.browser_ungroup": "取消编组",
|
||||
"cmd.browser_visibility": "显示/隐藏",
|
||||
"cmd.canvas_calibrate": "校准画布",
|
||||
"cmd.canvas_decal": "贴花",
|
||||
"cmd.canvas_delete": "删除画布",
|
||||
"cmd.canvas_edit": "编辑画布",
|
||||
"cmd.canvas_insert": "画布",
|
||||
"cmd.component_activate": "激活组件",
|
||||
"cmd.component_break_link": "断开链接",
|
||||
"cmd.component_capture_position": "捕获位置",
|
||||
"cmd.component_create": "新建组件",
|
||||
"cmd.component_delete": "删除组件",
|
||||
"cmd.component_from_bodies": "由实体创建组件",
|
||||
"cmd.component_insert_design": "插入设计",
|
||||
"cmd.component_links": "列出链接",
|
||||
"cmd.component_list": "列出组件",
|
||||
"cmd.component_move_bodies": "将实体移至组件",
|
||||
"cmd.component_move_sketches": "将草图移至组件",
|
||||
"cmd.component_paste_new": "粘贴为新对象",
|
||||
"cmd.component_properties": "组件物理属性",
|
||||
"cmd.component_rename": "重命名组件",
|
||||
"cmd.component_revert_position": "恢复位置",
|
||||
"cmd.component_update_links": "更新链接",
|
||||
"cmd.config_activate": "激活配置",
|
||||
"cmd.config_column": "配置列",
|
||||
"cmd.config_configure": "配置",
|
||||
"cmd.config_preview": "预览配置",
|
||||
"cmd.config_row": "配置行",
|
||||
"cmd.config_rule": "配置规则",
|
||||
"cmd.config_rules": "配置规则",
|
||||
"cmd.config_table": "显示配置表",
|
||||
"cmd.construct_axis_edge": "过边的轴",
|
||||
"cmd.construct_axis_normal_to_face": "垂直于面的轴",
|
||||
"cmd.construct_axis_through_cylinder": "过圆柱/圆锥/圆环的轴",
|
||||
"cmd.construct_axis_two_planes": "过两个平面的轴",
|
||||
"cmd.construct_axis_two_points": "过两点的轴",
|
||||
"cmd.construct_plane_along_path": "沿路径的平面",
|
||||
"cmd.construct_plane_midplane": "中平面",
|
||||
"cmd.construct_plane_perpendicular": "垂直平面",
|
||||
"cmd.construct_plane_tangent": "相切平面",
|
||||
"cmd.construct_plane_three_points": "过三点的平面",
|
||||
"cmd.construct_plane_two_edges": "过两条边的平面",
|
||||
"cmd.construct_point_along_path": "沿路径的点",
|
||||
"cmd.construct_point_center": "圆/球/圆环中心的点",
|
||||
"cmd.construct_point_edge_plane": "边与平面交点",
|
||||
"cmd.construct_point_three_planes": "过三个平面的点",
|
||||
"cmd.construct_point_two_edges": "过两条边的点",
|
||||
"cmd.construct_point_vertex": "顶点处的点",
|
||||
"cmd.contact_check": "检查接触",
|
||||
"cmd.contact_create": "新建接触集",
|
||||
"cmd.contact_disable_all": "禁用接触",
|
||||
"cmd.contact_edit": "编辑接触集",
|
||||
"cmd.contact_enable_all": "启用全部接触",
|
||||
"cmd.contact_enable_sets": "启用接触集",
|
||||
"cmd.contact_list": "列出接触集",
|
||||
"cmd.diagnostics_crash_report": "崩溃报告",
|
||||
"cmd.diagnostics_crash_reports": "崩溃报告",
|
||||
"cmd.diagnostics_crash_reports_clear": "删除崩溃报告",
|
||||
"cmd.diagnostics_crash_test": "崩溃测试",
|
||||
"cmd.doc_recover": "恢复设计",
|
||||
"cmd.doc_recovery_discard": "丢弃已恢复的设计",
|
||||
"cmd.doc_recovery_list": "可恢复的设计",
|
||||
"cmd.document_inspect": "检查设计",
|
||||
"cmd.document_units": "更改设计单位",
|
||||
"cmd.edit_checkpoint": "检查点",
|
||||
"cmd.edit_restore_checkpoint": "恢复检查点",
|
||||
"cmd.engine_commands": "列出命令",
|
||||
"cmd.explode_create": "爆炸图",
|
||||
"cmd.explode_delete": "删除爆炸图",
|
||||
"cmd.explode_list": "列出爆炸图",
|
||||
"cmd.explode_show": "显示爆炸图",
|
||||
"cmd.expr_evaluate": "计算表达式",
|
||||
"cmd.face_delete": "删除面",
|
||||
"cmd.file_export": "导出",
|
||||
"cmd.file_insert_mesh": "插入网格",
|
||||
"cmd.file_insert_step": "插入 STEP",
|
||||
"cmd.file_save_mesh": "另存为网格",
|
||||
"cmd.inspect_accessibility": "可达性分析",
|
||||
"cmd.inspect_center_of_mass": "质心",
|
||||
"cmd.inspect_curvature_comb": "曲率梳分析",
|
||||
"cmd.inspect_curvature_map": "曲率图分析",
|
||||
"cmd.inspect_draft": "拔模分析",
|
||||
"cmd.inspect_environment_map": "环境贴图分析",
|
||||
"cmd.inspect_interference": "干涉",
|
||||
"cmd.inspect_isocurve": "等参线分析",
|
||||
"cmd.inspect_minimum_radius": "最小半径分析",
|
||||
"cmd.inspect_zebra": "斑马纹分析",
|
||||
"cmd.joint_as_built": "现状接头",
|
||||
"cmd.joint_create": "接头",
|
||||
"cmd.joint_delete": "删除接头",
|
||||
"cmd.joint_drive": "驱动接头",
|
||||
"cmd.joint_edit": "编辑接头",
|
||||
"cmd.joint_limits": "接头限制",
|
||||
"cmd.joint_list": "列出接头",
|
||||
"cmd.joint_motion_link": "运动关联",
|
||||
"cmd.joint_origin": "接头原点",
|
||||
"cmd.joint_rigid_group": "刚性组",
|
||||
"cmd.joint_solve": "解算接头",
|
||||
"cmd.material_assign": "物理材质",
|
||||
"cmd.material_create": "新建物理材质",
|
||||
"cmd.material_delete": "删除物理材质",
|
||||
"cmd.material_edit": "编辑物理材质",
|
||||
"cmd.material_list": "列出材质",
|
||||
"cmd.mesh_convert": "转换网格",
|
||||
"cmd.model_edges": "列出边",
|
||||
"cmd.model_faces": "列出面",
|
||||
"cmd.model_threads": "列出螺纹",
|
||||
"cmd.motion_delete": "删除运动研究",
|
||||
"cmd.motion_export": "导出运动研究",
|
||||
"cmd.motion_list": "列出运动研究",
|
||||
"cmd.motion_play": "播放运动研究",
|
||||
"cmd.motion_study": "运动研究",
|
||||
"cmd.occurrence_copy": "粘贴(实例)",
|
||||
"cmd.occurrence_ground": "固定",
|
||||
"cmd.occurrence_move": "移动实例",
|
||||
"cmd.occurrence_properties": "实例物理属性",
|
||||
"cmd.parameters_add": "添加用户参数",
|
||||
"cmd.parameters_change": "更改参数",
|
||||
"cmd.parameters_comment": "注释参数",
|
||||
"cmd.parameters_complete": "补全表达式",
|
||||
"cmd.parameters_delete": "删除参数",
|
||||
"cmd.parameters_export": "导出参数",
|
||||
"cmd.parameters_favorite": "收藏参数",
|
||||
"cmd.parameters_graph": "参数关系图",
|
||||
"cmd.parameters_import": "导入参数",
|
||||
"cmd.parameters_list": "列出参数",
|
||||
"cmd.parameters_rename": "重命名参数",
|
||||
"cmd.parameters_users": "参数使用者",
|
||||
"cmd.parts_fastener": "插入紧固件",
|
||||
"cmd.parts_insert": "插入零件",
|
||||
"cmd.parts_library": "标准零件",
|
||||
"cmd.parts_resize": "更改零件尺寸",
|
||||
"cmd.plastic_assign_rule": "指定塑料规则",
|
||||
"cmd.plastic_boss": "凸台",
|
||||
"cmd.plastic_lip": "凸缘",
|
||||
"cmd.plastic_manage_rules": "管理塑料规则",
|
||||
"cmd.plastic_rest": "垫块",
|
||||
"cmd.plastic_rules": "列出塑料规则",
|
||||
"cmd.plastic_snap_fit": "卡扣",
|
||||
"cmd.select_clear": "清除选择",
|
||||
"cmd.sheet_convert": "转换为钣金",
|
||||
"cmd.sheet_export_bend_table": "导出折弯表",
|
||||
"cmd.sheet_export_dxf": "将展开图导出为 DXF",
|
||||
"cmd.sheet_flange": "法兰",
|
||||
"cmd.sheet_flat_pattern": "创建展开图",
|
||||
"cmd.sheet_fold": "折叠",
|
||||
"cmd.sheet_hem": "卷边",
|
||||
"cmd.sheet_manage_rules": "钣金规则",
|
||||
"cmd.sheet_refold": "重新折弯",
|
||||
"cmd.sheet_rules": "列出钣金规则",
|
||||
"cmd.sheet_unfold": "展开",
|
||||
"cmd.sketch_arc_center_point": "中心点圆弧",
|
||||
"cmd.sketch_arc_tangent": "相切圆弧",
|
||||
"cmd.sketch_arc_three_point": "三点圆弧",
|
||||
"cmd.sketch_auto_constrain": "自动约束",
|
||||
"cmd.sketch_auto_constrain_from_datum": "基于基准自动约束",
|
||||
"cmd.sketch_auto_project": "自动投影边到参考",
|
||||
"cmd.sketch_blend_curve": "混合曲线",
|
||||
"cmd.sketch_chain": "连续链",
|
||||
"cmd.sketch_chamfer_distance_angle": "距离和角度倒角",
|
||||
"cmd.sketch_chamfer_equal_distance": "等距倒角",
|
||||
"cmd.sketch_chamfer_two_distance": "双距离倒角",
|
||||
"cmd.sketch_circle_center": "圆心直径圆",
|
||||
"cmd.sketch_circle_three_point": "三点圆",
|
||||
"cmd.sketch_circle_three_tangent": "三切线圆",
|
||||
"cmd.sketch_circle_two_point": "两点圆",
|
||||
"cmd.sketch_circle_two_tangent": "两切线圆",
|
||||
"cmd.sketch_constrainer": "草图约束器",
|
||||
"cmd.sketch_constraint_fix": "固定/解除固定",
|
||||
"cmd.sketch_constraint_midpoint": "中点",
|
||||
"cmd.sketch_construction": "普通/构造",
|
||||
"cmd.sketch_delete": "删除草图图元",
|
||||
"cmd.sketch_dimension_text": "移动尺寸文字",
|
||||
"cmd.sketch_drag": "拖动草图几何",
|
||||
"cmd.sketch_drag_end": "结束草图拖动",
|
||||
"cmd.sketch_draw_dimensioned": "绘制带尺寸的草图图元",
|
||||
"cmd.sketch_edit": "编辑草图",
|
||||
"cmd.sketch_edit_text": "编辑文字",
|
||||
"cmd.sketch_export_dxf": "另存为 DXF",
|
||||
"cmd.sketch_finish_auto_constrain": "使用自动约束完成",
|
||||
"cmd.sketch_fit_curves_to_section": "将曲线拟合到网格截面",
|
||||
"cmd.sketch_glyphs": "约束符号",
|
||||
"cmd.sketch_include_3d": "包含三维几何",
|
||||
"cmd.sketch_insert_dxf": "插入 DXF",
|
||||
"cmd.sketch_insert_svg": "插入 SVG",
|
||||
"cmd.sketch_inspect": "检查草图",
|
||||
"cmd.sketch_intersection_curve": "相交曲线",
|
||||
"cmd.sketch_isoparametric_curve": "等参数曲线",
|
||||
"cmd.sketch_line3d": "三维直线",
|
||||
"cmd.sketch_line_midpoint": "中点线",
|
||||
"cmd.sketch_move": "移动/复制草图图元",
|
||||
"cmd.sketch_move3d": "移动三维点",
|
||||
"cmd.sketch_move_point": "拖动草图点",
|
||||
"cmd.sketch_offset_break": "断开偏移关联",
|
||||
"cmd.sketch_options": "草图选项板选项",
|
||||
"cmd.sketch_point3d": "三维点",
|
||||
"cmd.sketch_polygon_circumscribed": "外切多边形",
|
||||
"cmd.sketch_polygon_edge": "边长多边形",
|
||||
"cmd.sketch_polygon_inscribed": "内接多边形",
|
||||
"cmd.sketch_project_to_surface": "投影到曲面",
|
||||
"cmd.sketch_rectangle_center": "中心矩形",
|
||||
"cmd.sketch_rectangle_rounded": "圆角矩形",
|
||||
"cmd.sketch_rectangle_three_point": "三点矩形",
|
||||
"cmd.sketch_rectangle_two_point": "两点矩形",
|
||||
"cmd.sketch_redefine": "重定义草图平面",
|
||||
"cmd.sketch_slot_arc_center": "中心点圆弧槽",
|
||||
"cmd.sketch_slot_arc_three_point": "三点圆弧槽",
|
||||
"cmd.sketch_slot_center_point": "中心点槽",
|
||||
"cmd.sketch_slot_center_to_center": "中心距槽",
|
||||
"cmd.sketch_slot_overall": "全长槽",
|
||||
"cmd.sketch_snap": "捕捉推断",
|
||||
"cmd.sketch_solve": "解算草图",
|
||||
"cmd.sketch_spline3d": "三维样条曲线",
|
||||
"cmd.sketch_spline_control_point": "控制点样条曲线",
|
||||
"cmd.sketch_spline_control_point_5": "控制点样条曲线(5 次)",
|
||||
"cmd.sketch_spline_fit_point": "拟合点样条曲线",
|
||||
"cmd.sketch_spun_profile": "旋转轮廓",
|
||||
"cmd.sketch_toggle_driven": "切换被驱动尺寸",
|
||||
"cmd.solid_align": "对齐",
|
||||
"cmd.solid_boundary_fill": "边界填充",
|
||||
"cmd.solid_bounding_solid": "包围实体",
|
||||
"cmd.solid_emboss": "压印",
|
||||
"cmd.solid_move": "移动/复制",
|
||||
"cmd.solid_offset_face": "偏移面",
|
||||
"cmd.solid_press_pull": "按压拖动",
|
||||
"cmd.solid_remove": "移除",
|
||||
"cmd.solid_replace_face": "替换面",
|
||||
"cmd.solid_rib": "筋板",
|
||||
"cmd.solid_split_face": "拆分面",
|
||||
"cmd.solid_web": "网格筋",
|
||||
"cmd.surface_patch": "补面",
|
||||
"cmd.surface_stitch": "缝合",
|
||||
"cmd.surface_thicken": "加厚",
|
||||
"cmd.surface_extrude": "曲面拉伸",
|
||||
"cmd.surface_revolve": "曲面旋转",
|
||||
"cmd.surface_sweep": "曲面扫掠",
|
||||
"cmd.surface_loft": "曲面放样",
|
||||
"cmd.surface_ruled": "直纹曲面",
|
||||
"cmd.surface_boundary": "边界曲面",
|
||||
"cmd.surface_offset": "偏移曲面",
|
||||
"cmd.surface_blend": "混合曲面",
|
||||
"cmd.surface_fillet": "曲面圆角",
|
||||
"cmd.surface_untrim": "取消修剪",
|
||||
"cmd.surface_unstitch": "取消缝合",
|
||||
"cmd.surface_reverse_normal": "反转法向",
|
||||
"cmd.inspect_continuity": "连续性分析",
|
||||
"cmd.timeline_compute_all": "全部重新計算",
|
||||
"cmd.timeline_copy": "复制特征",
|
||||
"cmd.timeline_dependents": "特征依存项",
|
||||
"cmd.timeline_inspect": "检查特征",
|
||||
"cmd.timeline_paste": "粘贴特征",
|
||||
"cmd.timeline_redefine": "编辑特征",
|
||||
"cmd.timeline_rename": "重命名",
|
||||
"cmd.timeline_reorder": "重排特征",
|
||||
"cmd.timeline_roll_to": "滚动历史标记",
|
||||
"cmd.timeline_rollback": "移动时间线标记",
|
||||
"cmd.timeline_suppress": "抑制特征",
|
||||
"cmd.view_apply": "还原命名视图",
|
||||
"cmd.view_delete": "删除命名视图",
|
||||
"cmd.view_list": "列出命名视图",
|
||||
"cmd.view_rename": "重命名命名视图",
|
||||
"cmd.view_save": "新建命名视图",
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.select": "↖ 选择",
|
||||
"dialog.selected_count": "↖ 已选择 {count} 个",
|
||||
"dialog.step": "步骤",
|
||||
"dialog.selection_step": "第 {step} 步,共 {total} 步:选择{input}",
|
||||
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
|
||||
"document.save_changes": "保存对 {name} 的更改?",
|
||||
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
|
||||
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
|
||||
"settings.msaa_reset": "{setting}: {requested}× 不受此图形设备支持;已重置为 {reset}× (支持: {supported})。",
|
||||
"settings.msaa_unsupported": "此图形设备不支持",
|
||||
"settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:",
|
||||
"settings.reset_title": "设置已更改",
|
||||
"sketch.degrees_of_freedom": "{count} 个自由度",
|
||||
"sketch.draw_curves": "绘制为 {linetype}",
|
||||
"sketch.make_selection": "将选择转换为 {linetype}",
|
||||
"ui.1_degree_of_freedom": "1 个自由度",
|
||||
"ui.3d_mouse_speed": "3D 鼠标速度",
|
||||
"ui.3d_mouse_windows": "3D 鼠标(Windows)",
|
||||
"ui.3d_sketch": "3D 草图",
|
||||
"ui.about": "关于",
|
||||
"ui.activate": "激活",
|
||||
"ui.active_component": "活动组件",
|
||||
"ui.agent_cursor": "代理光标",
|
||||
"ui.agent_cursor_speed": "代理光标速度",
|
||||
"ui.all": "全部",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度小数位",
|
||||
"ui.animate_view_changes": "视图变化动画",
|
||||
"ui.animation": "动画",
|
||||
"ui.animation_speed": "动画速度",
|
||||
"ui.anti_aliasing": "抗锯齿",
|
||||
"ui.apply": "应用",
|
||||
"ui.arc": "圆弧",
|
||||
"ui.assemble": "装配",
|
||||
"ui.autosave_every": "自动保存间隔",
|
||||
"ui.axes": "轴",
|
||||
"ui.axis": "轴",
|
||||
"ui.base_point": "基点",
|
||||
"ui.bodies": "实体",
|
||||
"ui.box": "长方体",
|
||||
"ui.break": "打断",
|
||||
"ui.cancel": "取消",
|
||||
"ui.canvases": "画布",
|
||||
"ui.center": "中心",
|
||||
"ui.centerline": "中心线",
|
||||
"ui.chain": "链",
|
||||
"ui.chamfer": "倒角",
|
||||
"ui.chamfer_type": "倒角类型",
|
||||
"ui.change_workspace": "更改工作区",
|
||||
"ui.chord_length": "弦长",
|
||||
"ui.curvature_g2": "曲率连续 (G2)",
|
||||
"ui.equivalent_radius": "等效半径",
|
||||
"ui.circle": "圆",
|
||||
"ui.circular_pattern": "圆形阵列",
|
||||
"ui.clear": "清除",
|
||||
"ui.clear_recent": "清除最近",
|
||||
"ui.clear_the_selection": "清除选择",
|
||||
"ui.click_selects_whole_bodies": "单击选择整个实体",
|
||||
"ui.click_then_press_key": "单击,然后按新按键",
|
||||
"ui.close": "关闭",
|
||||
"ui.coil": "螺旋",
|
||||
"ui.coincident": "重合",
|
||||
"ui.collapse": "折叠",
|
||||
"ui.collinear": "共线",
|
||||
"ui.combine": "组合",
|
||||
"ui.coming_later": "即将推出",
|
||||
"ui.concentric": "同心",
|
||||
"ui.configurations": "配置",
|
||||
"ui.conic_curve": "圆锥曲线",
|
||||
"ui.constant": "恒定",
|
||||
"ui.constraints_conflict": "约束冲突",
|
||||
"ui.construct": "构造",
|
||||
"ui.construction": "构造",
|
||||
"ui.context_menu": "上下文菜单",
|
||||
"ui.copy": "复制",
|
||||
"ui.corners": "角点",
|
||||
"ui.corner": "角",
|
||||
"ui.counterbore": "沉头孔",
|
||||
"ui.countersink": "锥形沉孔",
|
||||
"ui.create": "创建",
|
||||
"ui.create_copy": "创建副本",
|
||||
"ui.create_sketch": "创建草图",
|
||||
"ui.current": "当前",
|
||||
"ui.curvature": "曲率",
|
||||
"ui.curves": "曲线",
|
||||
"ui.cut": "剪切",
|
||||
"ui.cylinder": "圆柱体",
|
||||
"ui.dark": "深色",
|
||||
"ui.default_modelling_orientation": "默认建模方向",
|
||||
"ui.degrees_of_freedom": "自由度",
|
||||
"ui.delete": "删除",
|
||||
"ui.depth": "深度",
|
||||
"ui.design": "设计",
|
||||
"ui.diameter": "直径",
|
||||
"ui.dimension": "尺寸",
|
||||
"ui.direction": "方向",
|
||||
"ui.distance": "距离",
|
||||
"ui.distance_1": "距离 1",
|
||||
"ui.distance_2": "距离 2",
|
||||
"ui.document_settings": "文档设置",
|
||||
"ui.don_t_save": "不保存",
|
||||
"ui.draft": "拔模",
|
||||
"ui.drawing": "绘图",
|
||||
"ui.drill_point": "钻尖",
|
||||
"ui.edit": "编辑",
|
||||
"ui.ellipse": "椭圆",
|
||||
"ui.end_radius": "结束半径",
|
||||
"ui.entities": "实体",
|
||||
"ui.equal": "相等",
|
||||
"ui.expand": "展开",
|
||||
"ui.export_ellipsis": "导出...",
|
||||
"ui.export_ellipsis_keymap": "导出…",
|
||||
"ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件",
|
||||
"ui.extend": "延伸",
|
||||
"ui.extent_type": "延伸类型",
|
||||
"ui.extrude": "拉伸",
|
||||
"ui.factor": "系数",
|
||||
"ui.feature_history": "特征历史",
|
||||
"ui.feature_options": "特征选项",
|
||||
"ui.features": "特征",
|
||||
"ui.file": "文件",
|
||||
"ui.fillet": "圆角",
|
||||
"ui.filter_commands_or_keys": "筛选命令或按键",
|
||||
"ui.finish_sketch": "完成草图",
|
||||
"ui.fix_unfix": "固定/取消固定",
|
||||
"ui.flat": "平面",
|
||||
"ui.flip": "翻转",
|
||||
"ui.front": "前视",
|
||||
"ui.fully_constrained": "完全约束",
|
||||
"ui.general": "常规",
|
||||
"ui.graphics": "图形",
|
||||
"ui.ground_shadow": "地面阴影",
|
||||
"ui.heading_add_ins": "加载项",
|
||||
"ui.heading_assemble": "装配",
|
||||
"ui.heading_box": "长方体",
|
||||
"ui.heading_chamfer": "倒角",
|
||||
"ui.heading_circular_pattern": "圆形阵列",
|
||||
"ui.heading_coil": "螺旋",
|
||||
"ui.heading_combine": "组合",
|
||||
"ui.heading_configure": "配置",
|
||||
"ui.heading_constraints": "约束",
|
||||
"ui.heading_construct": "构造",
|
||||
"ui.heading_create": "创建",
|
||||
"ui.heading_create_sketch": "创建草图",
|
||||
"ui.heading_cylinder": "圆柱体",
|
||||
"ui.heading_design": "设计",
|
||||
"ui.heading_draft": "拔模",
|
||||
"ui.heading_edit": "编辑",
|
||||
"ui.heading_extrude": "拉伸",
|
||||
"ui.heading_fillet": "圆角",
|
||||
"ui.heading_finish_sketch": "完成草图",
|
||||
"ui.heading_front": "前视",
|
||||
"ui.heading_hole": "孔",
|
||||
"ui.heading_home": "主页",
|
||||
"ui.heading_insert": "插入",
|
||||
"ui.heading_inspect": "检查",
|
||||
"ui.heading_loft": "放样",
|
||||
"ui.heading_make": "制作",
|
||||
"ui.heading_measure": "测量",
|
||||
"ui.heading_mesh": "网格",
|
||||
"ui.heading_mirror": "镜像",
|
||||
"ui.heading_modify": "修改",
|
||||
"ui.heading_move": "移动",
|
||||
"ui.heading_offset": "偏移",
|
||||
"ui.heading_move_copy": "移动/复制",
|
||||
"ui.heading_offset_plane": "偏移平面",
|
||||
"ui.heading_ok": "确定",
|
||||
"ui.heading_parameters": "参数",
|
||||
"ui.heading_pattern_on_path": "路径阵列",
|
||||
"ui.heading_pipe": "管道",
|
||||
"ui.heading_plane_at_angle": "角度平面",
|
||||
"ui.heading_plastic": "塑料",
|
||||
"ui.heading_prepare": "准备",
|
||||
"ui.heading_rectangular_pattern": "矩形阵列",
|
||||
"ui.heading_revolve": "旋转",
|
||||
"ui.heading_right": "右视",
|
||||
"ui.heading_scale": "缩放",
|
||||
"ui.heading_section_analysis": "截面分析",
|
||||
"ui.heading_select": "选择",
|
||||
"ui.heading_sheet_metal": "钣金",
|
||||
"ui.heading_shell": "抽壳",
|
||||
"ui.heading_sketch": "草图",
|
||||
"ui.heading_sketch_palette": "草图面板",
|
||||
"ui.heading_sketch_scale": "草图缩放",
|
||||
"ui.heading_solid": "实体",
|
||||
"ui.heading_sphere": "球体",
|
||||
"ui.heading_split_body": "分割实体",
|
||||
"ui.heading_surface": "曲面",
|
||||
"ui.heading_sweep": "扫掠",
|
||||
"ui.heading_thread": "螺纹",
|
||||
"ui.heading_top": "上视",
|
||||
"ui.heading_torus": "圆环体",
|
||||
"ui.heading_utilities": "工具",
|
||||
"ui.heading_utility": "实用工具",
|
||||
"ui.help": "帮助",
|
||||
"ui.hide": "隐藏",
|
||||
"ui.hole": "孔",
|
||||
"ui.hole_tap_type": "孔攻丝类型",
|
||||
"ui.hole_type": "孔类型",
|
||||
"ui.home": "主页",
|
||||
"ui.horizontal_vertical": "水平/垂直",
|
||||
"ui.import_ellipsis": "导入…",
|
||||
"ui.insert": "插入",
|
||||
"ui.inspect": "检查",
|
||||
"ui.intersect": "相交",
|
||||
"ui.join": "连接",
|
||||
"ui.joints": "关节",
|
||||
"ui.keyboard_shortcuts": "键盘快捷键",
|
||||
"ui.language": "语言",
|
||||
"ui.length_decimals": "长度小数位",
|
||||
"ui.light": "浅色",
|
||||
"ui.line": "直线",
|
||||
"ui.linetype": "线型",
|
||||
"ui.loft": "放样",
|
||||
"ui.look_at": "朝向",
|
||||
"ui.look_at_anim_desc": "轴视图、注视、主视图与适合窗口",
|
||||
"ui.manufacture": "制造",
|
||||
"ui.measure": "测量",
|
||||
"ui.midpoint": "中点",
|
||||
"ui.mirror": "镜像",
|
||||
"ui.mirror_line": "镜像线",
|
||||
"ui.modify": "修改",
|
||||
"ui.move": "移动",
|
||||
"ui.multiple": "多个",
|
||||
"ui.named_views": "命名视图",
|
||||
"ui.navigation": "导航",
|
||||
"ui.needs_design": "{command}: 请先打开一个设计",
|
||||
"ui.new_body": "新建实体",
|
||||
"ui.new_design": "新建设计",
|
||||
"ui.normal": "法向",
|
||||
"ui.object_type": "对象类型",
|
||||
"ui.objects": "对象",
|
||||
"ui.offer_to_stand_up_y_up_files": "提供站立 Y-up 文件",
|
||||
"ui.offset": "偏移",
|
||||
"ui.offset_plane": "偏移平面",
|
||||
"ui.one_side": "单侧",
|
||||
"ui.open": "打开",
|
||||
"ui.open_ellipsis": "打开...",
|
||||
"ui.open_preferences": "打开首选项",
|
||||
"ui.open_recent": "打开最近",
|
||||
"ui.operation": "操作",
|
||||
"ui.options": "选项",
|
||||
"ui.orbit_around": "环绕",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_points": "点",
|
||||
"ui.palette_profile": "轮廓",
|
||||
"ui.pan_zoom_orbit": "平移、缩放、环绕",
|
||||
"ui.parallel": "平行",
|
||||
"ui.parameters": "参数",
|
||||
"ui.paste": "粘贴",
|
||||
"ui.path": "路径",
|
||||
"ui.pattern_on_path": "路径阵列",
|
||||
"ui.perpendicular": "垂直",
|
||||
"ui.perspective_view": "透视图",
|
||||
"ui.pipe": "管道",
|
||||
"ui.placement": "放置",
|
||||
"ui.plane": "平面",
|
||||
"ui.plane_at_angle": "角度平面",
|
||||
"ui.point": "点",
|
||||
"ui.point_under_the_cursor": "光标下的点",
|
||||
"ui.points": "点",
|
||||
"ui.polygon": "多边形",
|
||||
"ui.preferences": "首选项",
|
||||
"ui.press_a_key": "按一个键…",
|
||||
"ui.profile": "轮廓",
|
||||
"ui.profile_plane": "轮廓平面",
|
||||
"ui.profiles": "轮廓",
|
||||
"ui.project": "投影",
|
||||
"ui.project_face_edges_sketch": "将面的边投影到新草图",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quit": "退出",
|
||||
"ui.radial_context_menu": "环形上下文菜单",
|
||||
"ui.radius": "半径",
|
||||
"ui.radius_type": "半径类型",
|
||||
"ui.rectangle": "矩形",
|
||||
"ui.rectangular_pattern": "矩形阵列",
|
||||
"ui.redo": "重做",
|
||||
"ui.render": "渲染",
|
||||
"ui.renderer": "渲染器",
|
||||
"ui.reset": "重置",
|
||||
"ui.reset_all": "全部重置",
|
||||
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
|
||||
"ui.restore_defaults": "恢复默认值",
|
||||
"ui.reverse_zoom_direction": "反转缩放方向",
|
||||
"ui.revolve": "旋转",
|
||||
"ui.right": "右视",
|
||||
"ui.save": "保存",
|
||||
"ui.save_as": "另存为",
|
||||
"ui.save_as_ellipsis": "另存为...",
|
||||
"ui.scale": "缩放",
|
||||
"ui.section_analysis": "截面分析",
|
||||
"ui.select": "选择",
|
||||
"ui.select_other_hint": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。",
|
||||
"ui.selection": "选择",
|
||||
"ui.shell": "抽壳",
|
||||
"ui.show": "显示",
|
||||
"ui.show_constraints": "约束",
|
||||
"ui.show_dimensions": "尺寸",
|
||||
"ui.show_points": "显示点",
|
||||
"ui.show_profile": "显示轮廓",
|
||||
"ui.show_projected_geometries": "投影几何",
|
||||
"ui.simple": "简单",
|
||||
"ui.simulation": "仿真",
|
||||
"ui.single": "单个",
|
||||
"ui.sketch": "草图",
|
||||
"ui.sketch_dimension": "草图尺寸",
|
||||
"ui.sketch_grid": "草图网格",
|
||||
"ui.sketch_scale": "草图比例",
|
||||
"ui.sketch_status": "草图状态",
|
||||
"ui.sketches": "草图",
|
||||
"ui.slice": "切片",
|
||||
"ui.slot": "槽",
|
||||
"ui.smooth": "平滑",
|
||||
"ui.smooth_wheel_zoom": "平滑滚轮缩放",
|
||||
"ui.smooth_wheel_zoom_tip": "关闭后每个滚轮事件立即生效,不做平滑延迟",
|
||||
"ui.snap": "捕捉",
|
||||
"ui.space_mouse_hint": "同时移动和旋转视图;草图中仅支持平移和缩放。",
|
||||
"ui.space_mouse_navigate": "使用 SpaceNavigator / SpaceMouse 导航",
|
||||
"ui.spacing_2": "间距 2",
|
||||
"ui.spacing": "间距",
|
||||
"ui.sphere": "球体",
|
||||
"ui.spline": "样条曲线",
|
||||
"ui.split_body": "分割实体",
|
||||
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
|
||||
"ui.start": "开始",
|
||||
"ui.start_radius": "起始半径",
|
||||
"ui.sweep": "扫掠",
|
||||
"ui.symmetric": "对称",
|
||||
"ui.symmetry": "对称",
|
||||
"ui.system": "系统",
|
||||
"ui.tangent": "相切",
|
||||
"ui.taper_angle": "锥角",
|
||||
"ui.tapped": "攻丝",
|
||||
"ui.text": "文本",
|
||||
"ui.theme": "主题",
|
||||
"ui.thickness": "厚度",
|
||||
"ui.thread": "螺纹",
|
||||
"ui.timeline_bottom": "底部时间轴",
|
||||
"ui.timeline_left_note": "左侧布局下,回退条显示在零件和场景浏览器的上方。",
|
||||
"ui.timeline_left_sidebar": "左侧边栏",
|
||||
"ui.tip_angle": "尖端角度",
|
||||
"ui.to_object": "到对象",
|
||||
"ui.top": "上视",
|
||||
"ui.torus": "圆环体",
|
||||
"ui.trackpad_pan_tip": "触控板手势用于平移(关闭:像滚轮一样缩放);鼠标滚轮缩放、拇指滚轮横向平移、双指捏合始终缩放",
|
||||
"ui.trim": "修剪",
|
||||
"ui.two_finger_scroll_pans": "双指滚动平移",
|
||||
"ui.two_sides": "双侧",
|
||||
"ui.ui_scale": "界面缩放",
|
||||
"ui.undo": "撤销",
|
||||
"ui.units": "单位",
|
||||
"ui.units_of_new_designs": "新设计的单位",
|
||||
"ui.untitled": "无标题",
|
||||
"ui.up_axis_note": "视图中朝上的轴(轴视图、主视图、注视)。设计本身不变。",
|
||||
"ui.variable": "变量",
|
||||
"ui.view_centre": "视图中心",
|
||||
"ui.visual_shaded_hidden_edges": "带隐藏边的着色",
|
||||
"ui.visual_shaded_with_edges": "带边着色",
|
||||
"ui.visual_style": "视觉样式",
|
||||
"ui.visual_wireframe": "线框",
|
||||
"ui.workspace": "工作区",
|
||||
"ui.y_distance": "Y 距离",
|
||||
"ui.x_distance": "X 距离",
|
||||
"ui.y_up": "Y 朝上",
|
||||
"ui.z_up": "Z 朝上",
|
||||
"ui.zoom_to_cursor_tip": "滚轮向光标所在点缩放(关闭:向视图中心缩放)",
|
||||
"ui.zoom_to_the_cursor": "缩放到光标",
|
||||
"A new configuration, a copy of the active one": "新建配置,为当前配置的副本",
|
||||
"About SolidCraft": "关于 SolidCraft",
|
||||
"Active": "活动",
|
||||
"Active rule": "活动规则",
|
||||
"Add": "添加",
|
||||
"Add Key": "添加按键",
|
||||
"Add Mid Radius": "添加中间半径",
|
||||
"Add Row": "添加行",
|
||||
"All Sketch Points": "所有草图点",
|
||||
"All open designs": "所有打开的设计",
|
||||
"Allowed expressions, separated by semicolons": "允许的表达式,以分号分隔",
|
||||
"Also deletes": "同时删除",
|
||||
"Angle 2": "角度 2",
|
||||
"Animate": "动画",
|
||||
"Appearance name": "外观名称",
|
||||
"Applied Radius": "应用半径",
|
||||
"Apply To": "应用于",
|
||||
"Area": "面积",
|
||||
"Attachment": "附着",
|
||||
"BROWSER": "浏览器",
|
||||
"Bend Line Position": "弯折线位置",
|
||||
"Bend Position": "折弯位置",
|
||||
"Bend Radius": "折弯半径",
|
||||
"Bends": "折弯",
|
||||
"Body": "实体",
|
||||
"Bounding box": "边界框",
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数",
|
||||
"Capture All Positions": "捕获所有位置",
|
||||
"Catch Direction": "卡扣方向",
|
||||
"Cells": "区域",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
"Center of mass": "质心",
|
||||
"Chamfer Type": "倒角类型",
|
||||
"Check collisions": "检查碰撞",
|
||||
"Checked": "已检查",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
|
||||
"Close draft": "关闭草稿",
|
||||
"Collisions": "碰撞",
|
||||
"Colour": "颜色",
|
||||
"Component1": "组件1",
|
||||
"Configuration": "配置",
|
||||
"Configuration rules": "配置规则",
|
||||
"Contact": "接触",
|
||||
"Copy all licences": "复制所有许可证",
|
||||
"Copy diagnostic information": "复制诊断信息",
|
||||
"Copy report": "复制报告",
|
||||
"Corner Relief Shape": "拐角释放形状",
|
||||
"Counterbore depth": "沉孔深度",
|
||||
"Countersink angle": "沉头角度",
|
||||
"Create Issue — review draft": "创建 Issue——审阅草稿",
|
||||
"Customize Toolbar": "自定义工具栏",
|
||||
"Delete all": "全部删除",
|
||||
"Delete rule": "删除规则",
|
||||
"Delete the row": "删除此行",
|
||||
"Delete the set": "删除此集合",
|
||||
"Delete this parameter": "删除此参数",
|
||||
"Density": "密度",
|
||||
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
|
||||
"Discard": "放弃",
|
||||
"Double-click to rename": "双击重命名",
|
||||
"Drive Joint": "驱动接头",
|
||||
"Edit Profile Sketch": "编辑轮廓草图",
|
||||
"Enable rule": "启用规则",
|
||||
"Enabled": "已启用",
|
||||
"Explode": "爆炸",
|
||||
"Extents": "范围",
|
||||
"Face attachment lost": "面附着丢失",
|
||||
"Faces / edges / vertices": "面 / 边 / 顶点",
|
||||
"Favourite": "收藏",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。",
|
||||
"Filter": "筛选",
|
||||
"Find in Browser": "在浏览器中查找",
|
||||
"Flat size": "平板尺寸",
|
||||
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Font": "字体",
|
||||
"Full Length": "全长",
|
||||
"Gap": "间隙",
|
||||
"Grid": "网格",
|
||||
"Height": "高度",
|
||||
"Home view": "主视图",
|
||||
"In This Design": "在此设计中",
|
||||
"Interferes": "发生干涉",
|
||||
"Issue title": "Issue 标题",
|
||||
"Joint Origin1": "接头原点1",
|
||||
"Keep Tools": "保留工具",
|
||||
"Later": "稍后",
|
||||
"Leave it": "保持原样",
|
||||
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "旧版面附着。重新绑定到经验证的面可创建精确附着。",
|
||||
"Length": "长度",
|
||||
"Length units": "长度单位",
|
||||
"Library": "零件库",
|
||||
"Loop": "循环",
|
||||
"Margin": "余量",
|
||||
"Mass": "质量",
|
||||
"Material": "材质",
|
||||
"Mid Radius": "中间半径",
|
||||
"Modeled": "已建模",
|
||||
"More toolbar panels": "更多工具栏面板",
|
||||
"Motion": "运动",
|
||||
"Move Type": "移动类型",
|
||||
"Move earlier": "向前移动",
|
||||
"Move later": "向后移动",
|
||||
"Name": "名称",
|
||||
"No crash reports.": "没有崩溃报告。",
|
||||
"No matching appearances": "没有匹配的外观",
|
||||
"Not available yet": "暂不可用",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "打开 GitHub 草稿",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
|
||||
"Other side": "另一侧",
|
||||
"Parameter values": "参数值",
|
||||
"Per Edge": "每条边",
|
||||
"Pick the tool bodies": "选择工具实体",
|
||||
"Place at the origin instead": "改为放置在原点",
|
||||
"Playback": "播放",
|
||||
"Position": "位置",
|
||||
"Position (0 to 1)": "位置(0 到 1)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1",
|
||||
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
|
||||
"RECENT": "最近使用",
|
||||
"Radius Fit": "半径拟合",
|
||||
"Ratio": "比率",
|
||||
"Recover": "恢复",
|
||||
"Recover unsaved design?": "恢复未保存的设计?",
|
||||
"Remove Tools": "移除工具",
|
||||
"Remove from list": "从列表移除",
|
||||
"Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。",
|
||||
"Remove the column": "移除此列",
|
||||
"Remove the track": "移除轨迹",
|
||||
"Remove this point": "移除此点",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿",
|
||||
"Reset Toolbar": "重置工具栏",
|
||||
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
|
||||
"Result": "结果",
|
||||
"Retake screenshot": "重新截图",
|
||||
"Reverse": "反向",
|
||||
"Reverse Direction": "反向",
|
||||
"Revert": "还原",
|
||||
"Revert All Positions": "还原所有位置",
|
||||
"Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。",
|
||||
"Rim Edge": "轮缘边",
|
||||
"Roll History Marker Here": "将历史标记滚动到此处",
|
||||
"Rule": "规则",
|
||||
"Saved preview. Geometry is being checked before editing.": "已保存预览。编辑前正在检查几何。",
|
||||
"Scale factor": "比例因子",
|
||||
"Search": "搜索",
|
||||
"Search commands": "搜索命令",
|
||||
"Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)",
|
||||
"Select bodies or faces instead": "改为选择实体或面",
|
||||
"Selection filter": "选择筛选",
|
||||
"Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。",
|
||||
"Shape": "形状",
|
||||
"Show Agent Cursor": "显示代理光标",
|
||||
"Show in folder": "在文件夹中显示",
|
||||
"Size": "大小",
|
||||
"Sketch plane has no body-face attachment.": "草图平面没有实体面附着。",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
|
||||
"Speed": "速度",
|
||||
"Stand it up": "将其立起",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
|
||||
"Stand this part up?": "立起此零件?",
|
||||
"Step": "步骤",
|
||||
"Steps": "步数",
|
||||
"Suppressed": "已抑制",
|
||||
"Swap Ends": "交换端点",
|
||||
"Tangent Chain": "相切链",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。",
|
||||
"Then": "操作",
|
||||
"These features will be deleted:": "以下特征将被删除:",
|
||||
"These features will fail:": "以下特征将失败:",
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "此子项是共享的。删除它会从其父项的每个副本中移除。",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。",
|
||||
"To Offset": "偏移",
|
||||
"Tolerance": "公差",
|
||||
"Type": "类型",
|
||||
"Updating the preview": "正在更新预览",
|
||||
"Use fixed plane": "使用固定平面",
|
||||
"VIEW": "视图",
|
||||
"Value": "值",
|
||||
"Volume": "体积",
|
||||
"Wall": "壁厚",
|
||||
"When": "条件",
|
||||
"Width": "宽度",
|
||||
"Will fail": "将失败",
|
||||
"angle, height": "角度、高度",
|
||||
"command name": "命令名称",
|
||||
"expression, e.g. 25 mm": "表达式,例如 25 mm",
|
||||
"fits the face, e.g. M8": "适配该面,例如 M8",
|
||||
"from the rule": "来自规则",
|
||||
"needs two joints": "需要两个接头",
|
||||
"new configuration": "新配置",
|
||||
"new name": "新名称",
|
||||
"new parameter": "新参数",
|
||||
"no interference": "无干涉",
|
||||
"no joint that moves yet": "尚无运动接头",
|
||||
"no picture": "无图片",
|
||||
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
|
||||
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
|
||||
"the parts library is not available": "零件库不可用",
|
||||
"to the next face": "到下一个面",
|
||||
"Revert Position": "恢复位置",
|
||||
"Construction": "构造",
|
||||
"Faces": "面",
|
||||
"Edges": "边",
|
||||
"Vertices": "顶点",
|
||||
"Sketch geometry and profiles": "草图几何和轮廓",
|
||||
"Sketch curves": "草图曲线",
|
||||
"Sketch points": "草图点",
|
||||
"Sketch dimensions": "草图尺寸",
|
||||
"Sketch constraints": "草图约束",
|
||||
"0 turns it off": "0 表示关闭",
|
||||
"Show the pointer an AI agent moves (#35)": "显示 AI 代理移动的指针(#35)",
|
||||
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Flat": "平面",
|
||||
"Save Rule": "保存规则",
|
||||
"Preview Row": "预览行",
|
||||
"Add condition": "添加条件",
|
||||
"Apply to selection": "应用到选择",
|
||||
"Remove override": "移除覆盖",
|
||||
"Copy screenshot": "复制截图",
|
||||
"Remove screenshot": "移除截图",
|
||||
"Use active author placement": "使用活动作者放置",
|
||||
"Reattach selected face": "重新附着所选面",
|
||||
"Use saved fixed plane": "使用已保存的固定平面",
|
||||
"Use selected face as fixed plane": "使用所选面作为固定平面",
|
||||
"View navigator": "视图导航器",
|
||||
"Signed axes": "带符号轴",
|
||||
"Cube": "立方体",
|
||||
"Middle double-click": "中键双击",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。",
|
||||
"cmd.render_backends": "渲染后端",
|
||||
"cmd.view_navigate": "导航视图",
|
||||
"ui.animateJoint": "动画接头",
|
||||
"ui.calibrate": "校准",
|
||||
"ui.customizeToolbar": "自定义工具栏…",
|
||||
"ui.editJointLimits": "编辑接头限制",
|
||||
"ui.exportDxf": "导出 DXF…",
|
||||
"ui.findInTimeline": "在时间线中查找",
|
||||
"ui.findInWindow": "在窗口中查找",
|
||||
"ui.fit": "适合",
|
||||
"ui.groupSelected": "将所选编组",
|
||||
"ui.hideAxes": "隐藏轴",
|
||||
"ui.hideDimension": "隐藏尺寸",
|
||||
"ui.hideOrigin": "隐藏原点",
|
||||
"ui.hidePlanes": "隐藏平面",
|
||||
"ui.hideProfile": "隐藏轮廓",
|
||||
"ui.homeView": "主视图",
|
||||
"ui.isolate": "隔离",
|
||||
"ui.lock": "锁定",
|
||||
"ui.newSketchGroup": "新建草图组",
|
||||
"ui.normalConstruction": "普通 / 构造",
|
||||
"ui.orbit": "环绕",
|
||||
"ui.heading_position": "位置",
|
||||
"ui.pan": "平移",
|
||||
"ui.pinToShortcuts": "固定到快捷方式",
|
||||
"ui.pinToToolbar": "固定到工具栏",
|
||||
"ui.properties": "属性",
|
||||
"ui.removeSection": "移除剖面",
|
||||
"ui.removeFromToolbar": "从工具栏移除",
|
||||
"ui.repeat": "重复",
|
||||
"ui.report_a_bug": "报告缺陷",
|
||||
"ui.restore": "还原",
|
||||
"ui.heading_setup": "设置",
|
||||
"ui.selectOther": "选择其他…",
|
||||
"ui.selectTangentChain": "选择相切链",
|
||||
"ui.showAll": "显示全部",
|
||||
"ui.showAllBodies": "显示所有实体",
|
||||
"ui.showAxes": "显示轴",
|
||||
"ui.showDimension": "显示尺寸",
|
||||
"ui.showOrigin": "显示原点",
|
||||
"ui.showPlanes": "显示平面",
|
||||
"ui.suppress": "抑制",
|
||||
"ui.command_palette_hint": "输入命令,例如 {example} — 或按 S 搜索",
|
||||
"ui.unground": "取消固定",
|
||||
"ui.unisolate": "取消隔离",
|
||||
"ui.unlock": "解锁",
|
||||
"ui.unsuppress": "取消抑制",
|
||||
"ui.updateToCurrentView": "更新为当前视图",
|
||||
"ui.zoom": "缩放",
|
||||
"ui.command_done": "{command} 完成",
|
||||
"timeline.group": "新建特征文件夹",
|
||||
"timeline.ungroup": "移除特征文件夹",
|
||||
"timeline.rename_group": "重命名特征文件夹",
|
||||
"timeline.move_to_group": "将特征移至文件夹",
|
||||
"timeline.move_many": "移动时间线项目",
|
||||
"timeline.reorder_many": "特征重新排序",
|
||||
"ui.show_construction_geometries": "构造几何图元",
|
||||
"ui.total_angle": "总角度",
|
||||
"sketch_text.builtin_font": "{family}(内置)",
|
||||
"sketch_text.font_search": "搜索字体",
|
||||
"sketch_text.loading_fonts": "正在查找已安装字体…",
|
||||
"sketch_text.use_font_file": "使用字体文件 {path}",
|
||||
"sketch_text.bold": "粗体",
|
||||
"sketch_text.italic": "斜体",
|
||||
"sketch_text.align_left": "左对齐",
|
||||
"sketch_text.align_center": "居中",
|
||||
"sketch_text.align_right": "右对齐",
|
||||
"sketch_text.path_hint": "文字所跟随的草图曲线:先单击此处,再选择曲线",
|
||||
"sketch_text.path_clear": "将文字移离其曲线",
|
||||
"Style": "样式",
|
||||
"Alignment": "对齐",
|
||||
"Letter Spacing": "字间距",
|
||||
"Line Spacing": "行间距",
|
||||
"Path": "路径",
|
||||
"sketch_text.on_path": "文字沿路径排列:由曲线放置",
|
||||
"ui.analysis": "分析",
|
||||
"ui.edit_section_analysis": "编辑截面分析",
|
||||
"cmd.material_properties_validate":"验证物理材料数据"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
("Default", DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
("Aluminum", 2.70),
|
||||
("Brass", 8.50),
|
||||
("Copper", 8.96),
|
||||
("Titanium", 4.43),
|
||||
("Cast Iron", 7.20),
|
||||
("ABS Plastic", 1.06),
|
||||
("PLA", 1.24),
|
||||
("Nylon", 1.14),
|
||||
("Polycarbonate", 1.20),
|
||||
("Oak", 0.75),
|
||||
("Glass", 2.50),
|
||||
];
|
||||
|
||||
/// How a physical material looks (sRGB); the default material has no colour of its own.
|
||||
pub fn material_color(name: &str) -> Option<[u8; 3]> {
|
||||
Some(match name {
|
||||
"Steel" => [158, 164, 172],
|
||||
"Stainless Steel" => [186, 191, 198],
|
||||
"Aluminum" => [204, 208, 214],
|
||||
"Brass" => [208, 172, 88],
|
||||
"Copper" => [204, 122, 80],
|
||||
"Titanium" => [150, 148, 158],
|
||||
"Cast Iron" => [104, 104, 110],
|
||||
"ABS Plastic" => [222, 222, 216],
|
||||
"PLA" => [110, 160, 222],
|
||||
"Nylon" => [236, 230, 214],
|
||||
"Polycarbonate" => [196, 216, 232],
|
||||
"Oak" => [190, 146, 92],
|
||||
"Glass" => [170, 208, 218],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
|
||||
struct U {
|
||||
vp: mat4x4<f32>,
|
||||
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
|
||||
screen: vec4<f32>, // viewport width, height (px), depth bias (see `biased_z`), silhouettes on (1)
|
||||
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
|
||||
cap: vec4<f32>, // colour of the inside of cut bodies
|
||||
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
|
||||
pull: vec4<f32>, // draft pull direction
|
||||
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
|
||||
acc_v: vec4<f32>,
|
||||
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
|
||||
};
|
||||
|
||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||
|
||||
fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
|
||||
fn to_linear(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
|
||||
}
|
||||
|
||||
fn out_color(c: vec4<f32>) -> vec4<f32> {
|
||||
if (u.back.w > 0.5) {
|
||||
return vec4<f32>(to_linear(c.rgb), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
struct TOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) n: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let c = solid(i, front);
|
||||
return c;
|
||||
}
|
||||
|
||||
struct CutOut {
|
||||
@location(0) c: vec4<f32>,
|
||||
@builtin(frag_depth) depth: f32,
|
||||
};
|
||||
|
||||
/// The section cap (the inside seen through the cut, drawn on the far walls' back faces where
|
||||
/// they are the visible surface) again, at the depth of the section plane rather than of the
|
||||
/// far wall, so the cut face hides what lies inside the body instead of showing it like a
|
||||
/// pane of glass.
|
||||
@fragment
|
||||
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
|
||||
var o: CutOut;
|
||||
o.c = solid(i, front);
|
||||
o.depth = cap_depth(i.wp, i.pos.z);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
|
||||
/// never behind the wall itself (`z`).
|
||||
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
|
||||
var dir = -u.back.xyz;
|
||||
if (u.eye.w > 0.5) { dir = wp - u.eye.xyz; }
|
||||
let dn = dot(u.clip.xyz, dir);
|
||||
if (abs(dn) < 1e-9) { return z; }
|
||||
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
|
||||
let c = u.vp * vec4<f32>(p, 1.0);
|
||||
if (c.w <= 0.0) { return z; }
|
||||
return clamp(c.z / c.w, 0.0, z);
|
||||
}
|
||||
|
||||
fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
// Derivatives first, in uniform control flow (browsers' WGSL compilers reject them after
|
||||
// a branch that returns; the whole shader module then fails and nothing is drawn).
|
||||
let k = curvature(i);
|
||||
let ndv = dot(normalize(i.n), normalize(u.back.xyz));
|
||||
let fw = fwidth(ndv);
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
}
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
return analysis(i, k);
|
||||
}
|
||||
let c = shade(i);
|
||||
// Silhouettes (styles with edges): where a curved surface turns away from the eye, a line
|
||||
// about 1.5 px wide, on the visible surface only (depth-correct).
|
||||
if (u.screen.w > 0.5 && length(i.n) > 0.5 && fw > 1e-6) {
|
||||
let a = 1.0 - smoothstep(fw * 0.8, fw * 2.0, abs(ndv));
|
||||
return vec4<f32>(mix(c.rgb, out_color(vec4<f32>(0.07, 0.08, 0.10, 1.0)).rgb, a), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/// How fast the normal turns across the pixel (per mm).
|
||||
fn curvature(i: TOut) -> f32 {
|
||||
let nn = normalize(i.n);
|
||||
let kx = length(dpdx(nn)) / max(length(dpdx(i.wp)), 1e-6);
|
||||
let ky = length(dpdy(nn)) / max(length(dpdy(i.wp)), 1e-6);
|
||||
return max(kx, ky);
|
||||
}
|
||||
|
||||
/// Surface analysis colours: zebra stripes, draft angle bands, curvature map (`k`: the
|
||||
/// surface's curvature here, worked out by the caller).
|
||||
fn analysis(i: TOut, k: f32) -> vec4<f32> {
|
||||
let v = normalize(u.back.xyz);
|
||||
var n = normalize(i.n);
|
||||
if (dot(n, v) < 0.0) { n = -n; }
|
||||
let lit = 0.55 + 0.45 * max(dot(n, v), 0.0);
|
||||
if (u.ana.x < 1.5) {
|
||||
// Reflections of upright light bars standing around the viewer: the stripe follows
|
||||
// the reflected ray's sideways direction (horizontal on screen).
|
||||
let r = reflect(-v, n);
|
||||
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
|
||||
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
|
||||
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
|
||||
let c = select(0.08, 0.95, s > 0.0);
|
||||
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
|
||||
}
|
||||
if (u.ana.x > 4.5) {
|
||||
// Reachable from the access direction: facing it and nothing above.
|
||||
let d = normalize(u.pull.xyz);
|
||||
let size = i32(u.ana.w);
|
||||
let px = vec2<i32>(floor(vec2<f32>(dot(i.wp, u.acc_u.xyz) + u.acc_u.w, dot(i.wp, u.acc_v.xyz) + u.acc_v.w) * u.ana.z));
|
||||
var top = -1e30;
|
||||
if (px.x >= 0 && px.y >= 0 && px.x < size && px.y < size) {
|
||||
top = textureLoad(acc_t, px, 0).x;
|
||||
}
|
||||
let cn = dot(normalize(i.n), d);
|
||||
let facing = cn > 0.02;
|
||||
// Slack of a pixel and a half, more on slopes (the map holds the pixel's highest point).
|
||||
let slack = u.ana.y * (1.0 + sqrt(max(1.0 - cn * cn, 0.0)) / max(cn, 0.05));
|
||||
let clear = dot(i.wp, d) >= top - slack;
|
||||
var c = vec3<f32>(0.85, 0.22, 0.20);
|
||||
if (facing && clear) { c = vec3<f32>(0.20, 0.75, 0.30); }
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
if (u.ana.x > 3.5) {
|
||||
// A studio around the model: sky above, a dark floor below, a bright horizon band and
|
||||
// two soft light panels, seen in a mirror.
|
||||
let r = reflect(-v, n);
|
||||
var c = mix(vec3<f32>(0.32, 0.34, 0.38), vec3<f32>(0.80, 0.86, 0.95), smoothstep(-0.05, 0.6, r.z));
|
||||
c = c + vec3<f32>(0.6) * exp(-abs(r.z) * 40.0);
|
||||
let side = atan2(r.y, r.x);
|
||||
c = c + vec3<f32>(0.9) * smoothstep(0.92, 0.98, cos(side * 2.0)) * smoothstep(0.1, 0.3, r.z) * smoothstep(0.8, 0.6, r.z);
|
||||
if (r.z < -0.02) {
|
||||
// The floor: lighter toward the horizon, with light-panel streaks along it.
|
||||
c = mix(vec3<f32>(0.55, 0.56, 0.60), vec3<f32>(0.20, 0.20, 0.22), clamp(-r.z * 1.6, 0.0, 1.0));
|
||||
c = c + vec3<f32>(0.35) * smoothstep(0.95, 0.99, cos(side * 3.0));
|
||||
}
|
||||
return out_color(vec4<f32>(min(c, vec3<f32>(1.0)), 1.0));
|
||||
}
|
||||
if (u.ana.x < 2.5) {
|
||||
let a = degrees(asin(clamp(dot(normalize(i.n), normalize(u.pull.xyz)), -1.0, 1.0)));
|
||||
var c = vec3<f32>(0.95, 0.80, 0.15);
|
||||
if (a >= u.ana.y) { c = vec3<f32>(0.20, 0.75, 0.30); }
|
||||
if (a <= -u.ana.y) { c = vec3<f32>(0.85, 0.22, 0.20); }
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
// Curvature from how fast the normal turns across the pixel.
|
||||
let t = clamp(k * u.ana.y, 0.0, 1.0);
|
||||
let c = select(mix(vec3<f32>(0.10, 0.80, 0.25), vec3<f32>(0.90, 0.15, 0.10), t * 2.0 - 1.0),
|
||||
mix(vec3<f32>(0.15, 0.35, 0.95), vec3<f32>(0.10, 0.80, 0.25), t * 2.0), t < 0.5);
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return shade(i);
|
||||
}
|
||||
|
||||
fn shade(i: TOut) -> vec4<f32> {
|
||||
// A zero normal marks flat colour (selection fills, translucent planes).
|
||||
if (length(i.n) < 0.5) {
|
||||
return out_color(i.c);
|
||||
}
|
||||
let v = normalize(u.back.xyz);
|
||||
var n = normalize(i.n);
|
||||
if (dot(n, v) < 0.0) { n = -n; }
|
||||
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
|
||||
let diff = max(dot(n, key), 0.0);
|
||||
let fill = max(dot(n, v), 0.0);
|
||||
let h = normalize(key + v);
|
||||
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill;
|
||||
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
||||
}
|
||||
|
||||
/// Clip-space z of `p` (clipped to `c`) pulled `s` depth biases toward the eye (pushed away
|
||||
/// when negative). Orthographic depth is linear in distance, so the bias is a fixed step of it.
|
||||
/// Perspective depth is not: near the far plane a fixed step spans tens of millimetres (edges
|
||||
/// showed through walls), so the point moves toward the eye by a fraction of its distance.
|
||||
fn biased_z(p: vec3<f32>, c: vec4<f32>, s: f32) -> f32 {
|
||||
if (u.eye.w > 0.5) {
|
||||
let q = u.vp * vec4<f32>(mix(p, u.eye.xyz, u.screen.z * 8.0 * s), 1.0);
|
||||
return q.z / max(q.w, 1e-6) * c.w;
|
||||
}
|
||||
return c.z - u.screen.z * s * c.w;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_ghost(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.pos.z = biased_z(p, o.pos, -4.0);
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
struct LOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) c: vec4<f32>,
|
||||
@location(1) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
|
||||
var pa = a;
|
||||
var pb = b;
|
||||
var ca = u.vp * vec4<f32>(a, 1.0);
|
||||
var cb = u.vp * vec4<f32>(b, 1.0);
|
||||
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
|
||||
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
|
||||
// the segment at the plane instead; clip space is linear in the world point.
|
||||
if (ca.z < 0.0 && cb.z > 0.0) {
|
||||
let k = ca.z / (ca.z - cb.z);
|
||||
ca = mix(ca, cb, k);
|
||||
pa = mix(a, b, k);
|
||||
} else if (cb.z < 0.0 && ca.z > 0.0) {
|
||||
let k = cb.z / (cb.z - ca.z);
|
||||
cb = mix(cb, ca, k);
|
||||
pb = mix(b, a, k);
|
||||
}
|
||||
let half = u.screen.xy * 0.5;
|
||||
let sa = ca.xy / max(ca.w, 1e-6) * half;
|
||||
let sb = cb.xy / max(cb.w, 1e-6) * half;
|
||||
var dir = sb - sa;
|
||||
let len = length(dir);
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
|
||||
let nrm = vec2<f32>(-dir.y, dir.x);
|
||||
// Two triangles: corners (t, side).
|
||||
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
|
||||
var ss = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||
let t = ts[vi];
|
||||
let s = ss[vi];
|
||||
let base = select(ca, cb, t > 0.5);
|
||||
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
|
||||
var o: LOut;
|
||||
o.pos = vec4<f32>(base.xy + off / half * base.w, biased_z(select(pa, pb, t > 0.5), base, 1.0), base.w);
|
||||
o.c = c;
|
||||
o.wp = select(pa, pb, t > 0.5);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
struct IOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) a: f32,
|
||||
@location(2) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var img_t: texture_2d<f32>;
|
||||
@group(1) @binding(1) var img_s: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
|
||||
var o: IOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
// Pulled toward the eye like lines, so a decal on a face is not lost in it.
|
||||
o.pos.z = biased_z(p, o.pos, 1.0);
|
||||
o.uv = uv;
|
||||
o.a = a;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_img(i: IOut) -> @location(0) vec4<f32> {
|
||||
let c = textureSample(img_t, img_s, i.uv);
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(vec4<f32>(c.rgb, c.a * i.a));
|
||||
}
|
||||
|
||||
/// Lines the section never cuts (the grid, highlights drawn on top).
|
||||
@fragment
|
||||
fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Original light and dark palette values. Compiled by the UI theme boundary.
|
||||
use crate::theme::Tokens;
|
||||
use egui::Color32;
|
||||
impl Tokens {
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
app_bar: Color32::from_rgb(52, 56, 64),
|
||||
toolbar: Color32::from_rgb(246, 247, 249),
|
||||
panel: Color32::from_rgb(250, 251, 252),
|
||||
panel_header: Color32::from_rgb(236, 238, 242),
|
||||
border: Color32::from_rgb(208, 212, 220),
|
||||
text: Color32::from_rgb(38, 42, 50),
|
||||
text_dim: Color32::from_rgb(110, 116, 128),
|
||||
accent: Color32::from_rgb(38, 120, 218),
|
||||
accent_soft: Color32::from_rgb(214, 230, 250),
|
||||
sketch_accent: Color32::from_rgb(46, 150, 90),
|
||||
error: Color32::from_rgb(214, 54, 54),
|
||||
warning: Color32::from_rgb(222, 150, 30),
|
||||
icon: Color32::from_rgb(70, 78, 92),
|
||||
icon_fill: Color32::from_rgb(178, 196, 222),
|
||||
timeline: Color32::from_rgb(232, 235, 240),
|
||||
hover: Color32::from_rgb(226, 234, 246),
|
||||
sel_face: Color32::from_rgb(76, 127, 203),
|
||||
sel_edge: Color32::from_rgb(0, 127, 255),
|
||||
sel_edge_rim: Color32::from_rgb(127, 191, 255),
|
||||
hover_edge: Color32::from_rgb(0, 0, 0),
|
||||
hover_edge_halo: Color32::from_rgb(203, 203, 203),
|
||||
hover_face_lift: 40,
|
||||
sel_profile: Color32::from_rgb(26, 84, 167),
|
||||
sel_profile_edge: Color32::from_rgb(47, 109, 198),
|
||||
hover_profile: Color32::from_rgba_unmultiplied_const(75, 129, 253, 48),
|
||||
hover_profile_edge: Color32::from_rgb(75, 129, 253),
|
||||
sel_vertex: Color32::from_rgb(70, 110, 170),
|
||||
origin_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 94),
|
||||
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
|
||||
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
|
||||
origin_point: Color32::from_rgb(214, 215, 218),
|
||||
axis_x: Color32::from_rgb(255, 0, 0),
|
||||
axis_y: Color32::from_rgb(0, 255, 0),
|
||||
axis_z: Color32::from_rgb(2, 2, 248),
|
||||
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
|
||||
construction_geom: Color32::from_rgb(232, 140, 48),
|
||||
body: Color32::from_rgb(176, 186, 198),
|
||||
body_edge: Color32::from_rgb(40, 44, 52),
|
||||
manipulator: Color32::from_rgb(40, 110, 220),
|
||||
preview_add: Color32::from_rgb(112, 160, 226),
|
||||
preview_sketch: Color32::from_rgb(32, 190, 96),
|
||||
preview_cut: Color32::from_rgba_unmultiplied_const(232, 84, 44, 110),
|
||||
cube_face: Color32::from_rgb(236, 239, 244),
|
||||
cube_border: Color32::from_rgb(85, 94, 110),
|
||||
cube_text: Color32::from_rgb(38, 42, 50),
|
||||
cube_hover: Color32::from_rgb(190, 220, 252),
|
||||
viewport_top: Color32::from_rgb(240, 243, 247),
|
||||
viewport_bottom: Color32::from_rgb(203, 210, 221),
|
||||
grid: Color32::from_rgb(196, 202, 212),
|
||||
grid_major: Color32::from_rgb(170, 178, 190),
|
||||
body_selected: Color32::from_rgb(110, 170, 235),
|
||||
sketch: Color32::from_rgb(30, 90, 200),
|
||||
sketch_fixed: Color32::from_rgb(20, 20, 20),
|
||||
sketch_point: Color32::BLACK,
|
||||
sketch_locked: Color32::from_rgb(0, 140, 60),
|
||||
dimension: Color32::from_rgb(36, 40, 48),
|
||||
dimension_driven: Color32::from_rgb(120, 126, 138),
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||
dialog_border: Color32::from_rgb(208, 212, 220),
|
||||
chip: Color32::from_rgb(38, 120, 218),
|
||||
heading_bg: Color32::from_rgb(218, 223, 231),
|
||||
subheading_bg: Color32::from_rgb(234, 237, 242),
|
||||
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
|
||||
glyph_bg: Color32::WHITE,
|
||||
glyph_edge: Color32::from_rgb(190, 110, 100),
|
||||
glyph_text: Color32::from_rgb(150, 40, 35),
|
||||
overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235),
|
||||
rubber_band: Color32::from_rgb(30, 120, 230),
|
||||
app_bar_text: Color32::from_rgb(230, 234, 242),
|
||||
timeline_item: Color32::WHITE,
|
||||
timeline_item_error: Color32::from_rgb(250, 220, 220),
|
||||
timeline_marker: Color32::from_rgb(70, 76, 88),
|
||||
field: Color32::WHITE,
|
||||
box_window: Color32::from_rgb(38, 120, 218),
|
||||
box_crossing: Color32::from_rgb(40, 160, 80),
|
||||
}
|
||||
}
|
||||
pub const fn dark() -> Tokens {
|
||||
let mut t = Tokens::light();
|
||||
t.app_bar = Color32::from_rgb(28, 31, 38);
|
||||
t.toolbar = Color32::from_rgb(37, 41, 49);
|
||||
t.panel = Color32::from_rgb(39, 43, 52);
|
||||
t.panel_header = Color32::from_rgb(47, 52, 62);
|
||||
t.border = Color32::from_rgb(63, 69, 82);
|
||||
t.text = Color32::from_rgb(222, 226, 232);
|
||||
t.text_dim = Color32::from_rgb(148, 155, 168);
|
||||
t.accent = Color32::from_rgb(58, 136, 238);
|
||||
t.accent_soft = Color32::from_rgb(42, 70, 110);
|
||||
t.sketch_accent = Color32::from_rgb(110, 200, 140);
|
||||
t.error = Color32::from_rgb(240, 100, 96);
|
||||
t.warning = Color32::from_rgb(236, 170, 70);
|
||||
t.icon = Color32::from_rgb(208, 214, 226);
|
||||
t.icon_fill = Color32::from_rgb(86, 112, 150);
|
||||
t.timeline = Color32::from_rgb(33, 37, 45);
|
||||
t.hover = Color32::from_rgb(54, 61, 74);
|
||||
t.hover_edge_halo = Color32::from_rgb(203, 203, 203);
|
||||
t.origin_point = Color32::from_rgb(214, 215, 218);
|
||||
t.viewport_top = Color32::from_rgb(46, 52, 64);
|
||||
t.viewport_bottom = Color32::from_rgb(46, 52, 64);
|
||||
t.grid = Color32::from_rgb(59, 65, 76);
|
||||
t.grid_major = Color32::from_rgb(73, 79, 90);
|
||||
t.body = Color32::from_rgb(152, 156, 162);
|
||||
t.body_edge = Color32::from_rgb(16, 16, 18);
|
||||
t.sketch = Color32::from_rgb(92, 150, 255);
|
||||
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_point = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_locked = Color32::from_rgb(96, 204, 116);
|
||||
t.dimension = Color32::from_rgb(224, 228, 234);
|
||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||
t.dialog_bg = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_border = Color32::from_rgb(72, 83, 99);
|
||||
t.chip = Color32::from_rgb(59, 101, 168);
|
||||
t.heading_bg = Color32::from_rgb(38, 44, 55);
|
||||
t.subheading_bg = Color32::from_rgb(48, 55, 68);
|
||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||
t.glyph_edge = Color32::from_rgb(200, 130, 120);
|
||||
t.glyph_text = Color32::from_rgb(240, 165, 155);
|
||||
t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240);
|
||||
t.rubber_band = Color32::from_rgb(92, 160, 255);
|
||||
t.timeline_item = Color32::from_rgb(52, 58, 70);
|
||||
t.timeline_item_error = Color32::from_rgb(110, 50, 54);
|
||||
t.timeline_marker = Color32::from_rgb(170, 178, 192);
|
||||
t.field = Color32::from_rgb(30, 34, 41);
|
||||
t.box_window = Color32::from_rgb(70, 150, 255);
|
||||
t.box_crossing = Color32::from_rgb(80, 200, 120);
|
||||
t
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
||||
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
@@ -103,28 +103,9 @@ impl Appearances {
|
||||
}
|
||||
|
||||
/// The built-in appearance library: (name, colour, opacity).
|
||||
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
|
||||
("Aluminum - Satin", [204, 208, 214], 1.0),
|
||||
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
|
||||
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
|
||||
("Steel - Satin", [158, 164, 172], 1.0),
|
||||
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
|
||||
("Cast Iron", [104, 104, 110], 1.0),
|
||||
("Brass - Polished", [208, 172, 88], 1.0),
|
||||
("Copper", [204, 122, 80], 1.0),
|
||||
("Titanium", [150, 148, 158], 1.0),
|
||||
("ABS - White", [236, 236, 230], 1.0),
|
||||
("ABS - Black", [36, 36, 38], 1.0),
|
||||
("ABS - Red", [196, 40, 40], 1.0),
|
||||
("ABS - Blue", [40, 90, 186], 1.0),
|
||||
("ABS - Yellow", [236, 196, 40], 1.0),
|
||||
("ABS - Green", [52, 150, 80], 1.0),
|
||||
("Rubber - Black", [28, 28, 28], 1.0),
|
||||
("Glass - Clear", [190, 220, 228], 0.25),
|
||||
("Glass - Smoked", [90, 96, 104], 0.45),
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
#[path = "../../../assets/appearances/library.rs"]
|
||||
mod defaults;
|
||||
pub use defaults::LIBRARY;
|
||||
|
||||
/// A library appearance by name (case-insensitive).
|
||||
pub fn library(name: &str) -> Option<Look> {
|
||||
@@ -263,7 +244,7 @@ pub fn pick_tolerance(b: &crate::ModelBody) -> f64 {
|
||||
}
|
||||
|
||||
/// The point of triangle abc nearest p.
|
||||
fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
pub(crate) fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
let (ab, ac, ap) = (b - a, c - a, p - a);
|
||||
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
|
||||
if d1 <= 0.0 && d2 <= 0.0 {
|
||||
|
||||
@@ -124,6 +124,10 @@ fn identity() -> Mat {
|
||||
IDENTITY
|
||||
}
|
||||
|
||||
/// How deep and how many placements [`Document::placements`] walks.
|
||||
pub const MAX_PLACEMENT_DEPTH: usize = 64;
|
||||
pub const MAX_PLACEMENTS: usize = 100_000;
|
||||
|
||||
fn map_plane(m: &Mat, p: &Plane) -> Plane {
|
||||
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
|
||||
Plane::new(o, x, y).unwrap_or(*p)
|
||||
@@ -221,7 +225,8 @@ impl FeatureKind {
|
||||
pt(&mut t.point);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. }
|
||||
| FeatureKind::SurfaceRevolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
pt(origin);
|
||||
dir(d);
|
||||
}
|
||||
@@ -232,9 +237,10 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Shell { faces, .. } | FeatureKind::OffsetFace { faces, .. } | FeatureKind::DeleteFace { faces, .. } => {
|
||||
faces.iter_mut().for_each(pt)
|
||||
}
|
||||
FeatureKind::Shell { faces, .. }
|
||||
| FeatureKind::OffsetFace { faces, .. }
|
||||
| FeatureKind::DeleteFace { faces, .. }
|
||||
| FeatureKind::SurfaceOffset { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::Draft { faces, neutral, pull, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, neutral);
|
||||
@@ -322,6 +328,9 @@ impl FeatureKind {
|
||||
FeatureKind::Revolve { .. }
|
||||
| FeatureKind::Extrude { .. }
|
||||
| FeatureKind::SurfaceExtrude { .. }
|
||||
| FeatureKind::SurfaceRevolve { .. }
|
||||
| FeatureKind::SurfaceSweep { .. }
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::Combine { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
@@ -337,6 +346,7 @@ impl FeatureKind {
|
||||
| FeatureKind::MeshConvert { .. }
|
||||
| FeatureKind::BodyTransfer { .. } => {}
|
||||
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
|
||||
map_plane_ref(m, plane);
|
||||
if let Some(t) = tool {
|
||||
@@ -370,13 +380,18 @@ impl Document {
|
||||
}
|
||||
|
||||
/// Every placement of each component: (component, occurrence path ids, world transform).
|
||||
/// A branch stops past [`MAX_PLACEMENT_DEPTH`] levels, the walk past [`MAX_PLACEMENTS`] placements.
|
||||
pub fn placements(&self) -> Vec<(u64, Vec<u64>, Mat)> {
|
||||
let mut out = vec![(0, Vec::new(), IDENTITY)];
|
||||
let mut stack: Vec<(u64, Vec<u64>, Mat)> = vec![(0, Vec::new(), IDENTITY)];
|
||||
while let Some((comp, path, m)) = stack.pop() {
|
||||
if path.len() > 64 || out.len() > 100_000 {
|
||||
if out.len() > MAX_PLACEMENTS {
|
||||
break;
|
||||
}
|
||||
// Only this branch stops past the depth limit; the others are still walked.
|
||||
if path.len() > MAX_PLACEMENT_DEPTH {
|
||||
continue;
|
||||
}
|
||||
for o in self.occurrences.iter().filter(|o| o.parent == comp) {
|
||||
let mut p = path.clone();
|
||||
p.push(o.id);
|
||||
|
||||
@@ -31,11 +31,14 @@ pub fn validate_body_transfer(doc: &Document, bodies: &[String], from: u64, to:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the bodies in `st`, which the evaluator discards when this fails (so it is changed in
|
||||
/// place rather than copied: a copy of every body for each moved one was most of the cost).
|
||||
pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], from: u64, to: u64, transform: Mat) -> Result<()> {
|
||||
validate_body_transfer(doc, bodies, from, to, &transform)?;
|
||||
let mut next = st.clone();
|
||||
let mut moved = Vec::with_capacity(bodies.len());
|
||||
for name in bodies {
|
||||
let old = st.body(name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
let (slot, old) =
|
||||
st.bodies.iter().enumerate().find(|(_, b)| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
if doc.model_body_component(old) != from {
|
||||
return Err(DocError::Invalid(format!("body `{name}` no longer belongs to transfer source component {from}")));
|
||||
}
|
||||
@@ -47,12 +50,14 @@ pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], f
|
||||
body.source_transform = crate::mat_mul(&transform, &old.source_transform);
|
||||
body.imported = old.imported.clone();
|
||||
body.names = crate::naming::NameCell::same(old);
|
||||
let slot = next.bodies.iter_mut().find(|b| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
*slot = body;
|
||||
moved.push((slot, body));
|
||||
}
|
||||
crate::body_transfer_sheet::remap_sheet_metadata(&mut next, bodies, &transform)?;
|
||||
*st = next;
|
||||
Ok(())
|
||||
for (slot, body) in moved {
|
||||
if let Some(b) = st.bodies.get_mut(slot) {
|
||||
*b = body;
|
||||
}
|
||||
}
|
||||
crate::body_transfer_sheet::remap_sheet_metadata(st, bodies, &transform)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -48,19 +48,21 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::Extrude { sketch, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch, .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch, .. }
|
||||
| FeatureKind::Revolve { sketch, .. }
|
||||
| FeatureKind::Emboss { sketch, .. }
|
||||
| FeatureKind::SheetBase { sketch, .. }
|
||||
| FeatureKind::SheetContour { sketch, .. }
|
||||
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
||||
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } => {
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
|
||||
sk(sketch);
|
||||
sk(path_sketch);
|
||||
}
|
||||
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
|
||||
FeatureKind::SheetFold { sketch: Some(s), .. } => sk(s),
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::Hole { points: Some(p), .. } => sk(&mut p.sketch),
|
||||
FeatureKind::Pattern { features, pattern, .. } => {
|
||||
feats(features);
|
||||
@@ -303,6 +305,7 @@ impl FeatureKind {
|
||||
| K::SplitFace { body, .. }
|
||||
| K::OffsetFace { body, .. }
|
||||
| K::DeleteFace { body, .. }
|
||||
| K::SurfaceOffset { body, .. }
|
||||
| K::ReplaceFace { body, .. }
|
||||
| K::SheetFlange { body, .. }
|
||||
| K::SheetHem { body, .. }
|
||||
@@ -321,6 +324,8 @@ impl FeatureKind {
|
||||
| K::SheetConvert { body, .. }
|
||||
| K::SurfaceExtend { body, .. }
|
||||
| K::SurfaceTrim { body, .. }
|
||||
| K::Unstitch { body, .. }
|
||||
| K::ReverseNormal { body, .. }
|
||||
| K::MeshConvert { body, .. } => mapped_name(body, bodies),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -457,6 +457,46 @@ impl<'a> ExactSketchCurves<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSketchCurves<'_> {
|
||||
/// The sketch curves a generated segment is drawn on: [`find_along`](Self::find_along)'s one
|
||||
/// curve, else every drawn curve that contains it or runs along and overlaps it (projections
|
||||
/// only where no drawn curve does), in the sketch's order. A segment two coincident curves
|
||||
/// were drawn over (four squares drawn edge to edge), or one a profile merged from collinear
|
||||
/// curves (the outline of several adjacent profiles extruded together), is drawn on all of
|
||||
/// them: the set is exact and fixed by the sketch, so its face is named by it (the first its
|
||||
/// name, the others aliases) rather than not at all. Empty when it lies on none.
|
||||
pub(crate) fn find_all_along(&self, generated: &Seg2) -> Vec<String> {
|
||||
if let Some(id) = self.find_along(generated) {
|
||||
return vec![id];
|
||||
}
|
||||
let runs_along = |original: &Seg2| {
|
||||
if exact_segment_contains(original, generated) {
|
||||
return true;
|
||||
}
|
||||
let (Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) = (*original, *generated) else { return false };
|
||||
let direction = b - a;
|
||||
let scale = direction.len().max((d - c).len()).max(1.);
|
||||
if direction.len() <= 1e-12
|
||||
|| direction.cross(c - a).abs() > 1e-10 * scale * scale
|
||||
|| direction.cross(d - a).abs() > 1e-10 * scale * scale
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (tc, td) = ((c - a).dot(direction) / direction.len2(), (d - a).dot(direction) / direction.len2());
|
||||
tc.min(td).max(0.) < tc.max(td).min(1.) - 1e-9
|
||||
};
|
||||
let pick = |linked: bool| -> Vec<String> {
|
||||
self.segments
|
||||
.iter()
|
||||
.filter(|(_, l, originals)| *l == linked && originals.iter().any(runs_along))
|
||||
.map(|(id, _, _)| (*id).to_owned())
|
||||
.collect()
|
||||
};
|
||||
let drawn = pick(false);
|
||||
if drawn.is_empty() { pick(true) } else { drawn }
|
||||
}
|
||||
}
|
||||
|
||||
fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
|
||||
match (original, generated) {
|
||||
(Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => {
|
||||
|
||||
@@ -485,7 +485,7 @@ pub enum FeatureKind {
|
||||
depth: Option<String>,
|
||||
#[serde(default)]
|
||||
hole: HoleKind,
|
||||
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch);
|
||||
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch, into the material: against the sketch normal, or along it when only that side has material);
|
||||
/// the holes follow the points when the sketch changes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
points: Option<SketchPoints>,
|
||||
@@ -607,6 +607,10 @@ pub enum FeatureKind {
|
||||
faces: Vec<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
/// Close the gap by extending the neighbouring faces (the default); `false` leaves it
|
||||
/// open, the faces left a surface body.
|
||||
#[serde(default = "yes", skip_serializing_if = "is_yes")]
|
||||
heal: bool,
|
||||
},
|
||||
/// A box around bodies, grown by a margin.
|
||||
BoundingSolid {
|
||||
@@ -937,6 +941,65 @@ pub enum FeatureKind {
|
||||
curves: Vec<String>,
|
||||
extent: Extent,
|
||||
},
|
||||
/// A body taken apart: each face at `faces` (every face when empty) its own surface body,
|
||||
/// the faces left one surface body under the body's name. A solid comes apart into surfaces.
|
||||
Unstitch {
|
||||
body: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
faces: Vec<Vec3>,
|
||||
},
|
||||
/// A surface body with the faces at `faces` (every face when empty) turned over.
|
||||
ReverseNormal {
|
||||
body: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
faces: Vec<Vec3>,
|
||||
},
|
||||
/// A surface body turned about an axis from open curve chains and the boundaries of
|
||||
/// profiles: walls only, no caps (a solid where a full turn closes them).
|
||||
SurfaceRevolve {
|
||||
sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
curves: Vec<String>,
|
||||
axis: AxisRef,
|
||||
angle: String,
|
||||
/// Two Sides: the angle on the other side.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
angle2: Option<String>,
|
||||
/// Symmetric: `angle` split evenly either side of the sketch.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
symmetric: bool,
|
||||
},
|
||||
/// A surface body swept along a chain of sketch curves from open curve chains and the
|
||||
/// boundaries of profiles: walls only, no caps.
|
||||
SurfaceSweep {
|
||||
sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
curves: Vec<String>,
|
||||
path_sketch: u64,
|
||||
path: Vec<String>,
|
||||
},
|
||||
/// A surface body lofted through sections in order, each one open or closed chain of
|
||||
/// sketch curves: walls only, no caps.
|
||||
SurfaceLoft {
|
||||
sections: Vec<SheetSection>,
|
||||
},
|
||||
/// The faces at `faces` copied out at a signed `distance` along their normals as a new
|
||||
/// surface body (0 copies them); `chain` takes each pick's whole smooth chain of faces;
|
||||
/// fitted faces within `tolerance`.
|
||||
SurfaceOffset {
|
||||
faces: Vec<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
distance: String,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
chain: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tolerance: Option<String>,
|
||||
},
|
||||
/// A planar surface body extended outward by `distance` past the edges through `edges`.
|
||||
SurfaceExtend {
|
||||
body: String,
|
||||
@@ -969,8 +1032,9 @@ pub enum FeatureKind {
|
||||
Import {
|
||||
/// Source file name (display only).
|
||||
file: String,
|
||||
/// The STEP (ISO 10303-21) text.
|
||||
step: String,
|
||||
/// The STEP (ISO 10303-21) text, shared by every copy of the design (undo states,
|
||||
/// previews, scratch sessions): a large file is held once, not once per edit.
|
||||
step: std::sync::Arc<str>,
|
||||
/// The file's product/assembly tree, kept for components.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
components: Vec<solidcraft_kernel::StepNode>,
|
||||
@@ -996,6 +1060,9 @@ pub enum FilletStyle {
|
||||
Constant,
|
||||
/// `radius` is the chord: the width across the blend.
|
||||
Chord,
|
||||
/// G2 quintic blend of planar faces along a straight edge. `radius` sets the
|
||||
/// equivalent circular setback, not a constant curvature radius.
|
||||
Curvature,
|
||||
/// `radius` at the end of each edge nearest `start`, `radius2` at the other; `mid` radii at
|
||||
/// positions between (0 to 1 from the start end); straight between them, or `smooth`.
|
||||
Variable {
|
||||
@@ -1068,6 +1135,9 @@ pub struct MeshData {
|
||||
pub struct SketchPoints {
|
||||
pub sketch: u64,
|
||||
pub ids: Vec<String>,
|
||||
/// Drill the other way: away from the side the holes would go into by themselves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub flip: bool,
|
||||
}
|
||||
|
||||
/// A component: a node of the design tree that owns sketches, features and bodies. The root
|
||||
@@ -1091,6 +1161,17 @@ pub struct LoftSection {
|
||||
pub point: Option<String>,
|
||||
}
|
||||
|
||||
/// A section of a surface loft: one chain from a sketch (one profile's boundary, or curves
|
||||
/// meeting end to end).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SheetSection {
|
||||
pub sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub curves: Vec<String>,
|
||||
}
|
||||
|
||||
/// Coil cross-sections.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -1238,14 +1319,16 @@ impl FeatureKind {
|
||||
match self {
|
||||
K::Extrude { sketch, .. }
|
||||
| K::SurfaceExtrude { sketch, .. }
|
||||
| K::SurfaceRevolve { sketch, .. }
|
||||
| K::Revolve { sketch, .. }
|
||||
| K::Emboss { sketch, .. }
|
||||
| K::Rib { sketch, .. }
|
||||
| K::SheetBase { sketch, .. }
|
||||
| K::SheetContour { sketch, .. } => vec![*sketch],
|
||||
K::Sweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
||||
K::Sweep { sketch, path_sketch, .. } | K::SurfaceSweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
||||
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
|
||||
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
|
||||
K::SurfaceLoft { sections } => sections.iter().map(|s| s.sketch).collect(),
|
||||
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
|
||||
K::Hole { points, .. } => points.iter().map(|p| p.sketch).collect(),
|
||||
K::Sketch { .. }
|
||||
@@ -1268,6 +1351,7 @@ impl FeatureKind {
|
||||
| K::Scale { .. }
|
||||
| K::OffsetFace { .. }
|
||||
| K::DeleteFace { .. }
|
||||
| K::SurfaceOffset { .. }
|
||||
| K::BoundingSolid { .. }
|
||||
| K::ReplaceFace { .. }
|
||||
| K::Align { .. }
|
||||
@@ -1286,6 +1370,8 @@ impl FeatureKind {
|
||||
| K::Thicken { .. }
|
||||
| K::SurfaceExtend { .. }
|
||||
| K::SurfaceTrim { .. }
|
||||
| K::Unstitch { .. }
|
||||
| K::ReverseNormal { .. }
|
||||
| K::Move { .. }
|
||||
| K::Import { .. }
|
||||
| K::MeshImport { .. }
|
||||
@@ -1345,6 +1431,7 @@ impl FeatureKind {
|
||||
FeatureKind::Scale { .. } => "ScaleFeature",
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFacesFeature",
|
||||
FeatureKind::DeleteFace { .. } => "DeleteFaceFeature",
|
||||
FeatureKind::SurfaceOffset { .. } => "OffsetSurfaceFeature",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
|
||||
FeatureKind::Pipe { .. } => "PipeFeature",
|
||||
FeatureKind::Emboss { .. } => "EmbossFeature",
|
||||
@@ -1370,10 +1457,15 @@ impl FeatureKind {
|
||||
FeatureKind::Remove { .. } => "RemoveFeature",
|
||||
FeatureKind::Patch { .. } => "PatchFeature",
|
||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoftFeature",
|
||||
FeatureKind::Stitch { .. } => "StitchFeature",
|
||||
FeatureKind::Thicken { .. } => "ThickenFeature",
|
||||
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
|
||||
FeatureKind::SurfaceExtend { .. } => "ExtendFeature",
|
||||
FeatureKind::Unstitch { .. } => "UnstitchFeature",
|
||||
FeatureKind::ReverseNormal { .. } => "ReverseNormalFeature",
|
||||
FeatureKind::Import { .. } => "BaseFeature",
|
||||
FeatureKind::MeshImport { .. } => "MeshFeature",
|
||||
FeatureKind::MeshConvert { .. } => "MeshConvertFeature",
|
||||
@@ -1412,6 +1504,7 @@ impl FeatureKind {
|
||||
FeatureKind::Scale { .. } => "Scale",
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFace",
|
||||
FeatureKind::DeleteFace { .. } => "DeleteFace",
|
||||
FeatureKind::SurfaceOffset { .. } => "OffsetSurface",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
|
||||
FeatureKind::Pipe { .. } => "Pipe",
|
||||
FeatureKind::Emboss { .. } => "Emboss",
|
||||
@@ -1437,10 +1530,15 @@ impl FeatureKind {
|
||||
FeatureKind::Remove { .. } => "Remove",
|
||||
FeatureKind::Patch { .. } => "Patch",
|
||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoft",
|
||||
FeatureKind::Stitch { .. } => "Stitch",
|
||||
FeatureKind::Thicken { .. } => "Thicken",
|
||||
FeatureKind::SurfaceTrim { .. } => "Trim",
|
||||
FeatureKind::SurfaceExtend { .. } => "Extend",
|
||||
FeatureKind::Unstitch { .. } => "Unstitch",
|
||||
FeatureKind::ReverseNormal { .. } => "ReverseNormal",
|
||||
FeatureKind::Import { .. } => "Import",
|
||||
FeatureKind::MeshImport { .. } => "Mesh",
|
||||
FeatureKind::MeshConvert { .. } => "ConvertMesh",
|
||||
@@ -1457,10 +1555,14 @@ impl FeatureKind {
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
| FeatureKind::Emboss { sketch, profiles, .. }
|
||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sk), profiles, .. } | FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. } => {
|
||||
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
|
||||
vec![(*sk, profiles)]
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().filter_map(|s| Some((s.sketch, s.profiles.as_mut()?))).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1473,12 +1575,16 @@ impl FeatureKind {
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
| FeatureKind::Emboss { sketch, profiles, .. }
|
||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sketch), profiles, .. } | FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. } => {
|
||||
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
|
||||
vec![(*sketch, profiles)]
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } => {
|
||||
sections.iter().filter(|section| section.point.is_none()).map(|section| (section.sketch, §ion.profiles)).collect()
|
||||
}
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter().filter_map(|s| Some((s.sketch, s.profiles.as_ref()?))).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1489,6 +1595,7 @@ impl FeatureKind {
|
||||
| FeatureKind::Draft { faces, .. }
|
||||
| FeatureKind::OffsetFace { faces, .. }
|
||||
| FeatureKind::DeleteFace { faces, .. }
|
||||
| FeatureKind::SurfaceOffset { faces, .. }
|
||||
| FeatureKind::SplitFace { faces, .. }
|
||||
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
|
||||
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
|
||||
@@ -1538,7 +1645,7 @@ impl FeatureKind {
|
||||
v.push(d);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { angle, angle2, .. } => {
|
||||
FeatureKind::Revolve { angle, angle2, .. } | FeatureKind::SurfaceRevolve { angle, angle2, .. } => {
|
||||
v.push(angle);
|
||||
v.extend(angle2.iter().map(String::as_str));
|
||||
}
|
||||
@@ -1550,7 +1657,7 @@ impl FeatureKind {
|
||||
v.extend(mid.iter().map(|(_, r)| r.as_str()));
|
||||
}
|
||||
FilletStyle::Spline { radii, .. } => v.extend(radii.iter().map(String::as_str)),
|
||||
FilletStyle::Constant | FilletStyle::Chord => {}
|
||||
FilletStyle::Constant | FilletStyle::Chord | FilletStyle::Curvature => {}
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
@@ -1586,7 +1693,7 @@ impl FeatureKind {
|
||||
},
|
||||
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::Shell { thickness, .. } => v.push(thickness),
|
||||
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } => {}
|
||||
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } | FeatureKind::SurfaceSweep { .. } | FeatureKind::SurfaceLoft { .. } => {}
|
||||
FeatureKind::Draft { angle, neutral, .. } => {
|
||||
v.push(angle);
|
||||
plane_exprs(neutral, &mut v);
|
||||
@@ -1664,11 +1771,18 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
|
||||
FeatureKind::Align { .. } | FeatureKind::Remove { .. } | FeatureKind::BoundaryFill { .. } | FeatureKind::DeleteFace { .. } => {}
|
||||
FeatureKind::SurfaceOffset { distance, tolerance, .. } => {
|
||||
v.push(distance);
|
||||
if let Some(t) = tolerance {
|
||||
v.push(t);
|
||||
}
|
||||
}
|
||||
FeatureKind::Stitch { tolerance, .. } => v.push(tolerance),
|
||||
FeatureKind::MeshConvert { tolerance, .. } => v.push(tolerance),
|
||||
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
|
||||
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::SurfaceExtend { distance, .. } => v.push(distance),
|
||||
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
|
||||
FeatureKind::Patch { .. } => {}
|
||||
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } | FeatureKind::BodyTransfer { .. } => {}
|
||||
}
|
||||
@@ -1725,6 +1839,14 @@ fn all_profiles() -> ProfileSel {
|
||||
ProfileSel::All
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_yes(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn zero_expr() -> String {
|
||||
"0".into()
|
||||
}
|
||||
@@ -2557,45 +2679,9 @@ impl Document {
|
||||
}
|
||||
|
||||
/// Density of the default material (g/cm³), as Fusion's default.
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
("Default", DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
("Aluminum", 2.70),
|
||||
("Brass", 8.50),
|
||||
("Copper", 8.96),
|
||||
("Titanium", 4.43),
|
||||
("Cast Iron", 7.20),
|
||||
("ABS Plastic", 1.06),
|
||||
("PLA", 1.24),
|
||||
("Nylon", 1.14),
|
||||
("Polycarbonate", 1.20),
|
||||
("Oak", 0.75),
|
||||
("Glass", 2.50),
|
||||
];
|
||||
|
||||
/// How a physical material looks (sRGB); the default material has no colour of its own.
|
||||
pub fn material_color(name: &str) -> Option<[u8; 3]> {
|
||||
Some(match name {
|
||||
"Steel" => [158, 164, 172],
|
||||
"Stainless Steel" => [186, 191, 198],
|
||||
"Aluminum" => [204, 208, 214],
|
||||
"Brass" => [208, 172, 88],
|
||||
"Copper" => [204, 122, 80],
|
||||
"Titanium" => [150, 148, 158],
|
||||
"Cast Iron" => [104, 104, 110],
|
||||
"ABS Plastic" => [222, 222, 216],
|
||||
"PLA" => [110, 160, 222],
|
||||
"Nylon" => [236, 230, 214],
|
||||
"Polycarbonate" => [196, 216, 232],
|
||||
"Oak" => [190, 146, 92],
|
||||
"Glass" => [170, 208, 218],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
#[path = "../../../assets/materials/library.rs"]
|
||||
mod materials;
|
||||
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
|
||||
|
||||
/// A physical material defined in a design (`material.create`): its name, its density and,
|
||||
/// optionally, the look it gives bodies without an appearance of their own.
|
||||
|
||||
@@ -5,7 +5,8 @@ use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use serde::Serialize;
|
||||
use solidcraft_geom::{Mesh, Plane, Region2, Vec2, Vec3};
|
||||
use solidcraft_geom::tess::TessPlan;
|
||||
use solidcraft_geom::{Mesh, Plane, Region2, Vec2, Vec3, regions_apart};
|
||||
use solidcraft_kernel::{self as kernel, Body, BoolOp};
|
||||
use solidcraft_sketch::{CurveKind, Profile, SolveReport, find_profiles};
|
||||
|
||||
@@ -27,6 +28,12 @@ pub struct ModelBody {
|
||||
// Timeline snapshots are cloned before meshes are requested. Share the cell itself so
|
||||
// meshing any snapshot also warms every unchanged copy of this body.
|
||||
mesh: Arc<OnceLock<Arc<Mesh>>>,
|
||||
/// The display plan (none when the body has no plan: a mesh body, or one the planner
|
||||
/// refuses), shared like the mesh. A GPU viewport draws it.
|
||||
plan: Arc<OnceLock<Option<Arc<TessPlan>>>>,
|
||||
/// The plan evaluated on the CPU (none without a plan), shared like the mesh: the triangles a
|
||||
/// GPU viewport draws, so a highlight over a face lies exactly on it.
|
||||
plan_mesh: Arc<OnceLock<Option<Arc<Mesh>>>>,
|
||||
/// Identity of this body's content for the evaluation cache: the key of the evaluation
|
||||
/// that produced it (kept while later features pass it through unchanged).
|
||||
key: u64,
|
||||
@@ -40,6 +47,9 @@ pub struct ModelBody {
|
||||
pub transferred: bool,
|
||||
/// The look it was imported with, carried through later features.
|
||||
pub imported: Option<Arc<crate::appearance::ImportedLook>>,
|
||||
/// [`Body::retained_bytes`] of `body`, measured when a feature made or changed it (0: not
|
||||
/// measured), so the evaluation cache's budget does not walk every body after every feature.
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl ModelBody {
|
||||
@@ -51,12 +61,15 @@ impl ModelBody {
|
||||
body,
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
plan: Arc::new(OnceLock::new()),
|
||||
plan_mesh: Arc::new(OnceLock::new()),
|
||||
key: 0,
|
||||
names: Default::default(),
|
||||
made: Default::default(),
|
||||
component: 0,
|
||||
transferred: false,
|
||||
imported: None,
|
||||
bytes: 0,
|
||||
}
|
||||
}
|
||||
pub fn definition_name(&self) -> &str {
|
||||
@@ -66,14 +79,42 @@ impl ModelBody {
|
||||
/// curved face or edge turning more than [`kernel::DISPLAY_ANGLE`], so that a bend or a
|
||||
/// hole that is small beside the body is still drawn round.
|
||||
pub fn mesh(&self) -> Arc<Mesh> {
|
||||
// From the reference mesher, not the display plan: the model reads this mesh (holes
|
||||
// finding their surface, picks naming their face, sketch planes), and a different
|
||||
// triangulation moved those and changed what designs build. A GPU viewport draws the
|
||||
// plan ([`ModelBody::display_plan`]).
|
||||
self.mesh
|
||||
.get_or_init(|| {
|
||||
let tol = (self.body.size() * 1e-3).max(1e-3);
|
||||
Arc::new(self.body.display_mesh_angular(tol, kernel::DISPLAY_ANGLE).unwrap_or_default())
|
||||
.get_or_init(|| Arc::new(self.body.reference_display_mesh_angular(self.display_tol(), kernel::DISPLAY_ANGLE).unwrap_or_default()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan of this body (the same tolerance and angle as [`ModelBody::mesh`]), made
|
||||
/// once; `None` when the body has no plan (a mesh body, or one the planner refuses).
|
||||
pub fn display_plan(&self) -> Option<Arc<TessPlan>> {
|
||||
self.plan
|
||||
.get_or_init(|| match self.body.display_plan_angular(self.display_tol(), kernel::DISPLAY_ANGLE) {
|
||||
Ok(plan) => Some(Arc::new(plan)),
|
||||
Err(e) => {
|
||||
log::debug!("{}: no display plan ({e})", self.name);
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan's mesh, evaluated on the CPU once: the same triangles a GPU viewport
|
||||
/// evaluates and draws for this body. Highlights over a face drawn from the plan use it, since
|
||||
/// the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and the two
|
||||
/// would fight at equal depth. `None` when the body has no plan.
|
||||
pub fn plan_mesh(&self) -> Option<Arc<Mesh>> {
|
||||
self.plan_mesh.get_or_init(|| self.display_plan().and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new)).clone()
|
||||
}
|
||||
|
||||
/// The display mesh's chord tolerance: 1/1000 of the body's size.
|
||||
fn display_tol(&self) -> f64 {
|
||||
(self.body.size() * 1e-3).max(1e-3)
|
||||
}
|
||||
|
||||
/// Already computed display geometry; saving must not trigger cold tessellation.
|
||||
pub fn cached_mesh(&self) -> Option<Arc<Mesh>> {
|
||||
self.mesh.get().cloned()
|
||||
@@ -446,19 +487,28 @@ impl ModelState {
|
||||
if !f.shell_piece_order {
|
||||
crate::lineage::order_lumps(&mut self.bodies, &origin);
|
||||
}
|
||||
// The bodies before the feature by name, in their order (names repeat only in damaged
|
||||
// states): every lookup below is the first match of a linear search, without walking
|
||||
// every earlier body for every body, after every feature.
|
||||
let mut by_name: std::collections::HashMap<&str, Vec<&ModelBody>> = std::collections::HashMap::with_capacity(before.bodies.len());
|
||||
for old in &before.bodies {
|
||||
by_name.entry(old.name.as_str()).or_default().push(old);
|
||||
}
|
||||
for b in &mut self.bodies {
|
||||
let olds: &[&ModelBody] = by_name.get(b.name.as_str()).map(Vec::as_slice).unwrap_or_default();
|
||||
if !matches!(f.kind, FeatureKind::BodyTransfer { .. })
|
||||
&& let Some(old) = before.bodies.iter().find(|old| old.name == b.name && old.transferred)
|
||||
&& let Some(old) = olds.iter().find(|old| old.transferred)
|
||||
{
|
||||
b.transferred = true;
|
||||
b.component = old.component;
|
||||
b.source_transform = old.source_transform;
|
||||
}
|
||||
let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body));
|
||||
let kept = olds.iter().find(|x| x.body.same(&b.body));
|
||||
match kept {
|
||||
Some(x) => {
|
||||
b.key = x.key;
|
||||
b.names = x.names.clone();
|
||||
b.bytes = if x.bytes > 0 { x.bytes } else { b.body.retained_bytes() };
|
||||
}
|
||||
None => {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
@@ -469,17 +519,18 @@ impl ModelState {
|
||||
}
|
||||
// Faces split on purpose stay split through later features (for export).
|
||||
if b.body.split_lines().is_none()
|
||||
&& let Some(lines) = before.bodies.iter().find(|x| x.name == b.name).and_then(|x| x.body.split_lines())
|
||||
&& let Some(lines) = olds.first().and_then(|x| x.body.split_lines())
|
||||
{
|
||||
b.body = b.body.clone().with_split_lines(Some(lines.clone()));
|
||||
}
|
||||
// An imported look outlives the operations on the body.
|
||||
if b.body.paint().is_none()
|
||||
&& b.body.color().is_none()
|
||||
&& let Some(old) = before.bodies.iter().find(|x| x.name == b.name)
|
||||
&& let Some(old) = olds.first()
|
||||
{
|
||||
b.imported = crate::appearance::ImportedLook::of(old);
|
||||
}
|
||||
b.bytes = b.body.retained_bytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,7 +621,7 @@ impl ModelState {
|
||||
/// topology is what makes a kept timeline snapshot expensive, so it dominates; sketches
|
||||
/// and the rest are counted from their entity counts.
|
||||
pub fn retained_bytes(&self) -> usize {
|
||||
let bodies: usize = self.bodies.iter().map(|b| b.body.retained_bytes()).sum();
|
||||
let bodies: usize = self.bodies.iter().map(|b| if b.bytes > 0 { b.bytes } else { b.body.retained_bytes() }).sum();
|
||||
let sketches: usize = self.sketches.iter().map(|s| s.retained_bytes()).sum();
|
||||
bodies + sketches + self.sheets.len() * 4096 + self.threads.len() * 192 + self.construct.len() * 192
|
||||
}
|
||||
@@ -578,6 +629,17 @@ impl ModelState {
|
||||
pub fn body(&self, name: &str) -> Option<&ModelBody> {
|
||||
self.bodies.iter().find(|b| b.name == name)
|
||||
}
|
||||
|
||||
/// Make every body's display mesh now, on all cores (bodies are meshed independently and
|
||||
/// each keeps its own mesh, so the meshes are those `ModelBody::mesh` makes one by one).
|
||||
/// Stops starting bodies once `go_on` says no.
|
||||
pub fn warm_meshes(&self, go_on: impl Fn() -> bool + Sync) {
|
||||
kernel::for_each_on_threads(&self.bodies, 16, |b| {
|
||||
if go_on() {
|
||||
let _ = b.mesh();
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn sketch(&self, feature: u64) -> Option<&SolvedSketch> {
|
||||
self.sketches.iter().find(|s| s.feature == feature).map(|s| &**s)
|
||||
}
|
||||
@@ -828,6 +890,7 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
||||
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
let path = match &f.kind {
|
||||
FeatureKind::Sweep { path_sketch, .. }
|
||||
| FeatureKind::SurfaceSweep { path_sketch, .. }
|
||||
| FeatureKind::Pipe { path_sketch, .. }
|
||||
| FeatureKind::Pattern { pattern: crate::PatternKind::Path { path_sketch, .. }, .. } => Some(*path_sketch),
|
||||
_ => None,
|
||||
@@ -840,12 +903,21 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
||||
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
if let FeatureKind::Sweep { sketch, .. } = &f.kind {
|
||||
if let FeatureKind::Sweep { sketch, .. } | FeatureKind::SurfaceSweep { sketch, .. } = &f.kind {
|
||||
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
// To / From Object finds its target where that target's component is placed (#928).
|
||||
let to_object = match &f.kind {
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
|
||||
FeatureKind::Revolve { to, .. } => to.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
if to_object {
|
||||
return if doc.occurrences.is_empty() && doc.body_offsets.is_empty() { 0 } else { placements_hash(doc) };
|
||||
}
|
||||
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|
||||
@@ -853,6 +925,11 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
}
|
||||
|
||||
/// Every occurrence's placement and every body offset.
|
||||
fn placements_hash(doc: &Document) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for o in &doc.occurrences {
|
||||
o.id.hash(&mut h);
|
||||
@@ -968,6 +1045,12 @@ impl Model {
|
||||
let mut restored = 0;
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let ctx = context_digest(doc, &vals);
|
||||
// Each feature's component, for `Document::legacy_body_component` on every body after
|
||||
// every feature without searching the timeline for each (the first feature with an id).
|
||||
let mut feature_component: std::collections::HashMap<u64, u64> = std::collections::HashMap::with_capacity(doc.features.len());
|
||||
for f in &doc.features {
|
||||
feature_component.entry(f.id).or_insert(f.component);
|
||||
}
|
||||
let watched = crate::progress::begin(doc.features.len());
|
||||
for (i, f) in doc.features.iter().enumerate() {
|
||||
let rolled_back = i >= marker;
|
||||
@@ -1048,17 +1131,18 @@ impl Model {
|
||||
FeatureKind::Hole { direction, points: None, .. } => direction.normalized(),
|
||||
_ => None,
|
||||
};
|
||||
let fst = face_point_state(doc, &state, &f.kind, f.component);
|
||||
for (k, (p, n)) in g.kind.face_points_mut().into_iter().zip(&f.face_names).enumerate() {
|
||||
if n.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match crate::naming::locate_face(&state, n, *p) {
|
||||
match crate::naming::locate_face(&fst, n, *p) {
|
||||
Some(hit) => {
|
||||
if let Some(note) = hit.note(n) {
|
||||
notes.push(note);
|
||||
}
|
||||
match (k, hole_axis) {
|
||||
(0, Some(axis)) => match hit.along(&state, *p, axis) {
|
||||
(0, Some(axis)) => match hit.along(&fst, *p, axis) {
|
||||
Some(q) => *p = q,
|
||||
// The place still lies on the face (the axis may run along it).
|
||||
None if hit.on_point => {}
|
||||
@@ -1110,7 +1194,10 @@ impl Model {
|
||||
next.stamp(&state, f, key, mats);
|
||||
for b in &mut next.bodies {
|
||||
if !b.transferred {
|
||||
b.component = doc.legacy_body_component(&b.name, b.feature);
|
||||
b.component = match doc.body_components.get(&b.name) {
|
||||
Some(c) => *c,
|
||||
None => feature_component.get(&b.feature).copied().unwrap_or(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
state = Arc::new(next);
|
||||
@@ -1728,6 +1815,82 @@ fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 {
|
||||
best.unwrap_or(p)
|
||||
}
|
||||
|
||||
/// The faces of the model near `p`: for each, how far `p` is from it, the outward normal there
|
||||
/// (the surface's own normal for planes, cylinders and spheres, turned the way the face looks out
|
||||
/// of its body; the nearest mesh facet's otherwise) and, for a plane, how far `p` is from the
|
||||
/// plane itself. Only faces within `reach` of `p`, nearest first.
|
||||
fn faces_through(st: &ModelState, p: Vec3, reach: f64) -> Vec<(f64, Vec3, Option<f64>)> {
|
||||
let mut out: Vec<(f64, Vec3, Option<f64>)> = Vec::new();
|
||||
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
|
||||
let m = b.mesh();
|
||||
// The nearest facet of each face of this body.
|
||||
let mut near: BTreeMap<u32, (f64, Vec3)> = BTreeMap::new();
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let Some([a, bb, c]) = m.tri(t) else { continue };
|
||||
let d = crate::appearance::closest_on_triangle(p, a, bb, c).dist(p);
|
||||
let Some(n) = (bb - a).cross(c - a).normalized() else { continue };
|
||||
if d <= reach && near.get(f).is_none_or(|(x, _)| d < *x) {
|
||||
near.insert(*f, (d, n));
|
||||
}
|
||||
}
|
||||
for (f, (d, facet)) in near {
|
||||
let (exact, off) = match kernel::face_surface(&b.body, f as usize) {
|
||||
Some(kernel::FaceSurface::Plane { normal, point }) => (Some(normal), Some((p - point).dot(normal).abs())),
|
||||
Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => {
|
||||
let v = p - origin;
|
||||
((v - axis * v.dot(axis)).normalized(), None)
|
||||
}
|
||||
Some(kernel::FaceSurface::Sphere { center, .. }) => ((p - center).normalized(), None),
|
||||
_ => (None, None),
|
||||
};
|
||||
let n = match exact {
|
||||
Some(n) if n.dot(facet) < 0.0 => -n,
|
||||
Some(n) => n,
|
||||
None => facet,
|
||||
};
|
||||
out.push((d, n, off));
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
out
|
||||
}
|
||||
|
||||
/// The way a hole placed at `p` drills: into the material, against the outward normal of the
|
||||
/// face `p` lies on. A point on no face (a planar face's centroid outside it, say) takes the
|
||||
/// nearest planar face whose plane it lies in, else the face nearest it within its body's pick
|
||||
/// tolerance. None when no face is near.
|
||||
pub fn hole_direction_at(st: &ModelState, p: Vec3) -> Option<Vec3> {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = faces_through(st, p, f64::INFINITY);
|
||||
if let Some((d, n, _)) = faces.first()
|
||||
&& *d <= size * 1e-4
|
||||
{
|
||||
return Some(-*n);
|
||||
}
|
||||
if let Some((_, n, _)) = faces.iter().find(|(_, _, off)| off.is_some_and(|o| o <= size * 1e-4)) {
|
||||
return Some(-*n);
|
||||
}
|
||||
let reach = st.bodies.iter().map(crate::appearance::pick_tolerance).fold(0.0, f64::max);
|
||||
faces.first().filter(|(d, _, _)| *d <= reach).map(|(_, n, _)| -*n)
|
||||
}
|
||||
|
||||
/// A hole placed on a face drills into the material: when its direction leaves every face it
|
||||
/// starts on (as designs saved before format 18 can say: their direction was taken from any face
|
||||
/// whose plane passed near the point, such as the other side of a thin plate or a face of another
|
||||
/// body at the same height), it is mirrored in the face it starts on, so it goes in at the same
|
||||
/// angle. A direction that enters a face there, or a spot on no face, is kept.
|
||||
fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = faces_through(st, p, size * 1e-5);
|
||||
if faces.is_empty() || faces.iter().any(|(_, n, _)| n.dot(dir) < -1e-9) {
|
||||
return dir;
|
||||
}
|
||||
match faces.iter().map(|(_, n, _)| *n).max_by(|a, b| a.dot(dir).total_cmp(&b.dot(dir))) {
|
||||
Some(n) if n.dot(dir) > 1e-9 => (dir - n * (2.0 * n.dot(dir))).normalized().unwrap_or(dir),
|
||||
_ => dir,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a picked edge sat in its body: its direction (zero for curved edges) and the body's
|
||||
/// bounding box at the time.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)]
|
||||
@@ -1895,6 +2058,49 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a drill from `p` along `d` reaches material: it starts into a body (`touches`), or,
|
||||
/// when `ahead` is set, a body lies further along `d`. A spot exactly on a surface counts only
|
||||
/// when the drill enters there, not when it leaves.
|
||||
fn hole_reaches(st: &ModelState, p: Vec3, d: Vec3, ahead: bool) -> bool {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let eps = size * 1e-6;
|
||||
let q = p + d * eps;
|
||||
st.bodies.iter().any(|b| {
|
||||
let m = b.mesh();
|
||||
m.contains(q) || (ahead && m.raycast(q, d).is_some())
|
||||
})
|
||||
}
|
||||
|
||||
/// The side hole centres on a sketch drill to: into the material, whichever way the sketch plane
|
||||
/// faces. Material that touches a spot (the drill starts in it) wins over material further away,
|
||||
/// so a sketch on a plate's face drills that plate rather than one across an air gap: `-n` (away
|
||||
/// from the plane's normal) if any spot touches material that way, else `+n` if any spot touches
|
||||
/// material that way. With no touching on either side it goes by the nearest body ahead: `-n` if
|
||||
/// any spot has a body ahead that way, else `+n` if any has, else `-n`.
|
||||
/// The way holes at these points of a sketch drill (see [`sketch_hole_dir`]), flipped or not.
|
||||
/// None when the sketch or a point is not there.
|
||||
pub fn sketch_hole_direction(st: &ModelState, sketch: u64, ids: &[String], flip: bool) -> Option<Vec3> {
|
||||
let ss = st.sketch(sketch)?;
|
||||
let spots: Vec<Vec3> =
|
||||
ids.iter().map(|id| ss.sketch.points.iter().find(|p| &p.id == id).map(|p| ss.plane.to_world(p.pos))).collect::<Option<_>>()?;
|
||||
let dir = sketch_hole_dir(st, &spots, ss.plane.normal());
|
||||
Some(if flip { -dir } else { dir })
|
||||
}
|
||||
|
||||
fn sketch_hole_dir(st: &ModelState, spots: &[Vec3], n: Vec3) -> Vec3 {
|
||||
let down = -n;
|
||||
let any = |d: Vec3, ahead: bool| spots.iter().any(|p| hole_reaches(st, *p, d, ahead));
|
||||
if any(down, false) {
|
||||
down
|
||||
} else if any(n, false) {
|
||||
n
|
||||
} else if any(down, true) || !any(n, true) {
|
||||
down
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a hole feature drills (each on the surface) and in which direction: at its sketch
|
||||
/// points perpendicular to the sketch (into the material), else at its position.
|
||||
fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
@@ -1907,9 +2113,16 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let p = ss.sketch.points.iter().find(|p| &p.id == id).ok_or_else(|| DocError::Unknown(format!("sketch point `{id}`")))?;
|
||||
v.push(ss.plane.to_world(p.pos));
|
||||
}
|
||||
(v, -ss.plane.normal())
|
||||
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
|
||||
let dir = if sp.flip { -dir } else { dir };
|
||||
(v, dir)
|
||||
}
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let at = hole_on_surface(st, *position, dir);
|
||||
let dir = hole_dir_into_face(st, at, dir);
|
||||
return Ok((vec![hole_on_surface(st, at, dir)], dir));
|
||||
}
|
||||
None => (vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?),
|
||||
};
|
||||
if spots.is_empty() || spots.len() > 10_000 {
|
||||
return Err(DocError::Invalid("a hole needs 1…10000 points".into()));
|
||||
@@ -1947,9 +2160,14 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
|
||||
let v: Vec<Vec3> =
|
||||
sp.ids.iter().filter_map(|id| ss.sketch.points.iter().find(|p| &p.id == id)).map(|p| ss.plane.to_world(p.pos)).take(10_000).collect();
|
||||
Ok((v, -ss.plane.normal()))
|
||||
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
|
||||
let dir = if sp.flip { -dir } else { dir };
|
||||
Ok((v, dir))
|
||||
}
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
Ok((vec![*position], hole_dir_into_face(st, hole_on_surface(st, *position, dir), dir)))
|
||||
}
|
||||
None => Ok((vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1983,9 +2201,21 @@ fn sheet_hole_cut(st: &mut ModelState, f: &Feature, tools: &[(Traced, Option<mor
|
||||
for (tool, prism) in tools {
|
||||
let tool = &tool.body;
|
||||
let done = match prism {
|
||||
Some(prism) => more::sheet_primitive_cut(f, st, tool, *prism, Operation::Cut)?,
|
||||
Some(prism) => match more::sheet_primitive_cut(f, st, tool, *prism, Operation::Cut, true) {
|
||||
Ok(done) => done,
|
||||
// Drilled along a panel (into a rim) the hole is no flat cut-out: a bore of
|
||||
// that panel, when it stays inside it.
|
||||
Err(e) => match more::sheet_bore_cut(f, st, *prism)? {
|
||||
Some(done) => done,
|
||||
None => return Err(e),
|
||||
},
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
let done = match (done.is_empty(), prism) {
|
||||
(true, Some(prism)) => more::sheet_bore_cut(f, st, *prism)?.unwrap_or_default(),
|
||||
_ => done,
|
||||
};
|
||||
if done.is_empty() {
|
||||
let mut meets = false;
|
||||
for sheet in &st.sheets {
|
||||
@@ -2265,6 +2495,9 @@ fn centered_extrude(plane: &Plane, region: &Region2, start: f64, end: f64, sketc
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// How far apart (mm) profiles must stay for their extruded tools to be cut as one.
|
||||
const APART_GAP: f64 = 1e-3;
|
||||
|
||||
/// The tool bodies a feature adds or removes (extrude, revolve, primitives), in the current state.
|
||||
fn feature_tools_with_constructor(
|
||||
doc: &Document,
|
||||
@@ -2283,15 +2516,22 @@ fn feature_tools_with_constructor(
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
// From / To Object: where the face or point lies along the normal, measured over the
|
||||
// middle of the profiles.
|
||||
// middle of the profiles. The points are in this feature's component frame, so the
|
||||
// model is seen from there: a target in another, moved component where it is shown.
|
||||
let n = ss.plane.normal();
|
||||
let tst =
|
||||
if extent.to.is_some() || extent.from.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
// The face names follow `face_points`: To, then From.
|
||||
let name = |k: usize| f.face_names.get(k).map(String::as_str);
|
||||
let (to_name, from_name) = if extent.to.is_some() { (name(0), name(1)) } else { (None, name(0)) };
|
||||
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p));
|
||||
let centre = {
|
||||
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let c = pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64);
|
||||
ss.plane.to_world(c)
|
||||
};
|
||||
let from = match extent.from {
|
||||
Some(p) => object_height(st, &ss.plane, centre, p),
|
||||
Some(p) => height_at(&tst, object_face(&tst, from_name, p), &ss.plane, centre, p),
|
||||
None => 0.0,
|
||||
};
|
||||
let d = match extent.to {
|
||||
@@ -2300,7 +2540,7 @@ fn feature_tools_with_constructor(
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let h = object_height(st, &ss.plane, centre, p);
|
||||
let h = height_at(&tst, to_face, &ss.plane, centre, p);
|
||||
let start = from + extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
|
||||
// Past the face by the offset, in the direction travelled.
|
||||
let to = h + if h >= start { off } else { -off };
|
||||
@@ -2344,9 +2584,9 @@ fn feature_tools_with_constructor(
|
||||
let (mut lo, mut hi) = if a <= b { (a, b) } else { (b, a) };
|
||||
// To a curved (or slanted) face: run right through the target body, then keep what
|
||||
// lies before its surface (the tool less the body, the piece at the start).
|
||||
let curved = extent.to.and_then(|p| curved_target(st, n, p));
|
||||
let curved = curved_face(&tst, to_face, Some(n));
|
||||
if let Some(bi) = curved
|
||||
&& let Some(tb) = st.bodies.get(bi)
|
||||
&& let Some(tb) = tst.bodies.get(bi)
|
||||
{
|
||||
let m = tb.mesh();
|
||||
let hs: Vec<f64> = m.positions.iter().map(|q| (*q - ss.plane.origin).dot(n) - off).collect();
|
||||
@@ -2359,6 +2599,8 @@ fn feature_tools_with_constructor(
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
let mut untrimmed = extent.to.is_none() && extent.from.is_none() && !extent.through_all;
|
||||
// The footprint of each tool in the sketch: a tool lies in the prism over it.
|
||||
let mut footprints = Vec::with_capacity(regions.len());
|
||||
for r in ®ions {
|
||||
if *operation == Operation::Join {
|
||||
let exact = centered_extrude(&ss.plane, r, lo + off, hi + off, Some(&ss))?;
|
||||
@@ -2371,6 +2613,7 @@ fn feature_tools_with_constructor(
|
||||
// Preserve a whole touching face: extending into a chamfer replaces
|
||||
// the sewable seam with a coincident curved intersection.
|
||||
tools.extend(exact);
|
||||
footprints.push(r.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2380,8 +2623,22 @@ fn feature_tools_with_constructor(
|
||||
let r = grow_profile_for_coplanar(st, r, *operation, targets, &probe);
|
||||
untrimmed &= l2 == lo + off && h2 == hi + off && &r == original;
|
||||
tools.extend(centered_extrude(&ss.plane, &r, l2, h2, Some(&ss))?);
|
||||
footprints.push(r);
|
||||
}
|
||||
if let Some(tb) = curved.and_then(|bi| st.bodies.get(bi)) {
|
||||
// A cut by profiles apart in the sketch (the letters of a text): each tool lies in the
|
||||
// prism over its footprint, so the tools are apart too (whatever their heights, and
|
||||
// trimmed or not). The body is then cut once by all of them rather than once per
|
||||
// profile, each cut working on a body grown by the one before.
|
||||
// (Sheet metal keeps its own way of cutting, tool by tool, to keep its flat pattern.)
|
||||
let sheet =
|
||||
if targets.is_empty() { !st.sheets.is_empty() } else { targets.iter().any(|t| st.sheets.iter().any(|sheet| &sheet.body == t)) };
|
||||
if *operation == Operation::Cut && !sheet && tools.len() > 1 && regions_apart(&footprints, APART_GAP) {
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
if let Ok(Some(one)) = kernel::join_apart(&bodies) {
|
||||
tools = vec![Traced::step(one, &tools.iter().collect::<Vec<_>>())];
|
||||
}
|
||||
}
|
||||
if let Some(tb) = curved.and_then(|bi| tst.bodies.get(bi)) {
|
||||
let start = if hi > 0.0 { lo + off } else { hi + off };
|
||||
tools = trim_to_body(tools, tb, |q: Vec3| ((q - ss.plane.origin).dot(n) - start).abs())?;
|
||||
}
|
||||
@@ -2439,13 +2696,15 @@ fn feature_tools_with_constructor(
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
// To Object is in this feature's component frame: the model as seen from there.
|
||||
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
let angles = if to.is_some() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
(val(vals, angle, Kind::Angle)?, angle2.as_ref().map(|a| val(vals, a, Kind::Angle)).transpose()?)
|
||||
};
|
||||
let extent = revolve_extent_with_convention(
|
||||
st,
|
||||
&tst,
|
||||
&ss.plane,
|
||||
®ions,
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
@@ -2460,7 +2719,7 @@ fn feature_tools_with_constructor(
|
||||
.map(|r| {
|
||||
if *convention == crate::RevolveConvention::SketchNormal && to.is_some() {
|
||||
revolve_extent_with_convention(
|
||||
st,
|
||||
&tst,
|
||||
&ss.plane,
|
||||
std::slice::from_ref(r),
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
@@ -2501,8 +2760,8 @@ fn feature_tools_with_constructor(
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// To a curved face: turn past the whole target body, then cut back to its surface
|
||||
// (the piece that starts at the profile).
|
||||
let curved = to.and_then(|p| curved_target_any(st, p));
|
||||
let curved_body = curved.and_then(|bi| st.bodies.get(bi));
|
||||
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p)), None);
|
||||
let curved_body = curved.and_then(|bi| tst.bodies.get(bi));
|
||||
// Each region's body: its sides named by the sketch curves, its caps start and end.
|
||||
let tool = |r: &Region2, body: Body, built: Option<&Body>| crate::lineage::ProfileRoles::swept(Some(&ss), Some(r)).tool(body, built);
|
||||
let build_curved = |regions: &[Region2], angle_axis: Vec3, tb: &ModelBody| -> Result<Vec<Traced>> {
|
||||
@@ -3214,9 +3473,14 @@ mod surface;
|
||||
/// there if it is planar and parallel (its plane), else where a ray from `centre` along the
|
||||
/// normal meets the body there, else the point itself.
|
||||
pub fn object_height(st: &ModelState, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
height_at(st, face_at(st, p), plane, centre, p)
|
||||
}
|
||||
|
||||
/// [`object_height`] of the point `p` on `face` (`(body index, face index)`, see [`object_face`]).
|
||||
fn height_at(st: &ModelState, face: Option<(usize, usize)>, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
let n = plane.normal();
|
||||
let h = |q: Vec3| (q - plane.origin).dot(n);
|
||||
let Some((bi, fi)) = face_at(st, p) else { return h(p) };
|
||||
let Some((bi, fi)) = face else { return h(p) };
|
||||
let Some(b) = st.bodies.get(bi) else { return h(p) };
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let Ok(faces) = b.body.faces(tol) else { return h(p) };
|
||||
@@ -3254,24 +3518,37 @@ fn face_at(st: &ModelState, p: Vec3) -> Option<(usize, usize)> {
|
||||
.map(|(_, bi, fi)| (bi, fi))
|
||||
}
|
||||
|
||||
/// The body at `p` when the face there is not a plane square to `n` (To Object must then
|
||||
/// follow the face's shape).
|
||||
fn curved_target(st: &ModelState, n: Vec3, p: Vec3) -> Option<usize> {
|
||||
let (bi, fi) = face_at(st, p)?;
|
||||
/// The face a To / From Object point stands for, `(body index, face index)`: the face its
|
||||
/// stored `name` finds when the point lies on it (evaluation has moved the point onto its named
|
||||
/// face), else the nearest face at the point. The name settles a tie: in an assembly a part of
|
||||
/// another component may rest on the target and touch it at the point.
|
||||
fn object_face(st: &ModelState, name: Option<&str>, p: Vec3) -> Option<(usize, usize)> {
|
||||
if let Some(n) = name.filter(|n| !n.is_empty())
|
||||
&& let Some(hit) = crate::naming::locate_face(st, n, p)
|
||||
&& hit.on_point
|
||||
{
|
||||
return Some((hit.body, hit.face));
|
||||
}
|
||||
face_at(st, p)
|
||||
}
|
||||
|
||||
/// The body of `face` when that face is not a plane (with `n`: not a plane square to `n`), so To
|
||||
/// Object must follow its shape.
|
||||
fn curved_face(st: &ModelState, face: Option<(usize, usize)>, n: Option<Vec3>) -> Option<usize> {
|
||||
let (bi, fi) = face?;
|
||||
let b = st.bodies.get(bi)?;
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let planar =
|
||||
b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).and_then(|f| f.plane_normal).is_some_and(|fnrm| fnrm.cross(n).len() < 1e-6);
|
||||
let normal = b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).map(|f| f.plane_normal);
|
||||
let planar = match n {
|
||||
Some(n) => normal.flatten().is_some_and(|fnrm| fnrm.cross(n).len() < 1e-6),
|
||||
None => normal.is_some_and(|fnrm| fnrm.is_some()),
|
||||
};
|
||||
(!planar).then_some(bi)
|
||||
}
|
||||
|
||||
/// The body at `p` when the face there is not planar.
|
||||
fn curved_target_any(st: &ModelState, p: Vec3) -> Option<usize> {
|
||||
let (bi, fi) = face_at(st, p)?;
|
||||
let b = st.bodies.get(bi)?;
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let planar = b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).is_some_and(|f| f.plane_normal.is_some());
|
||||
(!planar).then_some(bi)
|
||||
curved_face(st, face_at(st, p), None)
|
||||
}
|
||||
|
||||
/// Tools run through `target`, cut back to its surface: each tool less the body, keeping the
|
||||
@@ -3516,7 +3793,13 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Thicken { .. }
|
||||
| FeatureKind::SurfaceTrim { .. }
|
||||
| FeatureKind::SurfaceExtend { .. }
|
||||
| FeatureKind::SurfaceExtrude { .. } => surface::eval(doc, vals, f, st),
|
||||
| FeatureKind::SurfaceExtrude { .. }
|
||||
| FeatureKind::Unstitch { .. }
|
||||
| FeatureKind::ReverseNormal { .. }
|
||||
| FeatureKind::SurfaceRevolve { .. }
|
||||
| FeatureKind::SurfaceSweep { .. }
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::SurfaceOffset { .. } => surface::eval(doc, vals, f, st),
|
||||
FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::ReplaceFace { .. }
|
||||
@@ -3915,6 +4198,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let input = mb.traced();
|
||||
// The blended body, and the lineage of what the last blend started from.
|
||||
let (nb, from) = match &f.kind {
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Curvature, .. } => (kernel::fillet_g2(&mb.body, &edges, r)?, input),
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Chord, .. } => (kernel::fillet_chord(&mb.body, &edges, r)?, input),
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Variable { radius2, start, mid, smooth }, .. } => {
|
||||
let r2 = val(vals, radius2, Kind::Length)?;
|
||||
@@ -3968,7 +4252,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let corner = point(vals, corner)?;
|
||||
let b = kernel::box_solid(corner, corner + s)?;
|
||||
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_box_certificate(f.id)? } else { b };
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation)?;
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation, false)?;
|
||||
let tools = vec![crate::lineage::box_tool(b)];
|
||||
if !handled.is_empty() {
|
||||
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
|
||||
@@ -3980,7 +4264,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let b = kernel::cylinder(base, *axis, radius, height)?;
|
||||
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
|
||||
let prism = more::PrimitivePrism::Cylinder { base, axis: *axis, radius, height };
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation)?;
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false)?;
|
||||
let tools = vec![crate::lineage::cylinder_tool(b)];
|
||||
if !handled.is_empty() {
|
||||
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
|
||||
@@ -4051,7 +4335,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::DeleteFace { faces, body } => {
|
||||
FeatureKind::DeleteFace { faces, body, heal } => {
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let mut picked: Vec<usize> = Vec::new();
|
||||
@@ -4063,7 +4347,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
}
|
||||
let all = kernel::with_same_surface(&mb.body, &picked);
|
||||
let nb = Traced::step(kernel::delete_faces(&mb.body, &all)?, &[&mb.traced()]);
|
||||
let left = if *heal { kernel::delete_faces(&mb.body, &all)? } else { kernel::remove_faces(&mb.body, &all)? };
|
||||
let nb = Traced::step(left, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
}
|
||||
@@ -4134,9 +4419,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
FeatureKind::Import { step, .. } => {
|
||||
let imp = kernel::step_import_shared(step)?;
|
||||
let digest = crate::config::payload_digest(step);
|
||||
// Files from board and assembly tools give thousands of bodies the same name: name
|
||||
// them from a set, not by searching the bodies for every candidate (cubic).
|
||||
let mut names = BodyNames::new(st);
|
||||
for (k, b) in imp.bodies.iter().enumerate() {
|
||||
let base = f.body_names.get(k).filter(|n| !n.trim().is_empty()).unwrap_or(&b.name);
|
||||
let name = unique_body_name(st, base);
|
||||
let name = names.unique(base);
|
||||
let b = crate::lineage::numbered_tool(b.body.clone().with_import_certificate(&digest, f.id)?);
|
||||
st.bodies.push(ModelBody::traced_new(name, b, f.id));
|
||||
}
|
||||
@@ -4169,6 +4457,35 @@ fn mesh_bodies(meshes: &[crate::MeshData]) -> Result<Vec<(String, Body)>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// [`unique_body_name`] for many bodies in a row: the same names, in time linear in the bodies.
|
||||
struct BodyNames {
|
||||
taken: std::collections::HashSet<String>,
|
||||
/// The next suffix worth trying for each base (smaller ones are all taken).
|
||||
next: std::collections::HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl BodyNames {
|
||||
fn new(st: &ModelState) -> Self {
|
||||
BodyNames { taken: st.bodies.iter().map(|b| b.name.clone()).collect(), next: std::collections::HashMap::new() }
|
||||
}
|
||||
fn unique(&mut self, base: &str) -> String {
|
||||
let base = if base.trim().is_empty() { "Body" } else { base.trim() };
|
||||
if self.taken.insert(base.to_string()) {
|
||||
return base.to_string();
|
||||
}
|
||||
let mut i = self.next.get(base).copied().unwrap_or(2);
|
||||
let name = loop {
|
||||
let candidate = format!("{base} ({i})");
|
||||
i += 1;
|
||||
if self.taken.insert(candidate.clone()) {
|
||||
break candidate;
|
||||
}
|
||||
};
|
||||
self.next.insert(base.to_string(), i);
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/// `base`, or `base (2)`, `base (3)`… if a body already has that name.
|
||||
fn unique_body_name(st: &ModelState, base: &str) -> String {
|
||||
let base = if base.trim().is_empty() { "Body" } else { base.trim() };
|
||||
@@ -4196,6 +4513,44 @@ pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std:
|
||||
state_in_frame_at(doc, st, comp, doc.component_transform(comp))
|
||||
}
|
||||
|
||||
/// The model a feature's face points (`FeatureKind::face_points`) are found in. Extrude's To /
|
||||
/// From and Revolve's To place geometry, so they are kept in the feature's own component frame
|
||||
/// (`comp`): the model as that component sees it, a target in another, moved component where it
|
||||
/// is shown (#928). Other face points are picks, kept in the frame of the body they lie on: the
|
||||
/// model itself.
|
||||
pub fn face_point_state<'a>(doc: &Document, st: &'a ModelState, kind: &FeatureKind, comp: u64) -> std::borrow::Cow<'a, ModelState> {
|
||||
match kind {
|
||||
FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. } => state_in_frame(doc, st, comp),
|
||||
_ => std::borrow::Cow::Borrowed(st),
|
||||
}
|
||||
}
|
||||
|
||||
/// The names of the faces a feature's face points lie on (empty where none), found in
|
||||
/// [`face_point_state`]. A To / From point is named after the nearest face over every body, the
|
||||
/// one `object_height` resolves it to, not the first body with a face within tolerance: a part
|
||||
/// resting a hair from the picked face would otherwise take its name.
|
||||
pub fn face_point_names(doc: &Document, st: &ModelState, kind: &FeatureKind, comp: u64) -> Vec<String> {
|
||||
let pts = kind.face_points();
|
||||
if !matches!(kind, FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. }) {
|
||||
return crate::naming::face_names_at(st, &pts);
|
||||
}
|
||||
let fst = face_point_state(doc, st, kind, comp);
|
||||
pts.iter()
|
||||
.map(|p| {
|
||||
fst.bodies
|
||||
.iter()
|
||||
.filter(|b| !b.body.is_mesh())
|
||||
.filter_map(|b| {
|
||||
let (d, fi) = crate::appearance::nearest_face(b, *p)?;
|
||||
(d <= (b.body.size() * 1e-3).max(1e-3)).then_some((d, b, fi))
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
.and_then(|(_, b, fi)| crate::naming::face_names(b).get(fi).cloned())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// As `state_in_frame`, using the exact active placement for its shared source geometry.
|
||||
pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, frame: Mat) -> std::borrow::Cow<'a, ModelState> {
|
||||
use std::borrow::Cow;
|
||||
@@ -4469,3 +4824,33 @@ mod chain_tests {
|
||||
assert_eq!(faces.len(), 3, "{faces:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod body_name_tests {
|
||||
use super::*;
|
||||
|
||||
/// Imported bodies get the names one-at-a-time naming gives them, in time linear in the
|
||||
/// bodies: a board file with thousands of same-named parts once took minutes (#883).
|
||||
#[test]
|
||||
fn many_same_named_bodies_are_named_in_linear_time() {
|
||||
let mut st = ModelState::default();
|
||||
let body = kernel::box_solid(Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0)).unwrap();
|
||||
for name in ["R", "R (3)", "C"] {
|
||||
st.bodies.push(ModelBody::new(name.into(), body.clone(), 1));
|
||||
}
|
||||
// The same names as asking the bodies one at a time.
|
||||
let mut names = BodyNames::new(&st);
|
||||
let mut slow = st.clone();
|
||||
for base in ["R", "R", "C", " ", "Body", "R", "R (2)", "X"] {
|
||||
let expected = unique_body_name(&slow, base);
|
||||
assert_eq!(names.unique(base), expected);
|
||||
slow.bodies.push(ModelBody::new(expected, body.clone(), 2));
|
||||
}
|
||||
// 50,000 parts named alike: milliseconds (one-at-a-time naming would take hours).
|
||||
let began = std::time::Instant::now();
|
||||
let mut names = BodyNames::new(&ModelState::default());
|
||||
let last = (0..50_000).map(|_| names.unique("R_0603")).last().unwrap();
|
||||
assert_eq!(last, "R_0603 (50000)");
|
||||
assert!(began.elapsed() < std::time::Duration::from_secs(5), "{:?}", began.elapsed());
|
||||
}
|
||||
}
|
||||
@@ -345,9 +345,12 @@ fn on_sheet_material(st: &ModelState, i: usize, p: Vec3) -> bool {
|
||||
if mesh.triangles.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`).
|
||||
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`), and a quarter of the
|
||||
// thickness: on a large thin sheet the first steps from a point on its face clean through
|
||||
// it, so a point on the face would touch nothing (#685).
|
||||
let step = 2.0 * (mb.body.size() * 1e-3).max(1e-3);
|
||||
let offsets = [-step, 0.0, step];
|
||||
let thin = st.sheets.get(i).map_or(step, |sh| (sh.t * 0.25).min(step));
|
||||
let offsets = [-step, -thin, 0.0, thin, step];
|
||||
offsets.iter().any(|&dx| offsets.iter().any(|&dy| offsets.iter().any(|&dz| mesh.contains(p + Vec3::new(dx, dy, dz)))))
|
||||
}
|
||||
|
||||
@@ -593,13 +596,18 @@ pub(super) fn sheet_eval(
|
||||
let ds: Vec<f64> = mesh.positions.iter().map(|p| (*p - *face).dot(n)).collect();
|
||||
let (lo, hi) = (ds.iter().cloned().fold(f64::MAX, f64::min), ds.iter().cloned().fold(f64::MIN, f64::max));
|
||||
let t = hi - lo;
|
||||
// The face outline: boundary edges of the triangles on that face's plane.
|
||||
// The face outline: boundary edges of the triangles on that face's plane, where it runs
|
||||
// along a round wall an arc of the wall's circle (not chords: the plate check below
|
||||
// compares its area with the exact volume).
|
||||
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
|
||||
let mut loops = face_loops(&mesh, &plane, *face, n, lo, tol);
|
||||
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
|
||||
let mut walls = std::collections::HashMap::new();
|
||||
let mut wall = |f: u32| *walls.entry(f).or_insert_with(|| round_wall(&mb.body, &plane, n, f));
|
||||
let mut loops: Vec<Loop2> =
|
||||
face_outline(&mesh, &plane, *face, n, lo, tol).iter().map(|l| outline_loop(l, &mut wall, tol * 1e-2)).collect();
|
||||
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
|
||||
let mut it = loops.into_iter();
|
||||
let outer = Loop2::polygon(&it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?).ccw();
|
||||
let region = Region2 { outer, holes: it.map(|l| Loop2::polygon(&l).ccw().reversed()).collect() };
|
||||
let outer = it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?.ccw();
|
||||
let region = Region2 { outer, holes: it.map(|l| l.ccw().reversed()).collect() };
|
||||
let area = region.area().abs();
|
||||
let vol = kernel::measure(&mb.body)?.volume;
|
||||
if (area * t - vol).abs() > 1e-3 * vol.max(1.0) {
|
||||
@@ -726,9 +734,17 @@ fn any_eligible_sheet(f: &Feature, st: &ModelState, targets: &[String]) -> bool
|
||||
|
||||
/// A Box or Cylinder cut (or intersect) whose tool is a straight prism along the normal of
|
||||
/// the sheet it meets: machined like the equivalent extrude of its footprint, so the sheet
|
||||
/// keeps it as a flat cut-out or pocket. Returns the sheet bodies it handled; none when the
|
||||
/// keeps it as a flat cut-out or pocket. A hole's drill (`drill`) may also be square to a
|
||||
/// folded panel of the sheet, a flange's. Returns the sheet bodies it handled; none when the
|
||||
/// primitive is not such a prism (the generic preflight then refuses real 3D machining).
|
||||
pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body, prism: PrimitivePrism, operation: Operation) -> Result<Vec<String>> {
|
||||
pub(super) fn sheet_primitive_cut(
|
||||
f: &Feature,
|
||||
st: &mut ModelState,
|
||||
tool: &Body,
|
||||
prism: PrimitivePrism,
|
||||
operation: Operation,
|
||||
drill: bool,
|
||||
) -> Result<Vec<String>> {
|
||||
if !matches!(operation, Operation::Cut | Operation::Intersect) || !any_eligible_sheet(f, st, &[]) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -738,7 +754,14 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
|
||||
if let Some(mb) = st.body(&sh.body)
|
||||
&& kernel::boolean(&mb.body, tool, kernel::BoolOp::Intersect)?.is_some()
|
||||
{
|
||||
normal = Some(sh.frame.normal());
|
||||
normal = Some(match prism {
|
||||
PrimitivePrism::Cylinder { axis, .. }
|
||||
if drill && !sh.flat && Plane::from_normal(Vec3::ZERO, axis).is_some_and(|p| !sh.parallel_panels(&p).is_empty()) =>
|
||||
{
|
||||
axis
|
||||
}
|
||||
_ => sh.frame.normal(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -748,6 +771,45 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
|
||||
sheet_prism_cut(f, st, &plane, std::slice::from_ref(®ion), operation, &tools, &[], None, false)
|
||||
}
|
||||
|
||||
/// A hole's straight drill along a panel of a sheet (into its rim), kept as a bore of each
|
||||
/// panel it meets (see [`crate::sheet::SheetBore`]), so the sheet still unfolds. Returns the
|
||||
/// sheet bodies it machined; None, with the model as it was, when the drill is not a plain
|
||||
/// cylinder or a sheet it meets can't keep it that way (it runs into a bend, or crosses a
|
||||
/// panel at an angle).
|
||||
pub(super) fn sheet_bore_cut(f: &Feature, st: &mut ModelState, prism: PrimitivePrism) -> Result<Option<Vec<String>>> {
|
||||
let PrimitivePrism::Cylinder { base, axis, radius, height } = prism else { return Ok(None) };
|
||||
let mut plans = Vec::new();
|
||||
for original in &st.sheets {
|
||||
if !sheet_eligible(f, st, &[], original) {
|
||||
continue;
|
||||
}
|
||||
let mut sh = original.clone();
|
||||
match sh.add_bore(base, axis, radius, height) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => continue,
|
||||
// Refused here, the hole keeps the message of the machining it was first tried as.
|
||||
Err(DocError::Invalid(_)) => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let body = sh.solid()?;
|
||||
plans.push((sh, body));
|
||||
}
|
||||
if plans.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut handled = Vec::new();
|
||||
for (sh, body) in plans {
|
||||
if let Some(mb) = st.bodies.iter_mut().find(|b| b.name == sh.body) {
|
||||
replace_sheet_body(mb, body);
|
||||
}
|
||||
handled.push(sh.body.clone());
|
||||
if let Some(slot) = st.sheets.iter_mut().find(|s| s.body == sh.body) {
|
||||
*slot = sh;
|
||||
}
|
||||
}
|
||||
Ok(Some(handled))
|
||||
}
|
||||
|
||||
/// A primitive tool's exact geometry, for [`sheet_primitive_cut`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum PrimitivePrism {
|
||||
@@ -950,9 +1012,13 @@ fn sheet_prism_cut(
|
||||
}
|
||||
modeled = true;
|
||||
} else if z1 > z0 {
|
||||
// Each tool meets only the panel's cut-outs near it: arranging every one of a
|
||||
// perforation's holes against every new hole would grow with their square (#732).
|
||||
let near = sheet::NearHoles::new(&panel)?;
|
||||
for flat in mapped {
|
||||
let local = near.around(&flat)?;
|
||||
let clipped =
|
||||
sheet::material_boolean(std::slice::from_ref(&panel), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
||||
sheet::material_boolean(std::slice::from_ref(&local), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
||||
if !clipped.is_empty() && !depth_resolved {
|
||||
return Err(DocError::Invalid(
|
||||
"sheet machining depth is below the supported 0.000001 mm extrusion resolution; increase the layer depth".into(),
|
||||
@@ -1010,50 +1076,78 @@ fn sheet_prism_cut(
|
||||
/// Outline loops (in `plane` coordinates) of the mesh triangles lying in the plane at distance
|
||||
/// `level` from `at` along `n`: the edges used by one of them only, chained.
|
||||
fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
let on: Vec<[u32; 3]> = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|t| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol)))
|
||||
.collect();
|
||||
face_outline(mesh, plane, at, n, level, tol).into_iter().map(|l| l.into_iter().map(|(p, _)| p).collect()).collect()
|
||||
}
|
||||
|
||||
/// [`face_loops`] with each point's B-rep face across the outline from it to the next point (the
|
||||
/// wall the outline runs along there): the face off the plane that meets both points.
|
||||
fn face_outline(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<(Vec2, Option<u32>)>> {
|
||||
let on_plane = |t: &[u32; 3]| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol));
|
||||
let on: Vec<[u32; 3]> = mesh.triangles.iter().copied().filter(on_plane).collect();
|
||||
// Boundary edges by position (meshes may repeat vertices per triangle).
|
||||
let pos = |k: u32| mesh.positions.get(k as usize).copied().unwrap_or_default();
|
||||
let q = |p: Vec3| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64, (p.z * 1e6).round() as i64);
|
||||
let key = |a: Vec3, b: Vec3| {
|
||||
let (ka, kb) = (q(a), q(b));
|
||||
(ka.min(kb), ka.max(kb))
|
||||
};
|
||||
let mut count: std::collections::HashMap<((i64, i64, i64), (i64, i64, i64)), i32> = Default::default();
|
||||
let edges: Vec<(Vec3, Vec3)> = on.iter().flat_map(|t| [(pos(t[0]), pos(t[1])), (pos(t[1]), pos(t[2])), (pos(t[2]), pos(t[0]))]).collect();
|
||||
for (a, b) in &edges {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
*count.entry((ka.min(kb), ka.max(kb))).or_insert(0) += 1;
|
||||
*count.entry(key(*a, *b)).or_insert(0) += 1;
|
||||
}
|
||||
let mut boundary: Vec<(Vec3, Vec3)> = edges
|
||||
.into_iter()
|
||||
.filter(|(a, b)| {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
count.get(&(ka.min(kb), ka.max(kb))) == Some(&1)
|
||||
})
|
||||
.collect();
|
||||
// The faces off the plane at each point on it, by position to a millionth of a millimetre
|
||||
// either way (a neighbouring face's mesh may round a shared point differently).
|
||||
let mut touch: std::collections::HashMap<(i64, i64, i64), Vec<u32>> = Default::default();
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face).filter(|(t, _)| !on_plane(t)) {
|
||||
for p in t.iter().map(|k| pos(*k)).filter(|p| ((*p - at).dot(n) - level).abs() < tol) {
|
||||
let faces = touch.entry(q(p)).or_default();
|
||||
if !faces.contains(f) {
|
||||
faces.push(*f);
|
||||
}
|
||||
}
|
||||
}
|
||||
let near = |p: Vec3| {
|
||||
let (x, y, z) = q(p);
|
||||
let cells = (-1..=1).flat_map(|i| (-1..=1).flat_map(move |j| (-1..=1).map(move |k| (x + i, y + j, z + k))));
|
||||
let mut faces: Vec<u32> = cells.filter_map(|c| touch.get(&c)).flatten().copied().collect();
|
||||
faces.sort_unstable();
|
||||
faces.dedup();
|
||||
faces
|
||||
};
|
||||
let across = |a: Vec3, b: Vec3| {
|
||||
let at_b = near(b);
|
||||
near(a).into_iter().find(|f| at_b.contains(f))
|
||||
};
|
||||
let mut boundary: Vec<(Vec3, Vec3)> = edges.into_iter().filter(|(a, b)| count.get(&key(*a, *b)) == Some(&1)).collect();
|
||||
// Chain into loops (plane coordinates).
|
||||
let mut loops: Vec<Vec<Vec2>> = Vec::new();
|
||||
let mut loops: Vec<Vec<(Vec2, Option<u32>)>> = Vec::new();
|
||||
for _ in 0..10_000 {
|
||||
let Some((s0, mut cur)) = boundary.pop() else { break };
|
||||
let mut lp = vec![s0];
|
||||
let mut lp = vec![(s0, across(s0, cur))];
|
||||
for _ in 0..boundary.len() + 1 {
|
||||
if q(cur) == q(s0) {
|
||||
break;
|
||||
}
|
||||
lp.push(cur);
|
||||
match boundary.iter().position(|(a, _)| q(*a) == q(cur)) {
|
||||
Some(k) => cur = boundary.remove(k).1,
|
||||
None => break,
|
||||
Some(k) => {
|
||||
let b = boundary.remove(k).1;
|
||||
lp.push((cur, across(cur, b)));
|
||||
cur = b;
|
||||
}
|
||||
None => {
|
||||
lp.push((cur, None));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let raw: Vec<Vec2> = lp.iter().map(|p| plane.to_local(*p)).collect();
|
||||
let raw: Vec<(Vec2, Option<u32>)> = lp.iter().map(|(p, f)| (plane.to_local(*p), *f)).collect();
|
||||
// Drop points in the middle of straight runs.
|
||||
let m = raw.len();
|
||||
let pts: Vec<Vec2> = (0..m)
|
||||
let pts: Vec<(Vec2, Option<u32>)> = (0..m)
|
||||
.filter_map(|i| {
|
||||
let (p, c, n) = (*raw.get((i + m - 1) % m)?, *raw.get(i)?, *raw.get((i + 1) % m)?);
|
||||
((c - p).cross(n - c).abs() > 1e-9 * (c - p).len().max(1e-9) * (n - c).len().max(1e-9)).then_some(c)
|
||||
let (p, c, n) = (raw.get((i + m - 1) % m)?.0, *raw.get(i)?, raw.get((i + 1) % m)?.0);
|
||||
((c.0 - p).cross(n - c.0).abs() > 1e-9 * (c.0 - p).len().max(1e-9) * (n - c.0).len().max(1e-9)).then_some(c)
|
||||
})
|
||||
.collect();
|
||||
if pts.len() >= 3 {
|
||||
@@ -1063,6 +1157,62 @@ fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, le
|
||||
loops
|
||||
}
|
||||
|
||||
/// The circle (centre in `plane` coordinates, radius) of face `f` when it is a round wall
|
||||
/// standing square on the plane: a cylinder about the plane's normal `n`.
|
||||
fn round_wall(body: &Body, plane: &Plane, n: Vec3, f: u32) -> Option<(Vec2, f64)> {
|
||||
match kernel::face_surface(body, f as usize)? {
|
||||
kernel::FaceSurface::Cylinder { origin, axis, radius } if axis.cross(n).len() < 1e-6 => Some((plane.to_local(origin), radius)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop of [`face_outline`] as segments: each run along one round wall (`wall` gives a face's
|
||||
/// circle) is an arc of that circle, the rest stays straight. Coaxial cylinders of one radius are
|
||||
/// one wall, however many faces it comes in (many systems write a round hole's wall as two
|
||||
/// half-cylinders), so such a hole is one circle. `eps`: how far off its circle a point may be.
|
||||
fn outline_loop(pts: &[(Vec2, Option<u32>)], wall: &mut impl FnMut(u32) -> Option<(Vec2, f64)>, eps: f64) -> Loop2 {
|
||||
use std::f64::consts::{PI, TAU};
|
||||
let m = pts.len();
|
||||
let point = |i: usize| pts.get(i % m).map(|x| x.0).unwrap_or_default();
|
||||
// The circle each segment runs along: its wall's, when both its ends lie on it.
|
||||
let circles: Vec<Option<(Vec2, f64)>> = (0..m)
|
||||
.map(|i| {
|
||||
let (c, r) = wall(pts.get(i)?.1?)?;
|
||||
((point(i).dist(c) - r).abs() <= eps && (point(i + 1).dist(c) - r).abs() <= eps).then_some((c, r))
|
||||
})
|
||||
.collect();
|
||||
let circle = |i: usize| circles.get(i % m).copied().flatten();
|
||||
let same = |i: usize, (c, r): (Vec2, f64)| circle(i).is_some_and(|(d, s)| d.dist(c) <= eps && (s - r).abs() <= eps);
|
||||
// Start where no arc runs on across the start (anywhere when the whole loop is one circle).
|
||||
let start = (0..m).find(|i| circle(*i + m - 1).is_none_or(|c| !same(*i, c))).unwrap_or(0);
|
||||
let mut segs = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < m {
|
||||
let k = start + i;
|
||||
let Some((c, r)) = circle(k) else {
|
||||
segs.push(Seg2::Line { a: point(k), b: point(k + 1) });
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
let mut j = i + 1;
|
||||
while j < m && same(start + j, (c, r)) {
|
||||
j += 1;
|
||||
}
|
||||
let sweep: f64 = (i..j)
|
||||
.map(|s| {
|
||||
let d = (point(start + s + 1) - c).angle() - (point(start + s) - c).angle();
|
||||
(d + PI).rem_euclid(TAU) - PI
|
||||
})
|
||||
.sum();
|
||||
// Arcs of up to half a turn each.
|
||||
let pieces = ((sweep.abs() / PI - 1e-9).ceil() as usize).clamp(1, 4);
|
||||
let (a0, step) = ((point(k) - c).angle(), sweep / pieces as f64);
|
||||
segs.extend((0..pieces).map(|p| Seg2::Arc { center: c, radius: r, start: a0 + step * p as f64, sweep: step }));
|
||||
i = j;
|
||||
}
|
||||
Loop2 { segs }
|
||||
}
|
||||
|
||||
/// The body a placed plastic feature joins: by name, else the one under the point.
|
||||
fn body_under(st: &ModelState, body: &Option<String>, p: Vec3) -> Result<String> {
|
||||
let i = body_at(st, body, &[p])?;
|
||||
@@ -1239,8 +1389,32 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let mut loops = face_loops(&mesh, &plane, *face, n, 0.0, tol);
|
||||
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
|
||||
let ccw = |l: &[Vec2]| if poly_area(l) < 0.0 { l.iter().rev().copied().collect::<Vec<_>>() } else { l.to_vec() };
|
||||
let (Some(outer), Some(inner)) = (loops.first().map(|l| ccw(l)), loops.get(1).map(|l| ccw(l))) else {
|
||||
return Err(DocError::Invalid("pick the rim face of a shelled body (a face with an inner and an outer edge)".into()));
|
||||
// The inner edge is an opening (the cavity under a rim): not the foot of a boss or a
|
||||
// wall standing on the face, whose side the band would run against.
|
||||
let opening = |l: &[Vec2]| {
|
||||
let eps = (tol * 4.0).max(1e-3);
|
||||
l.iter().zip(l.iter().cycle().skip(1)).filter_map(|(a, b)| Some(((*a + *b) * 0.5, (*b - *a).normalized()?))).take(3).any(|(m, t)| {
|
||||
// Just over the face, inside the loop (to the left of a ccw loop's edges).
|
||||
let q = plane.to_world(m + Vec2::new(-t.y, t.x) * eps) + n * eps;
|
||||
!mesh.contains(q)
|
||||
})
|
||||
};
|
||||
let outer = loops.first().map(|l| ccw(l));
|
||||
let openings: Vec<Vec<Vec2>> = loops.iter().skip(1).map(|l| ccw(l)).filter(|l| opening(l)).collect();
|
||||
// A rim runs round one opening, the cavity; smaller ones (a screw hole) may sit in it.
|
||||
// Openings alike in size (a plate's holes) leave no edge that is the rim's.
|
||||
let alike = match openings.as_slice() {
|
||||
[a, b, ..] => poly_area(b).abs() > 0.5 * poly_area(a).abs(),
|
||||
_ => false,
|
||||
};
|
||||
let (Some(outer), Some(inner), false) = (outer, openings.first().cloned(), alike) else {
|
||||
let what = match (loops.len(), openings.len()) {
|
||||
(0 | 1, _) => "pick the rim face of a shelled body (a face with an inner and an outer edge)".to_string(),
|
||||
(_, 0) => "pick the rim face of a shelled body: this face's inner edges are where other features stand on it, not an opening"
|
||||
.to_string(),
|
||||
(_, k) => format!("pick the rim face of a shelled body (round one opening): this face has {k} openings alike in size"),
|
||||
};
|
||||
return Err(DocError::Invalid(what));
|
||||
};
|
||||
let bw = if *groove { w + g } else { w };
|
||||
// A lip: the band raised from the rim's own faces (its walls continue the walls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend and Surface Extrude
|
||||
//! (kernel `surfaces`).
|
||||
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
|
||||
//! Revolve, Sweep and Loft, Offset, Unstitch and Reverse Normal (kernel `surfaces`,
|
||||
//! `surface_offset`).
|
||||
|
||||
use super::*;
|
||||
use solidcraft_geom::Seg2;
|
||||
@@ -98,25 +99,167 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let d = val(vals, &extent.distance, Kind::Length)?;
|
||||
let off = extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
|
||||
let (a, b) = extent_sides(vals, extent, d, extent.direction)?;
|
||||
let mut chains: Vec<Vec<Seg2>> = Vec::new();
|
||||
if let Some(sel) = profiles {
|
||||
for r in solidcraft_sketch::merge_regions(&select_profiles(&ss, sel)?) {
|
||||
chains.extend(std::iter::once(r.outer).chain(r.holes).map(|lp| lp.segs));
|
||||
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
|
||||
let body = kernel::extrude_sheet(&ss.plane, &chains, a + off, b + off)?;
|
||||
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
let (start, sweep) = match (angle2, symmetric) {
|
||||
(Some(a2), _) => {
|
||||
let a2 = val(vals, a2, Kind::Angle)?;
|
||||
(-a2, a + a2)
|
||||
}
|
||||
(None, true) => (-a / 2.0, a),
|
||||
(None, false) => (0.0, a),
|
||||
};
|
||||
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
|
||||
let body = kernel::revolve_sheet(&ss.plane, &chains, o, d, start, sweep)?;
|
||||
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Unstitch { body, faces } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
|
||||
let source = mb.traced();
|
||||
// The faces left keep the body's name; each face taken out is a new surface body.
|
||||
match rest {
|
||||
Some(rest) => {
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), source.then(rest), mb.feature);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
st.bodies.remove(i);
|
||||
}
|
||||
}
|
||||
chains.extend(curve_chains(&ss, curves)?);
|
||||
if chains.is_empty() {
|
||||
return Err(DocError::Invalid("pick the profiles or curves to extrude".into()));
|
||||
for (k, piece) in pieces.into_iter().enumerate() {
|
||||
let name = new_name(st, f, k);
|
||||
st.bodies.push(ModelBody::traced_new(name, source.then(piece), f.id));
|
||||
}
|
||||
let body = kernel::extrude_sheet(&ss.plane, &chains, a + off, b + off)?;
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::ReverseNormal { body, faces } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
|
||||
let ps =
|
||||
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
|
||||
let profile;
|
||||
let ss = if !ps.sketch.wires.is_empty() {
|
||||
profile = profile_in_frame(doc, ss, f.component)?;
|
||||
&profile
|
||||
} else {
|
||||
ss
|
||||
};
|
||||
let mapped = path_in_frame(doc, ps, f.component)?;
|
||||
let ps = mapped.as_ref();
|
||||
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
|
||||
// The path runs on from the end nearest the middle of the curves.
|
||||
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
|
||||
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
|
||||
let body = match path_points(ps, path, start)? {
|
||||
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
|
||||
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
|
||||
};
|
||||
let traced = crate::lineage::ChainRoles { sketch: ss, chains: &chains }.tool(body);
|
||||
push_sheet(st, f, traced);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceLoft { sections } => {
|
||||
let mut secs = Vec::with_capacity(sections.len());
|
||||
let mut sketches = Vec::with_capacity(sections.len());
|
||||
for sec in sections {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(sec.sketch)
|
||||
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?,
|
||||
f.component,
|
||||
)?;
|
||||
let profiles = match (&sec.profiles, sec.curves.is_empty()) {
|
||||
(None, true) => Some(&ProfileSel::All),
|
||||
(p, _) => p.as_ref(),
|
||||
};
|
||||
let mut chains = sheet_chains(&ss, profiles, &sec.curves)?.into_iter();
|
||||
let (Some(chain), None) = (chains.next(), chains.next()) else {
|
||||
return Err(DocError::Invalid(
|
||||
"each loft section must be one chain: one profile without holes, or curves meeting end to end".into(),
|
||||
));
|
||||
};
|
||||
secs.push((ss.plane, chain));
|
||||
sketches.push(ss);
|
||||
}
|
||||
let body = kernel::loft_sheet(&secs)?;
|
||||
// Each wall named by the section curves it runs between, as a loft's sides are.
|
||||
let loops: Vec<solidcraft_geom::Loop2> = secs.iter().map(|(_, c)| solidcraft_geom::Loop2 { segs: c.clone() }).collect();
|
||||
let traced = crate::lineage::SectionRoles { sections: sketches.iter().zip(&loops).collect() }.tool(body);
|
||||
push_sheet(st, f, traced);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceOffset { faces, body, distance, chain, tolerance } => {
|
||||
let d = val(vals, distance, Kind::Length)?;
|
||||
let tol = match tolerance {
|
||||
Some(t) => val(vals, t, Kind::Length)?,
|
||||
None => 0.01,
|
||||
};
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let picks = if *chain { smooth_chain(&mb, faces) } else { faces.clone() };
|
||||
let mut picked: Vec<usize> = Vec::new();
|
||||
for p in &picks {
|
||||
let fi =
|
||||
crate::appearance::face_index_at(&mb, *p).ok_or_else(|| DocError::Invalid(format!("Offset: no face of {} at {p:?}", mb.name)))?;
|
||||
if !picked.contains(&fi) {
|
||||
picked.push(fi);
|
||||
}
|
||||
}
|
||||
let made = Traced::step(kernel::offset_surface(&mb.body, &picked, d, tol)?, &[&mb.traced()]);
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::new(name, body, f.id));
|
||||
st.bodies.push(ModelBody::traced_new(name, made, f.id));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DocError::Invalid("not a surface feature".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface builder's body, added to the model with what its faces came from.
|
||||
fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced) {
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
|
||||
}
|
||||
|
||||
/// What a surface builder sweeps: the boundary loops of the picked profiles, then the picked
|
||||
/// curves chained end to end.
|
||||
fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
let mut chains: Vec<Vec<Seg2>> = Vec::new();
|
||||
if let Some(sel) = profiles {
|
||||
for r in solidcraft_sketch::merge_regions(&select_profiles(ss, sel)?) {
|
||||
chains.extend(std::iter::once(r.outer).chain(r.holes).map(|lp| lp.segs));
|
||||
}
|
||||
}
|
||||
chains.extend(curve_chains(ss, curves)?);
|
||||
if chains.is_empty() {
|
||||
return Err(DocError::Invalid("pick the profiles or curves".into()));
|
||||
}
|
||||
Ok(chains)
|
||||
}
|
||||
|
||||
/// Most sketch curves one feature chains.
|
||||
const MAX_CHAIN_CURVES: usize = 1000;
|
||||
|
||||
|
||||
@@ -104,13 +104,47 @@
|
||||
//! Construction Geometries); a sketch that shows its construction geometry saves as before.
|
||||
//! A `SurfaceExtrude` feature (Surface Extrude: `sketch`, optional `profiles`, `curves` and
|
||||
//! `extent`, walls only) is first saved in 17; 0.8.0 would refuse the unknown feature.
|
||||
//! - 18: surface features 0.9.0 doesn't know: `Unstitch` (a body's faces taken out as surface
|
||||
//! bodies) and `ReverseNormal` (a surface body's faces turned over), each a `body` and
|
||||
//! optional `faces` points. Version 17 designs need no transformation; the version stamp makes
|
||||
//! 0.9.0 (which reads up to 17) ask for an update instead of meeting a feature it can't read.
|
||||
//! Under the same version, a sketch text's link source can carry a `style`: `bold` and
|
||||
//! `italic` (the font's bold or italic face), `align` (`left`, `center` or `right`),
|
||||
//! `letter_spacing` (mm), `line_spacing` (times the font's line distance), `path` (the id of a
|
||||
//! curve of the same sketch the baseline follows) and `reverse`. A text without `style` is set
|
||||
//! as it always was; 0.9.0 would drop the style and set the text plain, off its curve.
|
||||
//! Under the same version, Delete Face can leave the gap open (`heal: false` on the feature:
|
||||
//! the faces left are a surface body); a Delete Face without it heals, as it always did, and
|
||||
//! saves as before. 0.9.0 would heal a gap the design leaves open.
|
||||
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
|
||||
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
|
||||
//! A `SurfaceSweep` feature (Surface Sweep: `sketch`, optional `profiles` and `curves`,
|
||||
//! `path_sketch`, `path`) is also first saved in 18.
|
||||
//! A `SurfaceLoft` feature (Surface Loft: `sections`, each a `sketch` with optional `profiles`
|
||||
//! and `curves`) is also first saved in 18.
|
||||
//! A `SurfaceOffset` feature (Offset Surface: `faces` points, optional `body`, `distance`,
|
||||
//! `chain` and `tolerance`) is also first saved in 18.
|
||||
//! Nothing new is saved for holes, but a hole on a face is now drilled into the material: a
|
||||
//! saved `direction` that leaves the face the hole starts on (designs up to 17 could save one,
|
||||
//! taken from another face whose plane passed near the point, such as the far side of a thin
|
||||
//! plate or a face of another body at the same height) is mirrored into that face when the
|
||||
//! design is evaluated. Holes whose direction enters their face build as before.
|
||||
//! Holes at sketch points can be flipped (`flip: true` in the hole's `points`): they drill away
|
||||
//! from the side they would go into by themselves. Holes without it save as before; 0.9.0 would
|
||||
//! drop the flip and drill the other way.
|
||||
|
||||
//! - 19: editable associative offset-distance dimensions. Older offsets retain their saved
|
||||
//! fixed distances; migration does not invent dimensions or change geometry.
|
||||
//! Under the same version, a fillet's style can be `curvature` (a G2 quintic blend; `radius`
|
||||
//! is the equivalent circular setback). Fillets of the other styles save as before; 0.9.0
|
||||
//! would refuse the unknown style.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The format this build writes.
|
||||
pub const FORMAT: u32 = 17;
|
||||
pub const FORMAT: u32 = 19;
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solidcraft/{n}")
|
||||
@@ -127,7 +161,7 @@ pub fn version_of(s: &str) -> Option<u32> {
|
||||
|
||||
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
|
||||
type JsonStep = fn(&mut Value) -> Result<()>;
|
||||
const JSON_STEPS: [JsonStep; 16] = [
|
||||
const JSON_STEPS: [JsonStep; 18] = [
|
||||
|_| Ok(()),
|
||||
v2_to_v3,
|
||||
v3_to_v4,
|
||||
@@ -144,6 +178,8 @@ const JSON_STEPS: [JsonStep; 16] = [
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
];
|
||||
|
||||
/// Stored face and edge names were worked out from geometry before format 14; they get the
|
||||
|
||||
@@ -17,7 +17,8 @@ use solidcraft_geom::Vec3;
|
||||
|
||||
use crate::{DocError, Document, IDENTITY, Mat, ModelState, Result, apply_point, apply_vector, mat_inverse, mat_mul};
|
||||
|
||||
const MAX_JOINTS: usize = 10_000;
|
||||
/// Joints the solver (and the URDF export) reads at most.
|
||||
pub const MAX_JOINTS: usize = 10_000;
|
||||
|
||||
/// Where a joint origin sits on a component's geometry (component coordinates).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -30,6 +30,7 @@ pub mod motion;
|
||||
mod name_lookup;
|
||||
pub mod naming;
|
||||
mod params;
|
||||
pub mod physical_material;
|
||||
mod piece_names;
|
||||
mod profile_refs;
|
||||
pub mod progress;
|
||||
@@ -40,12 +41,15 @@ mod project3d;
|
||||
pub use project3d::iso_grid;
|
||||
pub mod sheet;
|
||||
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse, mat_mul, rigid};
|
||||
pub use assembly::{
|
||||
IDENTITY, MAX_PLACEMENT_DEPTH, MAX_PLACEMENTS, Mat, Occurrence, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse, mat_mul,
|
||||
rigid,
|
||||
};
|
||||
pub use document::*;
|
||||
pub use eval::{
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, RevolveExtent, SolvedSketch, ThreadInfo, curve_pick_error, edge_names_for, edge_refs,
|
||||
object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention, select_profiles, state_in_frame,
|
||||
state_in_frame_at, through_all_distance, world_state,
|
||||
face_point_names, face_point_state, hole_direction_at, object_height, parse_metric_thread, pattern_matrices, revolve_extent,
|
||||
revolve_extent_with_convention, select_profiles, sketch_hole_direction, state_in_frame, state_in_frame_at, through_all_distance, world_state,
|
||||
};
|
||||
pub use params::{ParamRow, ParamUser, cycles};
|
||||
pub use solidcraft_kernel as kernel;
|
||||
|
||||
@@ -207,23 +207,30 @@ impl<'a> ProfileRoles<'a> {
|
||||
let segments: Vec<&solidcraft_geom::Seg2> =
|
||||
self.region.map(|r| std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).collect()).unwrap_or_default();
|
||||
let curves = self.sketch.map(crate::config::ExactSketchCurves::new);
|
||||
let curve = |segment: usize| -> Option<String> { curves.as_ref()?.find_along(segments.get(segment)?).map(|id| format!("side:{id}")) };
|
||||
// Every curve the segment is drawn on (one, or several coincident or collinear ones).
|
||||
let curve = |segment: usize| -> Vec<String> {
|
||||
let (Some(curves), Some(segment)) = (curves.as_ref(), segments.get(segment)) else { return Vec::new() };
|
||||
curves.find_all_along(segment).into_iter().map(|id| format!("side:{id}")).collect()
|
||||
};
|
||||
let roles = history
|
||||
.faces
|
||||
.iter()
|
||||
.map(|face| {
|
||||
let mut roles: Vec<String> = Vec::new();
|
||||
for origin in &face.origins {
|
||||
let role = match *origin {
|
||||
let found = match *origin {
|
||||
FaceOrigin::Profile { segment } => curve(segment),
|
||||
FaceOrigin::Cap { end } => self.caps.get(usize::from(end)).cloned().flatten(),
|
||||
_ => None,
|
||||
FaceOrigin::Cap { end } => self.caps.get(usize::from(end)).cloned().flatten().into_iter().collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
match role {
|
||||
Some(role) if !roles.contains(&role) => roles.push(role),
|
||||
Some(_) => {}
|
||||
// One unknown origin leaves the face unknown: never a partial guess.
|
||||
None => return Vec::new(),
|
||||
// One unknown origin leaves the face unknown: never a partial guess.
|
||||
if found.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
for role in found {
|
||||
if !roles.contains(&role) {
|
||||
roles.push(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
roles
|
||||
@@ -249,6 +256,47 @@ impl<'a> ProfileRoles<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a surface built from chains of sketch segments (Surface Extrude, Revolve, Sweep) names
|
||||
/// its walls: each `side:<curve>` of the sketch curve its segment lies on exactly, segments
|
||||
/// numbered through the chains in order. It has no caps.
|
||||
pub(crate) struct ChainRoles<'a> {
|
||||
pub sketch: &'a SolvedSketch,
|
||||
pub chains: &'a [Vec<solidcraft_geom::Seg2>],
|
||||
}
|
||||
|
||||
impl ChainRoles<'_> {
|
||||
/// The surface `body`, its walls named by their roles where its history gives them.
|
||||
pub fn tool(&self, body: Body) -> Traced {
|
||||
let roles = body.history().filter(|h| h.operands == 0).map(|h| self.of(h));
|
||||
Traced::made(body, roles)
|
||||
}
|
||||
|
||||
fn of(&self, history: &History) -> Vec<Vec<String>> {
|
||||
let segments: Vec<&solidcraft_geom::Seg2> = self.chains.iter().flatten().collect();
|
||||
let curves = crate::config::ExactSketchCurves::new(self.sketch);
|
||||
history
|
||||
.faces
|
||||
.iter()
|
||||
.map(|face| {
|
||||
let mut roles: Vec<String> = Vec::new();
|
||||
for origin in &face.origins {
|
||||
let role = match *origin {
|
||||
FaceOrigin::Profile { segment } => segments.get(segment).and_then(|s| curves.find_along(s)).map(|id| format!("side:{id}")),
|
||||
_ => None,
|
||||
};
|
||||
match role {
|
||||
Some(role) if !roles.contains(&role) => roles.push(role),
|
||||
Some(_) => {}
|
||||
// One unknown origin leaves the face unknown: never a partial guess.
|
||||
None => return Vec::new(),
|
||||
}
|
||||
}
|
||||
roles
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// How a loft names its faces: each side by the sketch curves of the sections it runs between
|
||||
/// (`side:<curve>` for the first section's, `side:s<k>.<curve>` for section `k`'s), named after
|
||||
/// the section with the most segments (the one each side spans exactly one segment of) and
|
||||
|
||||
@@ -260,6 +260,14 @@ impl EdgeHit {
|
||||
/// answering to it (merged into it, or the named piece told apart by the faces around it); else
|
||||
/// the piece of it nearest `p` (`split`).
|
||||
pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
|
||||
// Every body's names are read below: work out the ones not known yet on all cores first
|
||||
// (each body's names are its own, whichever thread works them out).
|
||||
if legacy_name(name).is_none() {
|
||||
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
|
||||
let _ = naming::names(b);
|
||||
});
|
||||
}
|
||||
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,16 +60,19 @@ impl NameCell {
|
||||
pub fn known(names: Arc<Vec<String>>) -> Self {
|
||||
let legacy = OnceLock::new();
|
||||
let _ = legacy.set(names.clone());
|
||||
NameCell { legacy: Arc::new(legacy), ..Self::shared(Arc::new(Names::given(names)), None) }
|
||||
NameCell { legacy: Arc::new(legacy), ..Self::shared(Arc::new(Names::given(names))) }
|
||||
}
|
||||
/// The names of `body` (a moved copy of a body keeps its faces' names, and how sure they are).
|
||||
///
|
||||
/// Worked out on first request, from the source: moving a body (a transfer into a
|
||||
/// component, a placed instance) does not name every face of every moved body up front.
|
||||
pub fn same(body: &ModelBody) -> Self {
|
||||
Self::shared(names(body), Some(Arc::new(body.clone())))
|
||||
NameCell { copy_of: Some(Arc::new(body.clone())), ..NameCell::default() }
|
||||
}
|
||||
fn shared(names: Arc<Names>, copy_of: Option<Arc<ModelBody>>) -> Self {
|
||||
fn shared(names: Arc<Names>) -> Self {
|
||||
let cell = OnceLock::new();
|
||||
let _ = cell.set(names);
|
||||
NameCell { names: Arc::new(cell), copy_of, ..NameCell::default() }
|
||||
NameCell { names: Arc::new(cell), ..NameCell::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +209,10 @@ pub fn face_name_exact(b: &ModelBody, index: usize) -> bool {
|
||||
/// A body's names, with what finding them again needs: from the history of the feature that
|
||||
/// made it where it reports one, else from geometry (unverified).
|
||||
pub(crate) fn names(b: &ModelBody) -> Arc<Names> {
|
||||
// A moved copy has its source's names (the source's cell keeps them for both).
|
||||
if let Some(source) = &b.names.copy_of {
|
||||
return b.names.names.get_or_init(|| names(source)).clone();
|
||||
}
|
||||
b.names
|
||||
.names
|
||||
.get_or_init(|| {
|
||||
|
||||
@@ -69,7 +69,7 @@ impl FeatureKind {
|
||||
v.push(("To offset", d, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { angle, angle2, .. } => {
|
||||
FeatureKind::Revolve { angle, angle2, .. } | FeatureKind::SurfaceRevolve { angle, angle2, .. } => {
|
||||
v.push(("Angle", angle, A));
|
||||
if let Some(a) = angle2 {
|
||||
v.push(("Angle 2", a, A));
|
||||
@@ -77,6 +77,7 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::Fillet { radius, style, .. } => match style {
|
||||
crate::FilletStyle::Constant => v.push(("Radius", radius, L)),
|
||||
crate::FilletStyle::Curvature => v.push(("Equivalent radius", radius, L)),
|
||||
crate::FilletStyle::Chord => v.push(("Chord length", radius, L)),
|
||||
crate::FilletStyle::Variable { radius2, mid, .. } => {
|
||||
v.push(("Start radius", radius, L));
|
||||
@@ -277,6 +278,13 @@ impl FeatureKind {
|
||||
FeatureKind::Thicken { thickness, .. } => v.push(("Thickness", thickness, L)),
|
||||
FeatureKind::SurfaceTrim { plane, .. } => plane_inputs_mut(plane, &mut v),
|
||||
FeatureKind::SurfaceExtend { distance, .. } => v.push(("Distance", distance, L)),
|
||||
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
|
||||
FeatureKind::SurfaceOffset { distance, tolerance, .. } => {
|
||||
v.push(("Distance", distance, L));
|
||||
if let Some(t) = tolerance {
|
||||
v.push(("Tolerance", t, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::Patch { .. } => {}
|
||||
FeatureKind::Combine { .. }
|
||||
| FeatureKind::SheetBase { .. }
|
||||
@@ -288,6 +296,8 @@ impl FeatureKind {
|
||||
| FeatureKind::BoundaryFill { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::SurfaceSweep { .. }
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
| FeatureKind::MeshImport { .. }
|
||||
| FeatureKind::BodyTransfer { .. } => {}
|
||||
|
||||