Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b01b9689c8 | ||
|
|
4426da6f5d | ||
|
|
5ead82e627 | ||
|
|
cccbe1255d | ||
|
|
4e74fea3e5 | ||
|
|
229de1484c | ||
|
|
e3459edea8 | ||
|
|
e519fbd14d | ||
|
|
575c037f39 | ||
|
|
a6fbc63746 | ||
|
|
fd12325d90 | ||
|
|
39bc87a8cc | ||
|
|
9b0f028f13 | ||
|
|
764fcaa5a8 | ||
|
|
9a4e8fbd19 | ||
|
|
4cfa9c5d34 | ||
|
|
e489e56f3d | ||
|
|
c3aae64a63 | ||
|
|
c469adf456 | ||
|
|
6de8f6db95 | ||
|
|
37547f8eba | ||
|
|
f425089983 | ||
|
|
94faf9ce1b | ||
|
|
2e6951135a | ||
|
|
2c754c430b | ||
|
|
8f48544021 | ||
|
|
1a456d2d1f | ||
|
|
f79dfe41b6 | ||
|
|
6ebfcd16b6 | ||
|
|
ed943a74e6 | ||
|
|
aa2ace81af | ||
|
|
3dbdb71e59 | ||
|
|
c26da73f5f | ||
|
|
805ea45be6 | ||
|
|
54a8c807bf | ||
|
|
d399effbf7 | ||
|
|
8ea1fd643f | ||
|
|
563bcaff5b | ||
|
|
6df69e9def | ||
|
|
86ea018d05 | ||
|
|
110d00d07e | ||
|
|
d4a7b3abf3 | ||
|
|
a8258eb63a | ||
|
|
1e1722b7e1 | ||
|
|
fbd8ada754 | ||
|
|
d019f2d5bf | ||
|
|
c29b0d2f13 | ||
|
|
480fd8d517 | ||
|
|
ec64564f21 | ||
|
|
a6928abf48 | ||
|
|
88432d226a | ||
|
|
1ac8bf4aa8 | ||
|
|
dde6d48dd0 | ||
|
|
4dbc0efff9 | ||
|
|
d114c58540 | ||
|
|
b15180bdff | ||
|
|
37113c0c13 | ||
|
|
f2d7b220f7 | ||
|
|
14d928b2c8 | ||
|
|
435ad0fd8c | ||
|
|
7879875d49 | ||
|
|
ec8e739aad | ||
|
|
08b6a8fe0f | ||
|
|
cf93869c4d | ||
|
|
4c09fa5355 | ||
|
|
75f1cdd4c9 | ||
|
|
b5efde94c8 | ||
|
|
0897ee3781 | ||
|
|
52fb5ac0e6 | ||
|
|
377e12a059 | ||
|
|
3256fffe35 | ||
|
|
0f4cda0510 | ||
|
|
3341e2d60b | ||
|
|
35e58d18ed | ||
|
|
32d3cf6877 | ||
|
|
27374ca289 | ||
|
|
d506382aca | ||
|
|
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)
|
||||
@@ -27,3 +28,11 @@ slow-timeout = { period = "60s" }
|
||||
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
# The action matrix sample runs its designs in parallel worker processes, one per CPU. Run with
|
||||
# other tests beside it (the heavy ones on main), a pi-case hole that takes 8 s alone (#952 makes
|
||||
# it a real hole) went past the matrix's 90 s action limit. It runs on its own.
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = "num-cpus"
|
||||
|
||||
@@ -14,6 +14,8 @@ What did you expect, and what happened instead?
|
||||
|
||||
Include steps, SolidCraft version/commit and relevant environment details. Prefer a small example without private design data.
|
||||
|
||||
When the problem shows on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
|
||||
## Screenshots of the behavior (when observable)
|
||||
|
||||
For observable CAD, UI or model behavior, include actual descriptive screenshots or a short recording from a small reproduction. This includes kernel, geometry, assembly and backend effects that can be shown in the app. Coding agents should run the app and capture the evidence themselves, rather than asking a human for evidence they can produce. Show the actual failure/result, preferably before and after, with brief steps, expected/actual behavior and annotations where helpful.
|
||||
|
||||
@@ -5,91 +5,9 @@ parameter to watch the timeline rebuild. [Documentation](https://bherbruck.githu
|
||||
[issues](https://github.com/bherbruck/solidcraft/issues). AI agents can model with it over MCP:
|
||||
`solidcraft-cli mcp` ([how](https://bherbruck.github.io/solidcraft/guide/mcp.html)).
|
||||
|
||||
**Why 0.9:** this is a minor bump because the design file format moved to version 17, and
|
||||
because it adds many new features.
|
||||
|
||||
**What's new**
|
||||
- **Face actions work on about 90% of flat faces.** Sketches, offset planes and press-pull now
|
||||
attach to sheet metal rims and holes, plastic features (Boss, Lip, Rest, Snap Fit), lofts,
|
||||
revolved faces, pieces Offset Face moved and walls a Join merged
|
||||
([#677](https://github.com/bherbruck/solidcraft/pull/677), fixing
|
||||
[#681](https://github.com/bherbruck/solidcraft/issues/681)). Across the test models: sketch
|
||||
75% → 90%, offset plane 75% → 90%, press-pull 64% → 85%.
|
||||
- **Joins:** a logo-style part with pieces extruded up and down from one sketch now joins
|
||||
([#679](https://github.com/bherbruck/solidcraft/issues/679)), faces that only partly overlap
|
||||
fuse correctly ([#704](https://github.com/bherbruck/solidcraft/pull/704), fixing
|
||||
[#674](https://github.com/bherbruck/solidcraft/issues/674)), and booleans build the same body
|
||||
every time ([#748](https://github.com/bherbruck/solidcraft/pull/748)), so a design rebuilds
|
||||
identically on every run.
|
||||
- **Sketch:** type a dimension right after picking (D, click, type;
|
||||
[#695](https://github.com/bherbruck/solidcraft/pull/695)); redundant but consistent
|
||||
constraints are accepted, such as a rectangle clover
|
||||
([#699](https://github.com/bherbruck/solidcraft/pull/699)); tool windows for Scale, Offset,
|
||||
Mirror, Move/Copy, Fillet and Chamfer and the rectangular pattern (thanks @wdunn001), shown
|
||||
next to the Sketch Palette, which now lists the running tool's options; a typed Rho for the
|
||||
conic; a Construction Geometries row in the Sketch Palette that hides construction geometry
|
||||
([#804](https://github.com/bherbruck/solidcraft/pull/804)); sketch tool shortcuts open a sketch on the selected plane; Edit Sketch from the viewport
|
||||
menu.
|
||||
- **Features:** feature history on the left side with folders (opt in under Preferences;
|
||||
[#449](https://github.com/bherbruck/solidcraft/pull/449)); Convert to Sheet Metal from a solid
|
||||
or shelled body by its bend edges ([#725](https://github.com/bherbruck/solidcraft/pull/725));
|
||||
a tear corner relief ([#809](https://github.com/bherbruck/solidcraft/pull/809)); tapered and
|
||||
variable fillet radii ([#675](https://github.com/bherbruck/solidcraft/pull/675),
|
||||
[#749](https://github.com/bherbruck/solidcraft/pull/749)) with a radius spline you drag on the
|
||||
edge ([#752](https://github.com/bherbruck/solidcraft/pull/752)); free-form cut-outs stay exact
|
||||
in sheet-metal flat patterns ([#692](https://github.com/bherbruck/solidcraft/pull/692)) and edge
|
||||
flanges run along what cut-outs leave of an edge
|
||||
([#702](https://github.com/bherbruck/solidcraft/pull/702)); and Surface Extrude, sheets from
|
||||
open curves and profile walls ([#815](https://github.com/bherbruck/solidcraft/pull/815)).
|
||||
- **View:** Home and Fit frame only what is shown, and a middle double-click goes Home
|
||||
([#701](https://github.com/bherbruck/solidcraft/pull/701)); a cube navigator option
|
||||
(thanks @Karl-VonRichter, [#664](https://github.com/bherbruck/solidcraft/pull/664)); the window
|
||||
stays responsive and draggable while a design opens, with a Cancel button that stays put
|
||||
([#643](https://github.com/bherbruck/solidcraft/pull/643),
|
||||
[#686](https://github.com/bherbruck/solidcraft/pull/686)).
|
||||
- **Quality:** cuts no longer split walls they don't touch
|
||||
([#670](https://github.com/bherbruck/solidcraft/pull/670)); booleans are faster and use less
|
||||
memory ([#714](https://github.com/bherbruck/solidcraft/pull/714)); STL export welds shared
|
||||
vertices ([#816](https://github.com/bherbruck/solidcraft/pull/816)); complete Japanese
|
||||
translation ([#753](https://github.com/bherbruck/solidcraft/pull/753)).
|
||||
- **Fixes:** pasted features act on the pasted bodies (#613); a refused OK's error clears when
|
||||
you change the inputs (#641); dialogs show the units of their sizes (#630); the wheel over a
|
||||
dialog's list scrolls only the list (#678); a Single hole picked at a circle's centre lands on
|
||||
the centre (#657); axis or tangent plane on a drill tip works (#567); sketch text edits from
|
||||
letter interiors and moves as a whole; a projected edge stays the edge you picked; a size from
|
||||
a bare diameter keeps its unit (#716); Convert Mesh says how big a mesh is and what converts;
|
||||
the Save changes prompt takes arrow keys and Enter.
|
||||
- **MCP:** agent skills for modeling and editing over MCP
|
||||
([#728](https://github.com/bherbruck/solidcraft/pull/728)).
|
||||
- A new README demo and the SELENE rover showcase.
|
||||
|
||||
**File format 17.** Designs are now format 17 (`solidcraft/17`). SolidCraft 0.8 and older can't
|
||||
open designs saved by 0.9, as a container or as JSON; they ask you to update instead. Keep a
|
||||
copy of a file you may still open in 0.8. Files from every earlier version still open in 0.9.
|
||||
|
||||
**Upgrading from 0.8.** Nothing to do: open your designs and save (they're saved as format 17
|
||||
from then on). Feature-history folders on
|
||||
the left are off by default; turn them on in Preferences.
|
||||
|
||||
**Known limitations**
|
||||
- Some through-all holes along a wall or beside curved faces still fail
|
||||
([#559](https://github.com/bherbruck/solidcraft/issues/559)), as does a hole crossing an
|
||||
equal-radius hole ([#788](https://github.com/bherbruck/solidcraft/issues/788)).
|
||||
- Some fillets are still refused, such as along an outline of lines and arcs
|
||||
([#431](https://github.com/bherbruck/solidcraft/issues/431)).
|
||||
- Some faces still can't take an attached sketch: a revolve's flat faces before a later feature
|
||||
([#726](https://github.com/bherbruck/solidcraft/issues/726)) and faces after Split Body or
|
||||
Combine Intersect ([#565](https://github.com/bherbruck/solidcraft/issues/565)).
|
||||
- Fillet and chamfer accept an edge pick far from any edge and change the nearest one
|
||||
([#783](https://github.com/bherbruck/solidcraft/issues/783)); Press Pull fails on faces beside
|
||||
rounds ([#562](https://github.com/bherbruck/solidcraft/issues/562)).
|
||||
- `sheet.fold` refuses a fixed point exactly on the sheet face
|
||||
([#685](https://github.com/bherbruck/solidcraft/issues/685)), and some tabs on L-shaped or
|
||||
collinear blanks can't be folded ([#638](https://github.com/bherbruck/solidcraft/issues/638),
|
||||
[#639](https://github.com/bherbruck/solidcraft/issues/639)); an edge flange with different
|
||||
per-edge angles leaves gaps at the corners
|
||||
([#501](https://github.com/bherbruck/solidcraft/issues/501)).
|
||||
- More in [ROADMAP.md](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations).
|
||||
**What's new** in SolidCraft {version} is listed below, and the
|
||||
[ROADMAP](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations) lists
|
||||
the known limitations. Designs from every earlier version open in this one.
|
||||
|
||||
**Upgrading from 0.7 or earlier.** 0.8 changed the Linux app ID to `ai.storyteller.solidcraft`:
|
||||
`flatpak update` offers to replace the old app, and the `.deb`, `.rpm` and AppImage remove the old
|
||||
|
||||
@@ -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,58 @@ 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
|
||||
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||
# in SolidCraft or FXC; serial runs never hit it.
|
||||
- run: cargo test -p solidcraft-gpu --profile ci --locked -- --test-threads=1
|
||||
|
||||
# The Windows app starts D3D12 first and only the backend it uses (a Vulkan driver crashed the
|
||||
# app at startup on Intel). Start the real app on the runner (its D3D12 adapter is WARP) with
|
||||
# several saved settings and safe mode, and check each draws a frame and quits cleanly.
|
||||
windows-startup:
|
||||
name: Windows app startup
|
||||
needs: changes
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-startup
|
||||
- name: Backend order and probing on Windows
|
||||
run: cargo test -p solidcraft --profile ci --locked -- startup::tests
|
||||
- name: Build the app
|
||||
run: cargo build -p solidcraft --profile ci --locked
|
||||
- name: Start the app under different settings
|
||||
shell: pwsh
|
||||
run: tools/windows-startup-smoke.ps1 -Exe target/ci/solidcraft.exe
|
||||
|
||||
# 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, windows-startup]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@@ -285,6 +351,8 @@ jobs:
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||
WINDOWS_STARTUP: ${{ needs.windows-startup.result }}
|
||||
run: |
|
||||
test "$CHANGES" = success
|
||||
test "$CHECKS" = success
|
||||
@@ -294,6 +362,8 @@ jobs:
|
||||
test "$TESTS" = success
|
||||
test "$DOCS" = success
|
||||
test "$ORACLE" = success
|
||||
test "$WINDOWS_FXC" = success
|
||||
test "$WINDOWS_STARTUP" = 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
|
||||
|
||||
@@ -429,6 +429,32 @@ jobs:
|
||||
run: |
|
||||
(cd artifacts && sha256sum -- * | tee SHA256SUMS.txt)
|
||||
sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md
|
||||
- name: Sign the checksum list for in-app updates
|
||||
# Optional until the UPDATE_SIGNING_KEY secret exists (docs/releasing.md, "Update
|
||||
# signing"). The trusted comment names the release, which the app checks.
|
||||
env:
|
||||
UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }}
|
||||
run: |
|
||||
# The key the app checks against (crates/ui-egui/src/update/install.rs, PUBLIC_KEY).
|
||||
public_key=$(sed -n 's/^pub const PUBLIC_KEY: Option<&str> = Some("\([^"]*\)");/\1/p' crates/ui-egui/src/update/install.rs)
|
||||
if [ -z "$UPDATE_SIGNING_KEY" ]; then
|
||||
if [ -n "$public_key" ]; then
|
||||
echo "::error::the app expects signed updates (PUBLIC_KEY is set) but the UPDATE_SIGNING_KEY secret is missing"
|
||||
exit 1
|
||||
fi
|
||||
echo "UPDATE_SIGNING_KEY isn't set: SHA256SUMS.txt stays unsigned"
|
||||
exit 0
|
||||
fi
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y --no-install-recommends minisign
|
||||
umask 077
|
||||
printf '%s\n' "$UPDATE_SIGNING_KEY" >"$RUNNER_TEMP/update.key"
|
||||
minisign -S -s "$RUNNER_TEMP/update.key" -m artifacts/SHA256SUMS.txt \
|
||||
-x artifacts/SHA256SUMS.txt.minisig -t "SolidCraft v$VERSION" </dev/null
|
||||
rm -f "$RUNNER_TEMP/update.key"
|
||||
if [ -n "$public_key" ]; then
|
||||
minisign -V -P "$public_key" -m artifacts/SHA256SUMS.txt -x artifacts/SHA256SUMS.txt.minisig
|
||||
fi
|
||||
- name: Publish nightly pre-release
|
||||
if: github.event_name == 'schedule'
|
||||
working-directory: artifacts
|
||||
|
||||
@@ -27,7 +27,9 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
|
||||
## Never crash
|
||||
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The one exception is two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (that crate denies it instead): macOS hands Finder files
|
||||
only to an Objective-C method.
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
@@ -59,8 +61,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 +77,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 +127,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,46 @@ 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/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | 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 |
|
||||
@@ -83,9 +120,20 @@ Generated-in-code assets are original and have no file to list:
|
||||
| crates/mcp/src/tools.rs (tool schema helpers and argument checking) | CADCraft `crates/mcp/src/tools.rs`, GridCraft `crates/mcp/src/tools.rs` | MIT OR Apache-2.0 |
|
||||
| crates/io/src/threemf.rs (bounded zip part reading and writing) | GridCraft `crates/xlsx/src/package.rs` | MIT OR Apache-2.0 |
|
||||
| crates/ui-egui/src/titlebar.rs (title bar drag, caption buttons, resize zones, their tests) and the window setup in apps/solidcraft/src/main.rs | VectorCraft `crates/ui-egui/src/titlebar.rs`, `apps/vectorcraft/src/main.rs` | MIT OR Apache-2.0 |
|
||||
| apps/solidcraft/src/open_documents.rs (macOS: files opened from Finder and the Dock, its test) | VectorCraft `apps/vectorcraft/src/open_documents.rs` (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
|
||||
| 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 +144,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) |
|
||||
|
||||
@@ -27,7 +27,9 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
|
||||
## Never crash
|
||||
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The one exception is two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (that crate denies it instead): macOS hands Finder files
|
||||
only to an Objective-C method.
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
@@ -75,7 +77,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 +104,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.
|
||||
|
||||
@@ -107,7 +107,8 @@ The [README](README.md#architecture) has the layer table. The short version:
|
||||
`cargo xtask assets` enforces this.
|
||||
4. **Never crash.** People trust a CAD app with their work.
|
||||
- Non-test code never uses `unwrap`, `expect`, `panic!` or indexing on input-derived data.
|
||||
There's no `unsafe`, and clippy denies it.
|
||||
There's no `unsafe` outside `apps/solidcraft/src/open_documents.rs` (macOS file-open events),
|
||||
and the compiler denies it.
|
||||
- An unsupported case returns an error ("not supported yet: …"); it never panics.
|
||||
- Every call into truck goes through `solidcraft_kernel::guard`.
|
||||
- Every crash fix comes with a regression test.
|
||||
@@ -161,7 +162,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`
|
||||
|
||||
@@ -422,6 +422,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.10.0"
|
||||
@@ -710,6 +716,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -723,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -736,7 +752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1578,6 +1594,22 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -1987,6 +2019,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2449,6 +2487,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.55"
|
||||
@@ -2949,6 +2993,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "robust"
|
||||
version = "1.2.0"
|
||||
@@ -3016,6 +3074,80 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
@@ -3031,6 +3163,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -3043,6 +3184,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.3.0"
|
||||
@@ -3281,24 +3445,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
"egui",
|
||||
"embed-resource",
|
||||
"getrandom 0.3.4",
|
||||
"hidapi",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"pollster 0.4.0",
|
||||
"quick-xml",
|
||||
"rfd",
|
||||
"ron",
|
||||
"rustls",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-ui-egui",
|
||||
"ureq",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3307,7 +3480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3322,7 +3495,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3338,16 +3511,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.11.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.11.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@@ -3362,7 +3548,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rayon",
|
||||
@@ -3380,7 +3566,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3388,7 +3574,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -3398,7 +3584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3409,22 +3595,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"image",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"pollster 0.4.0",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"ttf-parser",
|
||||
"web-time",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3471,6 +3663,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -3893,6 +4091,40 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -3905,6 +4137,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -4227,6 +4465,15 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -4715,7 +4962,7 @@ dependencies = [
|
||||
"calloop 0.13.0",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics",
|
||||
"cursor-icon",
|
||||
"dpi",
|
||||
@@ -4847,9 +5094,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.9.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5022,6 +5270,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.5"
|
||||
|
||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.0"
|
||||
version = "0.11.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" }
|
||||
@@ -30,6 +31,7 @@ serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||
log = "0.4"
|
||||
sha2 = "0.10"
|
||||
semver = "1"
|
||||
|
||||
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
|
||||
truck-modeling = "0.6"
|
||||
@@ -50,7 +52,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 +60,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;
|
||||
@@ -133,6 +134,16 @@ rebuild with no errors, replayed here at 2× speed.
|
||||
(manifold solid B-reps, or trimmed and bounded surfaces sewn into solids) as a base feature
|
||||
that later features build on — open a `.step`/`.stp`/`.igs`/`.iges` file, insert one into a
|
||||
design, or drop it on the window ([docs/step-import.md](docs/step-import.md)).
|
||||
- **Display meshes** from our own tessellation planner: each face becomes a plan of (u, v)
|
||||
points and triangles that shares its edge nodes with its neighbours, so the mesh is watertight.
|
||||
Facets follow the chord tolerance, the facet angle and a longest side, with cells at most 2:1
|
||||
along straight directions. Box-bounded and four-cornered faces mesh as structured grids.
|
||||
Trimmed faces are cut into four-sided patches: squares around holes, butterflies in round
|
||||
faces, motorcycle traces from concave corners (after Lyon et al. 2019). Each patch then meshes
|
||||
as a Coons grid. Intersection edges are evaluated by Newton on both surfaces. A compute shader
|
||||
evaluates the plans for the whole scene in one dispatch, with a CPU fallback when the GPU
|
||||
can't build it. Planning and evaluating the 78 reference bodies takes about 65 ms, 50–65×
|
||||
faster than the previous mesher.
|
||||
- **Desktop app** (egui + wgpu): toolbar with workspace tabs, browser, timeline, 3D viewport with
|
||||
axis view and navigation bar, sketch tools, feature dialogs, command palette, and a JSON-lines
|
||||
control channel so agents and tests can drive it.
|
||||
@@ -199,7 +210,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
|
||||
@@ -212,7 +225,7 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
||||
| L2 | `doc` | parameters, expressions, the feature timeline and its incremental evaluation |
|
||||
| L3 | `io` | design files, STL/OBJ/STEP/3MF export, STEP and 3MF/STL import features |
|
||||
| L4 | `engine` | the session and the command registry (everything is a command) |
|
||||
| L5 | `ui-egui`, `mcp` | the swappable desktop front end; the MCP server (headless or bridged to the app) |
|
||||
| L5 | `gpu`, `ui-egui`, `mcp` | compute-shader evaluation of display tessellation plans; the swappable desktop front end; the MCP server (headless or bridged to the app) |
|
||||
| apps | `solidcraft`, `solidcraft-cli` | desktop app, headless CLI |
|
||||
<!-- ANCHOR_END: architecture -->
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SolidCraft roadmap
|
||||
|
||||
Status as of **2026-10-11**. SolidCraft supports parametric modelling (sketch →
|
||||
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
|
||||
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
||||
|
||||
## At a glance
|
||||
@@ -9,7 +9,7 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat
|
||||
|---|---|
|
||||
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
|
||||
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
|
||||
| Reference tests | **76 / 76 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (55) or FreeCAD-computed (21) expected values, run by CI on every PR. |
|
||||
| Reference tests | **82 / 82 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (61) or FreeCAD-computed (21) expected values, run by CI on every PR. |
|
||||
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
|
||||
@@ -47,20 +47,56 @@ 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 |
|
||||
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), 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, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), 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).
|
||||
|
||||
- Directional elasticity (#994) queries the validated card in material-local axes
|
||||
through read-only `material.properties.directional`, with source/condition review.
|
||||
It does not assign body frames, predict structural behavior or enable a solver.
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
|
||||
selected appearance/material revisions into existing design definitions in one undo step.
|
||||
Appearance offers local loading/search/export; physical materials use existing commands.
|
||||
This slice is independent of the render stack and does not change the document format.
|
||||
See [the contract and limits](docs/book/src/dev/local-libraries.md).
|
||||
|
||||
- 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
|
||||
|
||||
- The initial opaque surface numerical contract for #1004 provides bounded linear
|
||||
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
|
||||
It does not enable PBR rendering or change stored appearances. See
|
||||
[the model, evidence and remaining consumer gates](docs/render-surface.md).
|
||||
|
||||
- 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.
|
||||
@@ -134,6 +170,13 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
Crash Reports…) with a reviewed issue draft; agents read them through
|
||||
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
|
||||
anywhere. See [crash reports](docs/crash-reports.md).
|
||||
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
|
||||
writes a design's history as a script that `eval` and `run` replay into the same design —
|
||||
user parameters, units, sheet metal and plastic rules and materials as their own commands,
|
||||
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
|
||||
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
|
||||
script after a step gives the design at that step. Appearances, folders, views and
|
||||
configurations are listed as left out; components and joints are not supported yet.
|
||||
|
||||
## 0.2.0 release batch
|
||||
|
||||
@@ -194,6 +237,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 +257,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 +300,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.
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! `bench-agent`: a large design built the way an agent builds one over MCP, one small command
|
||||
//! at a time, timed as it grows and then opened again.
|
||||
//!
|
||||
//! The design is a bicycle of about 1,300 bodies: frame tubes joined at root and then moved into
|
||||
//! a component, two wheels whose spokes and nipples are placed one by one, a chainring with
|
||||
//! patterned teeth, a cassette, a chain of individual links, cockpit, seat and a few hundred
|
||||
//! fasteners. Nothing is read from a file; the script is generated here, so the benchmark needs
|
||||
//! no fixture.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.solidcraft]
|
||||
//! ```
|
||||
//! prints one JSON line per hundred commands (mean and max milliseconds, bodies so far), then the
|
||||
//! build total, then the phases `bench-open` prints for the saved design.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
|
||||
struct Script {
|
||||
commands: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
fn c(&mut self, command: &str, params: Value) {
|
||||
self.commands.push(json!({"command": command, "params": params}));
|
||||
}
|
||||
|
||||
/// A round tube from `a` to `b` (an agent's frame tube, spoke or post).
|
||||
fn tube(&mut self, a: [f64; 3], b: [f64; 3], radius: f64, body: &str) {
|
||||
let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
|
||||
let axis = [d[0] / len, d[1] / len, d[2] / len];
|
||||
self.c(
|
||||
"solid.cylinder",
|
||||
json!({"radius": radius, "height": round3(len), "base": a.map(round3), "axis": axis.map(round6), "body_name": body}),
|
||||
);
|
||||
}
|
||||
|
||||
fn component(&mut self, name: &str) {
|
||||
self.c("component.create", json!({"name": name}));
|
||||
}
|
||||
|
||||
fn root(&mut self) {
|
||||
self.c("component.activate", json!({"component": "root"}));
|
||||
}
|
||||
|
||||
/// A wheel built at the origin in its own component, then placed where it goes (the
|
||||
/// component's occurrence moved), as an agent builds one part and positions it.
|
||||
fn wheel(&mut self, label: &str, at: f64, spokes: usize) {
|
||||
let cx = 0.0;
|
||||
self.component(&format!("{label} wheel"));
|
||||
self.c("solid.torus", json!({"major": 311, "minor": 12, "center": [cx, 0, 340], "body_name": format!("{label} rim")}));
|
||||
self.c("solid.torus", json!({"major": 330, "minor": 18, "center": [cx, 0, 340], "body_name": format!("{label} tyre")}));
|
||||
self.c("solid.cylinder", json!({"radius": 18, "height": 110, "base": [cx, -55, 340], "axis": "Y", "body_name": format!("{label} hub")}));
|
||||
for k in 0..spokes {
|
||||
let a = std::f64::consts::TAU * k as f64 / spokes as f64;
|
||||
let side = if k % 2 == 1 { 1.0 } else { -1.0 };
|
||||
let hub = [cx + 30.0 * a.cos(), side * 40.0, 340.0 + 30.0 * a.sin()];
|
||||
let (ca, sa) = ((a + 0.12).cos(), (a + 0.12).sin());
|
||||
let rim = [cx + 300.0 * ca, side * 4.0, 340.0 + 300.0 * sa];
|
||||
self.tube(hub, rim, 1.0, &format!("{label} spoke {k}"));
|
||||
let nipple = json!({"radius": 2, "height": 12, "base": rim.map(round3), "axis": [round6(ca), 0, round6(sa)], "body_name": format!("{label} nipple {k}")});
|
||||
self.c("solid.cylinder", nipple);
|
||||
}
|
||||
self.root();
|
||||
self.c("occurrence.move", json!({"occurrence": format!("{label} wheel:1"), "translate": [at, 0, 0]}));
|
||||
}
|
||||
}
|
||||
|
||||
fn round3(v: f64) -> f64 {
|
||||
(v * 1e3).round() / 1e3
|
||||
}
|
||||
|
||||
fn round6(v: f64) -> f64 {
|
||||
(v * 1e6).round() / 1e6
|
||||
}
|
||||
|
||||
/// The bicycle's command script. `scale` multiplies the counts of repeated parts (spokes,
|
||||
/// chain links, fasteners); 1.0 gives about 1,300 bodies.
|
||||
pub fn bicycle(scale: f64) -> Value {
|
||||
let n = |k: f64| ((k * scale).round() as usize).max(1);
|
||||
let mut s = Script { commands: Vec::new() };
|
||||
// Frame: tubes at root, the main triangle joined, then everything moved into its component.
|
||||
let (bb, head_top, head_bot, seat_top) = ([0.0, 0.0, 300.0], [600.0, 0.0, 860.0], [580.0, 0.0, 740.0], [-150.0, 0.0, 820.0]);
|
||||
s.tube(bb, head_bot, 19.0, "Down tube");
|
||||
s.tube(seat_top, head_top, 16.0, "Top tube");
|
||||
s.tube(bb, seat_top, 16.0, "Seat tube");
|
||||
s.tube(head_bot, head_top, 22.0, "Head tube");
|
||||
for (side, y) in [("L", 60.0), ("R", -60.0)] {
|
||||
s.tube([0.0, y * 0.3, 300.0], [-420.0, y, 340.0], 10.0, &format!("Chainstay {side}"));
|
||||
s.tube([-140.0, y * 0.3, 800.0], [-420.0, y, 340.0], 9.0, &format!("Seatstay {side}"));
|
||||
s.c("solid.box", json!({"length": 20, "width": 6, "height": 40, "corner": [-430.0, y - 3.0, 320], "body_name": format!("Dropout {side}")}));
|
||||
}
|
||||
s.c("solid.cylinder", json!({"radius": 24, "height": 80, "base": [0, -40, 300], "axis": "Y", "body_name": "BB shell"}));
|
||||
s.c("solid.combine", json!({"target": "Down tube", "tools": ["Top tube", "Seat tube", "Head tube", "BB shell"]}));
|
||||
s.component("Frame");
|
||||
s.root();
|
||||
let frame = ["Down tube", "Chainstay L", "Seatstay L", "Dropout L", "Chainstay R", "Seatstay R", "Dropout R"];
|
||||
s.c("component.move_bodies", json!({"bodies": frame, "component": "Frame"}));
|
||||
// Wheels: every spoke and nipple a body of its own.
|
||||
s.wheel("Front", 640.0, n(36.0));
|
||||
s.wheel("Rear", -420.0, n(36.0));
|
||||
// Drivetrain: patterned chainring teeth, a cassette and a chain of separate links.
|
||||
s.component("Drivetrain");
|
||||
s.c("solid.cylinder", json!({"radius": 95, "height": 4, "base": [0, 50, 300], "axis": "Y", "body_name": "Chainring"}));
|
||||
s.c("solid.box", json!({"length": 6, "width": 4, "height": 8, "corner": [-3, 50, 395], "name": "Tooth", "body_name": "Tooth"}));
|
||||
s.c("solid.pattern.circular", json!({"bodies": ["Tooth"], "axis": {"origin": [0, 50, 300], "dir": [0, 1, 0]}, "count": 44}));
|
||||
for g in 0..11 {
|
||||
let r = 22.0 + 3.5 * g as f64;
|
||||
s.c(
|
||||
"solid.cylinder",
|
||||
json!({"radius": r, "height": 2, "base": [-420.0, round3(30.0 + 2.6 * g as f64), 340.0], "axis": "Y", "body_name": format!("Cog {g}")}),
|
||||
);
|
||||
}
|
||||
let links = n(112.0);
|
||||
for k in 0..links {
|
||||
let a = std::f64::consts::TAU * k as f64 / links as f64;
|
||||
let (x, z) = (round3(-210.0 + 230.0 * a.cos()), round3(320.0 + 80.0 * a.sin()));
|
||||
for (side, y) in [("in", 46), ("out", 56)] {
|
||||
s.c("solid.box", json!({"length": 12, "width": 1, "height": 8, "corner": [round3(x - 6.0), y, round3(z - 4.0)], "body_name": format!("Link {k} {side}")}));
|
||||
}
|
||||
s.c("solid.cylinder", json!({"radius": 3.5, "height": 10, "base": [x, 46, z], "axis": "Y", "body_name": format!("Roller {k}")}));
|
||||
}
|
||||
for (side, y) in [("L", -90.0), ("R", 90.0)] {
|
||||
let x0 = if side == "L" { -15.0 } else { 0.0 };
|
||||
s.c("solid.box", json!({"length": 170, "width": 12, "height": 30, "corner": [x0, y - 6.0, 285], "body_name": format!("Crank {side}")}));
|
||||
let py = if y > 0.0 { y + 8.0 } else { y - 68.0 };
|
||||
s.c("solid.box", json!({"length": 90, "width": 60, "height": 15, "corner": [150.0, py, 280], "body_name": format!("Pedal {side}")}));
|
||||
}
|
||||
s.root();
|
||||
// Cockpit and seat.
|
||||
s.component("Cockpit");
|
||||
s.tube([600.0, 0.0, 860.0], [620.0, 0.0, 940.0], 14.0, "Steerer");
|
||||
s.tube([620.0, 0.0, 940.0], [700.0, 0.0, 950.0], 16.0, "Stem");
|
||||
s.tube([700.0, -380.0, 950.0], [700.0, 380.0, 950.0], 11.0, "Handlebar");
|
||||
s.c("solid.cylinder", json!({"radius": 16, "height": 120, "base": [700, 260, 950], "axis": "Y", "body_name": "Grip R"}));
|
||||
s.c("solid.mirror", json!({"bodies": ["Grip R"], "plane": "XZ"}));
|
||||
s.root();
|
||||
s.component("Seat");
|
||||
s.tube([-150.0, 0.0, 820.0], [-170.0, 0.0, 960.0], 13.0, "Seatpost");
|
||||
s.c("solid.box", json!({"length": 260, "width": 140, "height": 40, "center": [-180, 0, 980], "body_name": "Saddle"}));
|
||||
s.root();
|
||||
// Fasteners: head, shank and washer, each its own body.
|
||||
s.component("Hardware");
|
||||
let bolts = n(240.0);
|
||||
for k in 0..bolts {
|
||||
let a = std::f64::consts::TAU * k as f64 / bolts as f64;
|
||||
let p = [round3(-300.0 + 2.5 * k as f64), round3(120.0 + 10.0 * a.sin()), round3(400.0 + 10.0 * a.cos())];
|
||||
s.c("solid.cylinder", json!({"radius": 5, "height": 4, "base": p, "axis": "Z", "body_name": format!("Bolt {k} head")}));
|
||||
s.c(
|
||||
"solid.cylinder",
|
||||
json!({"radius": 2.5, "height": 16, "base": [p[0], p[1], round3(p[2] - 16.0)], "axis": "Z", "body_name": format!("Bolt {k} shank")}),
|
||||
);
|
||||
s.c(
|
||||
"solid.cylinder",
|
||||
json!({"radius": 4.5, "height": 1, "base": [p[0], p[1], round3(p[2] - 1.0)], "axis": "Z", "body_name": format!("Bolt {k} washer")}),
|
||||
);
|
||||
}
|
||||
s.root();
|
||||
json!({"commands": s.commands})
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1));
|
||||
let scale = match flag("--scale") {
|
||||
Some(v) => v.parse::<f64>().ok().filter(|v| v.is_finite() && *v > 0.0 && *v <= 20.0).ok_or("--scale takes a number from 0 to 20")?,
|
||||
None => 1.0,
|
||||
};
|
||||
let script = bicycle(scale);
|
||||
if let Some(path) = flag("--script-out") {
|
||||
let text = serde_json::to_string_pretty(&script).map_err(|e| e.to_string())?;
|
||||
return solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"));
|
||||
}
|
||||
let commands = script["commands"].as_array().cloned().unwrap_or_default();
|
||||
let mut s = Session::default();
|
||||
let began = std::time::Instant::now();
|
||||
// Per command: the command itself, then what a window needs to draw its result (the
|
||||
// bodies placed in the world and their display meshes).
|
||||
let (mut run, mut shown): (Vec<f64>, Vec<f64>) = (Vec::new(), Vec::new());
|
||||
for (i, c) in commands.iter().enumerate() {
|
||||
let id = c["command"].as_str().unwrap_or_default();
|
||||
let t = std::time::Instant::now();
|
||||
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
|
||||
run.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().warm_meshes(|| true);
|
||||
shown.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
if run.len() == 100 || i + 1 == commands.len() {
|
||||
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
|
||||
let bodies = s.model.state().bodies.len();
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "build", "commands": i + 1, "bodies": bodies, "command_ms": mean(&run), "display_ms": mean(&shown),
|
||||
"max_ms": run.iter().zip(&shown).map(|(a, b)| a + b).fold(0.0, f64::max)})
|
||||
);
|
||||
run.clear();
|
||||
shown.clear();
|
||||
}
|
||||
}
|
||||
let world = s.world_state();
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
|
||||
);
|
||||
let path = match flag("--save") {
|
||||
Some(p) => p.clone(),
|
||||
None => std::env::temp_dir().join(format!("bench-agent-{}.solidcraft", std::process::id())).to_string_lossy().into_owned(),
|
||||
};
|
||||
s.execute("file.save", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
let r = crate::cmd_bench_open(std::slice::from_ref(&path));
|
||||
if flag("--save").is_none() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{path}.bak"));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The generated commands stay valid as the command set changes: a small bicycle builds
|
||||
/// without a failing command or feature.
|
||||
#[test]
|
||||
fn a_small_bicycle_builds() {
|
||||
let script = bicycle(0.03);
|
||||
let mut s = Session::default();
|
||||
for c in script["commands"].as_array().into_iter().flatten() {
|
||||
let id = c["command"].as_str().unwrap_or_default();
|
||||
if let Err(e) = s.execute(id, &c["params"]) {
|
||||
panic!("{id}: {e}");
|
||||
}
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()));
|
||||
let world = s.world_state();
|
||||
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
|
||||
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,23 @@
|
||||
//! ```text
|
||||
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--allow-errors]
|
||||
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|top|…]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
//! 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)
|
||||
//! ```
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod bench_agent;
|
||||
mod crashes;
|
||||
mod fuzz;
|
||||
mod oracle;
|
||||
@@ -25,7 +29,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;
|
||||
@@ -36,14 +40,17 @@ const USAGE: &str = "usage:
|
||||
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
|
||||
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
|
||||
solidcraft-cli eval <script.json|design.solidcraft|part.step> (alias: inspect; or --in FILE)
|
||||
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
|
||||
(the design's history as a command script that eval and run replay)
|
||||
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]
|
||||
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
|
||||
solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|back|top|bottom|left|right]
|
||||
solidcraft-cli exec <command> [json-params]
|
||||
solidcraft-cli commands
|
||||
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
|
||||
@@ -54,6 +61,7 @@ fn main() -> ExitCode {
|
||||
let r = match args.first().map(String::as_str) {
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("eval" | "inspect") => cmd_eval(&args[1..]),
|
||||
Some("script") => cmd_script(&args[1..]),
|
||||
Some("snapshot") => cmd_snapshot(&args[1..]),
|
||||
Some("exec") => cmd_exec(&args[1..]),
|
||||
Some("commands") => {
|
||||
@@ -90,6 +98,8 @@ fn main() -> ExitCode {
|
||||
// Hidden: time recomputes after parameter edits (docs/perf.md).
|
||||
Some("bench-edit") => cmd_bench_edit(&args[1..]),
|
||||
Some("bench-open") => cmd_bench_open(&args[1..]),
|
||||
// A ~1,300-body design built command by command, as an agent builds one over MCP.
|
||||
Some("bench-agent") => bench_agent::run(&args[1..]),
|
||||
Some("--version" | "-V") => {
|
||||
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
@@ -241,6 +251,23 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A design's history as a command script (`timeline.export_script`): printed, or written to
|
||||
/// `--out`; what the script leaves out goes to stderr.
|
||||
fn cmd_script(args: &[String]) -> Result<(), String> {
|
||||
let path = positional(args).ok_or_else(|| USAGE.to_string())?;
|
||||
let s = &mut load(path)?;
|
||||
let out = flag(args, "--out");
|
||||
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
for what in r["omitted"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
eprintln!("note: the script leaves out {what}");
|
||||
}
|
||||
match out {
|
||||
Some(out) => eprintln!("{out}: {} steps", r["steps"]),
|
||||
None => println!("{}", pretty(&r["script"])),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
let path = args.first().ok_or_else(|| USAGE.to_string())?;
|
||||
let out = flag(args, "--out").ok_or("snapshot needs --out FILE.png")?;
|
||||
@@ -312,11 +339,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 +371,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
|
||||
@@ -384,7 +427,7 @@ fn cmd_bench_edit(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Separate file reading, JSON decoding/repair, timeline evaluation and display meshing.
|
||||
fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]")?;
|
||||
let t = std::time::Instant::now();
|
||||
let step = solidcraft_engine::io::is_step_path(path);
|
||||
@@ -393,10 +436,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 +454,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(())
|
||||
|
||||
@@ -296,6 +296,17 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
Some("sketch") => sketch_cmds(f, &mut out)?,
|
||||
Some("extrude") => {
|
||||
let mut p = json!({"sketch": profile_sketch(f), "profiles": profiles_of(f), "operation": op(f), "name": name, "body_names": bodies});
|
||||
// Thin: {sketch, curves, thickness, side?, thickness2?} instead of profiles.
|
||||
if let (Some(thin), Some(params)) = (f.get("thin"), p.as_object_mut()) {
|
||||
params.remove("profiles");
|
||||
params.insert("thin".into(), json!(true));
|
||||
params.insert("sketch".into(), thin.get("sketch").cloned().unwrap_or(Value::Null));
|
||||
for key in ["curves", "thickness", "side", "thickness2"] {
|
||||
if let Some(v) = thin.get(key) {
|
||||
params.insert(key.into(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
extent(f, &mut p);
|
||||
out.push(json!({"command": "solid.extrude", "params": p}));
|
||||
}
|
||||
|
||||
@@ -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,17 +657,44 @@ 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
|
||||
&& stable_hash(&format!("{k} edit")).is_multiple_of(EDIT_SAMPLE)
|
||||
{
|
||||
// Marked like an action, so a hang here is put down to the edit round (not to loading).
|
||||
emit(json!({"start": i, "edit": true}));
|
||||
let t = Instant::now();
|
||||
let start = s.doc.features.len();
|
||||
let r = scratch
|
||||
@@ -753,7 +787,9 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
|
||||
.map(|f| std::io::BufReader::new(f).lines().map_while(Result::ok).filter_map(|l| serde_json::from_str(&l).ok()).collect())
|
||||
.unwrap_or_default();
|
||||
let done = lines.iter().any(|l| l.get("done").is_some());
|
||||
let last_start = lines.iter().rev().take_while(|l| l.get("start").is_some() || l.get("design").is_none()).find_map(|l| l["start"].as_u64());
|
||||
let last = lines.iter().rev().take_while(|l| l.get("start").is_some() || l.get("design").is_none()).find(|l| l.get("start").is_some());
|
||||
let last_start = last.and_then(|l| l["start"].as_u64());
|
||||
let in_edit = last.is_some_and(|l| l["edit"] == true);
|
||||
results = lines.iter().filter(|l| l.get("design").is_some()).cloned().collect();
|
||||
if done {
|
||||
break;
|
||||
@@ -768,7 +804,7 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
|
||||
};
|
||||
// Which action was it? The child enumerates them; ask it by running from there with a
|
||||
// marker: simplest is to record the index and let the next run name it.
|
||||
extra.push(json!({"design": design, "index": i, "kind": if timed_out { "timeout" } else { "crash" },
|
||||
extra.push(json!({"design": design, "index": i, "edit": in_edit, "kind": if timed_out { "timeout" } else { "crash" },
|
||||
"message": if timed_out { format!("no result after {} s", ACTION_TIMEOUT.as_secs()) } else { format!("the process ended: {status:?}") }}));
|
||||
from = i as usize + 1;
|
||||
}
|
||||
@@ -777,7 +813,10 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
|
||||
let names = action_names(design);
|
||||
for e in &mut extra {
|
||||
if let Some(i) = e.get("index").and_then(Value::as_u64) {
|
||||
let (entity, action) = names.get(i as usize).cloned().unwrap_or_else(|| (format!("action{i}"), "unknown".into()));
|
||||
let (entity, mut action) = names.get(i as usize).cloned().unwrap_or_else(|| (format!("action{i}"), "unknown".into()));
|
||||
if e["edit"] == true {
|
||||
action += "+edit";
|
||||
}
|
||||
*e = json!({"design": design, "entity": entity, "action": action, "ok": false, "kind": e["kind"], "message": e["message"]});
|
||||
}
|
||||
}
|
||||
@@ -1051,6 +1090,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 +1112,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();
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! A design's command script (`timeline.export_script`, #886) replays into the same design:
|
||||
//! every corpus design that the script supports comes back with the same timeline, parameters,
|
||||
//! settings and bodies.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
|
||||
fn corpus() -> Vec<PathBuf> {
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
|
||||
let mut v: Vec<PathBuf> =
|
||||
std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "solidcraft")).collect();
|
||||
v.sort();
|
||||
assert!(v.len() >= 7, "corpus files in {}", dir.display());
|
||||
v
|
||||
}
|
||||
|
||||
/// Designs with components or joints, which scripts do not support yet.
|
||||
const ASSEMBLIES: [&str; 2] = ["components-arm", "joints-hinge"];
|
||||
|
||||
fn bodies(s: &mut Session) -> Vec<(String, f64, f64, Value)> {
|
||||
let m = s.execute("inspect.measure", &json!({})).unwrap();
|
||||
let mut v: Vec<_> = m["bodies"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|b| {
|
||||
(
|
||||
b["name"].as_str().unwrap_or_default().to_string(),
|
||||
b["volume_mm3"].as_f64().unwrap(),
|
||||
b["area_mm2"].as_f64().unwrap(),
|
||||
b["faces"].clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
v.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
v
|
||||
}
|
||||
|
||||
fn errors(s: &Session) -> Vec<(u64, Option<String>)> {
|
||||
s.model.results.iter().map(|r| (r.id, r.error.clone())).collect()
|
||||
}
|
||||
|
||||
fn params(s: &Session) -> Vec<solidcraft_engine::doc::Param> {
|
||||
let mut v = s.doc.params.clone();
|
||||
v.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_designs_replay_from_their_scripts() {
|
||||
let mut replayed = 0;
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let mut s = Session::default();
|
||||
s.execute("doc.open", &json!({"path": p.to_string_lossy()})).unwrap();
|
||||
let r = match s.execute("timeline.export_script", &json!({})) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
assert!(ASSEMBLIES.contains(&name.as_str()), "{name}: {e}");
|
||||
assert!(e.to_string().contains("not supported yet"), "{name}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
assert!(!ASSEMBLIES.contains(&name.as_str()), "{name} has components: the script should refuse it");
|
||||
// Through text, as a script file would carry it.
|
||||
let script: Value = serde_json::from_str(&r["script"].to_string()).unwrap();
|
||||
let mut t = Session::default();
|
||||
t.run_script(&script).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(t.doc.features, s.doc.features, "{name}: timeline");
|
||||
assert_eq!(params(&t), params(&s), "{name}: parameters");
|
||||
assert_eq!(t.doc.marker, s.doc.marker, "{name}: marker");
|
||||
assert_eq!(t.doc.units, s.doc.units, "{name}: units");
|
||||
assert_eq!(t.doc.sheet, s.doc.sheet, "{name}: sheet metal rules");
|
||||
assert_eq!(t.doc.plastic, s.doc.plastic, "{name}: plastic rules");
|
||||
assert_eq!(t.doc.materials, s.doc.materials, "{name}: materials");
|
||||
assert_eq!(t.doc.user_materials, s.doc.user_materials, "{name}: user materials");
|
||||
assert_eq!(t.doc.favorites, s.doc.favorites, "{name}: favourites");
|
||||
assert_eq!(t.doc.param_comments, s.doc.param_comments, "{name}: comments");
|
||||
assert_eq!(errors(&t), errors(&s), "{name}: feature results");
|
||||
assert_eq!(bodies(&mut t), bodies(&mut s), "{name}: bodies");
|
||||
replayed += 1;
|
||||
}
|
||||
assert!(replayed >= 7, "replayed {replayed} corpus designs");
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! Straight edges stay one edge after many cuts (#1049). The owner's design
|
||||
//! (`tests/action_matrix/designs/crazy-geom-cuts.solidcraft`, saved by 0.10.0): five extrudes,
|
||||
//! joins and cuts, over four sketches, three of them on faces of the body with the faces'
|
||||
//! edges projected. Cuts used to leave the straight edges of the walls they sliced split at
|
||||
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
|
||||
//! as many pieces, and a sketch on such a face projected every piece.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
fn design() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.solidcraft")
|
||||
}
|
||||
|
||||
fn open(path: &Path) -> Session {
|
||||
let mut s = Session::default();
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
s
|
||||
}
|
||||
|
||||
/// The design built from scratch: saved without cached geometry and opened again.
|
||||
fn rebuilt() -> Session {
|
||||
let s = open(&design());
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-straight-edges-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("crazy-geom-cuts.json");
|
||||
let mut t = s.scratch();
|
||||
t.execute("file.save", &json!({"path": path.to_string_lossy(), "format": "json"})).unwrap();
|
||||
let out = open(&path);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
out
|
||||
}
|
||||
|
||||
fn errors(s: &Session) -> Vec<(u64, String)> {
|
||||
s.model.results.iter().filter_map(|r| r.error.clone().map(|e| (r.id, e))).collect()
|
||||
}
|
||||
|
||||
fn key(v: Vec3) -> [i64; 3] {
|
||||
[v.x, v.y, v.z].map(|c| (c * 1e5).round() as i64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn straight_edges_are_one_edge_after_many_cuts() {
|
||||
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
|
||||
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
|
||||
let st = s.world_state();
|
||||
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
assert_eq!(bodies.len(), 1, "{what}: one body");
|
||||
let body = &bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
// Every edge is drawn as one polyline; a straight one as one segment.
|
||||
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
|
||||
for (i, e) in mesh.edges.iter().enumerate() {
|
||||
let (Some(a), Some(z)) = (e.first(), e.last()) else { panic!("{what}: edge {i} is empty") };
|
||||
at.entry(key(*a)).or_default().push(i);
|
||||
at.entry(key(*z)).or_default().push(i);
|
||||
}
|
||||
let straight = |i: usize| mesh.edges.get(i).filter(|e| e.len() == 2).and_then(|e| (e[1] - e[0]).normalized());
|
||||
let split: Vec<_> = at
|
||||
.iter()
|
||||
.filter(|(_, es)| match es.as_slice() {
|
||||
[a, b] => straight(*a).zip(straight(*b)).is_some_and(|(u, w)| u.dot(w).abs() > 1. - 1e-9),
|
||||
_ => false,
|
||||
})
|
||||
.map(|(k, _)| k.map(|c| c as f64 / 1e5))
|
||||
.collect();
|
||||
assert_eq!(split, Vec::<[f64; 3]>::new(), "{what}: straight edges split where nothing else meets them");
|
||||
// No two faces side by side in one plane.
|
||||
let plane = |f: usize| match face_surface(body, f) {
|
||||
Some(FaceSurface::Plane { normal, point }) => Some((normal, normal.dot(point))),
|
||||
_ => None,
|
||||
};
|
||||
for (i, faces) in mesh.edge_faces.iter().enumerate() {
|
||||
if let [a, b] = faces.as_slice()
|
||||
&& a != b
|
||||
&& let (Some((na, da)), Some((nb, db))) = (plane(*a as usize), plane(*b as usize))
|
||||
{
|
||||
assert!(na.dot(nb) < 1. - 1e-9 || (da - db).abs() > 1e-6, "{what}: faces {a} and {b} meet at edge {i} in one plane");
|
||||
}
|
||||
}
|
||||
assert_eq!((body.face_count(), body.edge_count(), body.vertex_count()), (35, 107, 71), "{what}: faces, edges and vertices");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -73,6 +73,12 @@ mod web {
|
||||
if self.files.ctx.borrow().is_none() {
|
||||
*self.files.ctx.borrow_mut() = Some(ctx.clone());
|
||||
}
|
||||
// A ready picker answer is consumed in app.logic later in this frame. Keep
|
||||
// incoming files queued until then so they cannot replace its target design.
|
||||
if self.app.file_picker_busy() {
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
}
|
||||
files::flush(&self.files);
|
||||
for (text, error) in self.files.messages.borrow_mut().drain(..) {
|
||||
self.app.set_status(text, error);
|
||||
@@ -331,22 +337,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 +383,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.
|
||||
@@ -421,12 +440,12 @@ mod web {
|
||||
// Open shows the browser's files (and the computer's, from there).
|
||||
pick_open: Some(Box::new(move || {
|
||||
panel.panel.set(true);
|
||||
None
|
||||
Box::pin(std::future::ready(None))
|
||||
})),
|
||||
// Designs are kept in the browser; everything else is downloaded.
|
||||
pick_save: Some(Box::new(|name: &str, exts: &[&str]| {
|
||||
let design = exts.contains(&solidcraft_engine::io::DESIGN_EXT);
|
||||
Some(format!("{}{name}", if design { files::OPFS } else { files::DOWNLOADS }))
|
||||
Box::pin(std::future::ready(Some(format!("{}{name}", if design { files::OPFS } else { files::DOWNLOADS }))))
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
@@ -7,8 +7,17 @@ license.workspace = true
|
||||
publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs can
|
||||
# allow it (macOS file-open events have no safe API). Keep the rest in sync with [workspace.lints].
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
neg_cmp_op_on_partial_ord = "allow"
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
@@ -18,6 +27,14 @@ egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
# Checks eframe's settings file (app.ron) before eframe reads it (src/startup.rs).
|
||||
ron = "0.12"
|
||||
# Waits for wgpu's adapter list when choosing the graphics backend before the window opens.
|
||||
pollster = { workspace = true }
|
||||
getrandom = "0.3"
|
||||
# The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system.
|
||||
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
quick-xml = { workspace = true }
|
||||
@@ -25,6 +42,13 @@ quick-xml = { workspace = true }
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
hidapi = { version = "2.6.7", default-features = false, features = ["windows-native"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! SolidCraft desktop app.
|
||||
//!
|
||||
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [design.solidcraft | part.step]`
|
||||
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]`
|
||||
//!
|
||||
//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and
|
||||
//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md.
|
||||
//!
|
||||
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
|
||||
//! `.3mf`/`.stl` file as a new design holding mesh bodies.
|
||||
@@ -10,26 +13,34 @@
|
||||
//! See `solidcraft_ui_egui::control` and docs/control-protocol.md.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(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(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
mod space_mouse;
|
||||
mod startup;
|
||||
|
||||
use solidcraft_engine::{Session, io};
|
||||
use solidcraft_ui_egui::{Services, SolveApp};
|
||||
|
||||
struct App {
|
||||
solve: SolveApp,
|
||||
/// The settings folder whose start marker the first frame clears (`startup::started`).
|
||||
starting: Option<std::path::PathBuf>,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if let Some(dir) = self.starting.take() {
|
||||
startup::started(&dir);
|
||||
}
|
||||
// The design on screen is the one whose commands a crash report replays.
|
||||
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
|
||||
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
||||
@@ -38,9 +49,15 @@ impl eframe::App for App {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||
solidcraft_ui_egui::documents::request_quit(&mut self.solve);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
|
||||
self.solve.open_path_async(&f);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
if !self.solve.file_picker_busy() {
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
}
|
||||
if self.solve.quit_requested {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
@@ -57,13 +74,22 @@ impl eframe::App for App {
|
||||
fn on_exit(&mut self) {
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_engine::crash::end_session();
|
||||
// An update asked to restart into the new version or to run its installer.
|
||||
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve)
|
||||
&& let Err(e) = std::process::Command::new(&program).args(&args).spawn()
|
||||
{
|
||||
log::error!("update: starting {}: {e}", program.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn services() -> Services {
|
||||
fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
Services {
|
||||
pick_open: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
pick_open: Some(Box::new(move || {
|
||||
let future = open_parent
|
||||
.clone()
|
||||
.add_filter(
|
||||
"Designs, STEP, IGES and meshes",
|
||||
&[
|
||||
@@ -90,11 +116,11 @@ fn services() -> Services {
|
||||
.add_filter("Mesh (3MF, STL)", &["3mf", "3MF", "stl", "STL"])
|
||||
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
|
||||
.add_filter("All files", &["*"])
|
||||
.pick_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.pick_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
pick_save: Some(Box::new(|name: &str, exts: &[&str]| {
|
||||
let dialog = rfd::FileDialog::new().set_file_name(name);
|
||||
pick_save: Some(Box::new(move |name: &str, exts: &[&str]| {
|
||||
let dialog = parent.clone().set_file_name(name);
|
||||
let dialog = if exts == [io::DESIGN_EXT, "json"] {
|
||||
if name.ends_with(".json") {
|
||||
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
|
||||
@@ -104,9 +130,12 @@ fn services() -> Services {
|
||||
} else {
|
||||
dialog.add_filter("File", exts)
|
||||
};
|
||||
dialog.save_file().map(|p| p.to_string_lossy().to_string())
|
||||
let future = dialog.save_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
show_in_folder: Some(Box::new(|path| {
|
||||
let folder = std::path::Path::new(path).parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
|
||||
@@ -127,6 +156,79 @@ fn services() -> Services {
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTPS GET for the update check (docs/updates.md): certificates from the operating system, a
|
||||
/// 15 s limit, at most 1 MB, and no headers beyond what GitHub requires (a User-Agent without
|
||||
/// the version or anything about this machine, and the API's media type).
|
||||
fn http_get(url: &str) -> Result<Vec<u8>, solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
let mut response = http_call(url, std::time::Duration::from_secs(15))?;
|
||||
response.body_mut().with_config().limit(1 << 20).read_to_vec().map_err(|e| FetchError::Failed(e.to_string()))
|
||||
}
|
||||
|
||||
/// HTTPS download of an update package to `to` (docs/updates.md), reporting progress; stops when
|
||||
/// `progress` returns false. The caller verifies the file.
|
||||
fn http_download(url: &str, to: &std::path::Path, progress: &dyn Fn(u64, Option<u64>) -> bool) -> Result<(), solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
use std::io::{Read, Write};
|
||||
let fail = |e: String| FetchError::Failed(e);
|
||||
let mut response = http_call(url, std::time::Duration::from_secs(30 * 60))?;
|
||||
let total = response.headers().get("content-length").and_then(|v| v.to_str().ok()).and_then(|v| v.parse::<u64>().ok());
|
||||
let mut body = response.body_mut().with_config().limit(solidcraft_ui_egui::update::install::MAX_PACKAGE).reader();
|
||||
let mut file = std::fs::File::create(to).map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
let mut buf = vec![0u8; 1 << 16];
|
||||
let mut done = 0u64;
|
||||
loop {
|
||||
let n = body.read(&mut buf).map_err(|e| fail(e.to_string()))?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
file.write_all(buf.get(..n).unwrap_or_default()).map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
done += n as u64;
|
||||
if !progress(done, total) {
|
||||
return Err(fail("cancelled".into()));
|
||||
}
|
||||
}
|
||||
file.sync_all().map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One GET to GitHub: a 200 answer, or why not (rate limits included).
|
||||
fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Response<ureq::Body>, solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
use std::time::Duration;
|
||||
let tls = ureq::tls::TlsConfig::builder()
|
||||
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
|
||||
.unversioned_rustls_crypto_provider(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.build();
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.tls_config(tls)
|
||||
.https_only(true)
|
||||
.http_status_as_error(false)
|
||||
.timeout_global(Some(timeout))
|
||||
.timeout_connect(Some(Duration::from_secs(5)))
|
||||
.user_agent("SolidCraft-update-check")
|
||||
.max_redirects(3)
|
||||
.build()
|
||||
.into();
|
||||
let response = agent
|
||||
.get(url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.call()
|
||||
.map_err(|e| FetchError::Failed(e.to_string()))?;
|
||||
let status = response.status().as_u16();
|
||||
let header = |k: &str| response.headers().get(k).and_then(|v| v.to_str().ok()).and_then(|v| v.trim().parse::<u64>().ok());
|
||||
if status == 429 || (status == 403 && (header("x-ratelimit-remaining") == Some(0) || header("retry-after").is_some())) {
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
|
||||
let retry_after_s = header("retry-after").or_else(|| header("x-ratelimit-reset").map(|reset| reset.saturating_sub(now))).unwrap_or(3600);
|
||||
return Err(FetchError::RateLimited { retry_after_s });
|
||||
}
|
||||
if status != 200 {
|
||||
return Err(FetchError::Failed(format!("GitHub answered HTTP {status}")));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
|
||||
fn settings_dir() -> Option<std::path::PathBuf> {
|
||||
eframe::storage_dir("SolidCraft")
|
||||
@@ -174,11 +276,19 @@ fn adopt_settings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `graphics.json`, read before the window opens.
|
||||
fn graphics() -> Option<serde_json::Value> {
|
||||
graphics_path().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
}
|
||||
|
||||
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`).
|
||||
fn backend_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
|
||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||
fn msaa() -> u16 {
|
||||
graphics_path()
|
||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
graphics()
|
||||
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
||||
.and_then(|m| u16::try_from(m).ok())
|
||||
.filter(|m| [1, 2, 4, 8].contains(m))
|
||||
@@ -219,7 +329,7 @@ const APP_ID: &str = solidcraft_engine::io::APP_ID;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
adopt_legacy_settings();
|
||||
let samples = msaa();
|
||||
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
|
||||
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
let mut sample = false;
|
||||
@@ -228,6 +338,7 @@ fn main() -> eframe::Result {
|
||||
match a.as_str() {
|
||||
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
|
||||
"--sample" => sample = true,
|
||||
"--safe-mode" => safe_mode = true,
|
||||
"--version" | "-V" => {
|
||||
println!("solidcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
@@ -246,12 +357,30 @@ fn main() -> eframe::Result {
|
||||
return Ok(());
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [design.solidcraft | part.step]");
|
||||
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]");
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
// Settings that don't read are moved aside; launches that never got a window are counted,
|
||||
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
|
||||
let settings = settings_dir();
|
||||
let mut notes: Vec<String> = Vec::new();
|
||||
let mut failed_starts = 0;
|
||||
if let Some(dir) = &settings {
|
||||
notes.extend(startup::check_settings(dir, &[solidcraft_ui_egui::prefs::STORAGE_KEY, solidcraft_ui_egui::prefs::LEGACY_STORAGE_KEY]));
|
||||
failed_starts = startup::begin(dir);
|
||||
}
|
||||
if failed_starts >= startup::FAILED_STARTS_FOR_SAFE_MODE && !safe_mode {
|
||||
safe_mode = true;
|
||||
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
|
||||
}
|
||||
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
let samples = if safe_mode { MSAA } else { msaa() };
|
||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||
let crashed = solidcraft_engine::crash::default_dir()
|
||||
.map(|dir| {
|
||||
@@ -268,8 +397,17 @@ fn main() -> eframe::Result {
|
||||
depth_buffer: DEPTH_BITS,
|
||||
stencil_buffer: STENCIL_BITS,
|
||||
multisampling: samples,
|
||||
// Kept with the other settings, not in a folder named after the app id.
|
||||
persistence_path: settings_dir().map(|d| d.join("app.ron")),
|
||||
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
|
||||
// and writes a scratch copy, leaving the user's settings as they were.
|
||||
persistence_path: if safe_mode {
|
||||
let scratch = std::env::temp_dir().join(format!("solidcraft-safe-mode-{}.ron", std::process::id()));
|
||||
let _ = std::fs::remove_file(&scratch);
|
||||
Some(scratch)
|
||||
} else {
|
||||
settings.as_ref().map(|d| d.join("app.ron"))
|
||||
},
|
||||
persist_window: !safe_mode,
|
||||
wgpu_options: wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref()),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
@@ -286,15 +424,23 @@ fn main() -> eframe::Result {
|
||||
b.with_x11();
|
||||
}));
|
||||
}
|
||||
eframe::run_native(
|
||||
// Files opened from Finder and the Dock arrive as events, not arguments.
|
||||
#[cfg(target_os = "macos")]
|
||||
open_documents::install();
|
||||
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let created_in_app = created.clone();
|
||||
let starting = settings.clone();
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
created_in_app.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||
let mut app = SolveApp::new(Session::default(), services());
|
||||
let mut app = SolveApp::new(Session::default(), services(cc));
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.cpu_meshing = safe_mode;
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
@@ -320,6 +466,12 @@ fn main() -> eframe::Result {
|
||||
app.session.mark_saved();
|
||||
app.session.undo.clear();
|
||||
}
|
||||
// A launch from Finder: its files arrived before the window.
|
||||
#[cfg(target_os = "macos")]
|
||||
let files = {
|
||||
open_documents::set_ui(&cc.egui_ctx);
|
||||
[files, open_documents::take()].concat()
|
||||
};
|
||||
for f in &files {
|
||||
app.open_path_async(f);
|
||||
}
|
||||
@@ -333,18 +485,72 @@ fn main() -> eframe::Result {
|
||||
if let Some(dir) = solidcraft_engine::recovery::default_dir() {
|
||||
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
||||
solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
// Safe mode keeps autosaving but offers nothing: the entries wait for a normal start.
|
||||
if safe_mode {
|
||||
app.recoverable.clear();
|
||||
}
|
||||
}
|
||||
// Tell the user about crashes since the last start (after the recovery offer is set up).
|
||||
solidcraft_ui_egui::crash_ui::announce(&mut app, &crashed);
|
||||
if !notes.is_empty() {
|
||||
app.set_status(notes.join(" "), true);
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
Ok(Box::new(App {
|
||||
solve: app,
|
||||
starting,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
space_mouse,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
match result {
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
|
||||
/// backends come from `startup::choose_backends` (on Windows D3D12 first, and only the backend
|
||||
/// in use is started); `preference` is `graphics.json`'s `backend`.
|
||||
fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuConfiguration {
|
||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
|
||||
/// the software renderer, else a message box saying what failed instead of a silent exit.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
|
||||
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
|
||||
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
// The second try showed its own message.
|
||||
Ok(_) => return Err(error),
|
||||
Err(e) => log::error!("could not start again with the software renderer: {e}"),
|
||||
}
|
||||
}
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("SolidCraft could not start")
|
||||
.set_description(startup::no_window_message(&error.to_string(), settings))
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
Err(error)
|
||||
}
|
||||
|
||||
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//! macOS: files opened from Finder (double-click, Open With, a drop on the Dock icon) or with
|
||||
//! `open -a SolidCraft file` (#997).
|
||||
//!
|
||||
//! macOS doesn't pass these on the command line: AppKit hands them to the application delegate's
|
||||
//! `application:openURLs:`, and when the delegate has no such method it asks `NSDocumentController`,
|
||||
//! which fails with "SolidCraft cannot open files in the … format" because SolidCraft isn't an
|
||||
//! `NSDocument` app. winit 0.30's delegate doesn't implement the method, so [`install`] adds it to
|
||||
//! the delegate's class when the app is about to finish launching: after winit set the delegate and
|
||||
//! before AppKit delivers the files the app was launched with. The handler queues the paths and
|
||||
//! wakes the UI, which opens them with [`take`] like files named on the command line.
|
||||
//!
|
||||
//! Adding a method to another crate's Objective-C class has no safe API, hence the `unsafe_code`
|
||||
//! allowance on [`install`] and [`add_open_urls_method`] (winit overrides `sendEvent:` on
|
||||
//! `NSApplication` similarly).
|
||||
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Mutex, OnceLock, PoisonError};
|
||||
|
||||
use block2::RcBlock;
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2::runtime::{AnyObject, Imp, Sel};
|
||||
use objc2::sel;
|
||||
use objc2_app_kit::{NSApplication, NSApplicationWillFinishLaunchingNotification};
|
||||
use objc2_foundation::{NSArray, NSNotification, NSNotificationCenter, NSURL};
|
||||
|
||||
/// Paths that arrived and haven't been opened yet.
|
||||
static PENDING: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
/// The UI to wake when files arrive (unset until the window exists; the first frame opens them).
|
||||
static UI: OnceLock<egui::Context> = OnceLock::new();
|
||||
|
||||
/// Handle open-document events from now on. Call before the event loop runs.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn install() {
|
||||
let block = RcBlock::new(|_: NonNull<NSNotification>| add_open_urls_method());
|
||||
// SAFETY: the name is AppKit's notification constant; no object or queue filter, so the block
|
||||
// runs on the posting thread (AppKit posts it on the main thread). The block captures nothing,
|
||||
// so it is sendable. The notification center keeps the registration (and the block) for the
|
||||
// life of the app.
|
||||
unsafe {
|
||||
NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
|
||||
Some(NSApplicationWillFinishLaunchingNotification),
|
||||
None,
|
||||
None,
|
||||
&block,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wake `ctx` when files arrive while the app runs.
|
||||
pub fn set_ui(ctx: &egui::Context) {
|
||||
// Set once; a second call would hand over the same context.
|
||||
let _ = UI.set(ctx.clone());
|
||||
}
|
||||
|
||||
/// The paths that arrived since the last call.
|
||||
pub fn take() -> Vec<String> {
|
||||
std::mem::take(&mut *PENDING.lock().unwrap_or_else(PoisonError::into_inner))
|
||||
}
|
||||
|
||||
/// `application:openURLs:` on the application delegate's class.
|
||||
#[allow(unsafe_code)]
|
||||
fn add_open_urls_method() {
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
let Some(delegate) = NSApplication::sharedApplication(mtm).delegate() else {
|
||||
log::warn!("no application delegate; files opened from Finder won't open");
|
||||
return;
|
||||
};
|
||||
let class = AsRef::<AnyObject>::as_ref(&*delegate).class();
|
||||
let handler: extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>) = open_urls;
|
||||
// SAFETY: `Imp` is the untyped form of a method implementation; the Objective-C runtime calls
|
||||
// it with the arguments the type encoding declares, which match `open_urls`: no return value
|
||||
// (`v`), the receiver (`@`), the selector (`:`), the application (`@`) and the URL array (`@`).
|
||||
let added = unsafe {
|
||||
let imp = std::mem::transmute::<extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>), Imp>(handler);
|
||||
objc2::ffi::class_addMethod(std::ptr::from_ref(class).cast_mut(), sel!(application:openURLs:), imp, c"v@:@@".as_ptr())
|
||||
};
|
||||
if !added.as_bool() {
|
||||
log::warn!("the application delegate already handles open-document events");
|
||||
}
|
||||
}
|
||||
|
||||
/// AppKit calls this; a panic stops here instead of unwinding through AppKit's frames.
|
||||
extern "C-unwind" fn open_urls(_delegate: &AnyObject, _cmd: Sel, _app: &AnyObject, urls: &NSArray<NSURL>) {
|
||||
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| queue(urls))).is_err() {
|
||||
log::warn!("files opened from Finder were dropped after an internal error");
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue the files among `urls` and wake the UI.
|
||||
fn queue(urls: &NSArray<NSURL>) {
|
||||
// Only file URLs: macOS 27 turns an https URL's path into a file path too.
|
||||
let paths = urls.iter().filter(|u| u.isFileURL()).filter_map(|u| u.to_file_path()).map(|p| p.to_string_lossy().into_owned());
|
||||
PENDING.lock().unwrap_or_else(PoisonError::into_inner).extend(paths);
|
||||
if let Some(ctx) = UI.get() {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use objc2::runtime::NSObject;
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Files queue until the UI takes them; URLs that aren't files are left out.
|
||||
#[test]
|
||||
fn opened_files_wait_for_the_ui() {
|
||||
let web = NSURL::URLWithString(&NSString::from_str("https://example.com/a.solidcraft")).unwrap();
|
||||
let urls = NSArray::from_retained_slice(&[
|
||||
NSURL::from_file_path("/tmp/a b.solidcraft").unwrap(),
|
||||
web,
|
||||
NSURL::from_file_path("/tmp/c.step").unwrap(),
|
||||
]);
|
||||
let any = NSObject::new();
|
||||
open_urls(&any, sel!(application:openURLs:), &any, &urls);
|
||||
assert_eq!(take(), ["/tmp/a b.solidcraft", "/tmp/c.step"]);
|
||||
assert!(take().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
//! Getting the window open, whatever state the settings folder or the graphics device is in
|
||||
//! (docs/book/src/guide/troubleshooting.md).
|
||||
//!
|
||||
//! - **Settings that don't read** (`app.ron`, `graphics.json`): moved aside as
|
||||
//! `<name>.bad-<time>` and the app starts with defaults, instead of a newer or damaged file
|
||||
//! being dropped silently or stopping the start.
|
||||
//! - **Launches that die before the window is up**: a marker file counts them; after two in a
|
||||
//! row the next launch starts in safe mode by itself.
|
||||
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
|
||||
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
|
||||
//! - **No usable graphics device**: one retry on the software renderer
|
||||
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The environment variable that starts safe mode (any value but `0` or empty).
|
||||
pub const SAFE_MODE_ENV: &str = "SOLIDCRAFT_SAFE_MODE";
|
||||
/// `SOLIDCRAFT_RENDERER=cpu` picks the software renderer (WARP, lavapipe, llvmpipe).
|
||||
pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
|
||||
/// Consecutive launches that died before the window was up, after which safe mode starts.
|
||||
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
|
||||
const MARKER: &str = "starting";
|
||||
|
||||
/// Whether an environment value switches something on.
|
||||
pub fn env_on(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
|
||||
}
|
||||
|
||||
/// Whether `SOLIDCRAFT_RENDERER` asks for the software renderer.
|
||||
pub fn cpu_renderer_env(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|v| v.eq_ignore_ascii_case("cpu") || v.eq_ignore_ascii_case("software"))
|
||||
}
|
||||
|
||||
fn unix_now() -> u64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
|
||||
}
|
||||
|
||||
/// Move `path` aside as `<name>.bad-<time>` (kept for a bug report); returns where it went.
|
||||
pub fn back_up(path: &Path) -> std::io::Result<PathBuf> {
|
||||
let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "settings".into());
|
||||
let mut to = path.with_file_name(format!("{name}.bad-{}", unix_now()));
|
||||
let mut n = 1;
|
||||
while to.exists() && n < 100 {
|
||||
to = path.with_file_name(format!("{name}.bad-{}-{n}", unix_now()));
|
||||
n += 1;
|
||||
}
|
||||
std::fs::rename(path, &to)?;
|
||||
Ok(to)
|
||||
}
|
||||
|
||||
/// Whether eframe's `app.ron` reads, with the preferences inside it.
|
||||
fn app_ron_reads(text: &str, prefs_keys: &[&str]) -> bool {
|
||||
let Ok(kv) = ron::from_str::<std::collections::HashMap<String, String>>(text) else { return false };
|
||||
prefs_keys.iter().filter_map(|k| kv.get(*k)).all(|p| serde_json::from_str::<serde_json::Value>(p).is_ok_and(|v| v.is_object()))
|
||||
}
|
||||
|
||||
/// Check the settings files in `dir` before anything reads them: one that doesn't read is moved
|
||||
/// aside (the app then starts with defaults for it). Returns a line per file moved.
|
||||
pub fn check_settings(dir: &Path, prefs_keys: &[&str]) -> Vec<String> {
|
||||
let mut notes = Vec::new();
|
||||
let mut check = |name: &str, reads: &dyn Fn(&str) -> bool| {
|
||||
let path = dir.join(name);
|
||||
let Ok(bytes) = std::fs::read(&path) else { return };
|
||||
let ok = std::str::from_utf8(&bytes).is_ok_and(|t| t.trim().is_empty() || reads(t));
|
||||
if ok {
|
||||
return;
|
||||
}
|
||||
match back_up(&path) {
|
||||
Ok(to) => {
|
||||
log::warn!("settings: {} did not read; moved to {}", path.display(), to.display());
|
||||
notes.push(format!("{name} could not be read and was moved to {}; defaults are used.", to.display()));
|
||||
}
|
||||
Err(e) => log::warn!("settings: {} did not read and could not be moved: {e}", path.display()),
|
||||
}
|
||||
};
|
||||
check("app.ron", &|t| app_ron_reads(t, prefs_keys));
|
||||
check("graphics.json", &|t| serde_json::from_str::<serde_json::Value>(t).is_ok_and(|v| v.is_object()));
|
||||
notes
|
||||
}
|
||||
|
||||
/// A launch begins: count it in the marker in `dir`. Returns how many launches before this one
|
||||
/// died before their window was up.
|
||||
pub fn begin(dir: &Path) -> u32 {
|
||||
let marker = dir.join(MARKER);
|
||||
let before = std::fs::read_to_string(&marker).ok().and_then(|s| s.trim().parse::<u32>().ok()).unwrap_or(0).min(1000);
|
||||
if std::fs::create_dir_all(dir).is_ok() {
|
||||
let _ = std::fs::write(&marker, (before + 1).to_string());
|
||||
}
|
||||
before
|
||||
}
|
||||
|
||||
/// The window is up: this launch did not fail to start.
|
||||
pub fn started(dir: &Path) {
|
||||
let _ = std::fs::remove_file(dir.join(MARKER));
|
||||
}
|
||||
|
||||
/// The software adapter among `adapters` (WARP on Windows, lavapipe or llvmpipe elsewhere), else
|
||||
/// the first one that can draw to `surface`.
|
||||
pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&eframe::wgpu::Surface<'_>>) -> Result<eframe::wgpu::Adapter, String> {
|
||||
let fits = |a: &&eframe::wgpu::Adapter| surface.is_none_or(|s| a.is_surface_supported(s));
|
||||
adapters
|
||||
.iter()
|
||||
.filter(fits)
|
||||
.find(|a| a.get_info().device_type == eframe::wgpu::DeviceType::Cpu)
|
||||
.or_else(|| adapters.iter().find(fits))
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
let names: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().backend)).collect();
|
||||
format!("no graphics adapter can draw the window (found: {})", if names.is_empty() { "none".into() } else { names.join(", ") })
|
||||
})
|
||||
}
|
||||
|
||||
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
|
||||
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||
|
||||
/// The graphics backends to start with, or `None` to leave it to wgpu (`WGPU_BACKEND`, or all
|
||||
/// the backends it was built with).
|
||||
///
|
||||
/// On Windows, D3D12 comes first, then Vulkan, then OpenGL, and only the first one with a
|
||||
/// hardware adapter is started at all. wgpu would otherwise start Vulkan, D3D12 and OpenGL
|
||||
/// together and prefer Vulkan, and starting Vulkan loads every installed Vulkan driver: Intel's
|
||||
/// (`igvk64.dll`) crashed SolidCraft at startup that way, inside the driver, before anything
|
||||
/// could be reported. D3D12 is Windows' own graphics API, the one its drivers are tested with.
|
||||
/// `has_hardware(backends)` says whether those backends have an adapter that isn't software
|
||||
/// (it is only asked about the backends being tried, one at a time).
|
||||
///
|
||||
/// - `WGPU_BACKEND` set (`env_set`): `None`, whatever it says wins.
|
||||
/// - The software renderer (`cpu`, safe mode): D3D12 alone on Windows (WARP); `None` elsewhere.
|
||||
/// - `preference` (`graphics.json`'s `backend`: `dx12`, `vulkan`, `gl` or a comma list; `auto`
|
||||
/// or empty for the order above): those backends, on any system.
|
||||
/// - Elsewhere than Windows: `None`.
|
||||
/// - No hardware adapter at all: D3D12 (WARP, Windows' software renderer).
|
||||
pub fn choose_backends(
|
||||
windows: bool,
|
||||
env_set: bool,
|
||||
cpu: bool,
|
||||
preference: Option<&str>,
|
||||
mut has_hardware: impl FnMut(eframe::wgpu::Backends) -> bool,
|
||||
) -> Option<eframe::wgpu::Backends> {
|
||||
use eframe::wgpu::Backends;
|
||||
if env_set {
|
||||
return None;
|
||||
}
|
||||
if cpu {
|
||||
return windows.then_some(Backends::DX12);
|
||||
}
|
||||
if let Some(p) = preference.map(str::trim).filter(|p| !p.is_empty() && !p.eq_ignore_ascii_case("auto")) {
|
||||
let chosen = Backends::from_comma_list(p);
|
||||
if !chosen.is_empty() {
|
||||
return Some(chosen);
|
||||
}
|
||||
log::warn!("graphics: unknown backend preference `{p}`; using the default order");
|
||||
}
|
||||
if !windows {
|
||||
return None;
|
||||
}
|
||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||
}
|
||||
|
||||
/// The order backends are tried in on Windows.
|
||||
pub const WINDOWS_ORDER: [eframe::wgpu::Backends; 3] = [eframe::wgpu::Backends::DX12, eframe::wgpu::Backends::VULKAN, eframe::wgpu::Backends::GL];
|
||||
|
||||
/// Whether `backends` (started on their own, nothing else) have a hardware adapter.
|
||||
pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle();
|
||||
desc.backends = backends;
|
||||
desc.flags = eframe::wgpu::InstanceFlags::from_build_config().with_env();
|
||||
let instance = eframe::wgpu::Instance::new(desc);
|
||||
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
|
||||
let found: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().device_type)).collect();
|
||||
log::info!("graphics: {backends:?} adapters: {found:?}");
|
||||
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// The message shown when no window could be opened at all.
|
||||
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
||||
format!(
|
||||
"SolidCraft could not open its window.\n\n{error}\n\n\
|
||||
The software renderer was tried too. Things to try:\n\
|
||||
- Install or update the graphics driver (on Windows, from the GPU maker's site rather than Windows Update).\n\
|
||||
- Over Remote Desktop or in a virtual machine, enable 3D acceleration or run it on the machine itself.\n\
|
||||
- Start it once with `solidcraft --safe-mode`.\n\
|
||||
- Rename the settings folder: {folder}\n\n\
|
||||
More: https://bherbruck.github.io/solidcraft/guide/troubleshooting.html"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switches_read_from_the_environment() {
|
||||
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
|
||||
assert!(cpu_renderer_env(Some("cpu")) && cpu_renderer_env(Some("Software")) && !cpu_renderer_env(Some("gpu")) && !cpu_renderer_env(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_that_do_not_read_are_moved_aside_and_good_ones_stay() {
|
||||
let keys = ["solidcraft.prefs", "solvecraft.prefs"];
|
||||
let d = scratch("check");
|
||||
// Good files (and an empty one) stay where they are.
|
||||
std::fs::write(d.join("app.ron"), r#"{"solidcraft.prefs": "{\"grid\":true}", "egui": "(anything)"}"#).unwrap();
|
||||
std::fs::write(d.join("graphics.json"), r#"{"msaa":4}"#).unwrap();
|
||||
assert!(check_settings(&d, &keys).is_empty());
|
||||
assert!(d.join("app.ron").is_file() && d.join("graphics.json").is_file());
|
||||
// Damaged RON, preferences that aren't JSON, a truncated graphics.json, non-UTF-8 bytes.
|
||||
for (name, text) in [
|
||||
("app.ron", b"{\"solidcraft.prefs\": \"{\\\"gr".as_slice()),
|
||||
("app.ron", br#"{"solvecraft.prefs": "not json"}"#.as_slice()),
|
||||
("graphics.json", br#"{"msaa":"#.as_slice()),
|
||||
("graphics.json", b"\xff\xfe\x00".as_slice()),
|
||||
] {
|
||||
std::fs::write(d.join(name), text).unwrap();
|
||||
let notes = check_settings(&d, &keys);
|
||||
assert_eq!(notes.len(), 1, "{name}: {notes:?}");
|
||||
assert!(!d.join(name).exists(), "{name} moved");
|
||||
let kept: Vec<_> =
|
||||
std::fs::read_dir(&d).unwrap().flatten().filter(|e| e.file_name().to_string_lossy().starts_with(&format!("{name}.bad-"))).collect();
|
||||
assert!(kept.iter().any(|e| std::fs::read(e.path()).unwrap() == text), "{name}: the bad file is kept");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launches_that_never_got_a_window_are_counted_until_one_does() {
|
||||
let d = scratch("marker");
|
||||
assert_eq!(begin(&d), 0);
|
||||
assert_eq!(begin(&d), 1);
|
||||
assert_eq!(begin(&d), FAILED_STARTS_FOR_SAFE_MODE);
|
||||
started(&d);
|
||||
assert_eq!(begin(&d), 0);
|
||||
// A damaged marker counts as none.
|
||||
std::fs::write(d.join(MARKER), "zz").unwrap();
|
||||
assert_eq!(begin(&d), 0);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_starts_d3d12_first_and_vulkan_only_when_d3d12_has_no_gpu() {
|
||||
use eframe::wgpu::Backends;
|
||||
let probe = |with: &'static [Backends]| {
|
||||
move |b: Backends| {
|
||||
assert!(b == Backends::DX12 || b == Backends::VULKAN || b == Backends::GL, "one backend at a time: {b:?}");
|
||||
with.contains(&b)
|
||||
}
|
||||
};
|
||||
// D3D12 works: Vulkan and OpenGL are never even asked about (starting Vulkan loads
|
||||
// its drivers, which is what crashed).
|
||||
let mut asked = Vec::new();
|
||||
let chosen = choose_backends(true, false, false, None, |b| {
|
||||
asked.push(b);
|
||||
b == Backends::DX12
|
||||
});
|
||||
assert_eq!(chosen, Some(Backends::DX12));
|
||||
assert_eq!(asked, [Backends::DX12]);
|
||||
// Everything works: still D3D12 alone.
|
||||
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::DX12, Backends::VULKAN, Backends::GL])), Some(Backends::DX12));
|
||||
// No D3D12 hardware adapter: Vulkan, else OpenGL, else D3D12's software adapter (WARP).
|
||||
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::VULKAN, Backends::GL])), Some(Backends::VULKAN));
|
||||
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::GL])), Some(Backends::GL));
|
||||
assert_eq!(choose_backends(true, false, false, None, probe(&[])), Some(Backends::DX12));
|
||||
// `auto` and blanks are the default order.
|
||||
assert_eq!(choose_backends(true, false, false, Some(" auto "), probe(&[Backends::DX12])), Some(Backends::DX12));
|
||||
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||
use eframe::wgpu::Backends;
|
||||
let never = |b: Backends| -> bool { panic!("no probing expected, asked about {b:?}") };
|
||||
// WGPU_BACKEND: wgpu reads it itself, nothing is probed.
|
||||
assert_eq!(choose_backends(true, true, false, Some("gl"), never), None);
|
||||
assert_eq!(choose_backends(true, true, true, None, never), None);
|
||||
// The software renderer (safe mode, the retry): WARP through D3D12 alone, never Vulkan.
|
||||
assert_eq!(choose_backends(true, false, true, Some("vulkan"), never), Some(Backends::DX12));
|
||||
assert_eq!(choose_backends(false, false, true, None, never), None);
|
||||
// The preference, on any system, without probing.
|
||||
assert_eq!(choose_backends(true, false, false, Some("vulkan"), never), Some(Backends::VULKAN));
|
||||
assert_eq!(choose_backends(true, false, false, Some("GL"), never), Some(Backends::GL));
|
||||
assert_eq!(choose_backends(true, false, false, Some("dx12,vulkan"), never), Some(Backends::DX12 | Backends::VULKAN));
|
||||
assert_eq!(choose_backends(false, false, false, Some("vulkan"), never), Some(Backends::VULKAN));
|
||||
// An unknown preference is the default order.
|
||||
assert_eq!(choose_backends(true, false, false, Some("glide"), |b| b == Backends::DX12), Some(Backends::DX12));
|
||||
// Elsewhere: wgpu's own choice.
|
||||
assert_eq!(choose_backends(false, false, false, None, never), None);
|
||||
assert_eq!(choose_backends(false, false, false, Some("auto"), never), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probing_a_backend_alone_never_panics() {
|
||||
// Whatever this machine has (often nothing): an answer, not a crash.
|
||||
for b in WINDOWS_ORDER {
|
||||
let _ = has_hardware_adapter(b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_message_names_the_settings_folder_and_safe_mode() {
|
||||
let m = no_window_message("no adapter", Some(Path::new("/x/SolidCraft")));
|
||||
assert!(m.contains("no adapter") && m.contains("/x/SolidCraft") && m.contains("--safe-mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_software_adapter_is_preferred_and_its_absence_is_an_error_not_a_panic() {
|
||||
assert!(pick_cpu_adapter(&[], None).unwrap_err().contains("none"));
|
||||
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapters = pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all()));
|
||||
if adapters.is_empty() {
|
||||
return;
|
||||
}
|
||||
let picked = pick_cpu_adapter(&adapters, None).unwrap();
|
||||
if adapters.iter().any(|a| a.get_info().device_type == eframe::wgpu::DeviceType::Cpu) {
|
||||
assert_eq!(picked.get_info().device_type, eframe::wgpu::DeviceType::Cpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,756 @@
|
||||
{
|
||||
"Send your report on GitHub": "Bericht auf GitHub senden",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Unten den GitHub-Entwurf öffnen und den kopierten Bericht aus der Zwischenablage einfügen.",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Unten den GitHub-Entwurf öffnen. Titel und Bericht werden vorausgefüllt.",
|
||||
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Screenshot anhängen: kopieren und einfügen oder als PNG speichern und hochladen.",
|
||||
"3. Click Submit new issue on GitHub to send your report.": "3. Auf GitHub auf Submit new issue klicken, um den Bericht zu senden.",
|
||||
"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.material_properties_directional": "Richtungsabhängiger Elastizitätsmodul",
|
||||
"cmd.material_properties_validate": "Physikalische Materialdaten prüfen",
|
||||
"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_perforate": "Perforieren",
|
||||
"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_add": "Aufgezeichnetes Merkmal hinzufügen",
|
||||
"cmd.timeline_compute_all": "Alles berechnen",
|
||||
"cmd.timeline_copy": "Merkmale kopieren",
|
||||
"cmd.timeline_dependents": "Merkmalsabhängigkeiten",
|
||||
"cmd.timeline_export_script": "Befehlsskript exportieren",
|
||||
"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",
|
||||
"cmd.library_inspect": "Lokale Bibliothek prüfen",
|
||||
"cmd.library_export": "Designbibliothek exportieren",
|
||||
"cmd.library_apply": "Lokalen Bibliothekseintrag anwenden",
|
||||
"Thin": "Dünnwandig",
|
||||
"Thin Extrude": "Dünnwandig extrudieren",
|
||||
"Wall Location": "Wandlage",
|
||||
"Wall Thickness": "Wandstärke",
|
||||
"Thickness Outside": "Dicke außen",
|
||||
"Inside": "Innen",
|
||||
"Outside": "Außen",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"update.button": "Update",
|
||||
"update.button_tip": "SolidCraft {version} ist verfügbar",
|
||||
"update.title": "Update verfügbar",
|
||||
"update.available": "SolidCraft {version} ist verfügbar",
|
||||
"update.you_have": "Installiert ist {version}.",
|
||||
"update.no_notes": "Zu diesem Release gibt es keine Hinweise.",
|
||||
"update.system_hint": "Diese Installation ist ein {kind}. Aktualisieren mit:",
|
||||
"update.package_hint": "Diese Installation stammt aus einem {kind}. Neues Paket herunterladen und installieren mit:",
|
||||
"update.copy": "Kopieren",
|
||||
"update.file": "Herunterladen holt {file}.",
|
||||
"update.download": "Herunterladen",
|
||||
"update.release_page": "Release-Seite öffnen",
|
||||
"update.skip": "Diese Version überspringen",
|
||||
"update.later": "Später erinnern",
|
||||
"update.prefs_label": "Updates",
|
||||
"update.check_automatically": "Automatisch nach Updates suchen",
|
||||
"update.check_now": "Jetzt suchen",
|
||||
"update.checking": "Suche läuft…",
|
||||
"update.failed": "Suche fehlgeschlagen: {error}",
|
||||
"update.up_to_date": "SolidCraft ist aktuell.",
|
||||
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
|
||||
"update.prefs_dev": "Dies ist ein Entwicklungs-Build ({version}): er sucht nicht nach Updates.",
|
||||
"update.show": "Update auf {version} anzeigen",
|
||||
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
|
||||
"update.update_now": "Jetzt aktualisieren",
|
||||
"update.install_msi": "Installieren (schließt SolidCraft)",
|
||||
"update.download_open": "Herunterladen und öffnen",
|
||||
"update.downloading": "Herunterladen und prüfen… {done} von {total} MB",
|
||||
"update.installed": "SolidCraft {version} ist installiert. Zum Verwenden neu starten.",
|
||||
"update.restart": "Jetzt neu starten",
|
||||
"update.restart_later": "Später",
|
||||
"update.installer_started": "Das Installationsprogramm startet, wenn SolidCraft geschlossen wird.",
|
||||
"update.dmg_opened": "Das Disk-Image ist geöffnet und geprüft: SolidCraft in „Programme“ ziehen, die alte Version ersetzen und SolidCraft neu starten.",
|
||||
"update.install_failed": "Das Update wurde nicht installiert: {error}. Es wurde nichts verändert; „Herunterladen“ holt das Paket."
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"Send your report on GitHub": "Send your report on GitHub",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Open GitHub draft below, then paste the report copied to your clipboard.",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Open GitHub draft below. Your title and report will be filled in.",
|
||||
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Attach your screenshot: copy and paste it, or save and upload the PNG.",
|
||||
"3. Click Submit new issue on GitHub to send your report.": "3. Click Submit new issue on GitHub to send your report.",
|
||||
"browser.body_name": "Body{number}",
|
||||
"browser.component_name": "Component{number}",
|
||||
"browser.parameters": "Parameters ({count})",
|
||||
@@ -161,6 +166,7 @@
|
||||
"cmd.sheet_fold": "Fold",
|
||||
"cmd.sheet_hem": "Hem",
|
||||
"cmd.sheet_manage_rules": "Sheet Metal Rules",
|
||||
"cmd.sheet_perforate": "Perforate",
|
||||
"cmd.sheet_refold": "Refold",
|
||||
"cmd.sheet_rules": "List Sheet Metal Rules",
|
||||
"cmd.sheet_unfold": "Unfold",
|
||||
@@ -214,6 +220,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,9 +252,24 @@
|
||||
"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_add": "Add Recorded Feature",
|
||||
"cmd.timeline_compute_all": "Compute All",
|
||||
"cmd.timeline_copy": "Copy Features",
|
||||
"cmd.timeline_dependents": "Feature Dependents",
|
||||
"cmd.timeline_export_script": "Export Command Script",
|
||||
"cmd.timeline_inspect": "Inspect Feature",
|
||||
"cmd.timeline_paste": "Paste Features",
|
||||
"cmd.timeline_redefine": "Edit Feature",
|
||||
@@ -265,6 +287,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 +310,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 +336,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 +353,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 +391,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 +404,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 +585,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 +625,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 +891,125 @@
|
||||
"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.library_inspect": "Inspect Local Library",
|
||||
"cmd.library_export": "Export Design Library",
|
||||
"cmd.library_apply": "Apply Local Library Entry",
|
||||
"Thin": "Thin",
|
||||
"Thin Extrude": "Thin Extrude",
|
||||
"Wall Location": "Wall Location",
|
||||
"Wall Thickness": "Wall Thickness",
|
||||
"Thickness Outside": "Thickness Outside",
|
||||
"Inside": "Inside",
|
||||
"Outside": "Outside",
|
||||
"Profiles/Curves": "Profiles/Curves",
|
||||
"cmd.material_properties_validate": "Validate Physical Material Data",
|
||||
"cmd.material_properties_directional": "Directional Elastic Modulus",
|
||||
"update.button": "Update",
|
||||
"update.button_tip": "SolidCraft {version} is available",
|
||||
"update.title": "Update available",
|
||||
"update.available": "SolidCraft {version} is available",
|
||||
"update.you_have": "You have {version}.",
|
||||
"update.no_notes": "This release has no notes.",
|
||||
"update.system_hint": "This copy is a {kind}. Update it with:",
|
||||
"update.package_hint": "This copy came from a {kind}. Download the new package, then install it with:",
|
||||
"update.copy": "Copy",
|
||||
"update.file": "Download gets {file}.",
|
||||
"update.download": "Download",
|
||||
"update.release_page": "Open release page",
|
||||
"update.skip": "Skip this version",
|
||||
"update.later": "Remind me later",
|
||||
"update.prefs_label": "Updates",
|
||||
"update.check_automatically": "Check for updates automatically",
|
||||
"update.check_now": "Check now",
|
||||
"update.checking": "Checking…",
|
||||
"update.failed": "Couldn't check: {error}",
|
||||
"update.up_to_date": "SolidCraft is up to date.",
|
||||
"update.prefs_note": "This is SolidCraft {version} ({kind}). A check asks GitHub for the latest release and sends nothing else.",
|
||||
"update.prefs_dev": "This is a development build ({version}): it doesn't check for updates.",
|
||||
"update.show": "Show the update to {version}",
|
||||
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software center. SolidCraft doesn't check.",
|
||||
"update.update_now": "Update now",
|
||||
"update.install_msi": "Install (closes SolidCraft)",
|
||||
"update.download_open": "Download and open",
|
||||
"update.downloading": "Downloading and verifying… {done} of {total} MB",
|
||||
"update.installed": "SolidCraft {version} is installed. Restart to use it.",
|
||||
"update.restart": "Restart now",
|
||||
"update.restart_later": "Later",
|
||||
"update.installer_started": "The installer starts when SolidCraft closes.",
|
||||
"update.dmg_opened": "The disk image is open and verified: drag SolidCraft to Applications, replacing the old copy, then restart SolidCraft.",
|
||||
"update.install_failed": "The update didn't install: {error}. Nothing was changed; Download gets the package instead."
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"Send your report on GitHub": "GitHubでレポートを送信",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 下のボタンでGitHubの下書きを開き、クリップボードにコピーされたレポートを貼り付けます。",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 下のボタンでGitHubの下書きを開きます。タイトルとレポートは入力済みです。",
|
||||
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. スクリーンショットを添付します。コピーして貼り付けるか、PNGを保存してアップロードします。",
|
||||
"3. Click Submit new issue on GitHub to send your report.": "3. GitHubでSubmit new issueをクリックしてレポートを送信します。",
|
||||
"browser.body_name": "ボディ{number}",
|
||||
"browser.component_name": "コンポーネント{number}",
|
||||
"browser.parameters": "パラメータ ({count})",
|
||||
@@ -161,6 +166,7 @@
|
||||
"cmd.sheet_fold": "折り曲げ",
|
||||
"cmd.sheet_hem": "ヘミング",
|
||||
"cmd.sheet_manage_rules": "板金ルール",
|
||||
"cmd.sheet_perforate": "パンチング",
|
||||
"cmd.sheet_refold": "折り戻し",
|
||||
"cmd.sheet_rules": "板金ルールを一覧表示",
|
||||
"cmd.sheet_unfold": "展開",
|
||||
@@ -214,6 +220,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,9 +252,24 @@
|
||||
"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_add": "記録したフィーチャーを追加",
|
||||
"cmd.timeline_compute_all": "すべて再計算",
|
||||
"cmd.timeline_copy": "フィーチャーをコピー",
|
||||
"cmd.timeline_dependents": "従属フィーチャー",
|
||||
"cmd.timeline_export_script": "コマンド スクリプトをエクスポート",
|
||||
"cmd.timeline_inspect": "フィーチャーを検査",
|
||||
"cmd.timeline_paste": "フィーチャーを貼り付け",
|
||||
"cmd.timeline_redefine": "フィーチャーを編集",
|
||||
@@ -265,6 +287,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 +310,7 @@
|
||||
"ui.agent_cursor": "エージェントカーソル",
|
||||
"ui.agent_cursor_speed": "エージェントカーソル速度",
|
||||
"ui.all": "すべて",
|
||||
"ui.analysis": "解析",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度の小数点以下の桁数",
|
||||
"ui.animate_view_changes": "視点変更をアニメーション表示",
|
||||
@@ -310,6 +336,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 +353,7 @@
|
||||
"ui.combine": "結合",
|
||||
"ui.coming_later": "準備中",
|
||||
"ui.concentric": "同心",
|
||||
"ui.configurations": "構成",
|
||||
"ui.conic_curve": "円錐曲線",
|
||||
"ui.constant": "一定",
|
||||
"ui.constraints_conflict": "拘束が競合しています",
|
||||
@@ -362,6 +391,7 @@
|
||||
"ui.drawing": "図面",
|
||||
"ui.drill_point": "ドリル先端",
|
||||
"ui.edit": "編集",
|
||||
"ui.edit_section_analysis": "断面解析を編集",
|
||||
"ui.ellipse": "楕円",
|
||||
"ui.end_radius": "終了半径",
|
||||
"ui.entities": "エンティティ",
|
||||
@@ -374,6 +404,7 @@
|
||||
"ui.extent_type": "範囲タイプ",
|
||||
"ui.extrude": "押し出し",
|
||||
"ui.factor": "倍率",
|
||||
"ui.feature_history": "フィーチャー履歴",
|
||||
"ui.feature_options": "フィーチャーオプション",
|
||||
"ui.features": "フィーチャー",
|
||||
"ui.file": "ファイル",
|
||||
@@ -554,7 +585,6 @@
|
||||
"ui.shell": "シェル",
|
||||
"ui.show": "表示",
|
||||
"ui.show_constraints": "拘束",
|
||||
"ui.show_construction_geometries": "構築ジオメトリ",
|
||||
"ui.show_dimensions": "寸法",
|
||||
"ui.show_points": "点を表示",
|
||||
"ui.show_profile": "プロファイルを表示",
|
||||
@@ -595,6 +625,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 +891,125 @@
|
||||
"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.library_inspect": "ローカルライブラリを確認",
|
||||
"cmd.library_export": "デザインライブラリを書き出す",
|
||||
"cmd.library_apply": "ローカルライブラリ項目を適用",
|
||||
"Thin": "薄肉",
|
||||
"Thin Extrude": "薄肉押し出し",
|
||||
"Wall Location": "壁の位置",
|
||||
"Wall Thickness": "壁の厚さ",
|
||||
"Thickness Outside": "外側の厚さ",
|
||||
"Inside": "内側",
|
||||
"Outside": "外側",
|
||||
"Profiles/Curves": "プロファイル/曲線",
|
||||
"cmd.material_properties_validate": "物理材料データを検証",
|
||||
"cmd.material_properties_directional": "方向別弾性率",
|
||||
"update.button": "アップデート",
|
||||
"update.button_tip": "SolidCraft {version} が利用できます",
|
||||
"update.title": "アップデートがあります",
|
||||
"update.available": "SolidCraft {version} が利用できます",
|
||||
"update.you_have": "現在のバージョンは {version} です。",
|
||||
"update.no_notes": "このリリースにはリリースノートがありません。",
|
||||
"update.system_hint": "このコピーは {kind} です。次のコマンドで更新します:",
|
||||
"update.package_hint": "このコピーは {kind} からインストールされました。新しいパッケージをダウンロードし、次のコマンドでインストールします:",
|
||||
"update.copy": "コピー",
|
||||
"update.file": "ダウンロードするファイル: {file}",
|
||||
"update.download": "ダウンロード",
|
||||
"update.release_page": "リリースページを開く",
|
||||
"update.skip": "このバージョンをスキップ",
|
||||
"update.later": "後で通知",
|
||||
"update.prefs_label": "アップデート",
|
||||
"update.check_automatically": "アップデートを自動的に確認",
|
||||
"update.check_now": "今すぐ確認",
|
||||
"update.checking": "確認中…",
|
||||
"update.failed": "確認できませんでした: {error}",
|
||||
"update.up_to_date": "SolidCraft は最新です。",
|
||||
"update.prefs_note": "これは SolidCraft {version} ({kind}) です。確認では GitHub に最新リリースを問い合わせるだけで、ほかには何も送信しません。",
|
||||
"update.prefs_dev": "これは開発ビルド ({version}) のため、アップデートを確認しません。",
|
||||
"update.show": "{version} へのアップデートを表示",
|
||||
"update.prefs_flatpak": "このコピー ({version}) は Flatpak が更新します: {command} を実行するか、ソフトウェアセンターを使ってください。SolidCraft 自身は確認しません。",
|
||||
"update.update_now": "今すぐ更新",
|
||||
"update.install_msi": "インストール (SolidCraft を終了)",
|
||||
"update.download_open": "ダウンロードして開く",
|
||||
"update.downloading": "ダウンロードと検証中… {done} / {total} MB",
|
||||
"update.installed": "SolidCraft {version} をインストールしました。再起動すると使えます。",
|
||||
"update.restart": "今すぐ再起動",
|
||||
"update.restart_later": "後で",
|
||||
"update.installer_started": "SolidCraft の終了後にインストーラーが起動します。",
|
||||
"update.dmg_opened": "ディスクイメージを検証して開きました: SolidCraft をアプリケーションフォルダにドラッグして古いものを置き換え、SolidCraft を再起動してください。",
|
||||
"update.install_failed": "アップデートをインストールできませんでした: {error}。何も変更されていません。「ダウンロード」でパッケージを入手できます。"
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
{
|
||||
"Send your report on GitHub": "GitHub에서 보고서 보내기",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 아래에서 GitHub 초안을 열고 클립보드에 복사된 보고서를 붙여넣으세요.",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 아래에서 GitHub 초안을 여세요. 제목과 보고서가 자동으로 입력됩니다.",
|
||||
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. 스크린샷을 첨부하세요. 복사하여 붙여넣거나 PNG로 저장한 뒤 업로드하세요.",
|
||||
"3. Click Submit new issue on GitHub to send your report.": "3. GitHub에서 Submit new issue를 클릭하여 보고서를 보내세요.",
|
||||
"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.material_properties_directional": "방향별 탄성 계수",
|
||||
"cmd.material_properties_validate": "물리 재질 데이터 검증",
|
||||
"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_perforate": "타공",
|
||||
"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_add": "기록된 특성 추가",
|
||||
"cmd.timeline_compute_all": "모두 다시 계산",
|
||||
"cmd.timeline_copy": "특성 복사",
|
||||
"cmd.timeline_dependents": "특성 종속 항목",
|
||||
"cmd.timeline_export_script": "명령 스크립트 내보내기",
|
||||
"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": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다",
|
||||
"cmd.library_inspect": "로컬 라이브러리 확인",
|
||||
"cmd.library_export": "설계 라이브러리 내보내기",
|
||||
"cmd.library_apply": "로컬 라이브러리 항목 적용",
|
||||
"Thin": "얇은 벽",
|
||||
"Thin Extrude": "얇은 벽 돌출",
|
||||
"Wall Location": "벽 위치",
|
||||
"Wall Thickness": "벽 두께",
|
||||
"Thickness Outside": "바깥쪽 두께",
|
||||
"Inside": "안쪽",
|
||||
"Outside": "바깥쪽",
|
||||
"Profiles/Curves": "프로파일/곡선",
|
||||
"update.button": "업데이트",
|
||||
"update.button_tip": "SolidCraft {version} 사용 가능",
|
||||
"update.title": "업데이트 사용 가능",
|
||||
"update.available": "SolidCraft {version} 사용 가능",
|
||||
"update.you_have": "현재 버전은 {version}입니다.",
|
||||
"update.no_notes": "이 릴리스에는 릴리스 노트가 없습니다.",
|
||||
"update.system_hint": "이 사본은 {kind}입니다. 다음 명령으로 업데이트하세요:",
|
||||
"update.package_hint": "이 사본은 {kind}(으)로 설치되었습니다. 새 패키지를 내려받은 뒤 다음 명령으로 설치하세요:",
|
||||
"update.copy": "복사",
|
||||
"update.file": "내려받을 파일: {file}",
|
||||
"update.download": "다운로드",
|
||||
"update.release_page": "릴리스 페이지 열기",
|
||||
"update.skip": "이 버전 건너뛰기",
|
||||
"update.later": "나중에 알림",
|
||||
"update.prefs_label": "업데이트",
|
||||
"update.check_automatically": "업데이트 자동 확인",
|
||||
"update.check_now": "지금 확인",
|
||||
"update.checking": "확인 중…",
|
||||
"update.failed": "확인할 수 없습니다: {error}",
|
||||
"update.up_to_date": "SolidCraft가 최신 버전입니다.",
|
||||
"update.prefs_note": "SolidCraft {version} ({kind})입니다. 확인 시 GitHub에 최신 릴리스만 요청하며 다른 정보는 보내지 않습니다.",
|
||||
"update.prefs_dev": "개발 빌드({version})이므로 업데이트를 확인하지 않습니다.",
|
||||
"update.show": "{version} 업데이트 보기",
|
||||
"update.prefs_flatpak": "이 사본({version})은 Flatpak이 업데이트합니다: {command}를 실행하거나 소프트웨어 센터를 사용하세요. SolidCraft는 직접 확인하지 않습니다.",
|
||||
"update.update_now": "지금 업데이트",
|
||||
"update.install_msi": "설치 (SolidCraft 종료)",
|
||||
"update.download_open": "다운로드 후 열기",
|
||||
"update.downloading": "다운로드 및 확인 중… {done} / {total} MB",
|
||||
"update.installed": "SolidCraft {version}이(가) 설치되었습니다. 다시 시작하면 사용할 수 있습니다.",
|
||||
"update.restart": "지금 다시 시작",
|
||||
"update.restart_later": "나중에",
|
||||
"update.installer_started": "SolidCraft가 종료되면 설치 프로그램이 시작됩니다.",
|
||||
"update.dmg_opened": "디스크 이미지를 확인하고 열었습니다: SolidCraft를 응용 프로그램 폴더로 끌어 기존 버전을 바꾼 뒤 SolidCraft를 다시 시작하세요.",
|
||||
"update.install_failed": "업데이트를 설치하지 못했습니다: {error}. 아무것도 변경되지 않았습니다. 다운로드로 패키지를 받을 수 있습니다."
|
||||
}
|
||||
@@ -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": "Дуга по 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_rounded": "Скруглённый прямоугольник",
|
||||
"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": "Новый именованный вид",
|
||||
"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+щелчок тоже переключает. Проверьте предпросмотр и нажмите OK.",
|
||||
"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.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": "Очистить",
|
||||
"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.corner": "Угол",
|
||||
"ui.corners": "Углы",
|
||||
"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.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": "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": "Количество",
|
||||
"ui.quantity_2": "Количество 2",
|
||||
"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": "Интервал",
|
||||
"ui.spacing_2": "Интервал 2",
|
||||
"ui.sphere": "Сфера",
|
||||
"ui.spline": "Сплайн",
|
||||
"ui.split_body": "Разделить тело",
|
||||
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг 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.x_distance": "Расстояние X",
|
||||
"ui.y_distance": "Расстояние Y",
|
||||
"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.": "Открытая параметрическая 3D CAD на Rust. Лицензия 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 мм",
|
||||
"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": "выберите отверстие для установки, или OK разместит в начале координат",
|
||||
"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)": "Показывать указатель, которым двигается ИИ-агент (#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": "Текст следует по траектории: кривая задаёт его положение",
|
||||
"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,140 @@
|
||||
//! 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),
|
||||
app_bar_hover: Color32::from_rgba_unmultiplied_const(255, 255, 255, 28),
|
||||
app_bar_tab_edge: Color32::from_rgba_unmultiplied_const(255, 255, 255, 40),
|
||||
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> {
|
||||
@@ -181,12 +162,12 @@ impl Document {
|
||||
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
|
||||
// Face colours the body was imported with, under the design's own.
|
||||
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
let names = crate::naming::face_names(b);
|
||||
let mut out: Vec<(usize, Look)> = match b.body.paint() {
|
||||
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
|
||||
// Carried from an earlier version of the body: by face name.
|
||||
// Carried from an earlier version of the body: by face name (worked out only here:
|
||||
// the viewport asks every body for its face colours on every change).
|
||||
None => match &b.imported {
|
||||
Some(imp) => names
|
||||
Some(imp) => crate::naming::face_names(b)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
|
||||
@@ -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 {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::{Plane, Vec3};
|
||||
|
||||
use crate::{AxisRef, DocError, Document, FeatureKind, PatternKind, PlaneRef, Result};
|
||||
use crate::{DocError, Document, FeatureKind, PlaneRef, Result};
|
||||
|
||||
/// Column-major 4×4 affine transform (`m[col][row]`).
|
||||
pub type Mat = [[f64; 4]; 4];
|
||||
@@ -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)
|
||||
@@ -200,151 +204,16 @@ pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
|
||||
impl FeatureKind {
|
||||
/// Map the feature's world geometry (points, directions, explicit planes) by `m`.
|
||||
pub fn transform_geometry(&mut self, m: &Mat) {
|
||||
let pt = |p: &mut Vec3| *p = apply_point(m, *p);
|
||||
let dir = |v: &mut Vec3| *v = apply_vector(m, *v);
|
||||
match self {
|
||||
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Mirror { plane, .. } => {
|
||||
map_plane_ref(m, plane)
|
||||
}
|
||||
FeatureKind::ConstructionAxis { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
|
||||
FeatureKind::ConstructionPoint { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
|
||||
FeatureKind::Split { plane, tool, .. } => {
|
||||
map_plane_ref(m, plane);
|
||||
if let Some(t) = tool {
|
||||
pt(&mut t.point);
|
||||
}
|
||||
}
|
||||
FeatureKind::SplitFace { faces, plane, tool, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, plane);
|
||||
if let Some(t) = tool {
|
||||
pt(&mut t.point);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
pt(origin);
|
||||
dir(d);
|
||||
}
|
||||
FeatureKind::Fillet { edges, style, .. } => {
|
||||
edges.iter_mut().for_each(pt);
|
||||
if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style {
|
||||
pt(start);
|
||||
}
|
||||
}
|
||||
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::Draft { faces, neutral, pull, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, neutral);
|
||||
dir(pull);
|
||||
}
|
||||
FeatureKind::Box { corner, .. } => map_point_expr(m, corner),
|
||||
FeatureKind::Cylinder { base, axis, .. } => {
|
||||
map_point_expr(m, base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => map_point_expr(m, center),
|
||||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { dir1, dir2, .. }, .. } => {
|
||||
dir(dir1);
|
||||
if let Some(d) = dir2 {
|
||||
dir(d);
|
||||
}
|
||||
}
|
||||
FeatureKind::Pattern { pattern: PatternKind::Circular { origin, axis, .. }, .. } => {
|
||||
pt(origin);
|
||||
dir(axis);
|
||||
}
|
||||
// Follows its sketch.
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { .. }, .. } => {}
|
||||
FeatureKind::Hole { position, direction, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::SheetFlange { edges, .. } => edges.iter_mut().for_each(|e| pt(&mut e.point)),
|
||||
FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::SheetConvert { face, bends, .. } => {
|
||||
pt(face);
|
||||
bends.iter_mut().for_each(pt);
|
||||
}
|
||||
FeatureKind::SheetFold { a, b, fixed, .. } => {
|
||||
pt(a);
|
||||
pt(b);
|
||||
if let Some(f) = fixed {
|
||||
pt(f);
|
||||
}
|
||||
}
|
||||
FeatureKind::Boss { position, direction, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Lip { face, .. } => pt(face),
|
||||
FeatureKind::BoundaryFill { cells, .. } => cells.iter_mut().for_each(pt),
|
||||
FeatureKind::Rest { position, direction, along, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
if let Some(a) = along {
|
||||
dir(a);
|
||||
}
|
||||
}
|
||||
FeatureKind::SnapFit { position, direction, hook, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
dir(hook);
|
||||
}
|
||||
FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetUnfold { .. } => {}
|
||||
FeatureKind::Coil { base, axis, .. } => {
|
||||
map_point_expr(m, base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, target);
|
||||
}
|
||||
FeatureKind::Align { from, to, from_normal, to_normal, .. } => {
|
||||
pt(from);
|
||||
pt(to);
|
||||
if let Some(n) = from_normal {
|
||||
dir(n);
|
||||
}
|
||||
if let Some(n) = to_normal {
|
||||
dir(n);
|
||||
}
|
||||
}
|
||||
FeatureKind::Scale { origin, .. } => pt(origin),
|
||||
FeatureKind::Move { rotate_axis, .. } => {
|
||||
if let Some(a) = rotate_axis {
|
||||
dir(a);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { .. }
|
||||
| FeatureKind::Extrude { .. }
|
||||
| FeatureKind::SurfaceExtrude { .. }
|
||||
| FeatureKind::Combine { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::BoundingSolid { .. }
|
||||
| FeatureKind::Pipe { .. }
|
||||
| FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::Remove { .. }
|
||||
| FeatureKind::Stitch { .. }
|
||||
| FeatureKind::Thicken { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
| FeatureKind::MeshImport { .. }
|
||||
| FeatureKind::MeshConvert { .. }
|
||||
| FeatureKind::BodyTransfer { .. } => {}
|
||||
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
|
||||
map_plane_ref(m, plane);
|
||||
if let Some(t) = tool {
|
||||
pt(&mut t.point);
|
||||
}
|
||||
pt(keep);
|
||||
}
|
||||
}
|
||||
use crate::feature_geometry::Geo;
|
||||
self.walk_geometry_mut(&mut |g| match g {
|
||||
Geo::Pick(p) | Geo::Pos(p) => *p = apply_point(m, *p),
|
||||
Geo::Dir(v) => *v = apply_vector(m, *v),
|
||||
Geo::Plane(p) => map_plane_ref(m, p),
|
||||
Geo::Construct(r) => map_geo_ref(m, r),
|
||||
Geo::Placement(e) => map_point_expr(m, e),
|
||||
// An Emboss cylinder is picked in its component's frame (`EmbossFace`).
|
||||
Geo::Pinned(_) | Geo::Profiles(..) => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,13 +239,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);
|
||||
@@ -492,7 +366,7 @@ impl Document {
|
||||
return Err(DocError::Invalid("too many occurrences".into()));
|
||||
}
|
||||
let id = self.next_id.max(1);
|
||||
self.next_id = id + 1;
|
||||
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
|
||||
let cname = self.components.iter().find(|c| c.id == component).map(|c| c.name.clone()).unwrap_or_default();
|
||||
let k = self.occurrences.iter().filter(|o| o.component == component).count() + 1;
|
||||
self.occurrences.push(crate::Occurrence { id, component, parent, name: format!("{cname}:{k}"), transform, grounded: false });
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -3,81 +3,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{FeatureKind, PatternKind, PlaneRef};
|
||||
|
||||
fn plane_names(p: &mut PlaneRef, names: &BTreeMap<String, String>) {
|
||||
let mut p = p;
|
||||
for _ in 0..64 {
|
||||
match p {
|
||||
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base,
|
||||
PlaneRef::Construction { name } => {
|
||||
if let Some(n) = names.get(name) {
|
||||
*name = n.clone();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Point references to sketches (by id) and features (by name) at their copies.
|
||||
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
|
||||
let sk = |id: &mut u64| {
|
||||
if let Some(n) = sketches.get(id) {
|
||||
*id = *n;
|
||||
}
|
||||
};
|
||||
let feats = |list: &mut Vec<String>| {
|
||||
for x in list.iter_mut() {
|
||||
if let Some(n) = names.get(x) {
|
||||
*x = n.clone();
|
||||
}
|
||||
}
|
||||
};
|
||||
match self {
|
||||
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Split { plane, .. } => {
|
||||
plane_names(plane, names)
|
||||
}
|
||||
FeatureKind::SplitFace { plane, sketch, .. } => {
|
||||
plane_names(plane, names);
|
||||
if let Some(s) = sketch {
|
||||
sk(s);
|
||||
}
|
||||
}
|
||||
FeatureKind::Extrude { sketch, .. }
|
||||
| FeatureKind::SurfaceExtrude { 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, .. } => {
|
||||
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::Hole { points: Some(p), .. } => sk(&mut p.sketch),
|
||||
FeatureKind::Pattern { features, pattern, .. } => {
|
||||
feats(features);
|
||||
if let PatternKind::Path { path_sketch, .. } = pattern {
|
||||
sk(path_sketch);
|
||||
}
|
||||
}
|
||||
FeatureKind::Mirror { features, plane, .. } => {
|
||||
feats(features);
|
||||
plane_names(plane, names);
|
||||
}
|
||||
FeatureKind::Draft { neutral, .. } => plane_names(neutral, names),
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_names(target, names),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::{FeatureKind, PlaneRef};
|
||||
|
||||
/// Remap persistent topology tokens without rewriting arbitrary user expressions or labels.
|
||||
pub fn remap_topology(name: &str, ids: &BTreeMap<u64, u64>, bodies: &BTreeMap<String, String>) -> String {
|
||||
@@ -244,89 +170,6 @@ fn remap_link(
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Point the body names a feature acts on (targets, tools, the body it rounds…) at the
|
||||
/// copies in `bodies`; names not in the map stay as they are.
|
||||
pub fn remap_bodies(&mut self, bodies: &BTreeMap<String, String>) {
|
||||
use FeatureKind as K;
|
||||
match self {
|
||||
K::Emboss { face: Some(face), .. } => mapped_name(&mut face.body, bodies),
|
||||
K::Split { tool: Some(tool), .. } | K::SplitFace { tool: Some(tool), .. } | K::SurfaceTrim { tool: Some(tool), .. } => {
|
||||
mapped_name(&mut tool.body, bodies)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match self {
|
||||
K::Extrude { targets, .. }
|
||||
| K::Revolve { targets, .. }
|
||||
| K::Loft { targets, .. }
|
||||
| K::Sweep { targets, .. }
|
||||
| K::Pipe { targets, .. }
|
||||
| K::Emboss { targets, .. } => {
|
||||
for n in targets {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::Combine { target, tools, .. } => {
|
||||
mapped_name(target, bodies);
|
||||
for n in tools {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::Pattern { bodies: list, .. }
|
||||
| K::Mirror { bodies: list, .. }
|
||||
| K::Scale { bodies: list, .. }
|
||||
| K::BoundingSolid { bodies: list, .. }
|
||||
| K::Align { bodies: list, .. }
|
||||
| K::Remove { bodies: list }
|
||||
| K::Stitch { bodies: list, .. }
|
||||
| K::Move { bodies: list, .. }
|
||||
| K::BodyTransfer { bodies: list, .. } => {
|
||||
for n in list {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::Thicken { bodies: list, targets, .. } => {
|
||||
for n in list.iter_mut().chain(targets) {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::BoundaryFill { tools, .. } => {
|
||||
for n in tools {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::Fillet { body, .. }
|
||||
| K::Chamfer { body, .. }
|
||||
| K::Shell { body, .. }
|
||||
| K::Draft { body, .. }
|
||||
| K::SplitFace { body, .. }
|
||||
| K::OffsetFace { body, .. }
|
||||
| K::DeleteFace { body, .. }
|
||||
| K::ReplaceFace { body, .. }
|
||||
| K::SheetFlange { body, .. }
|
||||
| K::SheetHem { body, .. }
|
||||
| K::SheetFold { body, .. }
|
||||
| K::SheetUnfold { body, .. }
|
||||
| K::Boss { body, .. }
|
||||
| K::Lip { body, .. }
|
||||
| K::SnapFit { body, .. }
|
||||
| K::Rest { body, .. }
|
||||
| K::Patch { body, .. } => {
|
||||
if let Some(n) = body {
|
||||
mapped_name(n, bodies);
|
||||
}
|
||||
}
|
||||
K::Split { body, .. }
|
||||
| K::SheetConvert { body, .. }
|
||||
| K::SurfaceExtend { body, .. }
|
||||
| K::SurfaceTrim { body, .. }
|
||||
| K::MeshConvert { body, .. } => mapped_name(body, bodies),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Feature {
|
||||
/// Clone identities through typed reference fields. Sketch-local entity IDs stay local.
|
||||
pub fn remap_clone(
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -118,6 +118,7 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"2"})).unwrap(),
|
||||
@@ -280,6 +281,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
@@ -429,6 +431,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
@@ -553,6 +556,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
|
||||
let material = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
|
||||