Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29b0d2f13 | ||
|
|
480fd8d517 | ||
|
|
4c09fa5355 | ||
|
|
626fe6fdee | ||
|
|
01f2c78cd1 | ||
|
|
9ca7d307a4 | ||
|
|
2f91338d3d | ||
|
|
dfbf7dd6a8 | ||
|
|
cf9d7772f1 | ||
|
|
6e84c03946 | ||
|
|
3669d4b3d0 | ||
|
|
345c0882cd | ||
|
|
18347b3d0d | ||
|
|
ac84491638 | ||
|
|
6a67cc8f1e | ||
|
|
6cb3025b72 | ||
|
|
b34d45233d | ||
|
|
47a7f29485 | ||
|
|
9c51f9d433 | ||
|
|
4687e3b073 | ||
|
|
1ff16cc72b | ||
|
|
813bee8914 | ||
|
|
482abb4562 | ||
|
|
7bed28752e | ||
|
|
ea23401710 | ||
|
|
9f76cca8b4 | ||
|
|
d5ff5ea1a5 | ||
|
|
ce902be66c | ||
|
|
63095fedb3 | ||
|
|
00fdcf3e72 | ||
|
|
774a8b1d9a | ||
|
|
97ff7578c2 | ||
|
|
19bf854dc9 | ||
|
|
f204042bf2 | ||
|
|
4bea4c7785 | ||
|
|
09b5da3e9f | ||
|
|
a2c7396160 | ||
|
|
45f92277cb | ||
|
|
587cf865b8 | ||
|
|
c1c813e572 | ||
|
|
7498e3b367 | ||
|
|
f037c92370 | ||
|
|
2b70ed7b72 | ||
|
|
15b5b51bf5 | ||
|
|
abddd67d22 | ||
|
|
78d9aa0153 | ||
|
|
17db109c5d | ||
|
|
d2e409f40c | ||
|
|
8746376ee9 | ||
|
|
5808bcc0f0 | ||
|
|
d23593d3eb | ||
|
|
897b7d93b5 | ||
|
|
7c85970bfc | ||
|
|
8fb5742706 | ||
|
|
aba57f1ca2 | ||
|
|
57d738eac4 | ||
|
|
3f1c023563 | ||
|
|
7da7bbb335 | ||
|
|
2eb292dd4a | ||
|
|
b41ad23e3f | ||
|
|
2dcfea8df7 | ||
|
|
7135407268 | ||
|
|
5f7ce80a93 | ||
|
|
fdaf57daa3 | ||
|
|
7eac1de9ee | ||
|
|
fe07f79387 | ||
|
|
b52d4390a0 | ||
|
|
c616f50bd0 | ||
|
|
6c61442bb2 | ||
|
|
c333889fc0 | ||
|
|
648f0ff768 | ||
|
|
eaab205302 | ||
|
|
eea49922bd | ||
|
|
dd7790a43f | ||
|
|
fe3717f2e4 | ||
|
|
d9237d004e | ||
|
|
d8d54ded0d | ||
|
|
95d394dcab | ||
|
|
7ee351dc91 | ||
|
|
9297f81501 | ||
|
|
2afc10fbd5 | ||
|
|
5c98c1e24a | ||
|
|
0040ce6bf2 | ||
|
|
1f7f37d1ef | ||
|
|
96f6424aec | ||
|
|
3d3f9b626f | ||
|
|
c3ebe13991 | ||
|
|
b69cd2d0b7 | ||
|
|
753b0a2eab | ||
|
|
b702f0efde | ||
|
|
152418b624 | ||
|
|
e99b3bd4e8 | ||
|
|
8319c35a32 | ||
|
|
c3e0ea498a | ||
|
|
b6e3a6c366 | ||
|
|
4d8c80a86d | ||
|
|
ec5d7f9789 | ||
|
|
69050c81d9 | ||
|
|
53238fba5f | ||
|
|
5d661fa2af | ||
|
|
ff7a347013 | ||
|
|
534393ed5a | ||
|
|
200a83a629 | ||
|
|
d302aa1a00 | ||
|
|
9184994721 | ||
|
|
d2676da789 | ||
|
|
6a6a7e2865 | ||
|
|
3ea8562c5e | ||
|
|
e540832626 | ||
|
|
8dd583930e | ||
|
|
9bb18842e9 | ||
|
|
9cabc3532d | ||
|
|
bdcfe3ff1d | ||
|
|
dbfb689f6c | ||
|
|
17b4b9a32c | ||
|
|
c8d668108a | ||
|
|
dd084ec9e1 | ||
|
|
bfbf5f9432 | ||
|
|
facff1050e | ||
|
|
de1479029d | ||
|
|
16766bdbc7 | ||
|
|
d4fd760b0d | ||
|
|
d437c21368 | ||
|
|
a8b3b21fd0 | ||
|
|
be55a49b04 | ||
|
|
293d660d6c | ||
|
|
beca7eb126 | ||
|
|
1a0100cdd9 | ||
|
|
5267c96ab2 | ||
|
|
159f9913c1 | ||
|
|
dfa9e094cd | ||
|
|
8cdefd4cd5 | ||
|
|
7b0a1b296c | ||
|
|
25a3dc501b | ||
|
|
a3dfbaa0b0 | ||
|
|
58af8accfa | ||
|
|
4a989f6e2d | ||
|
|
aae70053ee | ||
|
|
9b5e39844a | ||
|
|
1d30e4e1d2 | ||
|
|
1a5e961362 | ||
|
|
ff609bc320 | ||
|
|
62a7f903f2 | ||
|
|
11c03fb0ab | ||
|
|
6f4a9d96fb | ||
|
|
4112189551 | ||
|
|
2bb24d1d2a | ||
|
|
a1cb8d4d36 | ||
|
|
a513304917 | ||
|
|
d7f5b2435e | ||
|
|
ce5762d873 | ||
|
|
831fa34752 | ||
|
|
11084a5963 | ||
|
|
2c4d197bf4 | ||
|
|
4ff8f3e14d | ||
|
|
eac4d66adc | ||
|
|
cb071bb93b | ||
|
|
0e500cacd3 | ||
|
|
1e4387f3ea | ||
|
|
9ba72e118b | ||
|
|
add4a18961 | ||
|
|
17e42e7b94 | ||
|
|
812669e861 | ||
|
|
c166c64b8e | ||
|
|
0b1e61504d | ||
|
|
7249623855 | ||
|
|
7b34d57b25 | ||
|
|
aa24d9f071 | ||
|
|
319d360c85 | ||
|
|
e10b0b7d22 | ||
|
|
d1a773a684 | ||
|
|
fe1e5f1b95 | ||
|
|
1e18ca9a46 | ||
|
|
5ef1dcba8e | ||
|
|
e30f3c9ebe | ||
|
|
738d532ce6 | ||
|
|
434fbc2d73 | ||
|
|
302dd7e343 | ||
|
|
d2086835c1 |
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)
|
||||
|
||||
@@ -5,68 +5,96 @@ 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.8:** this is a minor bump because the design file format moved to version 16, and
|
||||
**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 far more faces.** Sketches, offset planes and press-pull now attach to
|
||||
Cut, Hole and chamfer faces, each pattern or mirror copy, side walls a Join merged and the
|
||||
pieces a later feature split a face into
|
||||
([#625](https://github.com/bherbruck/solidcraft/pull/625)), to moved or pasted components
|
||||
([#610](https://github.com/bherbruck/solidcraft/pull/610)) and beside long thin holes
|
||||
([#629](https://github.com/bherbruck/solidcraft/pull/629)). Across the test models, flat-face
|
||||
actions pass 70% of the time, up from 61% (sketch 52% → 71%, offset plane 52% → 71%,
|
||||
press-pull 41% → 60%).
|
||||
- **Fillet and chamfer** handle a single edge whose ends meet curved or slanted faces, and blend
|
||||
smooth chains of edges ([#646](https://github.com/bherbruck/solidcraft/pull/646)): 83% → 93%
|
||||
of edges.
|
||||
- **New:** user-defined physical materials stored in the design, linked components (insert
|
||||
another design), saved body visibility, named views through `view.apply`, sketch text fonts,
|
||||
corner reliefs, flanges on flanges, edge flanges that trim the base along their edge, grouped
|
||||
sketch tools, Home thumbnails, and an improved Select Other list.
|
||||
- **Fixes:** filleting beside a modeled thread no longer freezes the app, a loft standing on a
|
||||
face fuses with it, and many sheet metal, hole and sketch fixes.
|
||||
- **Booleans are checked.** A Boolean retry that would change the part away from the tool is now
|
||||
refused instead of returning wrong geometry, which fixes stray cuts
|
||||
([#616](https://github.com/bherbruck/solidcraft/issues/616)). MCP feature replies include
|
||||
warnings.
|
||||
- **Opening a design** keeps the window draggable, shows progress, and can be cancelled with
|
||||
Cancel or Esc.
|
||||
- **Under the hood:** compact MCP replies, a kernel-neutral native B-rep format, and an opt-in
|
||||
preview from the native cache while a design rebuilds.
|
||||
- **Quality:** every ordinary action is run on every face, edge, vertex and body of the test
|
||||
models; CI blocks regressions and the full matrix runs nightly.
|
||||
- **Security:** private vulnerability reporting is on; see
|
||||
[SECURITY.md](https://github.com/bherbruck/solidcraft/blob/main/SECURITY.md).
|
||||
- **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 16.** Designs are now format 16 (`solidcraft/16`). SolidCraft 0.7 and older can't
|
||||
open designs saved by 0.8, as a container or as JSON; keep a copy of a file you may still open
|
||||
in 0.7. Files from every earlier version still open in 0.8.
|
||||
**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.
|
||||
|
||||
**Linux app ID.** The Linux app ID is now `ai.storyteller.solidcraft` (it was
|
||||
`ai.storyteller.solvecraft`). Flatpak users from the SolidCraft feed: `flatpak update` reports
|
||||
the old app as end-of-life and asks to replace it; say yes, and your settings come along. The
|
||||
`.deb`, `.rpm` and AppImage remove the old menu entry on upgrade or first launch.
|
||||
**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 faces still can't take an attached sketch: after Revolve, Offset Face or Boss
|
||||
([#494](https://github.com/bherbruck/solidcraft/issues/494)), sheet metal thickness faces
|
||||
([#495](https://github.com/bherbruck/solidcraft/issues/495)), Loft, Sweep and Coil faces
|
||||
([#561](https://github.com/bherbruck/solidcraft/issues/561)), faces after a Draft or a tapered
|
||||
Extrude ([#563](https://github.com/bherbruck/solidcraft/issues/563)) and a Shell's rim
|
||||
([#564](https://github.com/bherbruck/solidcraft/issues/564)).
|
||||
- A Hole on a sheet metal face is refused
|
||||
([#560](https://github.com/bherbruck/solidcraft/issues/560)); some through-all holes along a
|
||||
wall or beside curved faces still fail
|
||||
([#559](https://github.com/bherbruck/solidcraft/issues/559)).
|
||||
- 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)).
|
||||
- Cutting a coaxial cylinder from a thin disc can lose a little volume
|
||||
([#500](https://github.com/bherbruck/solidcraft/issues/500)); an edge flange with different
|
||||
- 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).
|
||||
|
||||
**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
|
||||
menu entry.
|
||||
|
||||
**Upgrading from 0.5 or earlier.** 0.6 renamed SolveCraft to SolidCraft and made the zip
|
||||
container the default save format. `.solvecraft` designs, old settings folders and
|
||||
`SOLVECRAFT_*` variables are still picked up, and `solvecraft-cli` still forwards to
|
||||
|
||||
@@ -21,8 +21,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
# Every push to main gets its full run; a new PR push replaces the old run.
|
||||
# A new PR push replaces the old run. Only one run per group may wait, so a busy main would
|
||||
# cancel the waiting nightly (or a Run workflow) before it starts: they get groups of their own.
|
||||
group: ci-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
@@ -47,6 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -59,12 +61,14 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
native=true
|
||||
else
|
||||
# Compare both old and new paths of renames; unknown paths run native checks.
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
case "$path" in
|
||||
.github/workflows/ci.yml) native=true ;;
|
||||
*.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;;
|
||||
@@ -74,6 +78,7 @@ jobs:
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
fi
|
||||
echo "native=$native" >> "$GITHUB_OUTPUT"
|
||||
echo "kernel=$kernel" >> "$GITHUB_OUTPUT"
|
||||
|
||||
checks:
|
||||
name: Format and repository checks
|
||||
@@ -95,6 +100,8 @@ jobs:
|
||||
run: cargo xtask licences --check
|
||||
- name: Dependency layers
|
||||
run: cargo xtask layers
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
run: cargo xtask web-offline-test
|
||||
|
||||
clippy:
|
||||
name: Native Linux Clippy
|
||||
@@ -150,7 +157,7 @@ jobs:
|
||||
|
||||
tests:
|
||||
name: Native Linux tests (${{ matrix.shard }}/4)
|
||||
needs: build-tests
|
||||
needs: [changes, build-tests]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -177,6 +184,16 @@ jobs:
|
||||
# the build job's path, which is the same checkout path here.
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --partition "slice:$SHARD/4" "${all[@]}"
|
||||
- name: Robustness goldens sample (pull requests that touch the kernel)
|
||||
# Every 8th seed of tests/robustness/goldens.json, a quarter per shard; pushes to main and
|
||||
# the nightly run every seed in the step above.
|
||||
if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true'
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --ignore-default-filter \
|
||||
-E "package(solidcraft-engine) & test(=tests::robustness_goldens::sample_$((SHARD - 1)))"
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
@@ -269,9 +286,31 @@ jobs:
|
||||
name: oracle-report
|
||||
path: docs/oracle.md
|
||||
|
||||
# D3D12's older shader compiler (FXC, used where dxcompiler.dll is absent) is stricter than
|
||||
# DXC, Vulkan and Metal: a shader it cannot compile once crashed the app at startup. Build the
|
||||
# GPU tessellation pipeline with FXC on the runner's WARP software adapter and compare its
|
||||
# results with the CPU.
|
||||
windows-fxc:
|
||||
name: Windows D3D12 FXC shaders
|
||||
needs: changes
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
WGPU_BACKEND: dx12
|
||||
WGPU_DX12_COMPILER: fxc
|
||||
SOLIDCRAFT_REQUIRE_GPU: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-fxc
|
||||
- run: cargo test -p solidcraft-gpu --profile ci --locked
|
||||
|
||||
# Preserve the existing check name for branch protection, including docs-only PRs.
|
||||
ci:
|
||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle]
|
||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@@ -285,6 +324,7 @@ jobs:
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||
run: |
|
||||
test "$CHANGES" = success
|
||||
test "$CHECKS" = success
|
||||
@@ -294,6 +334,7 @@ jobs:
|
||||
test "$TESTS" = success
|
||||
test "$DOCS" = success
|
||||
test "$ORACLE" = success
|
||||
test "$WINDOWS_FXC" = success
|
||||
fi
|
||||
|
||||
wasm:
|
||||
|
||||
@@ -6,9 +6,9 @@ name: Packaging lint
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: [".github/workflows/**", "packaging/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
pull_request:
|
||||
paths: [".github/workflows/**", "packaging/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
}
|
||||
- name: XML, desktop entry, AppStream, Flatpak manifest
|
||||
run: |
|
||||
xmllint --noout packaging/windows/solidcraft.wxs packaging/windows/solidcraft.en-us.wxl packaging/macos/*.plist* packaging/linux/*.xml*
|
||||
desktop-file-validate packaging/linux/ai.storyteller.solidcraft.desktop
|
||||
xmllint --noout assets/platforms/windows/solidcraft.wxs assets/platforms/windows/solidcraft.en-us.wxl assets/platforms/macos/*.plist* assets/platforms/linux/*.xml*
|
||||
desktop-file-validate assets/platforms/linux/ai.storyteller.solidcraft.desktop
|
||||
# The window's X11 class is its app id; the old id stays only as the rename shims.
|
||||
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' packaging/linux/ai.storyteller.solidcraft.desktop
|
||||
grep -q '<id>ai.storyteller.solvecraft</id>' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in
|
||||
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' assets/platforms/linux/ai.storyteller.solidcraft.desktop
|
||||
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in
|
||||
grep -qF -e '--end-of-life-rebase=' packaging/flatpak-site.sh
|
||||
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solidcraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solidcraft' and m['command'] == 'solidcraft'"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
|
||||
|
||||
@@ -50,6 +50,8 @@ jobs:
|
||||
- name: Build the web app
|
||||
working-directory: apps/solidcraft-web
|
||||
run: trunk build --release --public-url /solidcraft/app/
|
||||
- name: Test the web app offline and across an update (headless Chrome)
|
||||
run: cargo xtask web-offline-test --site dist/web
|
||||
- name: Assemble the site
|
||||
run: |
|
||||
mkdir -p site
|
||||
|
||||
@@ -59,8 +59,8 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
|
||||
operations are our own.
|
||||
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
|
||||
artwork from commercial CAD products — ever.** Every icon is drawn in code
|
||||
(`crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||
artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code
|
||||
(`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
|
||||
Breaking this rule is the most serious mistake you can make in this repo.
|
||||
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
|
||||
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.start(id)`. Colours come from `theme::Tokens`.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
|
||||
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
|
||||
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
|
||||
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
|
||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||
assets, layers). Commit after every feature arc that builds.
|
||||
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
|
||||
@@ -122,7 +125,10 @@ before the draft is verified.
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
|
||||
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
|
||||
request on a connection carries the launch's token as `"token"`: read it from
|
||||
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
|
||||
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
|
||||
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
|
||||
|
||||
@@ -8,45 +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.6.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo-0.6.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -68,6 +67,45 @@ Generated-in-code assets are original and have no file to list:
|
||||
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
|
||||
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/icons/app/linux/exports/hicolor/128x128/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/16x16/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/24x24/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/glyphs.rs | SolidCraft / VectorCraft contributors | Original glyphs; caption geometry adapted from VectorCraft titlebar.rs (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/icons.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/markers.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
|
||||
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
| SolidCraft file | Adapted from | Licence |
|
||||
@@ -84,6 +122,16 @@ Generated-in-code assets are original and have no file to list:
|
||||
| LICENSE-MIT, LICENSE-APACHE (licence text) | CADCraft | — |
|
||||
| .github/workflows/release.yml, .github/workflows/packaging-lint.yml and packaging/ (release pipeline: Linux AppImage/deb/rpm/Flatpak/tar.gz, FreeBSD, macOS dmg, Windows MSI/portable zip, web zip; AppRun, nfpm, Flatpak, WiX and Info.plist templates) | PhotoCraft (https://github.com/storytold/photocraft) `.github/workflows/{release,packaging-lint}.yml`, `packaging/` | Apache-2.0 |
|
||||
|
||||
## Incremental HDR codec adapter
|
||||
|
||||
`crates/render/src/lighting/hdr.rs` adapts the scanline/component/old-RLE decoding
|
||||
state machines from the Radiance decoder in `image` 0.25.10
|
||||
([source](https://docs.rs/crate/image/0.25.10/source/src/codecs/hdr/decoder.rs)),
|
||||
MIT OR Apache-2.0. Metadata parsing still uses that library. The adapter makes row
|
||||
work cooperative and adds checked run lengths, zero-run rejection, width validation,
|
||||
header/input/radiance bounds and cancellation. Original licence texts are retained
|
||||
in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
|
||||
|
||||
## Recovered contributions
|
||||
|
||||
| SolidCraft file | Original contribution | Licence |
|
||||
@@ -94,7 +142,7 @@ Generated-in-code assets are original and have no file to list:
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
|
||||
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro (vendor/README.md) |
|
||||
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
|
||||
|
||||
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.start(id)`. Colours come from `theme::Tokens`.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
|
||||
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
|
||||
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
|
||||
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
|
||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||
assets, layers). Commit after every feature arc that builds.
|
||||
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
|
||||
@@ -99,7 +102,10 @@ before the draft is verified.
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
|
||||
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
|
||||
request on a connection carries the launch's token as `"token"`: read it from
|
||||
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
|
||||
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
|
||||
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
|
||||
|
||||
@@ -161,7 +161,8 @@ Pull requests skip a few heavy tests (one to four minutes each), which run on ev
|
||||
`main` and nightly; check the `main` run after merging. They are listed in
|
||||
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
`tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`
|
||||
|
||||
@@ -3281,11 +3281,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"embed-resource",
|
||||
"getrandom 0.3.4",
|
||||
"hidapi",
|
||||
"log",
|
||||
"quick-xml",
|
||||
@@ -3298,7 +3299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3307,7 +3308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3322,7 +3323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3338,16 +3339,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
"pollster 0.4.0",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"thiserror 2.0.21",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@@ -3362,12 +3376,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
"truck-modeling",
|
||||
@@ -3378,7 +3394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3386,7 +3402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -3396,7 +3412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3407,22 +3423,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"image",
|
||||
"log",
|
||||
"pollster 0.4.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"ttf-parser",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4845,9 +4863,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -18,6 +18,7 @@ solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
solidcraft-engine = { path = "crates/engine" }
|
||||
@@ -37,6 +38,10 @@ truck-shapeops = { version = "0.4", default-features = false }
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
|
||||
truck-stepio = { version = "0.3", default-features = false }
|
||||
truck-topology = "0.6"
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
|
||||
# tree through truck-meshalgo.
|
||||
spade = { version = "2.15", default-features = false, features = ["std"] }
|
||||
rayon = "1.12"
|
||||
|
||||
# packages (3MF)
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
@@ -46,7 +51,7 @@ quick-xml = "0.41"
|
||||
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
|
||||
|
||||
# images
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
|
||||
|
||||
# ui
|
||||
egui = "0.36"
|
||||
@@ -54,6 +59,10 @@ eframe = { version = "0.36", default-features = false, features = ["persistence"
|
||||
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
|
||||
web-time = "1"
|
||||
egui-wgpu = "0.36"
|
||||
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
|
||||
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
|
||||
naga = { version = "30", default-features = false }
|
||||
pollster = "0.4"
|
||||
rfd = "0.17"
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -19,6 +19,11 @@ licensed MIT OR Apache-2.0; ATTRIBUTION.md lists the files.
|
||||
The release packaging (.github/workflows/release.yml, packaging/) is adapted from PhotoCraft
|
||||
(https://github.com/storytold/photocraft), licensed under the Apache License 2.0.
|
||||
|
||||
The incremental Radiance RGBE row adapter in crates/render/src/lighting/hdr.rs adapts
|
||||
decoding state machines from image 0.25.10 (https://github.com/image-rs/image),
|
||||
MIT OR Apache-2.0. ATTRIBUTION.md describes the bounded/cancellable changes; the
|
||||
original licence texts are included in THIRD-PARTY-LICENSES.txt.
|
||||
|
||||
Most bundled assets are drawn in code or contributed originally; third-party fonts (Liberation Sans
|
||||
for sketch text, BIZ UDPGothic for web Japanese display) are licensed under SIL OFL 1.1. The
|
||||
per-file list is in ATTRIBUTION.md.
|
||||
|
||||
@@ -6,9 +6,9 @@ Sketch, constrain, and build solids with a parametric feature timeline. Change a
|
||||
and watch your model rebuild. Run it on your desktop, in your browser, or let AI agents
|
||||
model through the built-in MCP server.
|
||||
|
||||

|
||||

|
||||
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots and a colour, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.6.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
|
||||
## Download
|
||||
|
||||
@@ -68,6 +68,16 @@ are listed by area in [docs/features.md](docs/features.md); status and plans are
|
||||
Open work and known bugs are in [GitHub issues](https://github.com/bherbruck/solidcraft/issues),
|
||||
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Built with SolidCraft
|
||||
|
||||
<a href="https://github.com/0xlillith/solidcraft-rover"><img src="docs/screenshots/rover-build.gif" alt="SELENE, a scientific rover by 0xlillith, assembling itself as the timeline plays through its 3,323 steps in SolidCraft: tub, wheels, electronics, suspension, sampling arm, solar wings, dish and camera turret" loading="lazy"></a>
|
||||
|
||||
*SELENE, a scientific rover built entirely through SolidCraft's MCP server by
|
||||
[@0xlillith](https://github.com/0xlillith/solidcraft-rover): 3,323 editable timeline steps that
|
||||
rebuild with no errors, replayed here at 2× speed.
|
||||
[Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/rover-build.mp4)
|
||||
· [download the editable model](https://github.com/0xlillith/solidcraft-rover/releases/tag/v1.0.0).*
|
||||
|
||||
## What works
|
||||
|
||||
- **Sketches** on origin planes, offset planes or planar faces: lines, rectangles, circles,
|
||||
@@ -91,7 +101,7 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
by parameters. Over-constraining edits are rejected; fully constrained geometry turns black.
|
||||
Dragging solves the sketch every frame and rebuilds the model once, when the drag ends.
|
||||
Select a line or its two endpoints, type its dimension and press Enter to commit one undo
|
||||
step. Fit keeps sketch geometry clear of the palette and navigation controls.
|
||||
step; with Sketch Dimension it's D, click, type, Enter. Fit keeps sketch geometry clear of the palette and navigation controls.
|
||||
- **Profiles** found automatically, including regions split by lines and holes inside outlines.
|
||||
Newly captured complete adaptive boundaries retain their outer and hole source identities
|
||||
when spline span counts change. Partial or ambiguous boundaries do not qualify.
|
||||
@@ -115,7 +125,8 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
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;
|
||||
@@ -189,7 +200,9 @@ stdio. Headless it runs its own design session; with `--connect 127.0.0.1:PORT`
|
||||
started with `solidcraft --control PORT`, so you can watch the agent model. Tools:
|
||||
`list_commands`, `execute`, `batch`, `inspect_design`, `measure`, `body_topology`,
|
||||
`set_parameter`, `screenshot` (returns the image), `export`, `undo`, `redo`, `new_design`, `open`,
|
||||
`save`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
|
||||
`save`. Tool calls reach files only in the open design's folder (when it lies inside your home
|
||||
folder, never home itself or a hidden folder), SolidCraft's recovery folder and folders given with
|
||||
`--allow-path DIR`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
|
||||
configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -36,7 +36,7 @@ In scope, in priority order:
|
||||
metal rules (thickness, K-factor, bend radius) (M10).
|
||||
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
|
||||
|
||||
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need, PCB / 3D
|
||||
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
|
||||
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
## Milestones
|
||||
@@ -47,18 +47,48 @@ 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, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
||||
| M8 | Surface workspace | 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 |
|
||||
| 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) chosen by the rule | in progress | 36 |
|
||||
| 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; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| M11 | 2D drawings | deferred | — |
|
||||
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
|
||||
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
|
||||
|
||||
## Physical material data
|
||||
|
||||
- Physical material card validation (#923/#922) provides typed SI mechanical and
|
||||
thermal data, per-property sources/conditions and isotropic/orthotropic stability
|
||||
checks through read-only `material.properties.validate`. It does not assign or
|
||||
persist properties, provide a solver or enable rendering; see
|
||||
[the contract and staged follow-ups](docs/physical-material-cards.md).
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Proposed phases cover offline bundled/project/user packs, physical materials and
|
||||
appearances, editable wood mapping, and parameterized component/assembly families.
|
||||
Stable identity/revision, copied design definitions, provenance and resource limits
|
||||
apply from the first implementation. Existing material, appearance and standard-part
|
||||
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
|
||||
for source/license review and acceptance gates. This is planning only: pack loading,
|
||||
texture rendering and expanded component families are not implemented by this entry.
|
||||
|
||||
## Render foundation
|
||||
|
||||
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
|
||||
it does not yet enable HDRI/PBR rendering or a document import command. See
|
||||
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
|
||||
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
|
||||
bounded image/scratch admission checks, exposed by the read-only `render.backends`
|
||||
command. It describes the current CPU raster snapshot path without evaluating the model.
|
||||
Existing snapshot and viewport routes remain unchanged. The Render workspace, HDRI/PBR,
|
||||
render jobs and optional engines remain future work; see [the contract](docs/render-backends.md)
|
||||
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
|
||||
|
||||
## Dimensioning and error reporting
|
||||
|
||||
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
|
||||
@@ -81,11 +111,16 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
arguments reject such a name when an older design's construction plane already has it. Use
|
||||
the other alias, the construction feature's numeric ID or an explicit plane; existing
|
||||
stored references keep their saved meaning. Section Analysis uses the same check.
|
||||
- Sketch text can be edited from a selected outline's context menu or the Edit Text command.
|
||||
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
|
||||
Interior selection requires every profile boundary to belong to the same text source;
|
||||
the editor highlights the whole source without replacing profile selections.
|
||||
Content, baseline X/Y, height and rotation update the original text source in one undo step;
|
||||
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
|
||||
use the document units captured when editing starts; generated outlines remain associated
|
||||
with their text source, and untouched inputs preserve their saved internal values.
|
||||
Dragging a text outline moves the entire source in one undo step; Escape restores the
|
||||
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
|
||||
about its baseline; copies keep an independent editable text source.
|
||||
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
|
||||
waits for a distinct point without a degenerate-shape error or history entry. Typed input
|
||||
and programmatic commands retain their validation.
|
||||
@@ -180,6 +215,9 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
- Associative shell-face sketches require a unique certified planar source. Nested shell
|
||||
attachments and ambiguous coincident sources remain unsupported.
|
||||
- G2 fillets: exact quintic blends of straight edges between planar faces, with one perpendicular
|
||||
planar end face at each end. The UI and `solid.fillet` expose `curvature` (alias `g2`); the size
|
||||
is an equivalent circular setback. Curved faces/edges, sloped ends and corner junctions remain unsupported.
|
||||
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
|
||||
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
|
||||
meet (a sphere for equal radii); straight edges (convex or concave) between planar faces with perpendicular
|
||||
@@ -197,7 +235,14 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
|
||||
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
|
||||
between two overlapping extruded arcs).
|
||||
between two overlapping extruded arcs); knife edges, where the two faces have opposite
|
||||
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
|
||||
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
|
||||
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
|
||||
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
|
||||
other, except a round after a round of the edge between the wall and a flat face that it
|
||||
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
|
||||
piece of sphere.
|
||||
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
|
||||
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
|
||||
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
|
||||
@@ -233,8 +278,12 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
|
||||
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
|
||||
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
|
||||
certificate and refine within bounded node limits while retaining existing IDs. Singular,
|
||||
self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
certificate and refine within bounded node limits while retaining existing IDs.
|
||||
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
|
||||
speeds without changing stored nodes or relaxing the distance tolerance (#971).
|
||||
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
|
||||
editable source updates the derived boundary live. Break Association restores point editing.
|
||||
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
|
||||
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
|
||||
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
|
||||
|
||||
@@ -13349,11 +13349,11 @@ truck-topology 0.6.0: Apache-2.0 (https://github.com/ricosjp/truck)
|
||||
Bundled fonts (SIL Open Font License, Version 1.1)
|
||||
================================================================================
|
||||
|
||||
- LiberationSans-Regular.ttf (crates/sketch/fonts/LiberationSans-Regular.ttf)
|
||||
- LiberationSans-Regular.ttf (assets/fonts/liberation-sans/LiberationSans-Regular.ttf)
|
||||
Copyright 2012 Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google)
|
||||
Source: https://github.com/liberationfonts
|
||||
|
||||
- BIZUDPGothic-Regular.ttf (apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf)
|
||||
- BIZUDPGothic-Regular.ttf (assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf)
|
||||
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)
|
||||
Source: https://github.com/googlefonts/morisawa-biz-ud-gothic
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! solidcraft-cli commands the command registry as JSON
|
||||
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
|
||||
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
|
||||
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT] MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
|
||||
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
|
||||
//! ```
|
||||
@@ -25,7 +26,7 @@ mod spatial;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
#[path = "../../app_icon.rs"]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
|
||||
use solidcraft_engine::Session;
|
||||
@@ -43,7 +44,7 @@ const USAGE: &str = "usage:
|
||||
solidcraft-cli recipe <recipe.json>
|
||||
solidcraft-cli oracle <case-dir>... [--json]
|
||||
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
|
||||
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT]
|
||||
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||
solidcraft-cli licences
|
||||
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
|
||||
@@ -312,11 +313,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
|
||||
/// a running app's control port. stdout carries only protocol messages.
|
||||
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
|
||||
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
|
||||
fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
|
||||
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
|
||||
if let Some(s) = &scope {
|
||||
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
|
||||
eprintln!(
|
||||
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
|
||||
roots.join(", ")
|
||||
);
|
||||
let design_dir = flag(args, "--in")
|
||||
.and_then(|p| std::path::Path::new(p).parent())
|
||||
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
|
||||
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
|
||||
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
}
|
||||
}
|
||||
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
|
||||
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
|
||||
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect(addr).map_err(|e| {
|
||||
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect_with_token(addr, flag(args, "--token")).map_err(|e| {
|
||||
let port = addr.rsplit(':').next().filter(|p| p.parse::<u16>().is_ok()).unwrap_or("PORT");
|
||||
format!(
|
||||
"cannot connect to {addr}: {e}\nno SolidCraft app is listening there; start it with \
|
||||
@@ -328,7 +345,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
};
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
solidcraft_mcp::Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
|
||||
@@ -393,10 +410,13 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let read_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
println!("{}", json!({"step": "read", "ms": read_ms, "bytes": bytes.len()}));
|
||||
let t = std::time::Instant::now();
|
||||
// The file as read, held until the timeline has evaluated it (as the import command does).
|
||||
let mut read = None;
|
||||
let doc = if step {
|
||||
let imported = solidcraft_engine::io::step_import_feature(&bytes, path).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::doc::Document::new(&imported.name);
|
||||
doc.add_feature(imported.kind, Some(&imported.name)).map_err(|e| e.to_string())?;
|
||||
read = Some(imported.import);
|
||||
doc
|
||||
} else {
|
||||
solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?
|
||||
@@ -408,12 +428,15 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::new(doc);
|
||||
drop(read);
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0}));
|
||||
for r in &s.model.results {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let st = s.world_state();
|
||||
// As the desktop app prepares a design it opened.
|
||||
st.warm_meshes(|| true);
|
||||
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
|
||||
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
|
||||
Ok(())
|
||||
|
||||
@@ -514,7 +514,23 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
out.push(json!({"command": "solid.mirror", "params": p}));
|
||||
}
|
||||
Some("fillet") => {
|
||||
out.push(json!({"command": "solid.fillet", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
|
||||
let mut p = json!({"edges": edge_points(f), "radius": f.get("radius"), "name": name});
|
||||
// Constant radius (the default), chord length, or a radius varying along each edge
|
||||
// (`radius` at the end nearest `start`, `radius2` at the other, `mid` between,
|
||||
// `smooth`), or a B-spline along each edge (`radius` its first control radius,
|
||||
// `radii` the rest, `knots`); `fit` for a radius too large for the geometry.
|
||||
match f.get("type").and_then(Value::as_str) {
|
||||
Some("chord_length") => p["type"] = json!("chord"),
|
||||
Some("variable_radius") => p["type"] = json!("variable"),
|
||||
Some("spline_radius") => p["type"] = json!("spline"),
|
||||
_ => {}
|
||||
}
|
||||
for key in ["radius2", "start", "mid", "smooth", "fit", "radii", "knots"] {
|
||||
if let Some(v) = f.get(key) {
|
||||
p[key] = v.clone();
|
||||
}
|
||||
}
|
||||
out.push(json!({"command": "solid.fillet", "params": p}))
|
||||
}
|
||||
Some("chamfer") => {
|
||||
out.push(json!({"command": "solid.chamfer", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
|
||||
|
||||
@@ -47,6 +47,10 @@ const SLOW: [&str; 7] = [
|
||||
const EDIT_SAMPLE: u64 = 6;
|
||||
/// An action that takes longer than this is recorded as a timeout.
|
||||
const ACTION_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
/// Building a design and listing its actions, before the first one runs: the biggest designs
|
||||
/// (oracle 41's tube with radial holes, about 25 s here) take several times that on a loaded CI
|
||||
/// runner, and a design that never loads loses all its actions, not one.
|
||||
const LOAD_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
/// Caps per body, so one large imported or patterned body cannot dominate the run.
|
||||
const MAX_FACES: usize = 120;
|
||||
const MAX_EDGES: usize = 240;
|
||||
@@ -635,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) {
|
||||
@@ -646,12 +657,37 @@ fn action_matrix_worker() {
|
||||
let mut scratch = s.scratch();
|
||||
let r = perform(&mut scratch, &a.act, &before);
|
||||
let mut line = outcome(&design, &a.entity, a.action, &r, t.elapsed().as_millis());
|
||||
if validity && r.is_ok() {
|
||||
let st = scratch.model.state();
|
||||
let bad: Vec<String> = st.bodies.iter().flat_map(|b| b.body.validity().into_iter().map(|v| format!("{}: {v}", b.name))).collect();
|
||||
line["validity"] = json!(bad);
|
||||
}
|
||||
// What was sent, for reproducing a failure (kept in the results, not the baseline).
|
||||
line["command"] = match &a.act {
|
||||
Act::Run(id, p) => json!({"command": id, "params": p}),
|
||||
Act::Project { plane, edge } => json!({"command": "sketch.project", "params": {"refs": [edge]}, "sketch_plane": plane}),
|
||||
};
|
||||
emit(line);
|
||||
// With ACTION_MATRIX_UNFOLD, sheet metal must still unfold and fold back after the action.
|
||||
if r.is_ok() && std::env::var_os("ACTION_MATRIX_UNFOLD").is_some() {
|
||||
let t = Instant::now();
|
||||
let sheets: Vec<String> = scratch.model.state().sheets.iter().map(|sh| sh.body.clone()).collect();
|
||||
let mut r = Ok(());
|
||||
for body in &sheets {
|
||||
for id in ["sheet.unfold", "sheet.refold"] {
|
||||
if r.is_ok() {
|
||||
r = scratch
|
||||
.execute(id, &json!({"body": body}))
|
||||
.map(|_| ())
|
||||
.map_err(|e| (command_kind(&e).to_string(), format!("{body}: {e}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sheets.is_empty() {
|
||||
let unfold: &'static str = Box::leak(format!("{}+unfold", a.action).into_boxed_str());
|
||||
emit(outcome(&design, &a.entity, unfold, &r, t.elapsed().as_millis()));
|
||||
}
|
||||
}
|
||||
// The edit round: a driving parameter changes; the new feature still resolves.
|
||||
if r.is_ok()
|
||||
&& let Some((name, expr)) = &edit
|
||||
@@ -725,7 +761,9 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut seen = 0usize;
|
||||
// Until the child writes, it is loading the design (again, after a restart).
|
||||
let loaded_at = std::fs::metadata(out).map(|m| m.len() as usize).unwrap_or(0);
|
||||
let mut seen = loaded_at;
|
||||
let mut last_change = Instant::now();
|
||||
let (status, timed_out) = loop {
|
||||
if let Some(status) = child.try_wait().unwrap() {
|
||||
@@ -735,7 +773,7 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
|
||||
if n != seen {
|
||||
seen = n;
|
||||
last_change = Instant::now();
|
||||
} else if last_change.elapsed() > ACTION_TIMEOUT {
|
||||
} else if last_change.elapsed() > if seen == loaded_at { LOAD_TIMEOUT } else { ACTION_TIMEOUT } {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
break (None, true);
|
||||
@@ -1045,8 +1083,19 @@ 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]
|
||||
#[ignore = "fails until #560 is fixed"]
|
||||
fn a_hole_through_a_sheet_metal_base() {
|
||||
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||
c.extend([
|
||||
@@ -1056,8 +1105,30 @@ 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]
|
||||
#[ignore = "fails until #561 is fixed"]
|
||||
fn sketches_on_loft_and_sweep_faces() {
|
||||
let mut loft: Vec<Value> = rectangle(json!("XY"), [-20., -20.], [20., 20.]).into();
|
||||
loft.extend([
|
||||
@@ -1096,7 +1167,6 @@ fn press_pull_on_the_pi_case_floor() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #563 is fixed"]
|
||||
fn sketches_after_a_draft_and_on_a_tapered_extrude() {
|
||||
reproduces(
|
||||
None,
|
||||
@@ -1117,7 +1187,6 @@ fn sketches_after_a_draft_and_on_a_tapered_extrude() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #564 is fixed"]
|
||||
fn a_sketch_on_the_rim_a_shell_leaves() {
|
||||
let mut c: Vec<Value> = rectangle(json!("XY"), [-35., -25.], [35., 25.]).into();
|
||||
c.extend([
|
||||
@@ -1173,8 +1242,36 @@ fn a_shell_of_a_swept_bar() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The construction a drill-tip script ends with.
|
||||
struct TipConstruction(solidcraft_engine::doc::construct::ConstructGeom);
|
||||
|
||||
impl TipConstruction {
|
||||
fn axis(&self) -> (Vec3, Vec3) {
|
||||
match &self.0 {
|
||||
solidcraft_engine::doc::construct::ConstructGeom::Axis { origin, dir } => (*origin, *dir),
|
||||
other => panic!("expected an axis, got {other:?}"),
|
||||
}
|
||||
}
|
||||
fn plane(&self) -> solidcraft_engine::geom::Plane {
|
||||
match &self.0 {
|
||||
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
|
||||
other => panic!("expected a plane, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replays the script and returns its last construction.
|
||||
fn drill_tip_construction(commands: Vec<Value>) -> TipConstruction {
|
||||
let mut s = Session::default();
|
||||
for c in &commands {
|
||||
let id = c["command"].as_str().unwrap();
|
||||
s.execute(id, &c["params"]).unwrap_or_else(|e| panic!("{id}: {e}"));
|
||||
}
|
||||
let state = s.model.state();
|
||||
TipConstruction(state.construct.last().map(|c| c.geom.clone()).expect("a construction"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #567 is fixed"]
|
||||
fn an_axis_through_a_drill_tip() {
|
||||
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||
c.extend([
|
||||
@@ -1182,7 +1279,30 @@ fn an_axis_through_a_drill_tip() {
|
||||
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
|
||||
json!({"command": "construct.axis.through_cylinder", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
|
||||
]);
|
||||
reproduces(None, Value::Array(c));
|
||||
reproduces(None, Value::Array(c.clone()));
|
||||
// Not just "no error": the axis runs along the drill, through the hole's centre line.
|
||||
let (origin, dir) = drill_tip_construction(c).axis();
|
||||
assert!((origin.x - 50.).abs() < 1e-6 && (origin.y - 20.).abs() < 1e-6, "{origin:?}");
|
||||
assert!(dir.z.abs() > 1. - 1e-9, "{dir:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tangent_plane_on_a_drill_tip() {
|
||||
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||
c.extend([
|
||||
json!({"command": "solid.extrude", "params": {"distance": 15}}),
|
||||
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
|
||||
json!({"command": "construct.plane.tangent", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
|
||||
]);
|
||||
reproduces(None, Value::Array(c.clone()));
|
||||
// The plane touches the cone at the picked point and leans like the cone does: a 118 degree tip
|
||||
// puts the wall 59 degrees off the drill axis, so its normal is 31 degrees off it.
|
||||
let plane = drill_tip_construction(c).plane();
|
||||
let picked = Vec3::new(48.44468907075447, 21.22448979591836, 4.399139380972439);
|
||||
let distance = (picked - plane.origin).dot(plane.normal());
|
||||
// The pick lies on the tessellated cone and the plane is the exact one, so allow for chord error.
|
||||
assert!(distance.abs() < 0.05, "the pick is {distance} mm off the plane");
|
||||
assert!((plane.normal().z.abs() - 31f64.to_radians().cos()).abs() < 1e-2, "{:?}", plane.normal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1199,3 +1319,30 @@ fn a_plane_on_a_patterned_counterbore_after_a_thickness_change() {
|
||||
]);
|
||||
reproduces(None, Value::Array(c));
|
||||
}
|
||||
|
||||
/// #707: the same design builds the same body every time, in one process too. Faces were put in
|
||||
/// the hash order of their addresses in a few places (truck's connected components, the planar
|
||||
/// boolean's rebuild, healing's merges), so a later boolean on the ribbed lid passed or failed
|
||||
/// from run to run.
|
||||
#[test]
|
||||
fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
let path = root().join("examples/regressions/lid_ribs_flush_with_window.json");
|
||||
let fingerprint = || {
|
||||
let s = load(&path).unwrap();
|
||||
let st = s.model.state();
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
format!("{} {} {bits:x}", b.name, b.body.face_count())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let first = fingerprint();
|
||||
for _ in 0..5 {
|
||||
assert_eq!(fingerprint(), first);
|
||||
}
|
||||
}
|
||||
@@ -137,13 +137,7 @@ fn body_names_saved_before_exact_history_keep_their_pieces() {
|
||||
/// ignored test below that checks its faces; a listed plane whose face attaches fails the main
|
||||
/// check until it is removed from here.
|
||||
type Planes = &'static [([f64; 3], f64)];
|
||||
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 5] = [
|
||||
("m0-revolve-primitives", "Body1", None, 494),
|
||||
("naming-split-pieces", "Body1", None, 494),
|
||||
("modern-plastic", "Body1", None, 494),
|
||||
("modern-sheet-metal", "Body1", None, 495),
|
||||
("issue-616-lid-ribs", "Lid", None, 628),
|
||||
];
|
||||
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 1] = [("issue-616-lid-ribs", "Lid", None, 628)];
|
||||
|
||||
/// The issue a face is listed under, and whether it is listed by its plane (not as every face).
|
||||
fn known_without_attachment(design: &str, body: &str, normal: [f64; 3], distance: f64) -> Option<(u32, bool)> {
|
||||
@@ -262,18 +256,6 @@ fn known_faces_attach(issue: u32) {
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #494 is fixed"]
|
||||
fn faces_after_revolve_offset_face_and_boss_take_an_attached_sketch() {
|
||||
known_faces_attach(494);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #495 is fixed"]
|
||||
fn sheet_metal_thickness_faces_take_an_attached_sketch() {
|
||||
known_faces_attach(495);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #628 is fixed"]
|
||||
fn faces_of_a_lid_with_joined_ribs_take_an_attached_sketch() {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! A parameter change gives the same topology as building at the new value from scratch, and
|
||||
//! changing it back gives the original topology: no stale splits, no faces added to regions
|
||||
//! nothing touched. Over the design corpus (`tests/corpus`, `tests/action_matrix/designs`), and
|
||||
//! the README demo's Raspberry Pi case pinned step by step.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
|
||||
fn root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
|
||||
}
|
||||
|
||||
/// A folder of its own for test `name` (tests run side by side).
|
||||
fn scratch_dir(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-rebuild-topology-{name}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A body's topology: its face and edge counts, and how many faces lie in each plane.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Topology {
|
||||
faces: usize,
|
||||
edges: usize,
|
||||
planes: BTreeMap<[i64; 4], usize>,
|
||||
}
|
||||
|
||||
/// Every solid body's topology, by name.
|
||||
fn topology(s: &Session) -> BTreeMap<String, Topology> {
|
||||
let st = s.world_state();
|
||||
st.bodies
|
||||
.iter()
|
||||
.filter(|b| !b.body.is_mesh())
|
||||
.map(|b| {
|
||||
let mut planes = BTreeMap::new();
|
||||
for i in 0..b.body.face_count() {
|
||||
if let Some(FaceSurface::Plane { normal, point }) = face_surface(&b.body, i) {
|
||||
let key = [normal.x, normal.y, normal.z, normal.dot(point) / 100.].map(|x| (x * 1e4).round() as i64);
|
||||
*planes.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
(b.name.clone(), Topology { faces: b.body.face_count(), edges: b.body.edge_count(), planes })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
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 as it is (timeline only, no cached geometry) and
|
||||
/// opened again.
|
||||
fn rebuilt(s: &Session, path: &Path) -> Session {
|
||||
let mut t = s.scratch();
|
||||
t.execute("file.save", &json!({"path": path.to_string_lossy(), "format": "json"})).unwrap();
|
||||
open(path)
|
||||
}
|
||||
|
||||
fn errors(s: &Session) -> Vec<u64> {
|
||||
s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect()
|
||||
}
|
||||
|
||||
/// The corpus's designs, as sessions.
|
||||
fn designs() -> Vec<(String, Session)> {
|
||||
let root = root();
|
||||
let mut out = Vec::new();
|
||||
for dir in ["apps/solidcraft-cli/tests/corpus", "tests/action_matrix/designs"] {
|
||||
let mut files: Vec<PathBuf> = std::fs::read_dir(root.join(dir))
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "solidcraft" || x == "json") && !p.to_string_lossy().ends_with(".expected.json"))
|
||||
.collect();
|
||||
files.sort();
|
||||
for p in files {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
|
||||
open(&p)
|
||||
} else {
|
||||
let mut s = Session::default();
|
||||
let script: Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap();
|
||||
s.run_script(&script).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
if s.active_sketch.is_some() {
|
||||
let _ = s.execute("sketch.finish", &json!({}));
|
||||
}
|
||||
s
|
||||
};
|
||||
out.push((name, s));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// For each design and its first length parameter that edits cleanly (the user's first): the parameter
|
||||
/// scaled by 1.1 gives what a fresh build at that value gives, and setting it back gives the
|
||||
/// original topology.
|
||||
#[test]
|
||||
fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
|
||||
let dir = scratch_dir("corpus");
|
||||
let mut problems = Vec::new();
|
||||
let mut checked = 0;
|
||||
for (name, s) in designs() {
|
||||
let original = topology(&s);
|
||||
let before = errors(&s);
|
||||
let mut params: Vec<_> = s.doc.params.iter().filter(|p| p.unit == "mm").cloned().collect();
|
||||
params.sort_by_key(|p| p.model);
|
||||
let mut tried = 0;
|
||||
for p in params {
|
||||
if tried == 1 {
|
||||
break;
|
||||
}
|
||||
let mut t = s.scratch();
|
||||
let expr = format!("({}) * 1.1", p.expr);
|
||||
if t.execute("parameters.change", &json!({"name": p.name, "expression": expr})).is_err() || errors(&t) != before {
|
||||
continue;
|
||||
}
|
||||
tried += 1;
|
||||
checked += 1;
|
||||
let edited = topology(&t);
|
||||
let fresh = topology(&rebuilt(&t, &dir.join(format!("{name}-{}.solidcraft", p.name))));
|
||||
if edited != fresh {
|
||||
problems.push(format!("{name}: {} = {expr}: edited {edited:?}\n fresh {fresh:?}", p.name));
|
||||
}
|
||||
t.execute("parameters.change", &json!({"name": p.name, "expression": p.expr})).unwrap();
|
||||
let back = topology(&t);
|
||||
if back != original {
|
||||
problems.push(format!("{name}: {} back to {}: {back:?}\n original {original:?}", p.name, p.expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert!(checked >= 8, "only {checked} parameter edits checked");
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
|
||||
/// face count after each feature, and each outer side (the four walls and the bottom) one face
|
||||
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
|
||||
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
|
||||
/// wall at 24) into strips along the openings' tops and bottoms.
|
||||
#[test]
|
||||
fn pi_case_faces_per_step_at_both_heights() {
|
||||
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
let mut d = design.clone();
|
||||
for p in d["params"].as_array_mut().unwrap() {
|
||||
if p["name"] == "height" {
|
||||
p["expr"] = json!(height);
|
||||
}
|
||||
}
|
||||
let path = dir.join(file);
|
||||
std::fs::write(&path, serde_json::to_vec(&d).unwrap()).unwrap();
|
||||
open(&path)
|
||||
};
|
||||
let a = at("24 mm", "pi-24.solidcraft");
|
||||
let mut b = at("24 mm", "pi-24-to-32.solidcraft");
|
||||
b.execute("parameters.change", &json!({"name": "height", "expression": "32 mm"})).unwrap();
|
||||
let c = at("32 mm", "pi-32.solidcraft");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let mut per_case = Vec::new();
|
||||
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
|
||||
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
|
||||
let mut steps = Vec::new();
|
||||
for (k, want) in FACES {
|
||||
let mut t = s.scratch();
|
||||
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
|
||||
let st = t.world_state();
|
||||
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
|
||||
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
|
||||
// Each outer side: the faces in its plane.
|
||||
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
|
||||
let on = (0..body.body.face_count())
|
||||
.filter(|&i| match face_surface(&body.body, i) {
|
||||
Some(FaceSurface::Plane { normal, point }) => {
|
||||
(normal.x - n[0]).abs() + (normal.y - n[1]).abs() + (normal.z - n[2]).abs() < 1e-6 && (normal.dot(point) - d).abs() < 1e-6
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.count();
|
||||
assert_eq!(on, 1, "{case}: after feature {k} the outer side facing {n:?} is {on} faces");
|
||||
}
|
||||
let bbox_top = body.mesh().bounds().max.z;
|
||||
assert!((bbox_top - h).abs() < 1e-6, "{case}: height {bbox_top}");
|
||||
steps.push(topology(&t));
|
||||
}
|
||||
per_case.push((case, steps));
|
||||
}
|
||||
// (b) and (c): the same topology after every feature.
|
||||
let (b, c) = (&per_case[1].1, &per_case[2].1);
|
||||
for (k, (x, y)) in b.iter().zip(c).enumerate() {
|
||||
assert_eq!(x, y, "after feature {}: (b) differs from (c)", k + 2);
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,9 @@ filehash = true
|
||||
[serve]
|
||||
addresses = ["127.0.0.1"]
|
||||
port = 8781
|
||||
|
||||
# Offline use and updates: release builds get sw.js listing this build's files (xtask/src/web_offline.rs).
|
||||
[[hooks]]
|
||||
stage = "post_build"
|
||||
command = "cargo"
|
||||
command_arguments = ["xtask", "web-offline"]
|
||||
@@ -20,17 +20,18 @@
|
||||
<meta name="twitter:image" content="https://bherbruck.github.io/solidcraft/og.png">
|
||||
<meta name="twitter:image:alt" content="SolidCraft: open-source parametric CAD built in Rust">
|
||||
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<link data-trunk rel="copy-file" href="fonts/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="fonts/OFL.txt" data-target-path="fonts" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
||||
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
</style>
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
|
||||
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="solidcraft_canvas"></canvas>
|
||||
<div id="solidcraft_loading">Loading SolidCraft…</div>
|
||||
<script>
|
||||
// Offline use and updates (sw.js; written into release builds by `cargo xtask web-offline`).
|
||||
if ("serviceWorker" in navigator) {
|
||||
addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -331,22 +331,34 @@ mod web {
|
||||
};
|
||||
if installed {
|
||||
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
|
||||
} else {
|
||||
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
|
||||
log::warn!("CJK font unavailable; showing English for this session");
|
||||
}
|
||||
self.font.finished(installed, &mut self.app.preferences.language);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
if self.font.tick(&mut self.app.preferences.language) {
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
|
||||
/// fetch the font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_font(&self, ctx: &egui::Context) {
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Shell {
|
||||
@@ -365,6 +377,7 @@ mod web {
|
||||
let ctx = ui.ctx().clone();
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
self.watch_text_for_font(&ctx);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
// Persist the user's own language, not a session-only English fallback.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// SolidCraft's service worker: the web app keeps working offline and updates itself.
|
||||
//
|
||||
// `cargo xtask web-offline` (Trunk's post_build hook) writes the build into the line below: the
|
||||
// version (workspace version + a hash of every built file) and the files to keep, each with its
|
||||
// SHA-256, and the same as `build.json` next to it. Files the app only fetches when it needs
|
||||
// them (the CJK font) aren't kept: they come from the network. Every download is checked
|
||||
// against its SHA-256: a deployment that lands mid-download (another build's `index.html`) fails
|
||||
// the download instead of mixing into the kept build. A new build changes this file, so the
|
||||
// browser installs the new worker, which keeps the whole new build before taking over.
|
||||
//
|
||||
// A launch only ever starts a build whose every file is kept, so a half-downloaded update can't
|
||||
// break it. It asks for `build.json` (waiting at most NAVIGATE_TIMEOUT_MS): offline, slow or
|
||||
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
|
||||
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
|
||||
// started. When the site has no `build.json` (the app was removed), launches go to the network,
|
||||
// the kept build answering when that fails. Each page then gets its files from the build it
|
||||
// started (remembered in PAGES), never from another build with a file of the same name.
|
||||
//
|
||||
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
|
||||
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
|
||||
// an open page uses the build it started. Then unfinished downloads are dropped, and so are
|
||||
// builds replaced by a newer worker (RETIRED). The origin is shared with other apps (github.io)
|
||||
// and other copies of this one (the release zip works from any path): only caches named after
|
||||
// this worker's path (CACHE_PREFIX…, PAGES) are ever touched.
|
||||
//
|
||||
// Unbuilt (VERSION "dev", as debug builds get it), the worker retires: it drops what a release
|
||||
// build kept and unregisters, so `trunk serve` after `trunk serve --release --no-autoreload`
|
||||
// shows the development build. Deploying this file unbuilt switches the worker off for everyone.
|
||||
/* @build */ const VERSION = "dev", FILES = [];
|
||||
const RETIRE = VERSION === "dev";
|
||||
|
||||
const PATH = new URL(self.registration.scope).pathname;
|
||||
const CACHE_PREFIX = `solidcraft-app ${PATH} `;
|
||||
const CACHE = CACHE_PREFIX + VERSION;
|
||||
/** Which build each open page started: page id → cache name (or NETWORK). */
|
||||
const PAGES = `solidcraft-app-pages ${PATH}`;
|
||||
/** A page that came from the network gets its files from there too. */
|
||||
const NETWORK = "network";
|
||||
const NAVIGATE_TIMEOUT_MS = 4000;
|
||||
const UPDATE_TIMEOUT_MS = 20000;
|
||||
/** The entry that marks a cache as holding its whole build. */
|
||||
const COMPLETE = "build-complete";
|
||||
/** The entry that marks a build replaced by a newer worker. */
|
||||
const RETIRED = "build-retired";
|
||||
/** A page remembered this recently may still be loading (not yet listed among the clients). */
|
||||
const PAGE_LOADING_MS = 30 * 1000;
|
||||
|
||||
const url = (file) => new URL(file, self.registration.scope).href;
|
||||
const has = (name, entry) => caches.match(url(entry), { cacheName: name }).then(Boolean);
|
||||
|
||||
/** Run `fn` while using the cache `name`: it isn't deleted meanwhile. */
|
||||
const using = (name, fn) => navigator.locks.request(name, { mode: "shared" }, fn);
|
||||
|
||||
/** Delete the cache `name` if nothing is using it and `unwanted()` (asked once no one can start to) agrees. */
|
||||
const drop = (name, unwanted) =>
|
||||
navigator.locks.request(name, { ifAvailable: true }, async (free) => free && (await unwanted()) && caches.delete(name));
|
||||
|
||||
const hex = (buffer) => Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
/**
|
||||
* Keep every file of a build (an earlier partial download is resumed); resolves to its cache's
|
||||
* name. The first file that fails cancels the others.
|
||||
*/
|
||||
function keep(version, files) {
|
||||
const name = CACHE_PREFIX + version;
|
||||
return using(name, async () => {
|
||||
const cache = await caches.open(name);
|
||||
await cache.delete(url(RETIRED)); // Back in use (a rollback).
|
||||
if (await has(name, COMPLETE)) return name;
|
||||
const cancel = new AbortController();
|
||||
const results = await Promise.allSettled(
|
||||
files.map(async ({ file, sha256 }) => {
|
||||
try {
|
||||
if (await cache.match(url(file))) return;
|
||||
// `no-cache`: revalidate with the server, so a stale HTTP-cached page never mixes builds.
|
||||
const response = await fetch(new Request(url(file), { cache: "no-cache" }), { signal: cancel.signal });
|
||||
if (!response.ok) throw new Error(`${file}: HTTP ${response.status}`);
|
||||
const body = await response.arrayBuffer();
|
||||
if (hex(await crypto.subtle.digest("SHA-256", body)) !== sha256) throw new Error(`${file}: not this build's`);
|
||||
await cache.put(url(file), new Response(body, { headers: response.headers }));
|
||||
} catch (error) {
|
||||
cancel.abort();
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const failed = results.find((r) => r.status === "rejected");
|
||||
if (failed) throw failed.reason;
|
||||
await cache.put(url(COMPLETE), new Response(version));
|
||||
return name;
|
||||
});
|
||||
}
|
||||
|
||||
const pageEntry = (id) => url(`~page/${encodeURIComponent(id)}`);
|
||||
|
||||
/** Remember which build a page started. */
|
||||
async function started(id, build) {
|
||||
if (!id) return;
|
||||
const record = JSON.stringify({ build, at: Date.now() });
|
||||
await (await caches.open(PAGES)).put(pageEntry(id), new Response(record));
|
||||
}
|
||||
|
||||
/** The build a page started; a page started before it was remembered gets this worker's. */
|
||||
async function startedBy(id) {
|
||||
const response = id ? await caches.match(pageEntry(id), { cacheName: PAGES }) : null;
|
||||
return response ? (await response.json()).build : CACHE;
|
||||
}
|
||||
|
||||
/** The builds open pages use (pages closed since are forgotten). */
|
||||
async function inUse() {
|
||||
const open = new Set((await self.clients.matchAll({ includeUncontrolled: true })).map((c) => c.id));
|
||||
const pages = await caches.open(PAGES);
|
||||
const builds = new Set();
|
||||
for (const request of await pages.keys()) {
|
||||
const id = decodeURIComponent(request.url.split("~page/")[1] ?? "");
|
||||
const record = await pages.match(request).then((r) => r?.json()).catch(() => null);
|
||||
if (record && (open.has(id) || Date.now() - record.at < PAGE_LOADING_MS)) builds.add(record.build);
|
||||
else await pages.delete(request);
|
||||
}
|
||||
return builds;
|
||||
}
|
||||
|
||||
/** Drop unfinished downloads nothing is using, and replaced builds no open page uses. */
|
||||
async function sweep() {
|
||||
for (const name of await caches.keys()) {
|
||||
if (!name.startsWith(CACHE_PREFIX) || name === CACHE) continue;
|
||||
await drop(name, async () => !(await has(name, COMPLETE)) || ((await has(name, RETIRED)) && !(await inUse()).has(name)));
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
// Clean up first: a page open for days sees updates (and failed ones) without any launch.
|
||||
const install = () => sweep().catch(() => {}).then(() => keep(VERSION, FILES));
|
||||
event.waitUntil(RETIRE ? self.skipWaiting() : install().then(() => self.skipWaiting()));
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
if (RETIRE) {
|
||||
for (const name of await caches.keys()) if (name.startsWith(CACHE_PREFIX) || name === PAGES) await caches.delete(name);
|
||||
return self.registration.unregister();
|
||||
}
|
||||
for (const name of await caches.keys()) {
|
||||
if (name.startsWith(CACHE_PREFIX) && name !== CACHE) await (await caches.open(name)).put(url(RETIRED), new Response(""));
|
||||
}
|
||||
await sweep();
|
||||
await self.clients.claim();
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
/** The promise's value, or null when it fails or takes longer than `ms`. */
|
||||
function within(ms, promise) {
|
||||
return Promise.race([promise.catch(() => null), new Promise((resolve) => setTimeout(resolve, ms, null))]);
|
||||
}
|
||||
|
||||
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
|
||||
const GONE = "gone";
|
||||
|
||||
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
|
||||
async function deployed() {
|
||||
const response = await fetch(url("build.json"), { cache: "no-cache" });
|
||||
if (response.status === 404 || response.status === 410) return GONE;
|
||||
const build = response.ok ? await response.json() : null;
|
||||
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
|
||||
return valid && build.files.every((f) => typeof f.file === "string" && typeof f.sha256 === "string") ? build : null;
|
||||
}
|
||||
|
||||
/** Keep the worker alive for `promise` (allowed while the launch is being answered). */
|
||||
function extend(event, promise) {
|
||||
try {
|
||||
event.waitUntil(promise.catch(() => {}));
|
||||
} catch {
|
||||
// The event has ended; the work goes on while the worker runs.
|
||||
}
|
||||
}
|
||||
|
||||
/** A launch: the start page of a complete build (whatever query the URL has: `?sample`), and that build. */
|
||||
async function launch(event) {
|
||||
const kept = await caches.match(url("index.html"), { cacheName: CACHE });
|
||||
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
|
||||
extend(event, sweep());
|
||||
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
|
||||
if (build === GONE) {
|
||||
const page = await fetch(event.request).catch(() => null);
|
||||
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
|
||||
}
|
||||
if (!build || build.version === VERSION) return [kept, CACHE];
|
||||
const download = keep(build.version, build.files);
|
||||
extend(event, download);
|
||||
const name = await within(UPDATE_TIMEOUT_MS, download);
|
||||
const page = name && (await caches.match(url("index.html"), { cacheName: name }));
|
||||
return page ? [page, name] : [kept, CACHE];
|
||||
}
|
||||
|
||||
/**
|
||||
* A launch that fails unexpectedly still starts the kept build (or asks the network). A newer
|
||||
* worker only takes over once the launch has ended, by when its page is remembered (in use).
|
||||
*/
|
||||
async function launchOrKept(event) {
|
||||
let page, build;
|
||||
try {
|
||||
[page, build] = await launch(event);
|
||||
} catch {
|
||||
page = await caches.match(url("index.html"), { cacheName: CACHE }).catch(() => null);
|
||||
build = page ? CACHE : NETWORK;
|
||||
page = page || (await fetch(event.request));
|
||||
}
|
||||
await started(event.resultingClientId, build).catch(() => {});
|
||||
return page;
|
||||
}
|
||||
|
||||
/** A page's files come from the build it started, else the network. */
|
||||
async function asset(event) {
|
||||
const build = await startedBy(event.clientId).catch(() => CACHE);
|
||||
const kept = build === NETWORK ? null : await caches.match(event.request, { cacheName: build, ignoreSearch: true });
|
||||
return kept || fetch(event.request);
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const request = event.request;
|
||||
if (RETIRE || request.method !== "GET" || new URL(request.url).origin !== self.location.origin) return;
|
||||
const path = request.url.split(/[?#]/)[0];
|
||||
const start = request.mode === "navigate" && (path === url("") || path === url("index.html"));
|
||||
event.respondWith(start ? launchOrKept(event) : asset(event));
|
||||
});
|
||||
@@ -18,6 +18,7 @@ egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
getrandom = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
quick-xml = { workspace = true }
|
||||
|
||||
@@ -1,61 +1,17 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#[path = "../app_icon.rs"]
|
||||
mod app_icon;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed=../app_icon.rs");
|
||||
let icon = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons/app/windows/exports/solidcraft-executable.ico");
|
||||
println!("cargo:rerun-if-changed={}", icon.display());
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
|
||||
return Ok(());
|
||||
}
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or("missing OUT_DIR")?);
|
||||
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
|
||||
let mut images = Vec::new();
|
||||
for size in sizes {
|
||||
let rgba = app_icon::rgba(size)?;
|
||||
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
|
||||
let mask_stride = size.div_ceil(32) * 4;
|
||||
let mut dib = Vec::new();
|
||||
dib.extend_from_slice(&40u32.to_le_bytes());
|
||||
dib.extend_from_slice(&(size as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&1u16.to_le_bytes());
|
||||
dib.extend_from_slice(&32u16.to_le_bytes());
|
||||
dib.extend_from_slice(&[0; 24]);
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
for p in row.as_chunks::<4>().0.iter() {
|
||||
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||||
}
|
||||
}
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
let mut mask = vec![0u8; mask_stride];
|
||||
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
if p[3] == 0 {
|
||||
mask[x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
dib.extend_from_slice(&mask);
|
||||
}
|
||||
images.push((size, dib));
|
||||
}
|
||||
let mut ico = vec![0, 0, 1, 0];
|
||||
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
|
||||
let mut offset = 6 + 16 * images.len();
|
||||
for (size, data) in &images {
|
||||
let dim = if *size == 256 { 0 } else { *size as u8 };
|
||||
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
|
||||
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&(offset as u32).to_le_bytes());
|
||||
offset += data.len();
|
||||
}
|
||||
for (_, data) in images {
|
||||
ico.extend_from_slice(&data);
|
||||
}
|
||||
let icon = out.join("solidcraft.ico");
|
||||
std::fs::write(&icon, ico)?;
|
||||
let copy = out.join("solidcraft.ico");
|
||||
std::fs::copy(icon, ©)?;
|
||||
let rc = out.join("solidcraft.rc");
|
||||
std::fs::write(&rc, format!("1 ICON \"{}\"\n", icon.display().to_string().replace('\\', "/")))?;
|
||||
// Missing resource tools are a build error, rather than silently shipping a generic icon.
|
||||
std::fs::write(&rc, format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/")))?;
|
||||
embed_resource::compile_for(&rc, ["solidcraft"], embed_resource::NONE).manifest_required()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,18 +9,39 @@ use std::time::Duration;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::MAX_LINE;
|
||||
use solidcraft_engine::control_auth;
|
||||
use solidcraft_ui_egui::ControlRequest;
|
||||
|
||||
/// Connections served at once. The channel has no authentication beyond being bound to
|
||||
/// loopback, so a flood must not be able to spawn threads without end.
|
||||
/// Connections served at once, so a flood must not be able to spawn threads without end.
|
||||
const MAX_CONNECTIONS: usize = 16;
|
||||
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
|
||||
const MAX_BAD_LINES: usize = 8;
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token file this launch wrote, removed when the app ends.
|
||||
static TOKEN_FILE: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||
|
||||
/// A new random token: 32 bytes from the operating system, as hex.
|
||||
fn new_token() -> Option<String> {
|
||||
let mut bytes = [0u8; 32];
|
||||
getrandom::fill(&mut bytes).ok()?;
|
||||
Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
|
||||
}
|
||||
|
||||
/// Remove this launch's token file (at exit).
|
||||
pub fn remove_token_file() {
|
||||
if let Some(p) = TOKEN_FILE.get() {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// Every connection must show the token before anything runs (docs/control-protocol.md).
|
||||
let Some(token) = control_auth::env_token().or_else(new_token) else {
|
||||
eprintln!("solidcraft: control server not started: no random token available");
|
||||
return rx;
|
||||
};
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
@@ -28,7 +49,23 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
return rx;
|
||||
}
|
||||
};
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}");
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
let _ = TOKEN_FILE.set(path);
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
|
||||
clients need SOLIDCRAFT_CONTROL_TOKEN",
|
||||
path.display()
|
||||
),
|
||||
},
|
||||
None => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
|
||||
}
|
||||
}
|
||||
let token = std::sync::Arc::new(token);
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||
@@ -39,8 +76,9 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
}
|
||||
let tx = tx.clone();
|
||||
let ctx = ctx.clone();
|
||||
let token = token.clone();
|
||||
std::thread::spawn(move || {
|
||||
serve(stream, tx, ctx);
|
||||
serve(stream, &token, tx, ctx);
|
||||
OPEN.fetch_sub(1, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
@@ -80,11 +118,28 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
/// The reply to a connection that didn't show the token; the connection is then closed.
|
||||
fn unauthorized() -> Value {
|
||||
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
|
||||
}
|
||||
|
||||
/// Refuse the connection: the reply, then the end of what we send, then what the client had
|
||||
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
use std::io::Read;
|
||||
let _ = writeln!(out, "{}", unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
let mut out = stream;
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
loop {
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
@@ -99,20 +154,36 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
}
|
||||
let reply = match serde_json::from_str::<Value>(&line) {
|
||||
Ok(msg) => {
|
||||
// The first request must carry the token; later ones on the same connection may.
|
||||
if !authenticated {
|
||||
match msg.get("token").and_then(Value::as_str) {
|
||||
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(json!({}));
|
||||
// A batch replaces many short requests: the whole of it may take long (#743).
|
||||
let wait = Duration::from_secs(if method == "engine.batch" { 3600 } else { 60 });
|
||||
let (req, rrx) = ControlRequest::new(method, params);
|
||||
if tx.send(req).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||
let mut r = rrx.recv_timeout(wait).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("id".into(), id);
|
||||
}
|
||||
r
|
||||
}
|
||||
Err(_) if !authenticated => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
bad += 1;
|
||||
json!({"ok": false, "error": format!("bad JSON: {e}")})
|
||||
@@ -131,6 +202,62 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
|
||||
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
|
||||
/// need not be repeated on that connection.
|
||||
#[test]
|
||||
fn requests_run_only_after_the_token() {
|
||||
use std::io::Read;
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
let ran = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let seen = ran.clone();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
seen.lock().unwrap().push(req.method.clone());
|
||||
let _ = req.reply.send(json!({"ok": true, "result": null}));
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve(stream, token, tx, egui::Context::default()));
|
||||
}
|
||||
});
|
||||
let session = |lines: &[String]| {
|
||||
let mut s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
for l in lines {
|
||||
let _ = writeln!(s, "{l}");
|
||||
}
|
||||
let _ = s.shutdown(std::net::Shutdown::Write);
|
||||
let mut text = String::new();
|
||||
let _ = s.read_to_string(&mut text);
|
||||
text.lines().map(|l| serde_json::from_str::<Value>(l).unwrap()).collect::<Vec<_>>()
|
||||
};
|
||||
for first in [
|
||||
json!({"id": 1, "method": "app.quit", "params": {"force": true}}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": "0123456789abcdef0123456789abcdee"}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": "0123"}).to_string(),
|
||||
json!({"id": 1, "method": "app.quit", "token": 7}).to_string(),
|
||||
"not json".to_string(),
|
||||
] {
|
||||
let replies = session(&[first.clone(), json!({"id": 2, "method": "app.quit", "token": token}).to_string()]);
|
||||
assert_eq!(replies.len(), 1, "{first}: closed after refusing: {replies:?}");
|
||||
assert!(replies[0]["error"].as_str().is_some_and(|e| e.starts_with("unauthorized")), "{first}: {replies:?}");
|
||||
}
|
||||
assert!(ran.lock().unwrap().is_empty(), "nothing ran without the token");
|
||||
let replies = session(&[
|
||||
json!({"id": 1, "method": "ui.inspect", "token": token}).to_string(),
|
||||
json!({"id": 2, "method": "engine.commands"}).to_string(),
|
||||
]);
|
||||
assert_eq!(replies.len(), 2, "{replies:?}");
|
||||
assert!(replies.iter().all(|r| r["ok"] == true), "{replies:?}");
|
||||
assert_eq!(*ran.lock().unwrap(), ["ui.inspect", "engine.commands"]);
|
||||
}
|
||||
|
||||
/// A request longer than the cap is refused instead of allocated.
|
||||
#[test]
|
||||
fn lines_past_the_cap_are_refused() {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[path = "../../app_icon.rs"]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
mod control_server;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
@@ -286,7 +286,7 @@ fn main() -> eframe::Result {
|
||||
b.with_x11();
|
||||
}));
|
||||
}
|
||||
eframe::run_native(
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
@@ -344,7 +344,9 @@ fn main() -> eframe::Result {
|
||||
space_mouse,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
result
|
||||
}
|
||||
|
||||
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
|
||||
|
||||
@@ -1,41 +1,98 @@
|
||||
# SolidCraft macOS icon
|
||||
# Application resources
|
||||
|
||||
`SolidCraft.icon/` is the approved Icon Composer source, containing separate
|
||||
grid and solid SVG layers and the owner's appearance settings. The saturated
|
||||
blue solid has transparent geometry cutouts instead of painted edge lines.
|
||||
The solid and grid are both scaled to 90%, with separate Default and Dark grid artwork.
|
||||
Icon Composer supplies the system backdrop and Mono material effects.
|
||||
Artwork is original and attributed in `ATTRIBUTION.md`.
|
||||
`assets/` is the design resource home for SolidCraft.
|
||||
[ATTRIBUTION.md](../ATTRIBUTION.md) is the file inventory and provenance record.
|
||||
Keep one editable master per resource and place shipped derivatives in `exports/`.
|
||||
|
||||
`Assets.car` is compiled by Apple's `actool` and supplies the layered icon on
|
||||
macOS 26 and later. It is checked in so the existing release workflow can still
|
||||
package on older Xcode runners. After editing and saving `SolidCraft.icon`,
|
||||
regenerate the catalog with Xcode 26 or later from the repository root:
|
||||
|
||||
```sh
|
||||
mkdir -p target/macos-icon
|
||||
xcrun actool assets/SolidCraft.icon \
|
||||
--compile target/macos-icon --platform macosx \
|
||||
--minimum-deployment-target 11.0 --app-icon SolidCraft \
|
||||
--output-partial-info-plist target/macos-icon/icon-info.plist \
|
||||
--output-format human-readable-text
|
||||
cp target/macos-icon/Assets.car assets/Assets.car
|
||||
cp target/macos-icon/SolidCraft.icns assets/icon.icns
|
||||
```text
|
||||
assets/
|
||||
icons/
|
||||
app/
|
||||
shared/source/ # original Rust mark and executable ICO encoder
|
||||
macos/source/SolidCraft.icon/ # approved Composer project and linked SVGs
|
||||
macos/exports/ # paired Assets.car and icon.icns
|
||||
windows/exports/ # executable DIB ICO and installer PNG ICO
|
||||
linux/exports/hicolor/ # Linux/FreeBSD package images, 16–512 px
|
||||
ui/source/ # original drawing library and other UI glyphs
|
||||
branding/web/source/og.png # shared social preview card
|
||||
styles/ui/palettes.rs # light/dark defaults
|
||||
styles/web/loading.css # web loading/canvas styles
|
||||
appearances/library.rs # built-in visual presets
|
||||
materials/library.rs # physical defaults and colours
|
||||
shaders/viewport.wgsl # original viewport shader
|
||||
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
|
||||
locales/{en,de,ja,ko}.json # shared offline catalogs
|
||||
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
|
||||
archive/icons/macos-v1/
|
||||
source/icon.vectorcraft # earlier editable work, retained
|
||||
exports/ # all historical SVG, PNG and iconset exports
|
||||
```
|
||||
|
||||
`icon.icns` is the older-macOS fallback generated by the same Apple compiler
|
||||
from the approved composition, retaining its layer sizes and positions.
|
||||
Regenerate both compiled resources together with the commands above.
|
||||
`icon.vectorcraft`, `icon.svg`, `icon.png` and `icon.iconset/` retain the earlier
|
||||
VectorCraft artwork; packaging uses the Icon Composer compilation instead.
|
||||
## Editing and regeneration
|
||||
|
||||
Only `packaging/macos/package.sh` consumes these resources. Its existing signed
|
||||
app/DMG workflow copies the catalog and ICNS into `Contents/Resources`, and
|
||||
`Info.plist` references `SolidCraft` through `CFBundleIconName` and
|
||||
`CFBundleIconFile`. The macOS viewport opts out of eframe's runtime bitmap so
|
||||
Finder, the Dock and the application switcher use the bundle icon. Other
|
||||
platforms retain the existing code-drawn icon and packaging.
|
||||
[UI artwork](icons/ui/README.md) identifies the drawing masters and their consumers.
|
||||
Edit geometry and IDs together in the original Rust source and review changes in
|
||||
both application themes. Palettes and model presets are also editable Rust data.
|
||||
|
||||
Design references: [Apple app-icon HIG](https://developer.apple.com/design/human-interface-guidelines/app-icons)
|
||||
and [Icon Composer documentation](https://developer.apple.com/documentation/xcode/creating-your-app-icon-using-icon-composer).
|
||||
No Apple artwork is shipped.
|
||||
The current macOS master is `icons/app/macos/source/SolidCraft.icon/`, including
|
||||
its linked SVG layers and appearance settings. The earlier VectorCraft document
|
||||
is preserved in the archive. No other native design projects were available for
|
||||
the current UI set or preview card. Keep any future `.vectorcraft`, `.ai` or other
|
||||
native work and linked inputs under that resource's `source/work/`, with an
|
||||
attribution row; retain the editable document alongside its exports.
|
||||
|
||||
Regenerate all exports of the shared Windows/Linux/FreeBSD mark together:
|
||||
|
||||
```sh
|
||||
cargo xtask app-icons
|
||||
```
|
||||
|
||||
For macOS, use Xcode with Icon Composer support:
|
||||
|
||||
```sh
|
||||
mkdir -p target/resources/macos-icon
|
||||
xcrun actool assets/icons/app/macos/source/SolidCraft.icon \
|
||||
--compile target/resources/macos-icon --platform macosx \
|
||||
--minimum-deployment-target 11.0 --app-icon SolidCraft \
|
||||
--output-partial-info-plist target/resources/macos-icon/icon-info.plist \
|
||||
--output-format human-readable-text
|
||||
cp target/resources/macos-icon/Assets.car assets/icons/app/macos/exports/Assets.car
|
||||
cp target/resources/macos-icon/SolidCraft.icns assets/icons/app/macos/exports/icon.icns
|
||||
```
|
||||
|
||||
Review and commit source changes and required exports together. Temporary renders,
|
||||
tool caches and package staging belong under ignored `target/` or `dist/`.
|
||||
Normal builds use the committed exports and require no design applications.
|
||||
The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
|
||||
|
||||
## Platform bindings
|
||||
|
||||
| Platform | Resources and output paths |
|
||||
| --- | --- |
|
||||
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
|
||||
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
|
||||
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
|
||||
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
|
||||
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
|
||||
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
|
||||
| Web | HTML/Trunk stays in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/` and uses the collected CSS; no favicon/PWA icon currently exists |
|
||||
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
|
||||
|
||||
## Code and provenance
|
||||
|
||||
Resources remain compiled by their owning layer through existing APIs:
|
||||
`ui-egui::icons::paint` and `Tokens` at L5, model presets in L2 `doc`, and sketch
|
||||
fonts at L1. State, projection, interaction, commands and GPU bindings stay in
|
||||
their current modules. Embedded data uses `CARGO_MANIFEST_DIR`; application code
|
||||
needs no runtime resource registry or repository lookup.
|
||||
|
||||
`cargo xtask assets` requires an exact attribution row for every collected file
|
||||
except Markdown documentation, and continues checking file assets elsewhere in
|
||||
the repository. This includes native work, Rust masters, catalogs, shaders and
|
||||
platform templates. Packaging selects explicit current files and fails when
|
||||
required inputs are absent. Archived artwork has no production consumers.
|
||||
|
||||
Dependency-owned egui fonts and vendored renderer shaders stay with their
|
||||
libraries; their licences remain in `THIRD-PARTY-LICENSES.txt` and `ATTRIBUTION.md`.
|
||||
System fonts, optional packs, user fonts/canvases/decals/materials and native OS
|
||||
controls retain external ownership. Root licence/notice texts stay canonical.
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Built-in model appearances: names, sRGB colours and metalness.
|
||||
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
|
||||
("Aluminum - Satin", [204, 208, 214], 1.0),
|
||||
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
|
||||
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
|
||||
("Steel - Satin", [158, 164, 172], 1.0),
|
||||
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
|
||||
("Cast Iron", [104, 104, 110], 1.0),
|
||||
("Brass - Polished", [208, 172, 88], 1.0),
|
||||
("Copper", [204, 122, 80], 1.0),
|
||||
("Titanium", [150, 148, 158], 1.0),
|
||||
("ABS - White", [236, 236, 230], 1.0),
|
||||
("ABS - Black", [36, 36, 38], 1.0),
|
||||
("ABS - Red", [196, 40, 40], 1.0),
|
||||
("ABS - Blue", [40, 90, 186], 1.0),
|
||||
("ABS - Yellow", [236, 196, 40], 1.0),
|
||||
("ABS - Green", [52, 150, 80], 1.0),
|
||||
("Rubber - Black", [28, 28, 28], 1.0),
|
||||
("Glass - Clear", [190, 220, 228], 0.25),
|
||||
("Glass - Smoked", [90, 96, 104], 0.45),
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,48 @@
|
||||
//! Original executable ICO encoder: DIB images and transparency masks.
|
||||
use super::app_icon;
|
||||
|
||||
pub fn encode() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
|
||||
let mut images = Vec::new();
|
||||
for size in sizes {
|
||||
let rgba = app_icon::rgba(size)?;
|
||||
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
|
||||
let mask_stride = size.div_ceil(32) * 4;
|
||||
let mut dib = Vec::new();
|
||||
dib.extend_from_slice(&40u32.to_le_bytes());
|
||||
dib.extend_from_slice(&(size as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
|
||||
dib.extend_from_slice(&1u16.to_le_bytes());
|
||||
dib.extend_from_slice(&32u16.to_le_bytes());
|
||||
dib.extend_from_slice(&[0; 24]);
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
for p in row.as_chunks::<4>().0.iter() {
|
||||
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||||
}
|
||||
}
|
||||
for row in rgba.chunks_exact(size * 4).rev() {
|
||||
let mut mask = vec![0u8; mask_stride];
|
||||
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
if p[3] == 0 {
|
||||
mask[x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
dib.extend_from_slice(&mask);
|
||||
}
|
||||
images.push((size, dib));
|
||||
}
|
||||
let mut ico = vec![0, 0, 1, 0];
|
||||
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
|
||||
let mut offset = 6 + 16 * images.len();
|
||||
for (size, data) in &images {
|
||||
let dim = if *size == 256 { 0 } else { *size as u8 };
|
||||
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
|
||||
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&(offset as u32).to_le_bytes());
|
||||
offset += data.len();
|
||||
}
|
||||
for (_, data) in images {
|
||||
ico.extend_from_slice(&data);
|
||||
}
|
||||
Ok(ico)
|
||||
}
|
||||
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,22 @@
|
||||
# UI artwork
|
||||
|
||||
Edit the Rust drawing masters in `source/`; the UI crate compiles them through
|
||||
`crates/ui-egui/src/icons.rs`. Use the 24 × 24 grid and semantic ink/fill/accent
|
||||
colours. Icon IDs and their geometry live together in `source/icons.rs`;
|
||||
unknown IDs retain the existing fallback. Add new artwork there without
|
||||
maintaining a separate ID registry.
|
||||
|
||||
| Source | Artwork | Consumers |
|
||||
| --- | --- | --- |
|
||||
| `icons.rs` | Toolbar, browser, menu and constraint icons | `icons::paint` |
|
||||
| `glyphs.rs` | Brand mark, captions, transport, badges, pin, caret, disclosure, radial centre and removal cross | toolbar, titlebar, timeline, browser, menus |
|
||||
| `navigation.rs` | Signed axes, cube faces/labels and arrows | viewport/navigation cube |
|
||||
| `markers.rs` | Constraint mappings/letters, snap markers and dimension arrowheads | sketch, assembly and dimensions |
|
||||
| `pointer.rs` | Agent pointer and tag | agent cursor |
|
||||
|
||||
Widget state, layout, projection, picking and commands stay with their widgets.
|
||||
Review drawing changes in the actual application in both light and dark themes.
|
||||
No separate native design projects were available for this set. Preserve any
|
||||
future native work and linked inputs under `source/work/`, with attribution.
|
||||
Original artwork is MIT OR Apache-2.0; caption geometry retains VectorCraft
|
||||
attribution in the root `ATTRIBUTION.md`.
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Collected glyph artwork. Caption geometry adapted from VectorCraft (MIT OR Apache-2.0).
|
||||
use crate::titlebar::Caption;
|
||||
use egui::{Color32, Pos2, Rect, Stroke, Ui, pos2, vec2};
|
||||
|
||||
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
let tri = |dir: f32, dx: f32| {
|
||||
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
|
||||
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
|
||||
};
|
||||
let bar = |dx: f32| p.line_segment([pos2(c.x + dx, c.y - 5.0), pos2(c.x + dx, c.y + 5.0)], Stroke::new(1.6, col));
|
||||
match what {
|
||||
"first" => {
|
||||
bar(-5.0);
|
||||
tri(-1.0, 1.0);
|
||||
}
|
||||
"back" => tri(-1.0, 0.0),
|
||||
"forward" => tri(1.0, 0.0),
|
||||
_ => {
|
||||
tri(1.0, -1.0);
|
||||
bar(5.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
|
||||
let p = ui.painter();
|
||||
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
|
||||
match c {
|
||||
Caption::Minimize => {
|
||||
p.line_segment([g.left_center(), g.right_center()], s);
|
||||
}
|
||||
// Restore: a front square with the corner of the one behind it.
|
||||
Caption::Maximize if maximized => {
|
||||
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
|
||||
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
|
||||
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
|
||||
}
|
||||
Caption::Maximize => {
|
||||
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
|
||||
}
|
||||
Caption::Close => {
|
||||
p.line_segment([g.left_top(), g.right_bottom()], s);
|
||||
p.line_segment([g.right_top(), g.left_bottom()], s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn badge(p: &egui::Painter, r: Rect, name: &str, c: Color32) {
|
||||
let s = Stroke::new(1.3, c);
|
||||
match name {
|
||||
"lock" => {
|
||||
let body = Rect::from_min_max(pos2(r.left() + 1.5, r.center().y - 0.5), pos2(r.right() - 1.5, r.bottom() - 0.5));
|
||||
p.rect_filled(body, 1.5, c);
|
||||
let top = pos2(r.center().x, r.top() + 4.0);
|
||||
p.add(egui::Shape::line(
|
||||
(0..=8)
|
||||
.map(|k| {
|
||||
let a = std::f32::consts::PI * (k as f32 / 8.0);
|
||||
pos2(top.x - 3.2 * a.cos(), top.y - 3.2 * a.sin() + 1.0)
|
||||
})
|
||||
.chain([pos2(top.x + 3.2, body.top())])
|
||||
.collect(),
|
||||
s,
|
||||
));
|
||||
p.line_segment([pos2(top.x - 3.2, top.y + 1.0), pos2(top.x - 3.2, body.top())], s);
|
||||
}
|
||||
"pin" => {
|
||||
let head = pos2(r.center().x + 2.0, r.top() + 4.0);
|
||||
p.circle_filled(head, 3.2, c);
|
||||
p.line_segment([head, pos2(r.left() + 1.5, r.bottom() - 1.0)], Stroke::new(1.6, c));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
|
||||
let c = r.center();
|
||||
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
|
||||
// Local coordinates: the pin points down, its head on top.
|
||||
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
|
||||
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
|
||||
let stroke = Stroke::new(1.4, col);
|
||||
if on {
|
||||
p.add(egui::Shape::convex_polygon(head, col, stroke));
|
||||
} else {
|
||||
p.add(egui::Shape::closed_line(head, stroke));
|
||||
}
|
||||
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
|
||||
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
|
||||
}
|
||||
|
||||
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
|
||||
}
|
||||
|
||||
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
let s = 20.0 / 412.217;
|
||||
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
|
||||
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
|
||||
let face = |col: Color32, pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
|
||||
};
|
||||
let (light, mid, dark) = (Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197));
|
||||
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
|
||||
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]);
|
||||
face(mid, &[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)]);
|
||||
face(mid, &[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)]);
|
||||
face(dark, &[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)]);
|
||||
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
|
||||
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
|
||||
// White edge lines.
|
||||
let edge = Stroke::new(10.0 * s, Color32::WHITE);
|
||||
let line = |pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::line(m(pts), edge));
|
||||
};
|
||||
line(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)]);
|
||||
line(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)]);
|
||||
line(&[(177.0, 206.1), (263.8, 156.0)]);
|
||||
p.add(egui::Shape::closed_line(
|
||||
m(&[
|
||||
(178.5, 5.8),
|
||||
(5.0, 105.9),
|
||||
(5.0, 206.1),
|
||||
(91.7, 256.2),
|
||||
(91.7, 356.4),
|
||||
(178.5, 406.4),
|
||||
(352.0, 306.3),
|
||||
(352.0, 206.1),
|
||||
(265.2, 156.0),
|
||||
(265.2, 55.9),
|
||||
]),
|
||||
edge,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn cross(p: &egui::Painter, r: Rect, s: Stroke) {
|
||||
p.line_segment([r.left_top(), r.right_bottom()], s);
|
||||
p.line_segment([r.right_top(), r.left_bottom()], s);
|
||||
}
|
||||
pub(crate) fn radial_centre(p: &egui::Painter, c: Pos2, direction: Option<(egui::Vec2, bool)>, t: crate::theme::Tokens) {
|
||||
p.circle(c, 15.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, t.border));
|
||||
if let Some((d, enabled)) = direction {
|
||||
p.line_segment([c + d * 6.0, c + d * 15.0], Stroke::new(3.0, if enabled { t.accent } else { t.border }));
|
||||
} else {
|
||||
p.circle_filled(c, 3.0, t.text_dim);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn disclosure(p: &egui::Painter, ar: Rect, open: bool, ink: Color32) {
|
||||
let pts = if open {
|
||||
vec![pos2(ar.left() + 2.0, ar.top() + 4.0), pos2(ar.right() - 2.0, ar.top() + 4.0), pos2(ar.center().x, ar.bottom() - 3.0)]
|
||||
} else {
|
||||
vec![pos2(ar.left() + 4.0, ar.top() + 2.0), pos2(ar.right() - 3.0, ar.center().y), pos2(ar.left() + 4.0, ar.bottom() - 2.0)]
|
||||
};
|
||||
p.add(egui::Shape::convex_polygon(pts, ink, Stroke::NONE));
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Constraint, assembly snap and dimension marker artwork.
|
||||
use crate::dialogs_assembly::SnapKind;
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
|
||||
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
|
||||
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
|
||||
let halo = Stroke::new(4.5, Color32::from_black_alpha(150));
|
||||
let white = Stroke::new(1.5, Color32::WHITE);
|
||||
if let Some(tip) = tip {
|
||||
let d = (tip - c).normalized();
|
||||
let n = vec2(-d.y, d.x);
|
||||
let head = vec![tip + d * 2.0, tip - d * 9.0 + n * 5.0, tip - d * 9.0 - n * 5.0];
|
||||
painter.line_segment([c, tip], halo);
|
||||
painter.add(egui::Shape::convex_polygon(head.clone(), Color32::TRANSPARENT, halo));
|
||||
painter.line_segment([c, tip], Stroke::new(2.0, col));
|
||||
painter.add(egui::Shape::convex_polygon(head, col, Stroke::NONE));
|
||||
}
|
||||
let shape = |fill: Color32, stroke: Stroke, grow: f32| -> egui::Shape {
|
||||
match kind {
|
||||
SnapKind::FaceCenter => egui::Shape::rect_filled(egui::Rect::from_center_size(c, vec2(12.0 + grow, 12.0 + grow)), 1.5, fill),
|
||||
SnapKind::CircleCenter => egui::Shape::circle_filled(c, 7.0 + grow / 2.0, fill),
|
||||
SnapKind::EdgeMid => {
|
||||
let r = 8.0 + grow / 2.0;
|
||||
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r * 0.93, r * 0.7), c + vec2(-r * 0.93, r * 0.7)], fill, stroke)
|
||||
}
|
||||
SnapKind::Vertex => {
|
||||
let r = 7.5 + grow / 2.0;
|
||||
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)], fill, stroke)
|
||||
}
|
||||
}
|
||||
};
|
||||
painter.add(shape(Color32::from_black_alpha(150), Stroke::NONE, 4.0));
|
||||
painter.add(shape(col, Stroke::NONE, 0.0));
|
||||
match kind {
|
||||
SnapKind::FaceCenter => {
|
||||
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(12.0, 12.0)), 1.5, white, egui::StrokeKind::Middle);
|
||||
}
|
||||
SnapKind::CircleCenter => {
|
||||
painter.circle_stroke(c, 7.0, white);
|
||||
painter.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], Stroke::new(1.2, Color32::WHITE));
|
||||
painter.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], Stroke::new(1.2, Color32::WHITE));
|
||||
}
|
||||
SnapKind::EdgeMid | SnapKind::Vertex => {
|
||||
painter.add(shape(Color32::TRANSPARENT, white, 0.0));
|
||||
}
|
||||
}
|
||||
// What it snaps to (hovered), or which component it is (picked): a small label.
|
||||
if let Some(tag) = tag {
|
||||
let t = Tokens::get();
|
||||
let font = egui::FontId::proportional(11.5);
|
||||
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
|
||||
let at = c + vec2(12.0, 10.0);
|
||||
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
|
||||
painter.rect(r, 3.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, col), egui::StrokeKind::Inside);
|
||||
painter.galley(at, g, t.text);
|
||||
}
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
"Parallel" => "c_parallel",
|
||||
"Perpendicular" => "c_perpendicular",
|
||||
"Collinear" => "c_collinear",
|
||||
"Tangent" => "c_tangent",
|
||||
"Smooth" => "c_smooth",
|
||||
"Equal" => "c_equal",
|
||||
"Concentric" => "c_concentric",
|
||||
"MidPoint" => "c_midpoint",
|
||||
"Symmetry" => "c_symmetry",
|
||||
"Fix" => "c_fix",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
pub(crate) fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
pub(crate) fn dimension_arrow(painter: &egui::Painter, t: Pos2, back: Pos2, col: Color32) {
|
||||
let d = (t - back).normalized();
|
||||
let n = vec2(-d.y, d.x);
|
||||
painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE));
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Align2, Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
pub(crate) struct Facet {
|
||||
pub(crate) direction: Vec3,
|
||||
pub(crate) points: Vec<Pos2>,
|
||||
pub(crate) outer_edges: Vec<bool>,
|
||||
pub(crate) centre: Pos2,
|
||||
pub(crate) label: String,
|
||||
pub(crate) text_axes: Option<(egui::Vec2, egui::Vec2)>,
|
||||
pub(crate) depth: f64,
|
||||
}
|
||||
// Private marker for label meshes with atlas-pixel UVs. The output hook replaces
|
||||
// it with the font texture before any renderer sees the frame.
|
||||
pub(crate) const LABEL_PIXELS: egui::TextureId = egui::TextureId::User(u64::from_le_bytes(*b"cubetext"));
|
||||
|
||||
struct LabelAtlas;
|
||||
|
||||
impl egui::plugin::Plugin for LabelAtlas {
|
||||
fn debug_name(&self) -> &'static str {
|
||||
"navigation cube label atlas"
|
||||
}
|
||||
|
||||
fn output_hook(&mut self, ctx: &egui::Context, output: &mut egui::FullOutput) {
|
||||
let size = ctx.fonts(|f| f.font_image_size());
|
||||
let normalize = vec2(1.0 / size[0].max(1) as f32, 1.0 / size[1].max(1) as f32);
|
||||
for shape in &mut output.shapes {
|
||||
if let Shape::Mesh(mesh) = &mut shape.shape
|
||||
&& mesh.texture_id == LABEL_PIXELS
|
||||
{
|
||||
let mesh = std::sync::Arc::make_mut(mesh);
|
||||
mesh.texture_id = egui::TextureId::default();
|
||||
for v in &mut mesh.vertices {
|
||||
v.uv = (v.uv.to_vec2() * normalize).to_pos2();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warp the font mesh into the projected face, so labels turn with the model.
|
||||
pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, font_size: f32) {
|
||||
let Some((r, down)) = face.text_axes else { return };
|
||||
let opacity = ((face.depth - 0.18) / 0.17).clamp(0.0, 1.0) as f32;
|
||||
if opacity <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ink = ink.gamma_multiply(opacity);
|
||||
let galley = painter.layout_no_wrap(face.label.clone(), FontId::proportional(font_size), ink);
|
||||
let centre = galley.rect.center();
|
||||
// Keep atlas-pixel UVs until the frame is complete: another label or widget
|
||||
// can grow the atlas after this mesh is created, invalidating early UVs.
|
||||
painter.ctx().add_plugin(LabelAtlas);
|
||||
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
|
||||
let atlas = painter.ctx().fonts(|f| f.font_image_size());
|
||||
let primitives =
|
||||
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
|
||||
for primitive in primitives {
|
||||
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
|
||||
mesh.texture_id = LABEL_PIXELS;
|
||||
for v in &mut mesh.vertices {
|
||||
v.uv = pos2(v.uv.x * atlas[0] as f32, v.uv.y * atlas[1] as f32);
|
||||
let local = v.pos - centre;
|
||||
v.pos = face.centre + r * local.x + down * (local.y * 1.35);
|
||||
}
|
||||
painter.add(Shape::mesh(mesh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint_cube(painter: &egui::Painter, faces: &[Facet], hovered: Option<usize>, t: Tokens, labels: bool, scale: f32) {
|
||||
// A shared, opaque triangle mesh has no antialiasing fringes between facets.
|
||||
// Per-polygon fringes and miter joins overlap as a face becomes edge-on.
|
||||
let mut fill_mesh = egui::epaint::Mesh::default();
|
||||
let mut edges: Vec<(Pos2, Pos2, usize, bool)> = Vec::new();
|
||||
let hovered_direction = hovered.and_then(|i| faces.get(i)).map(|f| f.direction);
|
||||
for face in faces {
|
||||
let base = t.cube_face;
|
||||
let shade = (0.72 + 0.28 * face.depth.min(1.0)) as f32;
|
||||
let highlighted = hovered_direction == Some(face.direction);
|
||||
let fill = if highlighted {
|
||||
t.cube_hover
|
||||
} else {
|
||||
Color32::from_rgb((f32::from(base.r()) * shade) as u8, (f32::from(base.g()) * shade) as u8, (f32::from(base.b()) * shade) as u8)
|
||||
};
|
||||
let centre = fill_mesh.vertices.len() as u32;
|
||||
fill_mesh.colored_vertex(face.centre, fill);
|
||||
for &p in &face.points {
|
||||
fill_mesh.colored_vertex(p, fill);
|
||||
}
|
||||
for (j, (&a, &b)) in face.points.iter().zip(face.points.iter().cycle().skip(1)).take(face.points.len()).enumerate() {
|
||||
fill_mesh.add_triangle(centre, centre + 1 + j as u32, centre + 1 + ((j + 1) % face.points.len()) as u32);
|
||||
// Coplanar picking regions have no visible seams. Only actual cube
|
||||
// edges and the hovered target's boundary receive an outline.
|
||||
let outer = face.outer_edges.get(j).copied().unwrap_or(false);
|
||||
if !highlighted && !outer {
|
||||
continue;
|
||||
}
|
||||
if let Some(edge) = edges
|
||||
.iter_mut()
|
||||
.find(|(p, q, _, _)| (p.distance_sq(a) < 1e-6 && q.distance_sq(b) < 1e-6) || (p.distance_sq(b) < 1e-6 && q.distance_sq(a) < 1e-6))
|
||||
{
|
||||
edge.2 += usize::from(highlighted);
|
||||
edge.3 |= outer;
|
||||
} else {
|
||||
edges.push((a, b, usize::from(highlighted), outer));
|
||||
}
|
||||
}
|
||||
}
|
||||
painter.add(Shape::mesh(fill_mesh));
|
||||
for (a, b, highlights, outer) in edges {
|
||||
// Two highlighted coplanar patches share an internal subdivision, not
|
||||
// a target boundary. Actual cube edges remain visible across faces.
|
||||
if !outer && highlights != 1 {
|
||||
continue;
|
||||
}
|
||||
let highlighted = highlights > 0;
|
||||
painter.line_segment([a, b], Stroke::new(1.0, if highlighted { t.accent } else { t.cube_border }));
|
||||
}
|
||||
if labels {
|
||||
for face in faces {
|
||||
face_text(painter, face, t.cube_text, 14.0 * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn arrow(painter: &egui::Painter, at: Pos2, direction: egui::Vec2, hover: bool, t: Tokens) {
|
||||
let side = vec2(-direction.y, direction.x);
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![at + direction * 7.0, at - direction * 5.0 + side * 7.0, at - direction * 5.0 - side * 7.0],
|
||||
if hover { t.accent_soft } else { t.cube_face },
|
||||
Stroke::new(1.0, if hover { t.accent } else { t.cube_border }),
|
||||
));
|
||||
}
|
||||
pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, hover: bool, t: Tokens, scale: f32) {
|
||||
let (start, end) =
|
||||
if clockwise { (-std::f32::consts::FRAC_PI_4 + 0.12, 0.0) } else { (-std::f32::consts::FRAC_PI_4 - 0.12, -std::f32::consts::FRAC_PI_2) };
|
||||
let points: Vec<_> = (0..=16)
|
||||
.map(|i| {
|
||||
let angle = egui::lerp(start..=end, f32::from(i as u8) / 16.0);
|
||||
at + vec2(angle.cos(), angle.sin()) * (32.0 * scale)
|
||||
})
|
||||
.collect();
|
||||
painter.add(Shape::line(points.clone(), Stroke::new(5.0 * scale, if hover { t.accent } else { t.cube_border })));
|
||||
painter.add(Shape::line(points.clone(), Stroke::new(3.0 * scale, if hover { t.accent_soft } else { t.cube_face })));
|
||||
if let Some(tip) = points.last() {
|
||||
let sign = if clockwise { 1.0 } else { -1.0 };
|
||||
arrow(painter, *tip, vec2(-end.sin(), end.cos()) * (sign * scale), hover, t);
|
||||
}
|
||||
}
|
||||
pub(crate) const AXIS_RADIUS: f32 = 11.0;
|
||||
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
|
||||
let col = if d.x != 0.0 {
|
||||
solidcraft_engine::view::colors::AXIS_X
|
||||
} else if d.y != 0.0 {
|
||||
solidcraft_engine::view::colors::AXIS_Y
|
||||
} else {
|
||||
solidcraft_engine::view::colors::AXIS_Z
|
||||
};
|
||||
let col = Color32::from_rgb(col.0, col.1, col.2);
|
||||
let positive = !name.starts_with('-');
|
||||
let ink = if depth < -0.01 { col.gamma_multiply(0.55) } else { col };
|
||||
painter.line_segment([c, at], Stroke::new(if positive { 2.0 } else { 1.0 }, ink));
|
||||
painter.circle_filled(at, AXIS_RADIUS, if hovered { t.accent_soft } else { t.overlay });
|
||||
painter.circle_stroke(at, AXIS_RADIUS, Stroke::new(if hovered { 2.0 } else { 1.0 }, if hovered { t.accent } else { ink }));
|
||||
painter.text(at, Align2::CENTER_CENTER, name.to_uppercase(), FontId::proportional(11.0), ink);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Original agent pointer and label. Animation stays with the UI controller.
|
||||
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
|
||||
pub(crate) const AGENT: Color32 = Color32::from_rgb(168, 85, 247);
|
||||
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
|
||||
// An arrow pointer, tip at `p`.
|
||||
let pts: Vec<Pos2> =
|
||||
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
|
||||
painter.add(Shape::convex_polygon(pts.clone(), AGENT, Stroke::NONE));
|
||||
painter.add(Shape::closed_line(pts, Stroke::new(1.5, Color32::WHITE)));
|
||||
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(11.0), Color32::WHITE);
|
||||
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
|
||||
painter.rect_filled(r, 4.0, AGENT);
|
||||
painter.galley(r.min + vec2(5.0, 2.0), label, Color32::WHITE);
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
{
|
||||
"browser.body_name": "Körper {number}",
|
||||
"browser.component_name": "Komponente {number}",
|
||||
"browser.parameters": "Parameter ({count})",
|
||||
"browser.precision": "Genauigkeit: {precision}",
|
||||
"browser.sketch_name": "Skizze {number}",
|
||||
"browser.units": "Einheiten: {units}",
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.select": "↖ Auswählen",
|
||||
"dialog.selected_count": "↖ {count} ausgewählt",
|
||||
"dialog.step": "Schritt",
|
||||
"dialog.selection_step": "{step} von {total}: {input} auswählen",
|
||||
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
|
||||
"document.save_changes": "Änderungen in {name} speichern?",
|
||||
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
|
||||
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
|
||||
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
|
||||
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
|
||||
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
|
||||
"settings.reset_title": "Einstellungen wurden geändert",
|
||||
"sketch.degrees_of_freedom": "{count} Freiheitsgrade",
|
||||
"sketch.draw_curves": "Als {linetype} zeichnen",
|
||||
"sketch.make_selection": "Auswahl als {linetype} markieren",
|
||||
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
|
||||
"ui.3d_sketch": "3D-Skizze",
|
||||
"ui.about": "Über",
|
||||
"ui.activate": "Aktivieren",
|
||||
"ui.active_component": "Aktive Komponente",
|
||||
"ui.agent_cursor": "Agent-Cursor",
|
||||
"ui.agent_cursor_speed": "Agent-Cursor-Geschwindigkeit",
|
||||
"ui.all": "Alle",
|
||||
"ui.analysis": "Analyse",
|
||||
"ui.angle": "Winkel",
|
||||
"ui.angle_decimals": "Winkel-Dezimalstellen",
|
||||
"ui.animate_view_changes": "Ansichtsänderungen animieren",
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animationsgeschwindigkeit",
|
||||
"ui.anti_aliasing": "Kantenglättung",
|
||||
"ui.apply": "Anwenden",
|
||||
"ui.arc": "Kreisbogen",
|
||||
"ui.assemble": "Zusammenbau",
|
||||
"ui.autosave_every": "Autospeichern alle",
|
||||
"ui.axes": "Achsen",
|
||||
"ui.axis": "Achse",
|
||||
"ui.base_point": "Basispunkt",
|
||||
"ui.bodies": "Körper",
|
||||
"ui.box": "Block",
|
||||
"ui.break": "Unterbrechen",
|
||||
"ui.cancel": "Abbrechen",
|
||||
"ui.canvases": "Kanvas",
|
||||
"ui.center": "Mittelpunkt",
|
||||
"ui.centerline": "Mittellinie",
|
||||
"ui.chain": "Kette",
|
||||
"ui.chamfer": "Abschrägung",
|
||||
"ui.chamfer_type": "Fasentyp",
|
||||
"ui.change_workspace": "Arbeitsbereich wechseln",
|
||||
"ui.chord_length": "Sehnenlänge",
|
||||
"ui.curvature_g2": "Krümmungsstetig (G2)",
|
||||
"ui.equivalent_radius": "Äquivalenter Radius",
|
||||
"ui.circle": "Kreis",
|
||||
"ui.circular_pattern": "Radiales Muster",
|
||||
"ui.clear_recent": "Zuletzt verwendet leeren",
|
||||
"ui.clear_the_selection": "Auswahl aufheben",
|
||||
"ui.close": "Schließen",
|
||||
"ui.coil": "Schraube",
|
||||
"ui.coincident": "Koinzident",
|
||||
"ui.collapse": "Einklappen",
|
||||
"ui.collinear": "Kollinear",
|
||||
"ui.combine": "Kombinieren",
|
||||
"ui.coming_later": "bald verfügbar",
|
||||
"ui.concentric": "Konzentrisch",
|
||||
"ui.configurations": "Konfigurationen",
|
||||
"ui.conic_curve": "Kegelschnitt",
|
||||
"ui.constant": "Konstant",
|
||||
"ui.constraints_conflict": "Bedingungen konfliktreich",
|
||||
"ui.construct": "Konstruktion",
|
||||
"ui.construction": "Konstruktionslinie",
|
||||
"ui.copy": "Kopieren",
|
||||
"ui.corners": "Ecken",
|
||||
"ui.corner": "Ecke",
|
||||
"ui.counterbore": "Tiefenbohrung",
|
||||
"ui.countersink": "Senkung",
|
||||
"ui.create": "Erstellen",
|
||||
"ui.create_copy": "Kopie erstellen",
|
||||
"ui.create_sketch": "Skizze erstellen",
|
||||
"ui.current": "aktuell",
|
||||
"ui.curvature": "Krümmung",
|
||||
"ui.curves": "Kurven",
|
||||
"ui.cut": "Ausschneiden",
|
||||
"ui.cylinder": "Zylinder",
|
||||
"ui.dark": "Dunkel",
|
||||
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
|
||||
"ui.degrees_of_freedom": "Freiheitsgrade",
|
||||
"ui.delete": "Löschen",
|
||||
"ui.depth": "Tiefe",
|
||||
"ui.design": "Design",
|
||||
"ui.diameter": "Durchmesser",
|
||||
"ui.dimension": "Maß",
|
||||
"ui.direction": "Richtung",
|
||||
"ui.distance": "Abstand",
|
||||
"ui.distance_1": "Abstand 1",
|
||||
"ui.distance_2": "Abstand 2",
|
||||
"ui.document_settings": "Dokumenteneinstellungen",
|
||||
"ui.don_t_save": "Nicht speichern",
|
||||
"ui.draft": "Abziehen",
|
||||
"ui.drawing": "Zeichnung",
|
||||
"ui.drill_point": "Bohrpunkt",
|
||||
"ui.edit": "Bearbeiten",
|
||||
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
|
||||
"ui.ellipse": "Ellipse",
|
||||
"ui.end_radius": "Endradius",
|
||||
"ui.entities": "Elemente",
|
||||
"ui.equal": "Gleich",
|
||||
"ui.expand": "Aufklappen",
|
||||
"ui.export_ellipsis": "Exportieren...",
|
||||
"ui.extend": "Verlängern",
|
||||
"ui.extent_type": "Ausdehnungstyp",
|
||||
"ui.extrude": "Extrudieren",
|
||||
"ui.factor": "Faktor",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
"ui.feature_options": "Funktionsoptionen",
|
||||
"ui.features": "Funktionen",
|
||||
"ui.file": "Datei",
|
||||
"ui.fillet": "Vollrundung",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
"ui.flat": "Flach",
|
||||
"ui.flip": "Umkehren",
|
||||
"ui.front": "Vorne",
|
||||
"ui.fully_constrained": "Vollständig bestimmt",
|
||||
"ui.general": "Allgemein",
|
||||
"ui.graphics": "Grafik",
|
||||
"ui.heading_assemble": "ZUSAMMENBAU",
|
||||
"ui.heading_box": "BLOCK",
|
||||
"ui.heading_chamfer": "ABSCHRÄGUNG",
|
||||
"ui.heading_circular_pattern": "RADIALES MUSTER",
|
||||
"ui.heading_coil": "SCHRAUBE",
|
||||
"ui.heading_combine": "KOMBINIEREN",
|
||||
"ui.heading_construct": "KONSTRUKTION",
|
||||
"ui.heading_create": "ERSTELLEN",
|
||||
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
|
||||
"ui.heading_cylinder": "ZYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "ABZIEHEN",
|
||||
"ui.heading_edit": "BEARBEITEN",
|
||||
"ui.heading_extrude": "EXTRUDIEREN",
|
||||
"ui.heading_fillet": "VOLLRUNDUNG",
|
||||
"ui.heading_finish_sketch": "SKIZZE BEENDEN",
|
||||
"ui.heading_front": "VORNE",
|
||||
"ui.heading_hole": "BOHRUNG",
|
||||
"ui.heading_home": "START",
|
||||
"ui.heading_insert": "EINFÜGEN",
|
||||
"ui.heading_inspect": "PRÜFEN",
|
||||
"ui.heading_loft": "LOFT",
|
||||
"ui.heading_measure": "Messen",
|
||||
"ui.heading_mesh": "MESH",
|
||||
"ui.heading_mirror": "SPIEGELN",
|
||||
"ui.heading_modify": "ÄNDERN",
|
||||
"ui.heading_move": "VERSCHIEBEN",
|
||||
"ui.heading_offset": "ABSTAND",
|
||||
"ui.heading_move_copy": "VERSCHIEBEN/KOPIEREN",
|
||||
"ui.heading_offset_plane": "ABSTANDSEBENE",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMETER",
|
||||
"ui.heading_pattern_on_path": "PFADMUSTER",
|
||||
"ui.heading_pipe": "ROHR",
|
||||
"ui.heading_plane_at_angle": "EBENE IM WINKEL",
|
||||
"ui.heading_plastic": "PLASTIK",
|
||||
"ui.heading_rectangular_pattern": "RECHTECKMUSTER",
|
||||
"ui.heading_revolve": "DREHEN",
|
||||
"ui.heading_right": "RECHTS",
|
||||
"ui.heading_scale": "SKALIEREN",
|
||||
"ui.heading_section_analysis": "QUERSCHNITTSANALYSE",
|
||||
"ui.heading_select": "AUSWÄHLEN",
|
||||
"ui.heading_sheet_metal": "BLECH",
|
||||
"ui.heading_shell": "HÜLLE",
|
||||
"ui.heading_sketch": "SKIZZE",
|
||||
"ui.heading_sketch_palette": "SKIZZENPALETTE",
|
||||
"ui.heading_sketch_scale": "SKIZZE SKALIEREN",
|
||||
"ui.heading_solid": "KÖRPER",
|
||||
"ui.heading_sphere": "SPHÄRE",
|
||||
"ui.heading_split_body": "KÖRPER TRENNEN",
|
||||
"ui.heading_surface": "FLÄCHE",
|
||||
"ui.heading_sweep": "FÜHREN",
|
||||
"ui.heading_thread": "GEWINDE",
|
||||
"ui.heading_top": "OBEN",
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.help": "Hilfe",
|
||||
"ui.hide": "Ausblenden",
|
||||
"ui.hole": "Bohrung",
|
||||
"ui.hole_tap_type": "Gewindetyp",
|
||||
"ui.hole_type": "Bohrungstyp",
|
||||
"ui.home": "Start",
|
||||
"ui.horizontal_vertical": "Horizontal/Vertikal",
|
||||
"ui.insert": "Einfügen",
|
||||
"ui.inspect": "Prüfen",
|
||||
"ui.intersect": "Schnitt",
|
||||
"ui.join": "Verbinden",
|
||||
"ui.joints": "Gelenke",
|
||||
"ui.keyboard_shortcuts": "Tastenkürzel",
|
||||
"ui.language": "Sprache",
|
||||
"ui.length_decimals": "Längen-Dezimalstellen",
|
||||
"ui.light": "Hell",
|
||||
"ui.line": "Linie",
|
||||
"ui.linetype": "Linientyp",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Ansehen",
|
||||
"ui.manufacture": "Fertigung",
|
||||
"ui.measure": "Messen",
|
||||
"ui.midpoint": "Mittelpunkt",
|
||||
"ui.mirror": "Spiegeln",
|
||||
"ui.mirror_line": "Spiegelachse",
|
||||
"ui.modify": "Ändern",
|
||||
"ui.move": "Verschieben",
|
||||
"ui.multiple": "Mehrfach",
|
||||
"ui.named_views": "Benannte Ansichten",
|
||||
"ui.navigation": "Navigation",
|
||||
"ui.needs_design": "{command}: Öffnen Sie zuerst ein Design",
|
||||
"ui.new_body": "Neuer Körper",
|
||||
"ui.new_design": "Neues Design",
|
||||
"ui.normal": "Normal",
|
||||
"ui.object_type": "Objekttyp",
|
||||
"ui.objects": "Objekte",
|
||||
"ui.offer_to_stand_up_y_up_files": "Y-hoch-Dateien aufrecht anbieten",
|
||||
"ui.offset": "Abstand",
|
||||
"ui.offset_plane": "Abstandsebene",
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ellipsis": "Öffnen...",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
"ui.options": "Optionen",
|
||||
"ui.orbit_around": "Umkreisen um",
|
||||
"ui.origin": "Ursprung",
|
||||
"ui.palette_points": "Punkte",
|
||||
"ui.palette_profile": "Profil",
|
||||
"ui.pan_zoom_orbit": "Verschieben, zoomen, umkreisen",
|
||||
"ui.parallel": "Parallel",
|
||||
"ui.parameters": "Parameter",
|
||||
"ui.paste": "Einfügen",
|
||||
"ui.path": "Pfad",
|
||||
"ui.pattern_on_path": "Pfadmuster",
|
||||
"ui.perpendicular": "Senkrecht",
|
||||
"ui.pipe": "Rohr",
|
||||
"ui.placement": "Platzierung",
|
||||
"ui.plane": "Ebene",
|
||||
"ui.plane_at_angle": "Ebene im Winkel",
|
||||
"ui.point": "Punkt",
|
||||
"ui.point_under_the_cursor": "Punkt unter dem Cursor",
|
||||
"ui.points": "Punkte",
|
||||
"ui.polygon": "Polygon",
|
||||
"ui.preferences": "Einstellungen",
|
||||
"ui.profile": "Profil",
|
||||
"ui.profile_plane": "Profilebene",
|
||||
"ui.profiles": "Profile",
|
||||
"ui.project": "Projizieren",
|
||||
"ui.quantity_2": "Anzahl 2",
|
||||
"ui.quantity": "Anzahl",
|
||||
"ui.quit": "Beenden",
|
||||
"ui.radius": "Radius",
|
||||
"ui.radius_type": "Radius-Typ",
|
||||
"ui.rectangle": "Rechteck",
|
||||
"ui.rectangular_pattern": "Rechteckmuster",
|
||||
"ui.redo": "Wiederholen",
|
||||
"ui.render": "Rendern",
|
||||
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
|
||||
"ui.restore_defaults": "Standardeinstellungen wiederherstellen",
|
||||
"ui.reverse_zoom_direction": "Zoomrichtung umkehren",
|
||||
"ui.revolve": "Drehen",
|
||||
"ui.right": "Rechts",
|
||||
"ui.save": "Speichern",
|
||||
"ui.save_as": "Speichern unter",
|
||||
"ui.save_as_ellipsis": "Speichern unter...",
|
||||
"ui.scale": "Skalieren",
|
||||
"ui.section_analysis": "Querschnittsanalyse",
|
||||
"ui.select": "Auswählen",
|
||||
"ui.selection": "Auswahl",
|
||||
"ui.shell": "Hülle",
|
||||
"ui.show": "Einblenden",
|
||||
"ui.show_constraints": "Bedingungen",
|
||||
"ui.show_dimensions": "Maße",
|
||||
"ui.show_points": "Punkte anzeigen",
|
||||
"ui.show_profile": "Profil anzeigen",
|
||||
"ui.show_projected_geometries": "Projizierte Geometrien",
|
||||
"ui.simple": "Einfach",
|
||||
"ui.simulation": "Simulation",
|
||||
"ui.single": "Einzel",
|
||||
"ui.sketch": "Skizze",
|
||||
"ui.sketch_dimension": "Skizzenmaß",
|
||||
"ui.sketch_grid": "Skizzenraster",
|
||||
"ui.sketch_scale": "Skizzenskalierung",
|
||||
"ui.sketch_status": "Skizzenstatus",
|
||||
"ui.sketches": "Skizzen",
|
||||
"ui.slice": "Scheibe",
|
||||
"ui.slot": "Langloch",
|
||||
"ui.smooth": "Glatt",
|
||||
"ui.snap": "Einrasten",
|
||||
"ui.spacing_2": "Abstand 2",
|
||||
"ui.spacing": "Abstand",
|
||||
"ui.sphere": "Sphäre",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Körper trennen",
|
||||
"ui.start": "Start",
|
||||
"ui.start_radius": "Startradius",
|
||||
"ui.sweep": "Führen",
|
||||
"ui.symmetric": "Symmetrisch",
|
||||
"ui.symmetry": "Symmetrie",
|
||||
"ui.system": "System",
|
||||
"ui.tangent": "Tangential",
|
||||
"ui.taper_angle": "Kegelwinkel",
|
||||
"ui.tapped": "Gewindebohrung",
|
||||
"ui.text": "Text",
|
||||
"ui.theme": "Design",
|
||||
"ui.thickness": "Dicke",
|
||||
"ui.thread": "Gewinde",
|
||||
"ui.timeline_bottom": "Timeline unten",
|
||||
"ui.timeline_left_note": "Im linken Layout wird die Rücksetzmarke über dem Teile- und Szenenbrowser eingeblendet.",
|
||||
"ui.timeline_left_sidebar": "Linke Seitenleiste",
|
||||
"ui.tip_angle": "Spitzenwinkel",
|
||||
"ui.to_object": "Bis Objekt",
|
||||
"ui.top": "Oben",
|
||||
"ui.torus": "Torus",
|
||||
"ui.trim": "Kürzen",
|
||||
"ui.two_sides": "Beidseitig",
|
||||
"ui.ui_scale": "UI-Skalierung",
|
||||
"ui.undo": "Rückgängig",
|
||||
"ui.units": "Einheiten",
|
||||
"ui.units_of_new_designs": "Einheiten für neue Designs",
|
||||
"ui.untitled": "Ohne Titel",
|
||||
"ui.variable": "Variabel",
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.visual_style": "Ansichtsstil",
|
||||
"ui.workspace": "Arbeitsbereich",
|
||||
"ui.y_distance": "Y-Abstand",
|
||||
"ui.x_distance": "X-Abstand",
|
||||
"ui.zoom_to_the_cursor": "Zum Cursor zoomen",
|
||||
"ui.animateJoint": "Gelenk animieren",
|
||||
"ui.calibrate": "Kalibrieren",
|
||||
"ui.customizeToolbar": "Symbolleiste anpassen…",
|
||||
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
|
||||
"ui.exportDxf": "Als DXF exportieren…",
|
||||
"ui.findInTimeline": "In der Zeitleiste suchen",
|
||||
"ui.findInWindow": "Im Fenster suchen",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.groupSelected": "Auswahl gruppieren",
|
||||
"ui.hideAxes": "Achsen ausblenden",
|
||||
"ui.hideDimension": "Bemaßung ausblenden",
|
||||
"ui.hideOrigin": "Ursprung ausblenden",
|
||||
"ui.hidePlanes": "Ebenen ausblenden",
|
||||
"ui.hideProfile": "Profil ausblenden",
|
||||
"ui.homeView": "Startansicht",
|
||||
"ui.isolate": "Isolieren",
|
||||
"ui.lock": "Sperren",
|
||||
"ui.newSketchGroup": "Neue Skizzengruppe",
|
||||
"ui.normalConstruction": "Normal / Konstruktion",
|
||||
"ui.orbit": "Drehen",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.pan": "Verschieben",
|
||||
"ui.pinToShortcuts": "An Verknüpfungen anheften",
|
||||
"ui.pinToToolbar": "An Symbolleiste anheften",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.removeSection": "Schnitt entfernen",
|
||||
"ui.removeFromToolbar": "Aus der Symbolleiste entfernen",
|
||||
"ui.repeat": "Wiederholen",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.restore": "Wiederherstellen",
|
||||
"ui.heading_setup": "EINRICHTEN",
|
||||
"ui.selectOther": "Andere auswählen…",
|
||||
"ui.selectTangentChain": "Tangentialkette auswählen",
|
||||
"ui.showAll": "Alle einblenden",
|
||||
"ui.showAllBodies": "Alle Volumenkörper einblenden",
|
||||
"ui.showAxes": "Achsen einblenden",
|
||||
"ui.showDimension": "Bemaßung einblenden",
|
||||
"ui.showOrigin": "Ursprung einblenden",
|
||||
"ui.showPlanes": "Ebenen einblenden",
|
||||
"ui.suppress": "Unterdrücken",
|
||||
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
|
||||
"ui.unground": "Fixierung lösen",
|
||||
"ui.unisolate": "Isolierung aufheben",
|
||||
"ui.unlock": "Entsperren",
|
||||
"ui.unsuppress": "Unterdrückung aufheben",
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.zoom": "Zoomen",
|
||||
"ui.command_done": "{command} fertig",
|
||||
"cmd.appearance_assign": "Erscheinungsbild",
|
||||
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
|
||||
"cmd.appearance_list": "Erscheinungsbilder auflisten",
|
||||
"cmd.body_rename": "Körper umbenennen",
|
||||
"cmd.browser_group": "Neue Gruppe",
|
||||
"cmd.browser_move": "Browser-Elemente verschieben",
|
||||
"cmd.browser_order": "Browser-Elemente ordnen",
|
||||
"cmd.browser_rename_group": "Gruppe umbenennen",
|
||||
"cmd.browser_ungroup": "Gruppierung aufheben",
|
||||
"cmd.browser_visibility": "Ein-/Ausblenden",
|
||||
"cmd.canvas_calibrate": "Zeichenfläche kalibrieren",
|
||||
"cmd.canvas_decal": "Dekal",
|
||||
"cmd.canvas_delete": "Zeichenfläche löschen",
|
||||
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
|
||||
"cmd.canvas_insert": "Zeichenfläche",
|
||||
"cmd.component_activate": "Komponente aktivieren",
|
||||
"cmd.component_break_link": "Verknüpfung trennen",
|
||||
"cmd.component_capture_position": "Position erfassen",
|
||||
"cmd.component_create": "Neue Komponente",
|
||||
"cmd.component_delete": "Komponente löschen",
|
||||
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
|
||||
"cmd.component_insert_design": "Konstruktion einfügen",
|
||||
"cmd.component_links": "Verknüpfungen auflisten",
|
||||
"cmd.component_list": "Komponenten auflisten",
|
||||
"cmd.component_move_bodies": "Körper in Komponente verschieben",
|
||||
"cmd.component_move_sketches": "Skizzen in Komponente verschieben",
|
||||
"cmd.component_paste_new": "Neu einfügen",
|
||||
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
|
||||
"cmd.component_rename": "Komponente umbenennen",
|
||||
"cmd.component_revert_position": "Position zurücksetzen",
|
||||
"cmd.component_update_links": "Verknüpfungen aktualisieren",
|
||||
"cmd.config_activate": "Konfiguration aktivieren",
|
||||
"cmd.config_column": "Konfigurationsspalte",
|
||||
"cmd.config_configure": "Konfigurieren",
|
||||
"cmd.config_preview": "Konfiguration vorschauen",
|
||||
"cmd.config_row": "Konfigurationszeile",
|
||||
"cmd.config_rule": "Konfigurationsregel",
|
||||
"cmd.config_rules": "Konfigurationsregeln",
|
||||
"cmd.config_table": "Konfigurationstabelle anzeigen",
|
||||
"cmd.construct_axis_edge": "Achse durch Kante",
|
||||
"cmd.construct_axis_normal_to_face": "Achse senkrecht zur Fläche",
|
||||
"cmd.construct_axis_through_cylinder": "Achse durch Zylinder/Kegel/Torus",
|
||||
"cmd.construct_axis_two_planes": "Achse durch zwei Ebenen",
|
||||
"cmd.construct_axis_two_points": "Achse durch zwei Punkte",
|
||||
"cmd.construct_plane_along_path": "Ebene entlang Pfad",
|
||||
"cmd.construct_plane_midplane": "Mittenebene",
|
||||
"cmd.construct_plane_perpendicular": "Rechtebene",
|
||||
"cmd.construct_plane_tangent": "Tangentialebene",
|
||||
"cmd.construct_plane_three_points": "Ebene durch drei Punkte",
|
||||
"cmd.construct_plane_two_edges": "Ebene durch zwei Kanten",
|
||||
"cmd.construct_point_along_path": "Punkt entlang Pfad",
|
||||
"cmd.construct_point_center": "Punkt im Mittelpunkt von Kreis/Kugel/Torus",
|
||||
"cmd.construct_point_edge_plane": "Punkt an Kante und Ebene",
|
||||
"cmd.construct_point_three_planes": "Punkt aus drei Ebenen",
|
||||
"cmd.construct_point_two_edges": "Punkt aus zwei Kanten",
|
||||
"cmd.construct_point_vertex": "Punkt an Scheitelpunkt",
|
||||
"cmd.contact_check": "Kontakt prüfen",
|
||||
"cmd.contact_create": "Neuer Kontaktsatz",
|
||||
"cmd.contact_disable_all": "Kontakt deaktivieren",
|
||||
"cmd.contact_edit": "Kontaktsatz bearbeiten",
|
||||
"cmd.contact_enable_all": "Alle Kontakte aktivieren",
|
||||
"cmd.contact_enable_sets": "Kontaktsätze aktivieren",
|
||||
"cmd.contact_list": "Kontaktsätze auflisten",
|
||||
"cmd.diagnostics_crash_report": "Absturzbericht",
|
||||
"cmd.diagnostics_crash_reports": "Absturzberichte",
|
||||
"cmd.diagnostics_crash_reports_clear": "Absturzberichte löschen",
|
||||
"cmd.diagnostics_crash_test": "Absturztest",
|
||||
"cmd.doc_recover": "Konstruktion wiederherstellen",
|
||||
"cmd.doc_recovery_discard": "Wiederhergestellte Konstruktion verwerfen",
|
||||
"cmd.doc_recovery_list": "Wiederherstellbare Konstruktionen",
|
||||
"cmd.document_inspect": "Konstruktion prüfen",
|
||||
"cmd.document_units": "Einheiten der Konstruktion ändern",
|
||||
"cmd.edit_checkpoint": "Prüfpunkt",
|
||||
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
|
||||
"cmd.engine_commands": "Befehle auflisten",
|
||||
"cmd.explode_create": "Explosionsdarstellung",
|
||||
"cmd.explode_delete": "Explosionsdarstellung löschen",
|
||||
"cmd.explode_list": "Explosionsdarstellungen auflisten",
|
||||
"cmd.explode_show": "Explosionsdarstellung anzeigen",
|
||||
"cmd.expr_evaluate": "Ausdruck auswerten",
|
||||
"cmd.face_delete": "Fläche löschen",
|
||||
"cmd.file_export": "Exportieren",
|
||||
"cmd.file_insert_mesh": "Netz einfügen",
|
||||
"cmd.file_insert_step": "STEP einfügen",
|
||||
"cmd.file_save_mesh": "Als Netz speichern",
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
|
||||
"cmd.inspect_curvature_map": "Krümmungskartenanalyse",
|
||||
"cmd.inspect_draft": "Auswinkelanalyse",
|
||||
"cmd.inspect_environment_map": "Umgebungskartenanalyse",
|
||||
"cmd.inspect_interference": "Eingriff",
|
||||
"cmd.inspect_isocurve": "Isokurvenanalyse",
|
||||
"cmd.inspect_minimum_radius": "Mindestradiusanalyse",
|
||||
"cmd.inspect_zebra": "Zebra-Analyse",
|
||||
"cmd.joint_as_built": "Fertigungsgelenk",
|
||||
"cmd.joint_create": "Gelenk",
|
||||
"cmd.joint_delete": "Gelenk löschen",
|
||||
"cmd.joint_drive": "Gelenke antreiben",
|
||||
"cmd.joint_edit": "Gelenk bearbeiten",
|
||||
"cmd.joint_limits": "Gelenkgrenzen",
|
||||
"cmd.joint_list": "Gelenke auflisten",
|
||||
"cmd.joint_motion_link": "Bewegungsverknüpfung",
|
||||
"cmd.joint_origin": "Gelenkursprung",
|
||||
"cmd.joint_rigid_group": "Starrgruppe",
|
||||
"cmd.joint_solve": "Gelenke lösen",
|
||||
"cmd.material_assign": "Physikalisches Material",
|
||||
"cmd.material_create": "Neues physikalisches Material",
|
||||
"cmd.material_delete": "Physisches Material löschen",
|
||||
"cmd.material_edit": "Physisches Material bearbeiten",
|
||||
"cmd.material_list": "Materialien auflisten",
|
||||
"cmd.mesh_convert": "Netz konvertieren",
|
||||
"cmd.model_edges": "Kanten auflisten",
|
||||
"cmd.model_faces": "Flächen auflisten",
|
||||
"cmd.model_threads": "Gewinde auflisten",
|
||||
"cmd.motion_delete": "Bewegungsuntersuchung löschen",
|
||||
"cmd.motion_export": "Bewegungsuntersuchung exportieren",
|
||||
"cmd.motion_list": "Bewegungsuntersuchungen auflisten",
|
||||
"cmd.motion_play": "Bewegungsuntersuchung abspielen",
|
||||
"cmd.motion_study": "Bewegungsuntersuchung",
|
||||
"cmd.occurrence_copy": "Einfügen (Instanz)",
|
||||
"cmd.occurrence_ground": "Festsetzen",
|
||||
"cmd.occurrence_move": "Instanz verschieben",
|
||||
"cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz",
|
||||
"cmd.parameters_add": "Benutzerparameter hinzufügen",
|
||||
"cmd.parameters_change": "Parameter ändern",
|
||||
"cmd.parameters_comment": "Parameter kommentieren",
|
||||
"cmd.parameters_complete": "Ausdruck vervollständigen",
|
||||
"cmd.parameters_delete": "Parameter löschen",
|
||||
"cmd.parameters_export": "Parameter exportieren",
|
||||
"cmd.parameters_favorite": "Favorisierter Parameter",
|
||||
"cmd.parameters_graph": "Parametergraph",
|
||||
"cmd.parameters_import": "Parameter importieren",
|
||||
"cmd.parameters_list": "Parameter auflisten",
|
||||
"cmd.parameters_rename": "Parameter umbenennen",
|
||||
"cmd.parameters_users": "Parameterverwendungen",
|
||||
"cmd.parts_fastener": "Verbindungselement einfügen",
|
||||
"cmd.parts_insert": "Bauteil einfügen",
|
||||
"cmd.parts_library": "Standardbauteile",
|
||||
"cmd.parts_resize": "Bauteilgröße ändern",
|
||||
"cmd.plastic_assign_rule": "Kunststoffregel zuweisen",
|
||||
"cmd.plastic_boss": "Erhöhung",
|
||||
"cmd.plastic_lip": "Lippe",
|
||||
"cmd.plastic_manage_rules": "Kunststoffregeln verwalten",
|
||||
"cmd.plastic_rest": "Auflage",
|
||||
"cmd.plastic_rules": "Kunststoffregeln auflisten",
|
||||
"cmd.plastic_snap_fit": "Rastverbindung",
|
||||
"cmd.select_clear": "Auswahl aufheben",
|
||||
"cmd.sheet_convert": "In Blech umwandeln",
|
||||
"cmd.sheet_export_bend_table": "Biegungstabelle exportieren",
|
||||
"cmd.sheet_export_dxf": "Flachmuster als DXF exportieren",
|
||||
"cmd.sheet_flange": "Flansch",
|
||||
"cmd.sheet_flat_pattern": "Flachmuster erstellen",
|
||||
"cmd.sheet_fold": "Falten",
|
||||
"cmd.sheet_hem": "Umkantung",
|
||||
"cmd.sheet_manage_rules": "Blechregeln",
|
||||
"cmd.sheet_refold": "Wieder falten",
|
||||
"cmd.sheet_rules": "Blechregeln auflisten",
|
||||
"cmd.sheet_unfold": "Entfalten",
|
||||
"cmd.sketch_arc_center_point": "Mittelpunkt-Kreisbogen",
|
||||
"cmd.sketch_arc_tangent": "Tangentialbogen",
|
||||
"cmd.sketch_arc_three_point": "3-Punkt-Kreisbogen",
|
||||
"cmd.sketch_auto_constrain": "Auto-Bedingung",
|
||||
"cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug",
|
||||
"cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren",
|
||||
"cmd.sketch_blend_curve": "Übergangskurve",
|
||||
"cmd.sketch_chain": "Kette",
|
||||
"cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel",
|
||||
"cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand",
|
||||
"cmd.sketch_chamfer_two_distance": "Abschrägung mit zwei Abständen",
|
||||
"cmd.sketch_circle_center": "Kreis aus Mittelpunkt und Durchmesser",
|
||||
"cmd.sketch_circle_three_point": "3-Punkt-Kreis",
|
||||
"cmd.sketch_circle_three_tangent": "3-Tangenten-Kreis",
|
||||
"cmd.sketch_circle_two_point": "2-Punkt-Kreis",
|
||||
"cmd.sketch_circle_two_tangent": "2-Tangenten-Kreis",
|
||||
"cmd.sketch_constrainer": "Skizzen-Bedingung",
|
||||
"cmd.sketch_constraint_fix": "Fixieren/Lösen",
|
||||
"cmd.sketch_constraint_midpoint": "Mittelpunkt",
|
||||
"cmd.sketch_construction": "Normal/Konstruktion",
|
||||
"cmd.sketch_delete": "Skizzenentitäten löschen",
|
||||
"cmd.sketch_dimension_text": "Maßtext verschieben",
|
||||
"cmd.sketch_drag": "Skizzengeometrie ziehen",
|
||||
"cmd.sketch_drag_end": "Skizzen-Ziehen beenden",
|
||||
"cmd.sketch_draw_dimensioned": "Skizzengeometrie mit Maßen zeichnen",
|
||||
"cmd.sketch_edit": "Skizze bearbeiten",
|
||||
"cmd.sketch_edit_text": "Text bearbeiten",
|
||||
"cmd.sketch_export_dxf": "Als DXF speichern",
|
||||
"cmd.sketch_finish_auto_constrain": "Mit Auto-Bedingung fertigstellen",
|
||||
"cmd.sketch_fit_curves_to_section": "Kurven an Netzquerschnitt anpassen",
|
||||
"cmd.sketch_glyphs": "Bedingungssymbole",
|
||||
"cmd.sketch_include_3d": "3D-Geometrie einbeziehen",
|
||||
"cmd.sketch_insert_dxf": "DXF einfügen",
|
||||
"cmd.sketch_insert_svg": "SVG einfügen",
|
||||
"cmd.sketch_inspect": "Skizze prüfen",
|
||||
"cmd.sketch_intersection_curve": "Schnittkurve",
|
||||
"cmd.sketch_isoparametric_curve": "Isoparametrische Kurve",
|
||||
"cmd.sketch_line3d": "3D-Linie",
|
||||
"cmd.sketch_line_midpoint": "Linie über den Mittelpunkt",
|
||||
"cmd.sketch_move": "Skizzenentitäten verschieben/kopieren",
|
||||
"cmd.sketch_move3d": "3D-Punkt verschieben",
|
||||
"cmd.sketch_move_point": "Skizzenpunkt ziehen",
|
||||
"cmd.sketch_offset_break": "Versatzverknüpfung trennen",
|
||||
"cmd.sketch_options": "Skizzenpalettenoptionen",
|
||||
"cmd.sketch_point3d": "3D-Punkt",
|
||||
"cmd.sketch_polygon_circumscribed": "Umgeschriebenes Polygon",
|
||||
"cmd.sketch_polygon_edge": "Polygon über eine Kante",
|
||||
"cmd.sketch_polygon_inscribed": "Eingeschriebenes Polygon",
|
||||
"cmd.sketch_project_to_surface": "Auf Fläche projizieren",
|
||||
"cmd.sketch_rectangle_center": "Rechteck vom Mittelpunkt",
|
||||
"cmd.sketch_rectangle_three_point": "3-Punkt-Rechteck",
|
||||
"cmd.sketch_rectangle_two_point": "2-Punkt-Rechteck",
|
||||
"cmd.sketch_redefine": "Skizzierebene neu definieren",
|
||||
"cmd.sketch_slot_arc_center": "Mittelpunkt-Bogenschlitz",
|
||||
"cmd.sketch_slot_arc_three_point": "3-Punkt-Bogenschlitz",
|
||||
"cmd.sketch_slot_center_point": "Mittelpunktschlitz",
|
||||
"cmd.sketch_slot_center_to_center": "Schlitz von Mittelpunkt zu Mittelpunkt",
|
||||
"cmd.sketch_slot_overall": "Gesamtschlitz",
|
||||
"cmd.sketch_snap": "Einrastinferenz",
|
||||
"cmd.sketch_solve": "Skizze lösen",
|
||||
"cmd.sketch_spline3d": "3D-Spline",
|
||||
"cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten",
|
||||
"cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)",
|
||||
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
|
||||
"cmd.sketch_spun_profile": "Gedrehtes Profil",
|
||||
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
|
||||
"cmd.solid_align": "Ausrichten",
|
||||
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
|
||||
"cmd.solid_bounding_solid": "Begrenzender Körper",
|
||||
"cmd.solid_emboss": "Prägen",
|
||||
"cmd.solid_move": "Verschieben/Kopieren",
|
||||
"cmd.solid_offset_face": "Fläche versetzen",
|
||||
"cmd.solid_press_pull": "Drücken/Ziehen",
|
||||
"cmd.solid_remove": "Entfernen",
|
||||
"cmd.solid_replace_face": "Fläche ersetzen",
|
||||
"cmd.solid_rib": "Rippe",
|
||||
"cmd.solid_split_face": "Fläche teilen",
|
||||
"cmd.solid_web": "Gewebe",
|
||||
"cmd.surface_patch": "Patch",
|
||||
"cmd.surface_stitch": "Nähen",
|
||||
"cmd.surface_thicken": "Dicke vergeben",
|
||||
"cmd.surface_extrude": "Flächen-Extrusion",
|
||||
"cmd.surface_revolve": "Flächen-Rotation",
|
||||
"cmd.surface_sweep": "Flächen-Sweep",
|
||||
"cmd.surface_loft": "Flächen-Loft",
|
||||
"cmd.surface_ruled": "Regelfläche",
|
||||
"cmd.surface_boundary": "Begrenzungsfläche",
|
||||
"cmd.surface_offset": "Offsetfläche",
|
||||
"cmd.surface_blend": "Verbindungsfläche",
|
||||
"cmd.surface_fillet": "Flächenverrundung",
|
||||
"cmd.surface_untrim": "Trimmen aufheben",
|
||||
"cmd.surface_unstitch": "Verbund aufheben",
|
||||
"cmd.surface_reverse_normal": "Normale umkehren",
|
||||
"cmd.inspect_continuity": "Kontinuitätsprüfung",
|
||||
"cmd.timeline_compute_all": "Alles berechnen",
|
||||
"cmd.timeline_copy": "Merkmale kopieren",
|
||||
"cmd.timeline_dependents": "Merkmalsabhängigkeiten",
|
||||
"cmd.timeline_inspect": "Merkmal prüfen",
|
||||
"cmd.timeline_paste": "Merkmale einfügen",
|
||||
"cmd.timeline_redefine": "Merkmal bearbeiten",
|
||||
"cmd.timeline_rename": "Umbenennen",
|
||||
"cmd.timeline_reorder": "Merkmal neu ordnen",
|
||||
"cmd.timeline_roll_to": "Verlaufsmarkierung verschieben",
|
||||
"cmd.timeline_rollback": "Zeitleistenmarkierung verschieben",
|
||||
"cmd.timeline_suppress": "Merkmal unterdrücken",
|
||||
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
|
||||
"cmd.view_delete": "Benannte Ansicht löschen",
|
||||
"cmd.view_list": "Benannte Ansichten auflisten",
|
||||
"cmd.view_rename": "Benannte Ansicht umbenennen",
|
||||
"cmd.view_save": "Neue benannte Ansicht",
|
||||
"ui.export_ellipsis_keymap": "Exportieren…",
|
||||
"ui.heading_configure": "KONFIGURIEREN",
|
||||
"ui.heading_constraints": "RANDBEDINGUNGEN",
|
||||
"ui.heading_utilities": "WERKZEUGE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Drive Joint": "Gelenk antreiben",
|
||||
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
|
||||
"Find in Browser": "Im Browser suchen",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
|
||||
"Reset Toolbar": "Symbolleiste zurücksetzen",
|
||||
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
|
||||
"Selection filter": "Auswahlfilter",
|
||||
"Revert Position": "Position zurücksetzen",
|
||||
"Construction": "Konstruktionslinie",
|
||||
"Faces": "Flächen",
|
||||
"Edges": "Kanten",
|
||||
"Vertices": "Scheitelpunkte",
|
||||
"Sketch geometry and profiles": "Skizzengeometrie und Profile",
|
||||
"Sketch curves": "Skizzenkurven",
|
||||
"Sketch points": "Skizzenpunkte",
|
||||
"Sketch dimensions": "Skizzenmaße",
|
||||
"Sketch constraints": "Skizzenbedingungen",
|
||||
"Flat": "Flach",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.view_navigate": "Ansicht navigieren",
|
||||
"timeline.group": "Neuen Merkmalsordner anlegen",
|
||||
"timeline.ungroup": "Merkmalsordner entfernen",
|
||||
"timeline.rename_group": "Merkmalsordner umbenennen",
|
||||
"timeline.move_to_group": "Merkmale in Ordner verschieben",
|
||||
"timeline.move_many": "Zeitleistenelemente verschieben",
|
||||
"timeline.reorder_many": "Merkmale neu ordnen",
|
||||
"ui.show_construction_geometries": "Konstruktionsgeometrien",
|
||||
"ui.total_angle": "Gesamtwinkel",
|
||||
"sketch_text.builtin_font": "{family} (integriert)",
|
||||
"sketch_text.font_search": "Schriftarten suchen",
|
||||
"sketch_text.loading_fonts": "Installierte Schriftarten werden gesucht…",
|
||||
"sketch_text.use_font_file": "Schriftdatei {path} verwenden",
|
||||
"sketch_text.bold": "Fett",
|
||||
"sketch_text.italic": "Kursiv",
|
||||
"sketch_text.align_left": "Links",
|
||||
"sketch_text.align_center": "Mitte",
|
||||
"sketch_text.align_right": "Rechts",
|
||||
"sketch_text.path_hint": "Eine Skizzenkurve, der der Text folgt: hier klicken, dann die Kurve",
|
||||
"sketch_text.path_clear": "Text von der Kurve lösen",
|
||||
"sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage"
|
||||
}
|
||||
@@ -0,0 +1,961 @@
|
||||
{
|
||||
"browser.body_name": "Body{number}",
|
||||
"browser.component_name": "Component{number}",
|
||||
"browser.parameters": "Parameters ({count})",
|
||||
"browser.precision": "Precision: {precision}",
|
||||
"browser.sketch_name": "Sketch{number}",
|
||||
"browser.units": "Units: {units}",
|
||||
"cmd.appearance_assign": "Appearance",
|
||||
"cmd.appearance_library": "Appearance Library",
|
||||
"cmd.appearance_list": "List Appearances",
|
||||
"cmd.body_rename": "Rename Body",
|
||||
"cmd.browser_group": "New Group",
|
||||
"cmd.browser_move": "Move Browser Items",
|
||||
"cmd.browser_order": "Order Browser Items",
|
||||
"cmd.browser_rename_group": "Rename Group",
|
||||
"cmd.browser_ungroup": "Ungroup",
|
||||
"cmd.browser_visibility": "Show/Hide",
|
||||
"cmd.canvas_calibrate": "Calibrate Canvas",
|
||||
"cmd.canvas_decal": "Decal",
|
||||
"cmd.canvas_delete": "Delete Canvas",
|
||||
"cmd.canvas_edit": "Edit Canvas",
|
||||
"cmd.canvas_insert": "Canvas",
|
||||
"cmd.component_activate": "Activate Component",
|
||||
"cmd.component_break_link": "Break Link",
|
||||
"cmd.component_capture_position": "Capture Position",
|
||||
"cmd.component_create": "New Component",
|
||||
"cmd.component_delete": "Delete Component",
|
||||
"cmd.component_from_bodies": "Create Components from Bodies",
|
||||
"cmd.component_insert_design": "Insert Design",
|
||||
"cmd.component_links": "List Links",
|
||||
"cmd.component_list": "List Components",
|
||||
"cmd.component_move_bodies": "Move Bodies to Component",
|
||||
"cmd.component_move_sketches": "Move Sketches to Component",
|
||||
"cmd.component_paste_new": "Paste New",
|
||||
"cmd.component_properties": "Component Physical Properties",
|
||||
"cmd.component_rename": "Rename Component",
|
||||
"cmd.component_revert_position": "Revert Position",
|
||||
"cmd.component_update_links": "Update Links",
|
||||
"cmd.config_activate": "Activate Configuration",
|
||||
"cmd.config_column": "Configuration Column",
|
||||
"cmd.config_configure": "Configure",
|
||||
"cmd.config_preview": "Preview Configuration",
|
||||
"cmd.config_row": "Configuration Row",
|
||||
"cmd.config_rule": "Configuration Rule",
|
||||
"cmd.config_rules": "Configuration Rules",
|
||||
"cmd.config_table": "Display Configuration Table",
|
||||
"cmd.construct_axis_edge": "Axis Through Edge",
|
||||
"cmd.construct_axis_normal_to_face": "Axis Perpendicular To Face",
|
||||
"cmd.construct_axis_through_cylinder": "Axis Through Cylinder/Cone/Torus",
|
||||
"cmd.construct_axis_two_planes": "Axis Through Two Planes",
|
||||
"cmd.construct_axis_two_points": "Axis Through Two Points",
|
||||
"cmd.construct_plane_along_path": "Plane Along Path",
|
||||
"cmd.construct_plane_midplane": "Midplane",
|
||||
"cmd.construct_plane_perpendicular": "Perpendicular Plane",
|
||||
"cmd.construct_plane_tangent": "Tangent Plane",
|
||||
"cmd.construct_plane_three_points": "Plane Through Three Points",
|
||||
"cmd.construct_plane_two_edges": "Plane Through Two Edges",
|
||||
"cmd.construct_point_along_path": "Point Along Path",
|
||||
"cmd.construct_point_center": "Point At Center Of Circle/Sphere/Torus",
|
||||
"cmd.construct_point_edge_plane": "Point At Edge And Plane",
|
||||
"cmd.construct_point_three_planes": "Point Through Three Planes",
|
||||
"cmd.construct_point_two_edges": "Point Through Two Edges",
|
||||
"cmd.construct_point_vertex": "Point At Vertex",
|
||||
"cmd.contact_check": "Check Contact",
|
||||
"cmd.contact_create": "New Contact Set",
|
||||
"cmd.contact_disable_all": "Disable Contact",
|
||||
"cmd.contact_edit": "Edit Contact Set",
|
||||
"cmd.contact_enable_all": "Enable All Contact",
|
||||
"cmd.contact_enable_sets": "Enable Contact Sets",
|
||||
"cmd.contact_list": "List Contact Sets",
|
||||
"cmd.diagnostics_crash_report": "Crash Report",
|
||||
"cmd.diagnostics_crash_reports": "Crash Reports",
|
||||
"cmd.diagnostics_crash_reports_clear": "Delete Crash Reports",
|
||||
"cmd.diagnostics_crash_test": "Crash Test",
|
||||
"cmd.doc_recover": "Recover Design",
|
||||
"cmd.doc_recovery_discard": "Discard Recovered Design",
|
||||
"cmd.doc_recovery_list": "Recoverable Designs",
|
||||
"cmd.document_inspect": "Inspect Design",
|
||||
"cmd.document_units": "Change Design Units",
|
||||
"cmd.edit_checkpoint": "Checkpoint",
|
||||
"cmd.edit_restore_checkpoint": "Restore Checkpoint",
|
||||
"cmd.engine_commands": "List Commands",
|
||||
"cmd.explode_create": "Exploded View",
|
||||
"cmd.explode_delete": "Delete Exploded View",
|
||||
"cmd.explode_list": "List Exploded Views",
|
||||
"cmd.explode_show": "Show Exploded View",
|
||||
"cmd.expr_evaluate": "Evaluate Expression",
|
||||
"cmd.face_delete": "Delete Face",
|
||||
"cmd.file_export": "Export",
|
||||
"cmd.file_insert_mesh": "Insert Mesh",
|
||||
"cmd.file_insert_step": "Insert STEP",
|
||||
"cmd.file_save_mesh": "Save As Mesh",
|
||||
"cmd.inspect_accessibility": "Accessibility Analysis",
|
||||
"cmd.inspect_center_of_mass": "Center of Mass",
|
||||
"cmd.inspect_curvature_comb": "Curvature Comb Analysis",
|
||||
"cmd.inspect_curvature_map": "Curvature Map Analysis",
|
||||
"cmd.inspect_draft": "Draft Analysis",
|
||||
"cmd.inspect_environment_map": "Environment Map Analysis",
|
||||
"cmd.inspect_interference": "Interference",
|
||||
"cmd.inspect_isocurve": "Isocurve Analysis",
|
||||
"cmd.inspect_minimum_radius": "Minimum Radius Analysis",
|
||||
"cmd.inspect_zebra": "Zebra Analysis",
|
||||
"cmd.joint_as_built": "As-Built Joint",
|
||||
"cmd.joint_create": "Joint",
|
||||
"cmd.joint_delete": "Delete Joint",
|
||||
"cmd.joint_drive": "Drive Joints",
|
||||
"cmd.joint_edit": "Edit Joint",
|
||||
"cmd.joint_limits": "Joint Limits",
|
||||
"cmd.joint_list": "List Joints",
|
||||
"cmd.joint_motion_link": "Motion Link",
|
||||
"cmd.joint_origin": "Joint Origin",
|
||||
"cmd.joint_rigid_group": "Rigid Group",
|
||||
"cmd.joint_solve": "Solve Joints",
|
||||
"cmd.material_assign": "Physical Material",
|
||||
"cmd.material_create": "New Physical Material",
|
||||
"cmd.material_delete": "Delete Physical Material",
|
||||
"cmd.material_edit": "Edit Physical Material",
|
||||
"cmd.material_list": "List Materials",
|
||||
"cmd.mesh_convert": "Convert Mesh",
|
||||
"cmd.model_edges": "List Edges",
|
||||
"cmd.model_faces": "List Faces",
|
||||
"cmd.model_threads": "List Threads",
|
||||
"cmd.motion_delete": "Delete Motion Study",
|
||||
"cmd.motion_export": "Export Motion Study",
|
||||
"cmd.motion_list": "List Motion Studies",
|
||||
"cmd.motion_play": "Play Motion Study",
|
||||
"cmd.motion_study": "Motion Study",
|
||||
"cmd.occurrence_copy": "Paste (Instance)",
|
||||
"cmd.occurrence_ground": "Ground",
|
||||
"cmd.occurrence_move": "Move Occurrence",
|
||||
"cmd.occurrence_properties": "Occurrence Physical Properties",
|
||||
"cmd.parameters_add": "Add User Parameter",
|
||||
"cmd.parameters_change": "Change Parameters",
|
||||
"cmd.parameters_comment": "Comment Parameter",
|
||||
"cmd.parameters_complete": "Complete Expression",
|
||||
"cmd.parameters_delete": "Delete Parameter",
|
||||
"cmd.parameters_export": "Export Parameters",
|
||||
"cmd.parameters_favorite": "Favourite Parameter",
|
||||
"cmd.parameters_graph": "Parameter Graph",
|
||||
"cmd.parameters_import": "Import Parameters",
|
||||
"cmd.parameters_list": "List Parameters",
|
||||
"cmd.parameters_rename": "Rename Parameter",
|
||||
"cmd.parameters_users": "Parameter Users",
|
||||
"cmd.parts_fastener": "Insert Fastener",
|
||||
"cmd.parts_insert": "Insert Part",
|
||||
"cmd.parts_library": "Standard Parts",
|
||||
"cmd.parts_resize": "Change Part Size",
|
||||
"cmd.plastic_assign_rule": "Assign Plastic Rule",
|
||||
"cmd.plastic_boss": "Boss",
|
||||
"cmd.plastic_lip": "Lip",
|
||||
"cmd.plastic_manage_rules": "Manage Plastic Rules",
|
||||
"cmd.plastic_rest": "Rest",
|
||||
"cmd.plastic_rules": "List Plastic Rules",
|
||||
"cmd.plastic_snap_fit": "Snap Fit",
|
||||
"cmd.select_clear": "Clear Selection",
|
||||
"cmd.sheet_convert": "Convert to Sheet Metal",
|
||||
"cmd.sheet_export_bend_table": "Export Bend Table",
|
||||
"cmd.sheet_export_dxf": "Export Flat Pattern as DXF",
|
||||
"cmd.sheet_flange": "Flange",
|
||||
"cmd.sheet_flat_pattern": "Create Flat Pattern",
|
||||
"cmd.sheet_fold": "Fold",
|
||||
"cmd.sheet_hem": "Hem",
|
||||
"cmd.sheet_manage_rules": "Sheet Metal Rules",
|
||||
"cmd.sheet_refold": "Refold",
|
||||
"cmd.sheet_rules": "List Sheet Metal Rules",
|
||||
"cmd.sheet_unfold": "Unfold",
|
||||
"cmd.sketch_arc_center_point": "Center Point Arc",
|
||||
"cmd.sketch_arc_tangent": "Tangent Arc",
|
||||
"cmd.sketch_arc_three_point": "3-Point Arc",
|
||||
"cmd.sketch_auto_constrain": "AutoConstrain",
|
||||
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from datum",
|
||||
"cmd.sketch_auto_project": "Auto-Project Edges on Reference",
|
||||
"cmd.sketch_blend_curve": "Blend Curve",
|
||||
"cmd.sketch_chain": "Chain",
|
||||
"cmd.sketch_chamfer_distance_angle": "Distance and Angle Chamfer",
|
||||
"cmd.sketch_chamfer_equal_distance": "Equal Distance Chamfer",
|
||||
"cmd.sketch_chamfer_two_distance": "Two Distance Chamfer",
|
||||
"cmd.sketch_circle_center": "Center Diameter Circle",
|
||||
"cmd.sketch_circle_three_point": "3-Point Circle",
|
||||
"cmd.sketch_circle_three_tangent": "3-Tangent Circle",
|
||||
"cmd.sketch_circle_two_point": "2-Point Circle",
|
||||
"cmd.sketch_circle_two_tangent": "2-Tangent Circle",
|
||||
"cmd.sketch_constrainer": "Sketch Constrainer",
|
||||
"cmd.sketch_constraint_fix": "Fix/UnFix",
|
||||
"cmd.sketch_constraint_midpoint": "MidPoint",
|
||||
"cmd.sketch_construction": "Normal/Construction",
|
||||
"cmd.sketch_delete": "Delete Sketch Entities",
|
||||
"cmd.sketch_dimension_text": "Move Dimension Text",
|
||||
"cmd.sketch_drag": "Drag Sketch Geometry",
|
||||
"cmd.sketch_drag_end": "End Sketch Drag",
|
||||
"cmd.sketch_draw_dimensioned": "Draw Dimensioned Sketch Geometry",
|
||||
"cmd.sketch_edit": "Edit Sketch",
|
||||
"cmd.sketch_edit_text": "Edit Text",
|
||||
"cmd.sketch_export_dxf": "Save As DXF",
|
||||
"cmd.sketch_finish_auto_constrain": "Finish with AutoConstrain",
|
||||
"cmd.sketch_fit_curves_to_section": "Fit Curves to Mesh Section",
|
||||
"cmd.sketch_glyphs": "Constraint Glyphs",
|
||||
"cmd.sketch_include_3d": "Include 3D Geometry",
|
||||
"cmd.sketch_insert_dxf": "Insert DXF",
|
||||
"cmd.sketch_insert_svg": "Insert SVG",
|
||||
"cmd.sketch_inspect": "Inspect Sketch",
|
||||
"cmd.sketch_intersection_curve": "Intersection Curve",
|
||||
"cmd.sketch_isoparametric_curve": "Isoparametric Curve",
|
||||
"cmd.sketch_line3d": "3D Line",
|
||||
"cmd.sketch_line_midpoint": "Midpoint Line",
|
||||
"cmd.sketch_move": "Move/Copy Sketch Entities",
|
||||
"cmd.sketch_move3d": "Move 3D Point",
|
||||
"cmd.sketch_move_point": "Drag Sketch Point",
|
||||
"cmd.sketch_offset_break": "Break Offset Association",
|
||||
"cmd.sketch_options": "Sketch Palette Options",
|
||||
"cmd.sketch_point3d": "3D Point",
|
||||
"cmd.sketch_polygon_circumscribed": "Circumscribed Polygon",
|
||||
"cmd.sketch_polygon_edge": "Edge Polygon",
|
||||
"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",
|
||||
"cmd.sketch_slot_arc_center": "Center Point Arc Slot",
|
||||
"cmd.sketch_slot_arc_three_point": "Three Point Arc Slot",
|
||||
"cmd.sketch_slot_center_point": "Center Point Slot",
|
||||
"cmd.sketch_slot_center_to_center": "Center to Center Slot",
|
||||
"cmd.sketch_slot_overall": "Overall Slot",
|
||||
"cmd.sketch_snap": "Snap Inference",
|
||||
"cmd.sketch_solve": "Solve Sketch",
|
||||
"cmd.sketch_spline3d": "3D Spline",
|
||||
"cmd.sketch_spline_control_point": "Control Point Spline",
|
||||
"cmd.sketch_spline_control_point_5": "Control Point Spline (Degree 5)",
|
||||
"cmd.sketch_spline_fit_point": "Fit Point Spline",
|
||||
"cmd.sketch_spun_profile": "Spun Profile",
|
||||
"cmd.sketch_toggle_driven": "Toggle Driven Dimension",
|
||||
"cmd.solid_align": "Align",
|
||||
"cmd.solid_boundary_fill": "Boundary Fill",
|
||||
"cmd.solid_bounding_solid": "Bounding Solid",
|
||||
"cmd.solid_emboss": "Emboss",
|
||||
"cmd.solid_move": "Move/Copy",
|
||||
"cmd.solid_offset_face": "Offset Face",
|
||||
"cmd.solid_press_pull": "Press Pull",
|
||||
"cmd.solid_remove": "Remove",
|
||||
"cmd.solid_replace_face": "Replace Face",
|
||||
"cmd.solid_rib": "Rib",
|
||||
"cmd.solid_split_face": "Split Face",
|
||||
"cmd.solid_web": "Web",
|
||||
"cmd.surface_patch": "Patch",
|
||||
"cmd.surface_stitch": "Stitch",
|
||||
"cmd.surface_thicken": "Thicken",
|
||||
"cmd.surface_extrude": "Surface Extrude",
|
||||
"cmd.surface_revolve": "Surface Revolve",
|
||||
"cmd.surface_sweep": "Surface Sweep",
|
||||
"cmd.surface_loft": "Surface Loft",
|
||||
"cmd.surface_ruled": "Ruled Surface",
|
||||
"cmd.surface_boundary": "Boundary Surface",
|
||||
"cmd.surface_offset": "Offset Surface",
|
||||
"cmd.surface_blend": "Blend Surface",
|
||||
"cmd.surface_fillet": "Surface Fillet",
|
||||
"cmd.surface_untrim": "Untrim",
|
||||
"cmd.surface_unstitch": "Unstitch",
|
||||
"cmd.surface_reverse_normal": "Reverse Normal",
|
||||
"cmd.inspect_continuity": "Continuity Analysis",
|
||||
"cmd.timeline_compute_all": "Compute All",
|
||||
"cmd.timeline_copy": "Copy Features",
|
||||
"cmd.timeline_dependents": "Feature Dependents",
|
||||
"cmd.timeline_inspect": "Inspect Feature",
|
||||
"cmd.timeline_paste": "Paste Features",
|
||||
"cmd.timeline_redefine": "Edit Feature",
|
||||
"cmd.timeline_rename": "Rename",
|
||||
"cmd.timeline_reorder": "Reorder Feature",
|
||||
"cmd.timeline_roll_to": "Roll History Marker",
|
||||
"cmd.timeline_rollback": "Move Timeline Marker",
|
||||
"cmd.timeline_suppress": "Suppress Feature",
|
||||
"cmd.view_apply": "Restore Named View",
|
||||
"cmd.view_delete": "Delete Named View",
|
||||
"cmd.view_list": "List Named Views",
|
||||
"cmd.view_rename": "Rename Named View",
|
||||
"cmd.view_save": "New Named View",
|
||||
"dialog.custom_axis": "↖ Custom Axis",
|
||||
"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",
|
||||
"settings.msaa_reset": "{setting}: {requested}× isn't supported by this graphics device; it is now {reset}× (supported: {supported}).",
|
||||
"settings.msaa_unsupported": "Not supported by this graphics device",
|
||||
"settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:",
|
||||
"settings.reset_title": "Settings were changed",
|
||||
"sketch.degrees_of_freedom": "{count} degrees of freedom",
|
||||
"sketch.draw_curves": "Draw as {linetype}",
|
||||
"sketch.make_selection": "Convert the selection to {linetype}",
|
||||
"ui.1_degree_of_freedom": "1 degree of freedom",
|
||||
"ui.3d_mouse_speed": "3D mouse speed",
|
||||
"ui.3d_mouse_windows": "3D mouse (Windows)",
|
||||
"ui.3d_sketch": "3D Sketch",
|
||||
"ui.about": "About",
|
||||
"ui.activate": "Activate",
|
||||
"ui.active_component": "Active component",
|
||||
"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",
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animation speed",
|
||||
"ui.anti_aliasing": "Anti-aliasing",
|
||||
"ui.apply": "Apply",
|
||||
"ui.arc": "Arc",
|
||||
"ui.assemble": "Assemble",
|
||||
"ui.autosave_every": "Autosave every",
|
||||
"ui.axes": "Axes",
|
||||
"ui.axis": "Axis",
|
||||
"ui.base_point": "Base point",
|
||||
"ui.bodies": "Bodies",
|
||||
"ui.box": "Box",
|
||||
"ui.break": "Break",
|
||||
"ui.cancel": "Cancel",
|
||||
"ui.canvases": "Canvases",
|
||||
"ui.center": "Center",
|
||||
"ui.centerline": "Centerline",
|
||||
"ui.chain": "Chain",
|
||||
"ui.chamfer": "Chamfer",
|
||||
"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",
|
||||
"ui.clear_recent": "Clear Recent",
|
||||
"ui.clear_the_selection": "Clear the selection",
|
||||
"ui.click_selects_whole_bodies": "Click selects whole bodies",
|
||||
"ui.click_then_press_key": "Click, then press the new key",
|
||||
"ui.close": "Close",
|
||||
"ui.coil": "Coil",
|
||||
"ui.coincident": "Coincident",
|
||||
"ui.collapse": "Collapse",
|
||||
"ui.collinear": "Collinear",
|
||||
"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",
|
||||
"ui.construct": "Construct",
|
||||
"ui.construction": "Construction",
|
||||
"ui.context_menu": "Context menu",
|
||||
"ui.copy": "Copy",
|
||||
"ui.corner": "Corner",
|
||||
"ui.corners": "Corners",
|
||||
"ui.counterbore": "Counterbore",
|
||||
"ui.countersink": "Countersink",
|
||||
"ui.create": "Create",
|
||||
"ui.create_copy": "Create Copy",
|
||||
"ui.create_sketch": "Create Sketch",
|
||||
"ui.current": "current",
|
||||
"ui.curvature": "Curvature",
|
||||
"ui.curves": "Curves",
|
||||
"ui.cut": "Cut",
|
||||
"ui.cylinder": "Cylinder",
|
||||
"ui.dark": "Dark",
|
||||
"ui.default_modelling_orientation": "Default modelling orientation",
|
||||
"ui.degrees_of_freedom": "degrees of freedom",
|
||||
"ui.delete": "Delete",
|
||||
"ui.depth": "Depth",
|
||||
"ui.design": "Design",
|
||||
"ui.diameter": "Diameter",
|
||||
"ui.dimension": "Dimension",
|
||||
"ui.direction": "Direction",
|
||||
"ui.distance": "Distance",
|
||||
"ui.distance_1": "Distance 1",
|
||||
"ui.distance_2": "Distance 2",
|
||||
"ui.document_settings": "Document Settings",
|
||||
"ui.don_t_save": "Don't Save",
|
||||
"ui.draft": "Draft",
|
||||
"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",
|
||||
"ui.equal": "Equal",
|
||||
"ui.expand": "Expand",
|
||||
"ui.export_ellipsis": "Export...",
|
||||
"ui.export_ellipsis_keymap": "Export…",
|
||||
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
|
||||
"ui.extend": "Extend",
|
||||
"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",
|
||||
"ui.fillet": "Fillet",
|
||||
"ui.filter_commands_or_keys": "Filter commands or keys",
|
||||
"ui.finish_sketch": "Finish Sketch",
|
||||
"ui.fix_unfix": "Fix/Unfix",
|
||||
"ui.flat": "Flat",
|
||||
"ui.flip": "Flip",
|
||||
"ui.front": "Front",
|
||||
"ui.fully_constrained": "Fully constrained",
|
||||
"ui.general": "General",
|
||||
"ui.graphics": "Graphics",
|
||||
"ui.ground_shadow": "Ground shadow",
|
||||
"ui.heading_add_ins": "ADD-INS",
|
||||
"ui.heading_assemble": "ASSEMBLE",
|
||||
"ui.heading_box": "BOX",
|
||||
"ui.heading_chamfer": "CHAMFER",
|
||||
"ui.heading_circular_pattern": "CIRCULAR PATTERN",
|
||||
"ui.heading_coil": "COIL",
|
||||
"ui.heading_combine": "COMBINE",
|
||||
"ui.heading_configure": "CONFIGURE",
|
||||
"ui.heading_constraints": "CONSTRAINTS",
|
||||
"ui.heading_construct": "CONSTRUCT",
|
||||
"ui.heading_create": "CREATE",
|
||||
"ui.heading_create_sketch": "CREATE SKETCH",
|
||||
"ui.heading_cylinder": "CYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "DRAFT",
|
||||
"ui.heading_edit": "EDIT",
|
||||
"ui.heading_extrude": "EXTRUDE",
|
||||
"ui.heading_fillet": "FILLET",
|
||||
"ui.heading_finish_sketch": "FINISH SKETCH",
|
||||
"ui.heading_front": "FRONT",
|
||||
"ui.heading_hole": "HOLE",
|
||||
"ui.heading_home": "HOME",
|
||||
"ui.heading_insert": "INSERT",
|
||||
"ui.heading_inspect": "INSPECT",
|
||||
"ui.heading_loft": "LOFT",
|
||||
"ui.heading_make": "MAKE",
|
||||
"ui.heading_measure": "MEASURE",
|
||||
"ui.heading_mesh": "MESH",
|
||||
"ui.heading_mirror": "MIRROR",
|
||||
"ui.heading_modify": "MODIFY",
|
||||
"ui.heading_move": "MOVE",
|
||||
"ui.heading_offset": "OFFSET",
|
||||
"ui.heading_move_copy": "MOVE/COPY",
|
||||
"ui.heading_offset_plane": "OFFSET PLANE",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMETERS",
|
||||
"ui.heading_pattern_on_path": "PATTERN ON PATH",
|
||||
"ui.heading_pipe": "PIPE",
|
||||
"ui.heading_plane_at_angle": "PLANE AT ANGLE",
|
||||
"ui.heading_plastic": "PLASTIC",
|
||||
"ui.heading_prepare": "PREPARE",
|
||||
"ui.heading_rectangular_pattern": "RECTANGULAR PATTERN",
|
||||
"ui.heading_revolve": "REVOLVE",
|
||||
"ui.heading_right": "RIGHT",
|
||||
"ui.heading_scale": "SCALE",
|
||||
"ui.heading_section_analysis": "SECTION ANALYSIS",
|
||||
"ui.heading_select": "SELECT",
|
||||
"ui.heading_sheet_metal": "SHEET METAL",
|
||||
"ui.heading_shell": "SHELL",
|
||||
"ui.heading_sketch": "SKETCH",
|
||||
"ui.heading_sketch_palette": "SKETCH PALETTE",
|
||||
"ui.heading_sketch_scale": "SKETCH SCALE",
|
||||
"ui.heading_solid": "SOLID",
|
||||
"ui.heading_sphere": "SPHERE",
|
||||
"ui.heading_split_body": "SPLIT BODY",
|
||||
"ui.heading_surface": "SURFACE",
|
||||
"ui.heading_sweep": "SWEEP",
|
||||
"ui.heading_thread": "THREAD",
|
||||
"ui.heading_top": "TOP",
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITIES",
|
||||
"ui.heading_utility": "UTILITY",
|
||||
"ui.help": "Help",
|
||||
"ui.hide": "Hide",
|
||||
"ui.hole": "Hole",
|
||||
"ui.hole_tap_type": "Hole Tap Type",
|
||||
"ui.hole_type": "Hole Type",
|
||||
"ui.home": "Home",
|
||||
"ui.horizontal_vertical": "Horizontal/Vertical",
|
||||
"ui.import_ellipsis": "Import…",
|
||||
"ui.insert": "Insert",
|
||||
"ui.inspect": "Inspect",
|
||||
"ui.intersect": "Intersect",
|
||||
"ui.join": "Join",
|
||||
"ui.joints": "Joints",
|
||||
"ui.keyboard_shortcuts": "Keyboard Shortcuts",
|
||||
"ui.language": "Language",
|
||||
"ui.length_decimals": "Length decimals",
|
||||
"ui.light": "Light",
|
||||
"ui.line": "Line",
|
||||
"ui.linetype": "Linetype",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Look At",
|
||||
"ui.look_at_anim_desc": "Axis view, Look At, Home and Fit",
|
||||
"ui.manufacture": "Manufacture",
|
||||
"ui.measure": "Measure",
|
||||
"ui.midpoint": "Midpoint",
|
||||
"ui.mirror": "Mirror",
|
||||
"ui.mirror_line": "Mirror line",
|
||||
"ui.modify": "Modify",
|
||||
"ui.move": "Move",
|
||||
"ui.multiple": "Multiple",
|
||||
"ui.named_views": "Named Views",
|
||||
"ui.navigation": "Navigation",
|
||||
"ui.needs_design": "{command}: open a design first",
|
||||
"ui.new_body": "New Body",
|
||||
"ui.new_design": "New Design",
|
||||
"ui.normal": "Normal",
|
||||
"ui.object_type": "Object Type",
|
||||
"ui.objects": "Objects",
|
||||
"ui.offer_to_stand_up_y_up_files": "Offer to stand up Y-up files",
|
||||
"ui.offset": "Offset",
|
||||
"ui.offset_plane": "Offset Plane",
|
||||
"ui.one_side": "One Side",
|
||||
"ui.open": "Open",
|
||||
"ui.open_ellipsis": "Open...",
|
||||
"ui.open_preferences": "Open Preferences",
|
||||
"ui.open_recent": "Open Recent",
|
||||
"ui.operation": "Operation",
|
||||
"ui.options": "Options",
|
||||
"ui.orbit_around": "Orbit around",
|
||||
"ui.origin": "Origin",
|
||||
"ui.palette_points": "Points",
|
||||
"ui.palette_profile": "Profile",
|
||||
"ui.pan_zoom_orbit": "Pan, zoom, orbit",
|
||||
"ui.parallel": "Parallel",
|
||||
"ui.parameters": "Parameters",
|
||||
"ui.paste": "Paste",
|
||||
"ui.path": "Path",
|
||||
"ui.pattern_on_path": "Pattern on Path",
|
||||
"ui.perpendicular": "Perpendicular",
|
||||
"ui.perspective_view": "Perspective view",
|
||||
"ui.pipe": "Pipe",
|
||||
"ui.placement": "Placement",
|
||||
"ui.plane": "Plane",
|
||||
"ui.plane_at_angle": "Plane at Angle",
|
||||
"ui.point": "Point",
|
||||
"ui.point_under_the_cursor": "Point under the cursor",
|
||||
"ui.points": "Points",
|
||||
"ui.polygon": "Polygon",
|
||||
"ui.preferences": "Preferences",
|
||||
"ui.press_a_key": "press a key…",
|
||||
"ui.profile": "Profile",
|
||||
"ui.profile_plane": "Profile Plane",
|
||||
"ui.profiles": "Profiles",
|
||||
"ui.project": "Project",
|
||||
"ui.project_face_edges_sketch": "Project face edges into new sketches",
|
||||
"ui.quantity": "Quantity",
|
||||
"ui.quantity_2": "Quantity 2",
|
||||
"ui.quit": "Quit",
|
||||
"ui.radial_context_menu": "Radial context menu",
|
||||
"ui.radius": "Radius",
|
||||
"ui.radius_type": "Radius Type",
|
||||
"ui.rectangle": "Rectangle",
|
||||
"ui.rectangular_pattern": "Rectangular Pattern",
|
||||
"ui.redo": "Redo",
|
||||
"ui.render": "Render",
|
||||
"ui.renderer": "Renderer",
|
||||
"ui.reset": "Reset",
|
||||
"ui.reset_all": "Reset All",
|
||||
"ui.reset_the_settings_on_this_page": "Reset the settings on this page",
|
||||
"ui.restore_defaults": "Restore Defaults",
|
||||
"ui.reverse_zoom_direction": "Reverse zoom direction",
|
||||
"ui.revolve": "Revolve",
|
||||
"ui.right": "Right",
|
||||
"ui.save": "Save",
|
||||
"ui.save_as": "Save As",
|
||||
"ui.save_as_ellipsis": "Save As...",
|
||||
"ui.scale": "Scale",
|
||||
"ui.section_analysis": "Section Analysis",
|
||||
"ui.select": "Select",
|
||||
"ui.select_other_hint": "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.",
|
||||
"ui.selection": "Selection",
|
||||
"ui.shell": "Shell",
|
||||
"ui.show": "Show",
|
||||
"ui.show_constraints": "Constraints",
|
||||
"ui.show_dimensions": "Dimensions",
|
||||
"ui.show_points": "Show Points",
|
||||
"ui.show_profile": "Show Profile",
|
||||
"ui.show_projected_geometries": "Projected Geometries",
|
||||
"ui.simple": "Simple",
|
||||
"ui.simulation": "Simulation",
|
||||
"ui.single": "Single",
|
||||
"ui.sketch": "Sketch",
|
||||
"ui.sketch_dimension": "Sketch Dimension",
|
||||
"ui.sketch_grid": "Sketch Grid",
|
||||
"ui.sketch_scale": "Sketch Scale",
|
||||
"ui.sketch_status": "Sketch Status",
|
||||
"ui.sketches": "Sketches",
|
||||
"ui.slice": "Slice",
|
||||
"ui.slot": "Slot",
|
||||
"ui.smooth": "Smooth",
|
||||
"ui.smooth_wheel_zoom": "Smooth wheel zoom",
|
||||
"ui.smooth_wheel_zoom_tip": "Turn off to apply each wheel event immediately, without a smoothing tail",
|
||||
"ui.snap": "Snap",
|
||||
"ui.space_mouse_hint": "Move and rotate the view together; sketches use pan and zoom only.",
|
||||
"ui.space_mouse_navigate": "Navigate with SpaceNavigator / SpaceMouse",
|
||||
"ui.spacing": "Spacing",
|
||||
"ui.spacing_2": "Spacing 2",
|
||||
"ui.sphere": "Sphere",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Split Body",
|
||||
"ui.stand_up_tip": "After a STEP or IGES file opens with Z up, offer a quarter turn about X",
|
||||
"ui.start": "Start",
|
||||
"ui.start_radius": "Start Radius",
|
||||
"ui.sweep": "Sweep",
|
||||
"ui.symmetric": "Symmetric",
|
||||
"ui.symmetry": "Symmetry",
|
||||
"ui.system": "System",
|
||||
"ui.tangent": "Tangent",
|
||||
"ui.taper_angle": "Taper Angle",
|
||||
"ui.tapped": "Tapped",
|
||||
"ui.text": "Text",
|
||||
"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",
|
||||
"ui.torus": "Torus",
|
||||
"ui.trackpad_pan_tip": "Trackpad gestures pan (off: they zoom like the wheel); mouse wheel zooms, thumb wheel pans sideways, pinch always zooms",
|
||||
"ui.trim": "Trim",
|
||||
"ui.two_finger_scroll_pans": "Two-finger scroll pans",
|
||||
"ui.two_sides": "Two Sides",
|
||||
"ui.ui_scale": "UI scale",
|
||||
"ui.undo": "Undo",
|
||||
"ui.units": "Units",
|
||||
"ui.units_of_new_designs": "Units of new designs",
|
||||
"ui.untitled": "Untitled",
|
||||
"ui.up_axis_note": "Which axis points up in the views (axis view, Home, Look At). Designs are unchanged.",
|
||||
"ui.variable": "Variable",
|
||||
"ui.view_centre": "View centre",
|
||||
"ui.visual_shaded_hidden_edges": "Shaded with hidden edges",
|
||||
"ui.visual_shaded_with_edges": "Shaded with edges",
|
||||
"ui.visual_style": "Visual style",
|
||||
"ui.visual_wireframe": "Wireframe",
|
||||
"ui.workspace": "workspace",
|
||||
"ui.x_distance": "X Distance",
|
||||
"ui.y_distance": "Y Distance",
|
||||
"ui.y_up": "Y up",
|
||||
"ui.z_up": "Z up",
|
||||
"ui.zoom_to_cursor_tip": "The wheel zooms toward the point under the cursor (off: toward the view's centre)",
|
||||
"ui.zoom_to_the_cursor": "Zoom to the cursor",
|
||||
"A new configuration, a copy of the active one": "A new configuration, a copy of the active one",
|
||||
"About SolidCraft": "About SolidCraft",
|
||||
"Active": "Active",
|
||||
"Active rule": "Active rule",
|
||||
"Add": "Add",
|
||||
"Add Key": "Add Key",
|
||||
"Add Mid Radius": "Add Mid Radius",
|
||||
"Add Row": "Add Row",
|
||||
"All Sketch Points": "All Sketch Points",
|
||||
"All open designs": "All open designs",
|
||||
"Allowed expressions, separated by semicolons": "Allowed expressions, separated by semicolons",
|
||||
"Also deletes": "Also deletes",
|
||||
"Angle 2": "Angle 2",
|
||||
"Animate": "Animate",
|
||||
"Appearance name": "Appearance name",
|
||||
"Applied Radius": "Applied Radius",
|
||||
"Apply To": "Apply To",
|
||||
"Area": "Area",
|
||||
"Attachment": "Attachment",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Line Position": "Bend Line Position",
|
||||
"Bend Position": "Bend Position",
|
||||
"Bend Radius": "Bend Radius",
|
||||
"Bends": "Bends",
|
||||
"Body": "Body",
|
||||
"Bounding box": "Bounding box",
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
|
||||
"Capture All Positions": "Capture All Positions",
|
||||
"Catch Direction": "Catch Direction",
|
||||
"Cells": "Cells",
|
||||
"Center X": "Center X",
|
||||
"Center Y": "Center Y",
|
||||
"Center of mass": "Center of mass",
|
||||
"Chamfer Type": "Chamfer Type",
|
||||
"Check collisions": "Check collisions",
|
||||
"Checked": "Checked",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.",
|
||||
"Close draft": "Close draft",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Colour",
|
||||
"Component1": "Component1",
|
||||
"Configuration": "Configuration",
|
||||
"Configuration rules": "Configuration rules",
|
||||
"Contact": "Contact",
|
||||
"Copy all licences": "Copy all licences",
|
||||
"Copy diagnostic information": "Copy diagnostic information",
|
||||
"Copy report": "Copy report",
|
||||
"Corner Relief Shape": "Corner Relief Shape",
|
||||
"Counterbore depth": "Counterbore depth",
|
||||
"Countersink angle": "Countersink angle",
|
||||
"Create Issue — review draft": "Create Issue — review draft",
|
||||
"Customize Toolbar": "Customize Toolbar",
|
||||
"Delete all": "Delete all",
|
||||
"Delete rule": "Delete rule",
|
||||
"Delete the row": "Delete the row",
|
||||
"Delete the set": "Delete the set",
|
||||
"Delete this parameter": "Delete this parameter",
|
||||
"Density": "Density",
|
||||
"Designs you open or save appear here.": "Designs you open or save appear here.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
|
||||
"Discard": "Discard",
|
||||
"Double-click to rename": "Double-click to rename",
|
||||
"Drive Joint": "Drive Joint",
|
||||
"Edit Profile Sketch": "Edit Profile Sketch",
|
||||
"Enable rule": "Enable rule",
|
||||
"Enabled": "Enabled",
|
||||
"Explode": "Explode",
|
||||
"Extents": "Extents",
|
||||
"Face attachment lost": "Face attachment lost",
|
||||
"Faces / edges / vertices": "Faces / edges / vertices",
|
||||
"Favourite": "Favourite",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).",
|
||||
"Filter": "Filter",
|
||||
"Find in Browser": "Find in Browser",
|
||||
"Flat size": "Flat size",
|
||||
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
|
||||
"Font": "Font",
|
||||
"Full Length": "Full Length",
|
||||
"Gap": "Gap",
|
||||
"Grid": "Grid",
|
||||
"Height": "Height",
|
||||
"Home view": "Home view",
|
||||
"In This Design": "In This Design",
|
||||
"Interferes": "Interferes",
|
||||
"Issue title": "Issue title",
|
||||
"Joint Origin1": "Joint Origin1",
|
||||
"Keep Tools": "Keep Tools",
|
||||
"Later": "Later",
|
||||
"Leave it": "Leave it",
|
||||
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Legacy face attachment. Rebinding to a certified face creates an exact attachment.",
|
||||
"Length": "Length",
|
||||
"Length units": "Length units",
|
||||
"Library": "Library",
|
||||
"Loop": "Loop",
|
||||
"Margin": "Margin",
|
||||
"Mass": "Mass",
|
||||
"Material": "Material",
|
||||
"Mid Radius": "Mid Radius",
|
||||
"Modeled": "Modeled",
|
||||
"More toolbar panels": "More toolbar panels",
|
||||
"Motion": "Motion",
|
||||
"Move Type": "Move Type",
|
||||
"Move earlier": "Move earlier",
|
||||
"Move later": "Move later",
|
||||
"Name": "Name",
|
||||
"No crash reports.": "No crash reports.",
|
||||
"No matching appearances": "No matching appearances",
|
||||
"Not available yet": "Not available yet",
|
||||
"Opacity": "Opacity",
|
||||
"Open GitHub draft": "Open GitHub draft",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
|
||||
"Other side": "Other side",
|
||||
"Parameter values": "Parameter values",
|
||||
"Per Edge": "Per Edge",
|
||||
"Pick the tool bodies": "Pick the tool bodies",
|
||||
"Place at the origin instead": "Place at the origin instead",
|
||||
"Playback": "Playback",
|
||||
"Position": "Position",
|
||||
"Position (0 to 1)": "Position (0 to 1)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other",
|
||||
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
|
||||
"RECENT": "RECENT",
|
||||
"Radius Fit": "Radius Fit",
|
||||
"Ratio": "Ratio",
|
||||
"Recover": "Recover",
|
||||
"Recover unsaved design?": "Recover unsaved design?",
|
||||
"Remove Tools": "Remove Tools",
|
||||
"Remove from list": "Remove from list",
|
||||
"Remove private design details or paths before sharing.": "Remove private design details or paths before sharing.",
|
||||
"Remove the column": "Remove the column",
|
||||
"Remove the track": "Remove the track",
|
||||
"Remove this point": "Remove this point",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
|
||||
"Reset Toolbar": "Reset Toolbar",
|
||||
"Restore the inherited component or physical material appearance": "Restore the inherited component or physical material appearance",
|
||||
"Result": "Result",
|
||||
"Retake screenshot": "Retake screenshot",
|
||||
"Reverse": "Reverse",
|
||||
"Reverse Direction": "Reverse Direction",
|
||||
"Revert": "Revert",
|
||||
"Revert All Positions": "Revert All Positions",
|
||||
"Review the report and screenshot before opening GitHub.": "Review the report and screenshot before opening GitHub.",
|
||||
"Rim Edge": "Rim Edge",
|
||||
"Roll History Marker Here": "Roll History Marker Here",
|
||||
"Rule": "Rule",
|
||||
"Saved preview. Geometry is being checked before editing.": "Saved preview. Geometry is being checked before editing.",
|
||||
"Scale factor": "Scale factor",
|
||||
"Search": "Search",
|
||||
"Search commands": "Search commands",
|
||||
"Select bodies (a face picks its body)": "Select bodies (a face picks its body)",
|
||||
"Select bodies or faces instead": "Select bodies or faces instead",
|
||||
"Selection filter": "Selection filter",
|
||||
"Semantic faces share their appearance across instances.": "Semantic faces share their appearance across instances.",
|
||||
"Shape": "Shape",
|
||||
"Show Agent Cursor": "Show Agent Cursor",
|
||||
"Show in folder": "Show in folder",
|
||||
"Size": "Size",
|
||||
"Sketch plane has no body-face attachment.": "Sketch plane has no body-face attachment.",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft closed before these designs were saved. Their last autosave can be opened.",
|
||||
"Speed": "Speed",
|
||||
"Stand it up": "Stand it up",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stand it up: a quarter turn about X, as one Move feature you can undo.",
|
||||
"Stand this part up?": "Stand this part up?",
|
||||
"Step": "Step",
|
||||
"Steps": "Steps",
|
||||
"Suppressed": "Suppressed",
|
||||
"Swap Ends": "Swap Ends",
|
||||
"Tangent Chain": "Tangent Chain",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "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.": "The saved author occurrence is missing. Select a valid author placement before rebinding.",
|
||||
"Then": "Then",
|
||||
"These features will be deleted:": "These features will be deleted:",
|
||||
"These features will fail:": "These features will fail:",
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "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 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.": "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.",
|
||||
"To Offset": "To Offset",
|
||||
"Tolerance": "Tolerance",
|
||||
"Type": "Type",
|
||||
"Updating the preview": "Updating the preview",
|
||||
"Use fixed plane": "Use fixed plane",
|
||||
"VIEW": "VIEW",
|
||||
"Value": "Value",
|
||||
"Volume": "Volume",
|
||||
"Wall": "Wall",
|
||||
"When": "When",
|
||||
"Width": "Width",
|
||||
"Will fail": "Will fail",
|
||||
"angle, height": "angle, height",
|
||||
"command name": "command name",
|
||||
"expression, e.g. 25 mm": "expression, e.g. 25 mm",
|
||||
"fits the face, e.g. M8": "fits the face, e.g. M8",
|
||||
"from the rule": "from the rule",
|
||||
"needs two joints": "needs two joints",
|
||||
"new configuration": "new configuration",
|
||||
"new name": "new name",
|
||||
"new parameter": "new parameter",
|
||||
"no interference": "no interference",
|
||||
"no joint that moves yet": "no joint that moves yet",
|
||||
"no picture": "no picture",
|
||||
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
|
||||
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
|
||||
"the parts library is not available": "the parts library is not available",
|
||||
"to the next face": "to the next face",
|
||||
"Revert Position": "Revert Position",
|
||||
"Construction": "Construction",
|
||||
"Faces": "Faces",
|
||||
"Edges": "Edges",
|
||||
"Vertices": "Vertices",
|
||||
"Sketch geometry and profiles": "Sketch geometry and profiles",
|
||||
"Sketch curves": "Sketch curves",
|
||||
"Sketch points": "Sketch points",
|
||||
"Sketch dimensions": "Sketch dimensions",
|
||||
"Sketch constraints": "Sketch constraints",
|
||||
"0 turns it off": "0 turns it off",
|
||||
"Show the pointer an AI agent moves (#35)": "Show the pointer an AI agent moves (#35)",
|
||||
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
|
||||
"Flat": "Flat",
|
||||
"Save Rule": "Save Rule",
|
||||
"Preview Row": "Preview Row",
|
||||
"Add condition": "Add condition",
|
||||
"Apply to selection": "Apply to selection",
|
||||
"Remove override": "Remove override",
|
||||
"Copy screenshot": "Copy screenshot",
|
||||
"Remove screenshot": "Remove screenshot",
|
||||
"Use active author placement": "Use active author placement",
|
||||
"Reattach selected face": "Reattach selected face",
|
||||
"Use saved fixed plane": "Use saved fixed plane",
|
||||
"Use selected face as fixed plane": "Use selected face as fixed plane",
|
||||
"View navigator": "View navigator",
|
||||
"Signed axes": "Signed axes",
|
||||
"Cube": "Cube",
|
||||
"Middle double-click": "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": "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",
|
||||
"ui.animateJoint": "Animate Joint",
|
||||
"ui.calibrate": "Calibrate",
|
||||
"ui.customizeToolbar": "Customize Toolbar…",
|
||||
"ui.editJointLimits": "Edit Joint Limits",
|
||||
"ui.exportDxf": "Export DXF…",
|
||||
"ui.findInTimeline": "Find in Timeline",
|
||||
"ui.findInWindow": "Find in Window",
|
||||
"ui.fit": "Fit",
|
||||
"ui.groupSelected": "Group Selected",
|
||||
"ui.hideAxes": "Hide Axes",
|
||||
"ui.hideDimension": "Hide Dimension",
|
||||
"ui.hideOrigin": "Hide Origin",
|
||||
"ui.hidePlanes": "Hide Planes",
|
||||
"ui.hideProfile": "Hide Profile",
|
||||
"ui.homeView": "Home View",
|
||||
"ui.isolate": "Isolate",
|
||||
"ui.lock": "Lock",
|
||||
"ui.newSketchGroup": "New Sketch Group",
|
||||
"ui.normalConstruction": "Normal / Construction",
|
||||
"ui.orbit": "Orbit",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.pan": "Pan",
|
||||
"ui.pinToShortcuts": "Pin to Shortcuts",
|
||||
"ui.pinToToolbar": "Pin to Toolbar",
|
||||
"ui.properties": "Properties",
|
||||
"ui.removeSection": "Remove Section",
|
||||
"ui.removeFromToolbar": "Remove from Toolbar",
|
||||
"ui.repeat": "Repeat",
|
||||
"ui.report_a_bug": "Report a bug",
|
||||
"ui.restore": "Restore",
|
||||
"ui.heading_setup": "SETUP",
|
||||
"ui.selectOther": "Select Other…",
|
||||
"ui.selectTangentChain": "Select Tangent Chain",
|
||||
"ui.showAll": "Show All",
|
||||
"ui.showAllBodies": "Show All Bodies",
|
||||
"ui.showAxes": "Show Axes",
|
||||
"ui.showDimension": "Show Dimension",
|
||||
"ui.showOrigin": "Show Origin",
|
||||
"ui.showPlanes": "Show Planes",
|
||||
"ui.suppress": "Suppress",
|
||||
"ui.command_palette_hint": "Type a command, e.g. {example} — or press S to search",
|
||||
"ui.unground": "Unground",
|
||||
"ui.unisolate": "Unisolate",
|
||||
"ui.unlock": "Unlock",
|
||||
"ui.unsuppress": "Unsuppress",
|
||||
"ui.updateToCurrentView": "Update to Current View",
|
||||
"ui.zoom": "Zoom",
|
||||
"ui.command_done": "{command} done",
|
||||
"timeline.group": "New Feature Folder",
|
||||
"timeline.ungroup": "Remove Feature Folders",
|
||||
"timeline.rename_group": "Rename Feature Folder",
|
||||
"timeline.move_to_group": "Move Features to Folder",
|
||||
"timeline.move_many": "Move Timeline Items",
|
||||
"timeline.reorder_many": "Reorder Features",
|
||||
"ui.show_construction_geometries": "Construction Geometries",
|
||||
"ui.total_angle": "Total angle",
|
||||
"sketch_text.builtin_font": "{family} (built-in)",
|
||||
"sketch_text.font_search": "Search fonts",
|
||||
"sketch_text.loading_fonts": "Looking for installed fonts…",
|
||||
"sketch_text.use_font_file": "Use the font file {path}",
|
||||
"sketch_text.bold": "Bold",
|
||||
"sketch_text.italic": "Italic",
|
||||
"sketch_text.align_left": "Left",
|
||||
"sketch_text.align_center": "Center",
|
||||
"sketch_text.align_right": "Right",
|
||||
"sketch_text.path_hint": "A curve of the sketch the text follows: click here, then the curve",
|
||||
"sketch_text.path_clear": "Take the text off its curve",
|
||||
"Style": "Style",
|
||||
"Alignment": "Alignment",
|
||||
"Letter Spacing": "Letter Spacing",
|
||||
"Line Spacing": "Line Spacing",
|
||||
"Path": "Path",
|
||||
"sketch_text.on_path": "The text follows its path: the curve places it",
|
||||
"cmd.material_properties_validate":"Validate Physical Material Data"
|
||||
}
|
||||
@@ -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": "2平面を通る軸",
|
||||
"cmd.construct_axis_two_points": "2点を通る軸",
|
||||
"cmd.construct_plane_along_path": "パスに沿った平面",
|
||||
"cmd.construct_plane_midplane": "中間平面",
|
||||
"cmd.construct_plane_perpendicular": "垂直平面",
|
||||
"cmd.construct_plane_tangent": "正接平面",
|
||||
"cmd.construct_plane_three_points": "3点を通る平面",
|
||||
"cmd.construct_plane_two_edges": "2エッジを通る平面",
|
||||
"cmd.construct_point_along_path": "パスに沿った点",
|
||||
"cmd.construct_point_center": "円/球/トーラスの中心点",
|
||||
"cmd.construct_point_edge_plane": "エッジと平面の交点",
|
||||
"cmd.construct_point_three_planes": "3平面の交点",
|
||||
"cmd.construct_point_two_edges": "2エッジの交点",
|
||||
"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": "2距離面取り",
|
||||
"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": "{total} ステップ中 {step}: {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": "SolidCraft について",
|
||||
"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": "Z軸上向きで開いたSTEPまたはIGESファイルに対し、X軸周りに90度回転させるか確認します",
|
||||
"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": "2本指スクロールでパン",
|
||||
"ui.two_sides": "両側",
|
||||
"ui.ui_scale": "UIスケール",
|
||||
"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.": "新しい平面を選択してください。スケッチジオメトリと寸法は2次元座標系のまま維持されます。",
|
||||
"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軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。",
|
||||
"Filter": "フィルター",
|
||||
"Find in Browser": "ブラウザで検索",
|
||||
"Flat size": "展開サイズ",
|
||||
"Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Font": "フォント",
|
||||
"Full Length": "全長",
|
||||
"Gap": "ギャップ",
|
||||
"Grid": "グリッド",
|
||||
"Height": "高さ",
|
||||
"Home view": "ホームビュー",
|
||||
"In This Design": "このデザイン内",
|
||||
"Interferes": "干渉あり",
|
||||
"Issue title": "Issueタイトル",
|
||||
"Joint Origin1": "ジョイント原点1",
|
||||
"Keep Tools": "ツールボディを保持",
|
||||
"Later": "後で",
|
||||
"Leave it": "そのままにする",
|
||||
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "従来の面アタッチメントです。確定された面に再バインドすると正確なアタッチメントが作成されます。",
|
||||
"Length": "長さ",
|
||||
"Length units": "長さの単位",
|
||||
"Library": "ライブラリ",
|
||||
"Loop": "ループ",
|
||||
"Margin": "マージン",
|
||||
"Mass": "質量",
|
||||
"Material": "材料",
|
||||
"Mid Radius": "中間半径",
|
||||
"Modeled": "モデル化済み",
|
||||
"More toolbar panels": "その他のツールバーパネル",
|
||||
"Motion": "モーション",
|
||||
"Move Type": "移動タイプ",
|
||||
"Move earlier": "前へ移動",
|
||||
"Move later": "後へ移動",
|
||||
"Name": "名前",
|
||||
"No crash reports.": "クラッシュレポートはありません。",
|
||||
"No matching appearances": "一致する外観がありません",
|
||||
"Not available yet": "まだ利用できません",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "GitHubの下書きを開く",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または 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.": "正立させる: 取り消し可能な1つの移動フィーチャーとして、X軸周りに90度回転します。",
|
||||
"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": "次を実行 (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": "条件 (When)",
|
||||
"Width": "幅",
|
||||
"Will fail": "失敗します",
|
||||
"angle, height": "角度, 高さ",
|
||||
"command name": "コマンド名",
|
||||
"expression, e.g. 25 mm": "計算式 (例: 25 mm)",
|
||||
"fits the face, e.g. M8": "面に適合するサイズ (例: M8)",
|
||||
"from the rule": "ルールから",
|
||||
"needs two joints": "2つのジョイントが必要",
|
||||
"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": "1つまたは2つの頂点、エッジ、面を選択",
|
||||
"the parts library is not available": "パーツライブラリは利用できません",
|
||||
"to the next face": "次の面まで",
|
||||
"Revert Position": "位置を元に戻す",
|
||||
"Construction": "コンストラクション",
|
||||
"Flat": "展開",
|
||||
"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)": "カメラ追従(スケッチを正面視)",
|
||||
"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,701 @@
|
||||
{
|
||||
"browser.body_name": "바디{number}",
|
||||
"browser.component_name": "컴포넌트{number}",
|
||||
"browser.parameters": "매개변수 ({count})",
|
||||
"browser.precision": "정밀도: {precision}",
|
||||
"browser.sketch_name": "스케치{number}",
|
||||
"browser.units": "단위: {units}",
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.select": "↖ 선택",
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"dialog.step": "단계",
|
||||
"dialog.selection_step": "{step}/{total} 단계: {input} 선택",
|
||||
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
|
||||
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
|
||||
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
|
||||
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
|
||||
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
|
||||
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
|
||||
"settings.reset_intro": "다음 설정은 이 컴퓨터에서 작동하지 않아 SolidCraft를 시작할 수 없게 하므로 변경되었습니다:",
|
||||
"settings.reset_title": "설정이 변경되었습니다",
|
||||
"sketch.degrees_of_freedom": "자유도 {count}개",
|
||||
"sketch.draw_curves": "{linetype}(으)로 그리기",
|
||||
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
|
||||
"ui.1_degree_of_freedom": "자유도 1개",
|
||||
"ui.3d_sketch": "3D 스케치",
|
||||
"ui.about": "정보",
|
||||
"ui.activate": "활성화",
|
||||
"ui.active_component": "활성 컴포넌트",
|
||||
"ui.agent_cursor": "에이전트 커서",
|
||||
"ui.agent_cursor_speed": "에이전트 커서 속도",
|
||||
"ui.all": "모두",
|
||||
"ui.analysis": "분석",
|
||||
"ui.angle": "각도",
|
||||
"ui.angle_decimals": "각도 소수점 자릿수",
|
||||
"ui.animate_view_changes": "뷰 변경 애니메이션",
|
||||
"ui.animation": "애니메이션",
|
||||
"ui.animation_speed": "애니메이션 속도",
|
||||
"ui.anti_aliasing": "안티앨리어싱",
|
||||
"ui.apply": "적용",
|
||||
"ui.arc": "호",
|
||||
"ui.assemble": "조립",
|
||||
"ui.autosave_every": "자동 저장 간격",
|
||||
"ui.axes": "축",
|
||||
"ui.axis": "축",
|
||||
"ui.base_point": "기준점",
|
||||
"ui.bodies": "바디",
|
||||
"ui.box": "박스",
|
||||
"ui.break": "끊기",
|
||||
"ui.cancel": "취소",
|
||||
"ui.canvases": "캔버스",
|
||||
"ui.center": "중심",
|
||||
"ui.centerline": "중심선",
|
||||
"ui.chain": "체인",
|
||||
"ui.chamfer": "모따기",
|
||||
"ui.chamfer_type": "모따기 유형",
|
||||
"ui.change_workspace": "작업 공간 변경",
|
||||
"ui.chord_length": "현 길이",
|
||||
"ui.curvature_g2": "곡률 연속 (G2)",
|
||||
"ui.equivalent_radius": "등가 반지름",
|
||||
"ui.circle": "원",
|
||||
"ui.circular_pattern": "원형 패턴",
|
||||
"ui.clear_recent": "최근 항목 지우기",
|
||||
"ui.clear_the_selection": "선택 해제",
|
||||
"ui.close": "닫기",
|
||||
"ui.coil": "코일",
|
||||
"ui.coincident": "일치",
|
||||
"ui.collapse": "접기",
|
||||
"ui.collinear": "동일 선상",
|
||||
"ui.combine": "결합",
|
||||
"ui.coming_later": "추후 지원 예정",
|
||||
"ui.concentric": "동심",
|
||||
"ui.configurations": "구성",
|
||||
"ui.conic_curve": "원뿔 곡선",
|
||||
"ui.constant": "상수",
|
||||
"ui.constraints_conflict": "구속 조건 충돌",
|
||||
"ui.construct": "구성",
|
||||
"ui.construction": "구성선",
|
||||
"ui.copy": "복사",
|
||||
"ui.corners": "코너",
|
||||
"ui.corner": "코너",
|
||||
"ui.counterbore": "카운터보어",
|
||||
"ui.countersink": "카운터싱크",
|
||||
"ui.create": "만들기",
|
||||
"ui.create_copy": "복사본 만들기",
|
||||
"ui.create_sketch": "스케치 만들기",
|
||||
"ui.current": "현재",
|
||||
"ui.curvature": "곡률",
|
||||
"ui.curves": "곡선",
|
||||
"ui.cut": "잘라내기",
|
||||
"ui.cylinder": "원통",
|
||||
"ui.dark": "어둡게",
|
||||
"ui.default_modelling_orientation": "기본 모델링 방향",
|
||||
"ui.degrees_of_freedom": "자유도",
|
||||
"ui.delete": "삭제",
|
||||
"ui.depth": "깊이",
|
||||
"ui.design": "디자인",
|
||||
"ui.diameter": "지름",
|
||||
"ui.dimension": "치수",
|
||||
"ui.direction": "방향",
|
||||
"ui.distance": "거리",
|
||||
"ui.distance_1": "거리 1",
|
||||
"ui.distance_2": "거리 2",
|
||||
"ui.document_settings": "문서 설정",
|
||||
"ui.don_t_save": "저장 안 함",
|
||||
"ui.draft": "구배",
|
||||
"ui.drawing": "도면",
|
||||
"ui.drill_point": "드릴 선단",
|
||||
"ui.edit": "편집",
|
||||
"ui.edit_section_analysis": "단면 분석 편집",
|
||||
"ui.ellipse": "타원",
|
||||
"ui.end_radius": "끝 반지름",
|
||||
"ui.entities": "요소",
|
||||
"ui.equal": "동일",
|
||||
"ui.expand": "펼치기",
|
||||
"ui.export_ellipsis": "내보내기...",
|
||||
"ui.extend": "연장",
|
||||
"ui.extent_type": "범위 유형",
|
||||
"ui.extrude": "돌출",
|
||||
"ui.factor": "배율",
|
||||
"ui.feature_history": "피처 기록",
|
||||
"ui.feature_options": "피처 옵션",
|
||||
"ui.features": "피처",
|
||||
"ui.file": "파일",
|
||||
"ui.fillet": "필렛",
|
||||
"ui.finish_sketch": "스케치 종료",
|
||||
"ui.fix_unfix": "고정/고정 해제",
|
||||
"ui.flat": "플랫",
|
||||
"ui.flip": "뒤집기",
|
||||
"ui.front": "정면",
|
||||
"ui.fully_constrained": "완전 구속",
|
||||
"ui.general": "일반",
|
||||
"ui.graphics": "그래픽",
|
||||
"ui.heading_assemble": "조립",
|
||||
"ui.heading_box": "박스",
|
||||
"ui.heading_chamfer": "모따기",
|
||||
"ui.heading_circular_pattern": "원형 패턴",
|
||||
"ui.heading_coil": "코일",
|
||||
"ui.heading_combine": "결합",
|
||||
"ui.heading_construct": "구성",
|
||||
"ui.heading_create": "만들기",
|
||||
"ui.heading_create_sketch": "스케치 만들기",
|
||||
"ui.heading_cylinder": "원통",
|
||||
"ui.heading_design": "디자인",
|
||||
"ui.heading_draft": "구배",
|
||||
"ui.heading_edit": "편집",
|
||||
"ui.heading_extrude": "돌출",
|
||||
"ui.heading_fillet": "필렛",
|
||||
"ui.heading_finish_sketch": "스케치 종료",
|
||||
"ui.heading_front": "정면",
|
||||
"ui.heading_hole": "구멍",
|
||||
"ui.heading_home": "홈",
|
||||
"ui.heading_insert": "삽입",
|
||||
"ui.heading_inspect": "검사",
|
||||
"ui.heading_loft": "로프트",
|
||||
"ui.heading_measure": "측정",
|
||||
"ui.heading_mesh": "메시",
|
||||
"ui.heading_mirror": "대칭 복사",
|
||||
"ui.heading_modify": "수정",
|
||||
"ui.heading_move": "이동",
|
||||
"ui.heading_offset": "오프셋",
|
||||
"ui.heading_move_copy": "이동/복사",
|
||||
"ui.heading_offset_plane": "오프셋 평면",
|
||||
"ui.heading_ok": "확인",
|
||||
"ui.heading_parameters": "매개변수",
|
||||
"ui.heading_pattern_on_path": "경로 패턴",
|
||||
"ui.heading_pipe": "파이프",
|
||||
"ui.heading_plane_at_angle": "각도 평면",
|
||||
"ui.heading_plastic": "플라스틱",
|
||||
"ui.heading_rectangular_pattern": "직사각형 패턴",
|
||||
"ui.heading_revolve": "회전",
|
||||
"ui.heading_right": "우측면",
|
||||
"ui.heading_scale": "축척",
|
||||
"ui.heading_section_analysis": "단면 분석",
|
||||
"ui.heading_select": "선택",
|
||||
"ui.heading_sheet_metal": "판금",
|
||||
"ui.heading_shell": "쉘",
|
||||
"ui.heading_sketch": "스케치",
|
||||
"ui.heading_sketch_palette": "스케치 팔레트",
|
||||
"ui.heading_sketch_scale": "스케치 배율",
|
||||
"ui.heading_solid": "솔리드",
|
||||
"ui.heading_sphere": "구",
|
||||
"ui.heading_split_body": "바디 분할",
|
||||
"ui.heading_surface": "서피스",
|
||||
"ui.heading_sweep": "스윕",
|
||||
"ui.heading_thread": "나사산",
|
||||
"ui.heading_top": "상면",
|
||||
"ui.heading_torus": "토러스",
|
||||
"ui.help": "도움말",
|
||||
"ui.hide": "숨기기",
|
||||
"ui.hole": "구멍",
|
||||
"ui.hole_tap_type": "탭 유형",
|
||||
"ui.hole_type": "구멍 유형",
|
||||
"ui.home": "홈",
|
||||
"ui.horizontal_vertical": "수평/수직",
|
||||
"ui.insert": "삽입",
|
||||
"ui.inspect": "검사",
|
||||
"ui.intersect": "교차",
|
||||
"ui.join": "접합",
|
||||
"ui.joints": "조인트",
|
||||
"ui.keyboard_shortcuts": "키보드 단축키",
|
||||
"ui.language": "언어",
|
||||
"ui.length_decimals": "길이 소수점 자릿수",
|
||||
"ui.light": "밝게",
|
||||
"ui.line": "선",
|
||||
"ui.linetype": "선 종류",
|
||||
"ui.loft": "로프트",
|
||||
"ui.look_at": "바라보기",
|
||||
"ui.manufacture": "제조",
|
||||
"ui.measure": "측정",
|
||||
"ui.midpoint": "중간점",
|
||||
"ui.mirror": "대칭 복사",
|
||||
"ui.mirror_line": "대칭선",
|
||||
"ui.modify": "수정",
|
||||
"ui.move": "이동",
|
||||
"ui.multiple": "다중",
|
||||
"ui.named_views": "명명된 뷰",
|
||||
"ui.navigation": "탐색",
|
||||
"ui.needs_design": "{command}: 먼저 디자인을 열어 주세요",
|
||||
"ui.new_body": "새 바디",
|
||||
"ui.new_design": "새 디자인",
|
||||
"ui.normal": "일반",
|
||||
"ui.object_type": "객체 유형",
|
||||
"ui.objects": "객체",
|
||||
"ui.offer_to_stand_up_y_up_files": "Y-up 파일을 Z-up으로 세우도록 제안",
|
||||
"ui.offset": "오프셋",
|
||||
"ui.offset_plane": "오프셋 평면",
|
||||
"ui.one_side": "한 방향",
|
||||
"ui.open": "열기",
|
||||
"ui.open_ellipsis": "열기...",
|
||||
"ui.open_preferences": "환경 설정 열기",
|
||||
"ui.open_recent": "최근 항목 열기",
|
||||
"ui.operation": "작업",
|
||||
"ui.options": "옵션",
|
||||
"ui.orbit_around": "궤도 회전 기준",
|
||||
"ui.origin": "원점",
|
||||
"ui.palette_points": "점",
|
||||
"ui.palette_profile": "프로파일",
|
||||
"ui.pan_zoom_orbit": "이동, 확대/축소, 궤도 회전",
|
||||
"ui.parallel": "평행",
|
||||
"ui.parameters": "매개변수",
|
||||
"ui.paste": "붙여넣기",
|
||||
"ui.path": "경로",
|
||||
"ui.pattern_on_path": "경로 패턴",
|
||||
"ui.perpendicular": "수직",
|
||||
"ui.pipe": "파이프",
|
||||
"ui.placement": "배치",
|
||||
"ui.plane": "평면",
|
||||
"ui.plane_at_angle": "각도 평면",
|
||||
"ui.point": "점",
|
||||
"ui.point_under_the_cursor": "커서 아래의 점",
|
||||
"ui.points": "점",
|
||||
"ui.polygon": "다각형",
|
||||
"ui.preferences": "환경 설정",
|
||||
"ui.profile": "프로파일",
|
||||
"ui.profile_plane": "프로파일 평면",
|
||||
"ui.profiles": "프로파일",
|
||||
"ui.project": "투영",
|
||||
"ui.quantity_2": "수량 2",
|
||||
"ui.quantity": "수량",
|
||||
"ui.quit": "종료",
|
||||
"ui.radius": "반지름",
|
||||
"ui.radius_type": "반지름 유형",
|
||||
"ui.rectangle": "직사각형",
|
||||
"ui.rectangular_pattern": "직사각형 패턴",
|
||||
"ui.redo": "다시 실행",
|
||||
"ui.render": "렌더",
|
||||
"ui.reset_the_settings_on_this_page": "이 페이지의 설정 초기화",
|
||||
"ui.restore_defaults": "기본값 복원",
|
||||
"ui.reverse_zoom_direction": "확대/축소 방향 반전",
|
||||
"ui.revolve": "회전",
|
||||
"ui.right": "우측면",
|
||||
"ui.save": "저장",
|
||||
"ui.save_as": "다른 이름으로 저장",
|
||||
"ui.save_as_ellipsis": "다른 이름으로 저장...",
|
||||
"ui.scale": "축척",
|
||||
"ui.section_analysis": "단면 분석",
|
||||
"ui.select": "선택",
|
||||
"ui.selection": "선택",
|
||||
"ui.shell": "쉘",
|
||||
"ui.show": "표시",
|
||||
"ui.show_constraints": "구속 조건",
|
||||
"ui.show_dimensions": "치수",
|
||||
"ui.show_points": "점 표시",
|
||||
"ui.show_profile": "프로파일 표시",
|
||||
"ui.show_projected_geometries": "투영된 형상",
|
||||
"ui.simple": "단순",
|
||||
"ui.simulation": "시뮬레이션",
|
||||
"ui.single": "단일",
|
||||
"ui.sketch": "스케치",
|
||||
"ui.sketch_dimension": "스케치 치수",
|
||||
"ui.sketch_grid": "스케치 격자",
|
||||
"ui.sketch_scale": "스케치 축척",
|
||||
"ui.sketch_status": "스케치 상태",
|
||||
"ui.sketches": "스케치",
|
||||
"ui.slice": "슬라이스",
|
||||
"ui.slot": "슬롯",
|
||||
"ui.smooth": "부드럽게",
|
||||
"ui.snap": "스냅",
|
||||
"ui.spacing_2": "간격 2",
|
||||
"ui.spacing": "간격",
|
||||
"ui.sphere": "구",
|
||||
"ui.spline": "스플라인",
|
||||
"ui.split_body": "바디 분할",
|
||||
"ui.start": "시작",
|
||||
"ui.start_radius": "시작 반지름",
|
||||
"ui.sweep": "스윕",
|
||||
"ui.symmetric": "대칭",
|
||||
"ui.symmetry": "대칭",
|
||||
"ui.system": "시스템",
|
||||
"ui.tangent": "접선",
|
||||
"ui.taper_angle": "테이퍼 각도",
|
||||
"ui.tapped": "탭",
|
||||
"ui.text": "텍스트",
|
||||
"ui.theme": "테마",
|
||||
"ui.thickness": "두께",
|
||||
"ui.thread": "나사산",
|
||||
"ui.timeline_bottom": "하단 타임라인",
|
||||
"ui.timeline_left_note": "왼쪽 레이아웃에서는 롤백 바가 부품 및 장면 브라우저 위에 표시됩니다.",
|
||||
"ui.timeline_left_sidebar": "왼쪽 사이드바",
|
||||
"ui.tip_angle": "선단 각도",
|
||||
"ui.to_object": "객체까지",
|
||||
"ui.top": "상면",
|
||||
"ui.torus": "토러스",
|
||||
"ui.trim": "트림",
|
||||
"ui.two_sides": "양방향",
|
||||
"ui.ui_scale": "UI 배율",
|
||||
"ui.undo": "실행 취소",
|
||||
"ui.units": "단위",
|
||||
"ui.units_of_new_designs": "새 디자인의 단위",
|
||||
"ui.untitled": "제목 없음",
|
||||
"ui.variable": "가변",
|
||||
"ui.view_centre": "뷰 중심",
|
||||
"ui.visual_style": "시각 스타일",
|
||||
"ui.workspace": "작업 공간",
|
||||
"ui.y_distance": "Y 거리",
|
||||
"ui.x_distance": "X 거리",
|
||||
"ui.zoom_to_the_cursor": "커서 위치로 확대/축소",
|
||||
"ui.animateJoint": "조인트 애니메이션",
|
||||
"ui.calibrate": "캘리브레이션",
|
||||
"ui.customizeToolbar": "도구 모음 사용자 지정…",
|
||||
"ui.editJointLimits": "조인트 제한 편집",
|
||||
"ui.exportDxf": "DXF 내보내기…",
|
||||
"ui.findInTimeline": "타임라인에서 찾기",
|
||||
"ui.findInWindow": "창에서 찾기",
|
||||
"ui.fit": "맞춤",
|
||||
"ui.groupSelected": "선택 항목 그룹화",
|
||||
"ui.hideAxes": "축 숨기기",
|
||||
"ui.hideDimension": "치수 숨기기",
|
||||
"ui.hideOrigin": "원점 숨기기",
|
||||
"ui.hidePlanes": "평면 숨기기",
|
||||
"ui.hideProfile": "윤곽 숨기기",
|
||||
"ui.homeView": "홈 보기",
|
||||
"ui.isolate": "분리 표시",
|
||||
"ui.lock": "잠금",
|
||||
"ui.newSketchGroup": "새 스케치 그룹",
|
||||
"ui.normalConstruction": "일반 / 생성",
|
||||
"ui.orbit": "회전",
|
||||
"ui.heading_position": "배치",
|
||||
"ui.pan": "이동",
|
||||
"ui.pinToShortcuts": "바로 가기에 고정",
|
||||
"ui.pinToToolbar": "도구 모음에 고정",
|
||||
"ui.properties": "속성",
|
||||
"ui.removeSection": "단면 제거",
|
||||
"ui.removeFromToolbar": "도구 모음에서 제거",
|
||||
"ui.repeat": "반복",
|
||||
"ui.report_a_bug": "버그 신고",
|
||||
"ui.restore": "복원",
|
||||
"ui.heading_setup": "설정",
|
||||
"ui.selectOther": "기타 선택…",
|
||||
"ui.selectTangentChain": "접선 체인 선택",
|
||||
"ui.showAll": "모두 표시",
|
||||
"ui.showAllBodies": "모두 표시",
|
||||
"ui.showAxes": "축 표시",
|
||||
"ui.showDimension": "치수 표시",
|
||||
"ui.showOrigin": "원점 표시",
|
||||
"ui.showPlanes": "평면 표시",
|
||||
"ui.suppress": "비활성화",
|
||||
"ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
|
||||
"ui.unground": "고정 해제",
|
||||
"ui.unisolate": "분리 표시 해제",
|
||||
"ui.unlock": "잠금 해제",
|
||||
"ui.unsuppress": "비활성화 해제",
|
||||
"ui.updateToCurrentView": "현재 보기로 업데이트",
|
||||
"ui.zoom": "확대/축소",
|
||||
"ui.command_done": "{command} 완료",
|
||||
"cmd.appearance_assign": "외관",
|
||||
"cmd.appearance_library": "외관 라이브러리",
|
||||
"cmd.appearance_list": "외관 목록",
|
||||
"cmd.body_rename": "바디 이름 변경",
|
||||
"cmd.browser_group": "새 그룹",
|
||||
"cmd.browser_move": "브라우저 항목 이동",
|
||||
"cmd.browser_order": "브라우저 항목 정렬",
|
||||
"cmd.browser_rename_group": "그룹 이름 변경",
|
||||
"cmd.browser_ungroup": "그룹 해제",
|
||||
"cmd.browser_visibility": "표시/숨기기",
|
||||
"cmd.canvas_calibrate": "캔버스 보정",
|
||||
"cmd.canvas_decal": "데칼",
|
||||
"cmd.canvas_delete": "캔버스 삭제",
|
||||
"cmd.canvas_edit": "캔버스 편집",
|
||||
"cmd.canvas_insert": "캔버스",
|
||||
"cmd.component_activate": "구성 요소 활성화",
|
||||
"cmd.component_break_link": "링크 끊기",
|
||||
"cmd.component_capture_position": "위치 캡처",
|
||||
"cmd.component_create": "새 구성 요소",
|
||||
"cmd.component_delete": "구성 요소 삭제",
|
||||
"cmd.component_from_bodies": "바디로 구성 요소 만들기",
|
||||
"cmd.component_insert_design": "설계 삽입",
|
||||
"cmd.component_links": "링크 목록",
|
||||
"cmd.component_list": "구성 요소 목록",
|
||||
"cmd.component_move_bodies": "바디를 구성 요소로 이동",
|
||||
"cmd.component_move_sketches": "스케치를 구성 요소로 이동",
|
||||
"cmd.component_paste_new": "새로 붙여넣기",
|
||||
"cmd.component_properties": "구성 요소 물리 속성",
|
||||
"cmd.component_rename": "구성 요소 이름 변경",
|
||||
"cmd.component_revert_position": "위치 복원",
|
||||
"cmd.component_update_links": "링크 업데이트",
|
||||
"cmd.config_activate": "구성 활성화",
|
||||
"cmd.config_column": "구성 열",
|
||||
"cmd.config_configure": "구성",
|
||||
"cmd.config_preview": "구성 미리 보기",
|
||||
"cmd.config_row": "구성 행",
|
||||
"cmd.config_rule": "구성 규칙",
|
||||
"cmd.config_rules": "구성 규칙",
|
||||
"cmd.config_table": "구성 표시",
|
||||
"cmd.construct_axis_edge": "모서리를 통과하는 축",
|
||||
"cmd.construct_axis_normal_to_face": "면에 수직인 축",
|
||||
"cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축",
|
||||
"cmd.construct_axis_two_planes": "두 평면을 통과하는 축",
|
||||
"cmd.construct_axis_two_points": "두 점을 통과하는 축",
|
||||
"cmd.construct_plane_along_path": "경로를 따르는 평면",
|
||||
"cmd.construct_plane_midplane": "중간 평면",
|
||||
"cmd.construct_plane_perpendicular": "수직 평면",
|
||||
"cmd.construct_plane_tangent": "접선 평면",
|
||||
"cmd.construct_plane_three_points": "세 점을 통과하는 평면",
|
||||
"cmd.construct_plane_two_edges": "두 모서리를 통과하는 평면",
|
||||
"cmd.construct_point_along_path": "경로를 따르는 점",
|
||||
"cmd.construct_point_center": "원/구/토러스 중심의 점",
|
||||
"cmd.construct_point_edge_plane": "모서리와 평면의 점",
|
||||
"cmd.construct_point_three_planes": "세 평면이 만나는 점",
|
||||
"cmd.construct_point_two_edges": "두 모서리가 만나는 점",
|
||||
"cmd.construct_point_vertex": "꼭짓점의 점",
|
||||
"cmd.contact_check": "접촉 확인",
|
||||
"cmd.contact_create": "새 접촉 집합",
|
||||
"cmd.contact_disable_all": "접촉 비활성화",
|
||||
"cmd.contact_edit": "접촉 집합 편집",
|
||||
"cmd.contact_enable_all": "모든 접촉 활성화",
|
||||
"cmd.contact_enable_sets": "접촉 집합 활성화",
|
||||
"cmd.contact_list": "접촉 집합 목록",
|
||||
"cmd.diagnostics_crash_report": "충돌 보고서",
|
||||
"cmd.diagnostics_crash_reports": "충돌 보고서",
|
||||
"cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제",
|
||||
"cmd.diagnostics_crash_test": "충돌 테스트",
|
||||
"cmd.doc_recover": "설계 복구",
|
||||
"cmd.doc_recovery_discard": "복구된 설계 폐기",
|
||||
"cmd.doc_recovery_list": "복구 가능한 설계",
|
||||
"cmd.document_inspect": "설계 검사",
|
||||
"cmd.document_units": "설계 단위 변경",
|
||||
"cmd.edit_checkpoint": "체크포인트",
|
||||
"cmd.edit_restore_checkpoint": "체크포인트 복원",
|
||||
"cmd.engine_commands": "명령 목록",
|
||||
"cmd.explode_create": "분해 보기",
|
||||
"cmd.explode_delete": "분해 보기 삭제",
|
||||
"cmd.explode_list": "분해 보기 목록",
|
||||
"cmd.explode_show": "분해 보기 표시",
|
||||
"cmd.expr_evaluate": "식 평가",
|
||||
"cmd.face_delete": "면 삭제",
|
||||
"cmd.file_export": "내보내기",
|
||||
"cmd.file_insert_mesh": "메쉬 삽입",
|
||||
"cmd.file_insert_step": "STEP 삽입",
|
||||
"cmd.file_save_mesh": "메쉬로 저장",
|
||||
"cmd.inspect_accessibility": "접근성 분석",
|
||||
"cmd.inspect_center_of_mass": "무게중심",
|
||||
"cmd.inspect_curvature_comb": "곡률빗 분석",
|
||||
"cmd.inspect_curvature_map": "곡률맵 분석",
|
||||
"cmd.inspect_draft": "블록각 분석",
|
||||
"cmd.inspect_environment_map": "환경 맵 분석",
|
||||
"cmd.inspect_interference": "간섭",
|
||||
"cmd.inspect_isocurve": "등가곡선 분석",
|
||||
"cmd.inspect_minimum_radius": "최소 반경 분석",
|
||||
"cmd.inspect_zebra": "얼룩말줄무늬 분석",
|
||||
"cmd.joint_as_built": "조립 상태 관절",
|
||||
"cmd.joint_create": "관절",
|
||||
"cmd.joint_delete": "관절 삭제",
|
||||
"cmd.joint_drive": "관절 구동",
|
||||
"cmd.joint_edit": "관절 편집",
|
||||
"cmd.joint_limits": "관절 제한",
|
||||
"cmd.joint_list": "관절 목록",
|
||||
"cmd.joint_motion_link": "모션 링크",
|
||||
"cmd.joint_origin": "관절 원점",
|
||||
"cmd.joint_rigid_group": "강체 그룹",
|
||||
"cmd.joint_solve": "관절 해석",
|
||||
"cmd.material_assign": "물리 재질",
|
||||
"cmd.material_create": "새 물리 재질",
|
||||
"cmd.material_delete": "물리 재질 삭제",
|
||||
"cmd.material_edit": "물리 재질 편집",
|
||||
"cmd.material_list": "재질 목록",
|
||||
"cmd.mesh_convert": "메쉬 변환",
|
||||
"cmd.model_edges": "모서리 목록",
|
||||
"cmd.model_faces": "면 목록",
|
||||
"cmd.model_threads": "나사산 목록",
|
||||
"cmd.motion_delete": "운동 연구 삭제",
|
||||
"cmd.motion_export": "운동 연구 내보내기",
|
||||
"cmd.motion_list": "운동 연구 목록",
|
||||
"cmd.motion_play": "운동 연구 재생",
|
||||
"cmd.motion_study": "운동 연구",
|
||||
"cmd.occurrence_copy": "붙여넣기(인스턴스)",
|
||||
"cmd.occurrence_ground": "고정",
|
||||
"cmd.occurrence_move": "인스턴스 이동",
|
||||
"cmd.occurrence_properties": "인스턴스 물리 속성",
|
||||
"cmd.parameters_add": "사용자 매개 변수 추가",
|
||||
"cmd.parameters_change": "매개 변수 변경",
|
||||
"cmd.parameters_comment": "매개 변수에 설명 추가",
|
||||
"cmd.parameters_complete": "식 완성",
|
||||
"cmd.parameters_delete": "매개 변수 삭제",
|
||||
"cmd.parameters_export": "매개 변수 내보내기",
|
||||
"cmd.parameters_favorite": "즐겨찾는 매개 변수",
|
||||
"cmd.parameters_graph": "매개 변수 그래프",
|
||||
"cmd.parameters_import": "매개 변수 가져오기",
|
||||
"cmd.parameters_list": "매개 변수 목록",
|
||||
"cmd.parameters_rename": "매개 변수 이름 변경",
|
||||
"cmd.parameters_users": "매개 변수 사용처",
|
||||
"cmd.parts_fastener": "체결 부품 삽입",
|
||||
"cmd.parts_insert": "부품 삽입",
|
||||
"cmd.parts_library": "표준 부품",
|
||||
"cmd.parts_resize": "부품 크기 변경",
|
||||
"cmd.plastic_assign_rule": "플라스틱 규칙 지정",
|
||||
"cmd.plastic_boss": "보스",
|
||||
"cmd.plastic_lip": "리프",
|
||||
"cmd.plastic_manage_rules": "플라스틱 규칙 관리",
|
||||
"cmd.plastic_rest": "레스트",
|
||||
"cmd.plastic_rules": "플라스틱 규칙 목록",
|
||||
"cmd.plastic_snap_fit": "스냅핏",
|
||||
"cmd.select_clear": "선택 지우기",
|
||||
"cmd.sheet_convert": "판금으로 변환",
|
||||
"cmd.sheet_export_bend_table": "벤딩 표 내보내기",
|
||||
"cmd.sheet_export_dxf": "전개도를 DXF로 내보내기",
|
||||
"cmd.sheet_flange": "플랜지",
|
||||
"cmd.sheet_flat_pattern": "전개도 만들기",
|
||||
"cmd.sheet_fold": "접기",
|
||||
"cmd.sheet_hem": "헴",
|
||||
"cmd.sheet_manage_rules": "판금 규칙",
|
||||
"cmd.sheet_refold": "다시 접기",
|
||||
"cmd.sheet_rules": "판금 규칙 목록",
|
||||
"cmd.sheet_unfold": "펼치기",
|
||||
"cmd.sketch_arc_center_point": "중심점 호",
|
||||
"cmd.sketch_arc_tangent": "접선 호",
|
||||
"cmd.sketch_arc_three_point": "3점 호",
|
||||
"cmd.sketch_auto_constrain": "자동 구속",
|
||||
"cmd.sketch_auto_constrain_from_datum": "기준으로 자동 구속",
|
||||
"cmd.sketch_auto_project": "참조에 모서리 자동 투영",
|
||||
"cmd.sketch_blend_curve": "블렌드 곡선",
|
||||
"cmd.sketch_chain": "연속",
|
||||
"cmd.sketch_chamfer_distance_angle": "거리와 각도 모따기",
|
||||
"cmd.sketch_chamfer_equal_distance": "등거리 모따기",
|
||||
"cmd.sketch_chamfer_two_distance": "두 거리 모따기",
|
||||
"cmd.sketch_circle_center": "중심 지름 원",
|
||||
"cmd.sketch_circle_three_point": "3점 원",
|
||||
"cmd.sketch_circle_three_tangent": "3접선 원",
|
||||
"cmd.sketch_circle_two_point": "2점 원",
|
||||
"cmd.sketch_circle_two_tangent": "2접선 원",
|
||||
"cmd.sketch_constrainer": "스케치 구속기",
|
||||
"cmd.sketch_constraint_fix": "고정/고정 해제",
|
||||
"cmd.sketch_constraint_midpoint": "중간점",
|
||||
"cmd.sketch_construction": "일반/구성선",
|
||||
"cmd.sketch_delete": "스케치 개체 삭제",
|
||||
"cmd.sketch_dimension_text": "치수 텍스트 이동",
|
||||
"cmd.sketch_drag": "스케치 기하 드래그",
|
||||
"cmd.sketch_drag_end": "스케치 드래그 종료",
|
||||
"cmd.sketch_draw_dimensioned": "치수가 있는 스케치 개체 그리기",
|
||||
"cmd.sketch_edit": "스케치 편집",
|
||||
"cmd.sketch_edit_text": "텍스트 편집",
|
||||
"cmd.sketch_export_dxf": "DXF로 저장",
|
||||
"cmd.sketch_finish_auto_constrain": "자동 구속으로 완료",
|
||||
"cmd.sketch_fit_curves_to_section": "메시 단면에 곡선 맞추기",
|
||||
"cmd.sketch_glyphs": "구속 기호",
|
||||
"cmd.sketch_include_3d": "3D 기하 포함",
|
||||
"cmd.sketch_insert_dxf": "DXF 삽입",
|
||||
"cmd.sketch_insert_svg": "SVG 삽입",
|
||||
"cmd.sketch_inspect": "스케치 검사",
|
||||
"cmd.sketch_intersection_curve": "교차 곡선",
|
||||
"cmd.sketch_isoparametric_curve": "등매개 곡선",
|
||||
"cmd.sketch_line3d": "3D 선",
|
||||
"cmd.sketch_line_midpoint": "중점 선",
|
||||
"cmd.sketch_move": "스케치 개체 이동/복사",
|
||||
"cmd.sketch_move3d": "3D 점 이동",
|
||||
"cmd.sketch_move_point": "스케치 점 드래그",
|
||||
"cmd.sketch_offset_break": "오프셋 연결 끊기",
|
||||
"cmd.sketch_options": "스케치 팔레트 옵션",
|
||||
"cmd.sketch_point3d": "3D 점",
|
||||
"cmd.sketch_polygon_circumscribed": "외접 다각형",
|
||||
"cmd.sketch_polygon_edge": "변 다각형",
|
||||
"cmd.sketch_polygon_inscribed": "내접 다각형",
|
||||
"cmd.sketch_project_to_surface": "곡면으로 투영",
|
||||
"cmd.sketch_rectangle_center": "중심 사각형",
|
||||
"cmd.sketch_rectangle_three_point": "3점 사각형",
|
||||
"cmd.sketch_rectangle_two_point": "2점 사각형",
|
||||
"cmd.sketch_redefine": "스케치 평면 재정의",
|
||||
"cmd.sketch_slot_arc_center": "중심점 호 슬롯",
|
||||
"cmd.sketch_slot_arc_three_point": "3점 호 슬롯",
|
||||
"cmd.sketch_slot_center_point": "중심점 슬롯",
|
||||
"cmd.sketch_slot_center_to_center": "중심에서 중심 슬롯",
|
||||
"cmd.sketch_slot_overall": "전장 슬롯",
|
||||
"cmd.sketch_snap": "스냅 추론",
|
||||
"cmd.sketch_solve": "스케치 해석",
|
||||
"cmd.sketch_spline3d": "3D 스플라인",
|
||||
"cmd.sketch_spline_control_point": "제어점 스플라인",
|
||||
"cmd.sketch_spline_control_point_5": "제어점 스플라인(5차)",
|
||||
"cmd.sketch_spline_fit_point": "맞춤점 스플라인",
|
||||
"cmd.sketch_spun_profile": "회전 프로파일",
|
||||
"cmd.sketch_toggle_driven": "종속 치수 전환",
|
||||
"cmd.solid_align": "정렬",
|
||||
"cmd.solid_boundary_fill": "경계 채우기",
|
||||
"cmd.solid_bounding_solid": "둘러싸는 솔리드",
|
||||
"cmd.solid_emboss": "엠보스",
|
||||
"cmd.solid_move": "이동/복사",
|
||||
"cmd.solid_offset_face": "면 오프셋",
|
||||
"cmd.solid_press_pull": "밀기/당기기",
|
||||
"cmd.solid_remove": "제거",
|
||||
"cmd.solid_replace_face": "면 교체",
|
||||
"cmd.solid_rib": "리브",
|
||||
"cmd.solid_split_face": "면 분할",
|
||||
"cmd.solid_web": "웹",
|
||||
"cmd.surface_patch": "패치",
|
||||
"cmd.surface_stitch": "재봉",
|
||||
"cmd.surface_thicken": "두께 주기",
|
||||
"cmd.surface_extrude": "서피스 돌출",
|
||||
"cmd.surface_revolve": "서피스 회전",
|
||||
"cmd.surface_sweep": "서피스 스윕",
|
||||
"cmd.surface_loft": "서피스 로프트",
|
||||
"cmd.surface_ruled": "룰드 서피스",
|
||||
"cmd.surface_boundary": "바운더리 서피스",
|
||||
"cmd.surface_offset": "오프셋 서피스",
|
||||
"cmd.surface_blend": "블렌드 서피스",
|
||||
"cmd.surface_fillet": "서피스 필렛",
|
||||
"cmd.surface_untrim": "트림 해제",
|
||||
"cmd.surface_unstitch": "스티치 해제",
|
||||
"cmd.surface_reverse_normal": "법선 반전",
|
||||
"cmd.inspect_continuity": "연속성 분석",
|
||||
"cmd.timeline_compute_all": "모두 다시 계산",
|
||||
"cmd.timeline_copy": "특성 복사",
|
||||
"cmd.timeline_dependents": "특성 종속 항목",
|
||||
"cmd.timeline_inspect": "특성 검사",
|
||||
"cmd.timeline_paste": "특성 붙여넣기",
|
||||
"cmd.timeline_redefine": "특성 편집",
|
||||
"cmd.timeline_rename": "이름 변경",
|
||||
"cmd.timeline_reorder": "특성 순서 변경",
|
||||
"cmd.timeline_roll_to": "이력 마커 이동",
|
||||
"cmd.timeline_rollback": "타임라인 마커 이동",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.view_apply": "이름 있는 보기 복원",
|
||||
"cmd.view_delete": "이름 있는 보기 삭제",
|
||||
"cmd.view_list": "이름 있는 보기 목록",
|
||||
"cmd.view_rename": "이름 있는 보기 이름 변경",
|
||||
"cmd.view_save": "새 이름 있는 보기",
|
||||
"ui.export_ellipsis_keymap": "내보내기…",
|
||||
"ui.heading_configure": "구성",
|
||||
"ui.heading_constraints": "구속 조건",
|
||||
"ui.heading_utilities": "유틸리티",
|
||||
"BROWSER": "브라우저",
|
||||
"Drive Joint": "조인트 구동",
|
||||
"Edit Profile Sketch": "윤곽 스케치 편집",
|
||||
"Find in Browser": "브라우저에서 찾기",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기",
|
||||
"Reset Toolbar": "도구 모음 재설정",
|
||||
"Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)",
|
||||
"Selection filter": "선택 필터",
|
||||
"Revert Position": "위치 복원",
|
||||
"Construction": "구성선",
|
||||
"Faces": "면",
|
||||
"Edges": "모서리",
|
||||
"Vertices": "꼭짓점",
|
||||
"Sketch geometry and profiles": "스케치 기하 및 프로파일",
|
||||
"Sketch curves": "스케치 곡선",
|
||||
"Sketch points": "스케치 점",
|
||||
"Sketch dimensions": "스케치 치수",
|
||||
"Sketch constraints": "스케치 구속 조건",
|
||||
"Flat": "플랫",
|
||||
"cmd.render_backends": "렌더링 백엔드",
|
||||
"cmd.view_navigate": "보기 탐색",
|
||||
"timeline.group": "새 특성 폴더",
|
||||
"timeline.ungroup": "특성 폴더 제거",
|
||||
"timeline.rename_group": "특성 폴더 이름 변경",
|
||||
"timeline.move_to_group": "특성을 폴더로 이동",
|
||||
"timeline.move_many": "타임라인 항목 이동",
|
||||
"timeline.reorder_many": "특성 순서 변경",
|
||||
"ui.show_construction_geometries": "구성 형상",
|
||||
"ui.total_angle": "전진 각도",
|
||||
"sketch_text.builtin_font": "{family}(내장)",
|
||||
"sketch_text.font_search": "글꼴 검색",
|
||||
"sketch_text.loading_fonts": "설치된 글꼴을 찾는 중…",
|
||||
"sketch_text.use_font_file": "글꼴 파일 {path} 사용",
|
||||
"sketch_text.bold": "굵게",
|
||||
"sketch_text.italic": "기울임꼴",
|
||||
"sketch_text.align_left": "왼쪽",
|
||||
"sketch_text.align_center": "가운데",
|
||||
"sketch_text.align_right": "오른쪽",
|
||||
"sketch_text.path_hint": "텍스트가 따라갈 스케치 곡선: 여기를 클릭한 다음 곡선을 클릭",
|
||||
"sketch_text.path_clear": "곡선에서 텍스트 떼기",
|
||||
"sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다"
|
||||
}
|
||||
@@ -0,0 +1,961 @@
|
||||
{
|
||||
"browser.body_name": "实体{number}",
|
||||
"browser.component_name": "组件{number}",
|
||||
"browser.parameters": "参数 ({count})",
|
||||
"browser.precision": "精度: {precision}",
|
||||
"browser.sketch_name": "草图{number}",
|
||||
"browser.units": "单位: {units}",
|
||||
"cmd.appearance_assign": "外观",
|
||||
"cmd.appearance_library": "外观库",
|
||||
"cmd.appearance_list": "列出外观",
|
||||
"cmd.body_rename": "重命名实体",
|
||||
"cmd.browser_group": "新建组",
|
||||
"cmd.browser_move": "移动浏览器项目",
|
||||
"cmd.browser_order": "排列浏览器项目",
|
||||
"cmd.browser_rename_group": "重命名组",
|
||||
"cmd.browser_ungroup": "取消编组",
|
||||
"cmd.browser_visibility": "显示/隐藏",
|
||||
"cmd.canvas_calibrate": "校准画布",
|
||||
"cmd.canvas_decal": "贴花",
|
||||
"cmd.canvas_delete": "删除画布",
|
||||
"cmd.canvas_edit": "编辑画布",
|
||||
"cmd.canvas_insert": "画布",
|
||||
"cmd.component_activate": "激活组件",
|
||||
"cmd.component_break_link": "断开链接",
|
||||
"cmd.component_capture_position": "捕获位置",
|
||||
"cmd.component_create": "新建组件",
|
||||
"cmd.component_delete": "删除组件",
|
||||
"cmd.component_from_bodies": "由实体创建组件",
|
||||
"cmd.component_insert_design": "插入设计",
|
||||
"cmd.component_links": "列出链接",
|
||||
"cmd.component_list": "列出组件",
|
||||
"cmd.component_move_bodies": "将实体移至组件",
|
||||
"cmd.component_move_sketches": "将草图移至组件",
|
||||
"cmd.component_paste_new": "粘贴为新对象",
|
||||
"cmd.component_properties": "组件物理属性",
|
||||
"cmd.component_rename": "重命名组件",
|
||||
"cmd.component_revert_position": "恢复位置",
|
||||
"cmd.component_update_links": "更新链接",
|
||||
"cmd.config_activate": "激活配置",
|
||||
"cmd.config_column": "配置列",
|
||||
"cmd.config_configure": "配置",
|
||||
"cmd.config_preview": "预览配置",
|
||||
"cmd.config_row": "配置行",
|
||||
"cmd.config_rule": "配置规则",
|
||||
"cmd.config_rules": "配置规则",
|
||||
"cmd.config_table": "显示配置表",
|
||||
"cmd.construct_axis_edge": "过边的轴",
|
||||
"cmd.construct_axis_normal_to_face": "垂直于面的轴",
|
||||
"cmd.construct_axis_through_cylinder": "过圆柱/圆锥/圆环的轴",
|
||||
"cmd.construct_axis_two_planes": "过两个平面的轴",
|
||||
"cmd.construct_axis_two_points": "过两点的轴",
|
||||
"cmd.construct_plane_along_path": "沿路径的平面",
|
||||
"cmd.construct_plane_midplane": "中平面",
|
||||
"cmd.construct_plane_perpendicular": "垂直平面",
|
||||
"cmd.construct_plane_tangent": "相切平面",
|
||||
"cmd.construct_plane_three_points": "过三点的平面",
|
||||
"cmd.construct_plane_two_edges": "过两条边的平面",
|
||||
"cmd.construct_point_along_path": "沿路径的点",
|
||||
"cmd.construct_point_center": "圆/球/圆环中心的点",
|
||||
"cmd.construct_point_edge_plane": "边与平面交点",
|
||||
"cmd.construct_point_three_planes": "过三个平面的点",
|
||||
"cmd.construct_point_two_edges": "过两条边的点",
|
||||
"cmd.construct_point_vertex": "顶点处的点",
|
||||
"cmd.contact_check": "检查接触",
|
||||
"cmd.contact_create": "新建接触集",
|
||||
"cmd.contact_disable_all": "禁用接触",
|
||||
"cmd.contact_edit": "编辑接触集",
|
||||
"cmd.contact_enable_all": "启用全部接触",
|
||||
"cmd.contact_enable_sets": "启用接触集",
|
||||
"cmd.contact_list": "列出接触集",
|
||||
"cmd.diagnostics_crash_report": "崩溃报告",
|
||||
"cmd.diagnostics_crash_reports": "崩溃报告",
|
||||
"cmd.diagnostics_crash_reports_clear": "删除崩溃报告",
|
||||
"cmd.diagnostics_crash_test": "崩溃测试",
|
||||
"cmd.doc_recover": "恢复设计",
|
||||
"cmd.doc_recovery_discard": "丢弃已恢复的设计",
|
||||
"cmd.doc_recovery_list": "可恢复的设计",
|
||||
"cmd.document_inspect": "检查设计",
|
||||
"cmd.document_units": "更改设计单位",
|
||||
"cmd.edit_checkpoint": "检查点",
|
||||
"cmd.edit_restore_checkpoint": "恢复检查点",
|
||||
"cmd.engine_commands": "列出命令",
|
||||
"cmd.explode_create": "爆炸图",
|
||||
"cmd.explode_delete": "删除爆炸图",
|
||||
"cmd.explode_list": "列出爆炸图",
|
||||
"cmd.explode_show": "显示爆炸图",
|
||||
"cmd.expr_evaluate": "计算表达式",
|
||||
"cmd.face_delete": "删除面",
|
||||
"cmd.file_export": "导出",
|
||||
"cmd.file_insert_mesh": "插入网格",
|
||||
"cmd.file_insert_step": "插入 STEP",
|
||||
"cmd.file_save_mesh": "另存为网格",
|
||||
"cmd.inspect_accessibility": "可达性分析",
|
||||
"cmd.inspect_center_of_mass": "质心",
|
||||
"cmd.inspect_curvature_comb": "曲率梳分析",
|
||||
"cmd.inspect_curvature_map": "曲率图分析",
|
||||
"cmd.inspect_draft": "拔模分析",
|
||||
"cmd.inspect_environment_map": "环境贴图分析",
|
||||
"cmd.inspect_interference": "干涉",
|
||||
"cmd.inspect_isocurve": "等参线分析",
|
||||
"cmd.inspect_minimum_radius": "最小半径分析",
|
||||
"cmd.inspect_zebra": "斑马纹分析",
|
||||
"cmd.joint_as_built": "现状接头",
|
||||
"cmd.joint_create": "接头",
|
||||
"cmd.joint_delete": "删除接头",
|
||||
"cmd.joint_drive": "驱动接头",
|
||||
"cmd.joint_edit": "编辑接头",
|
||||
"cmd.joint_limits": "接头限制",
|
||||
"cmd.joint_list": "列出接头",
|
||||
"cmd.joint_motion_link": "运动关联",
|
||||
"cmd.joint_origin": "接头原点",
|
||||
"cmd.joint_rigid_group": "刚性组",
|
||||
"cmd.joint_solve": "解算接头",
|
||||
"cmd.material_assign": "物理材质",
|
||||
"cmd.material_create": "新建物理材质",
|
||||
"cmd.material_delete": "删除物理材质",
|
||||
"cmd.material_edit": "编辑物理材质",
|
||||
"cmd.material_list": "列出材质",
|
||||
"cmd.mesh_convert": "转换网格",
|
||||
"cmd.model_edges": "列出边",
|
||||
"cmd.model_faces": "列出面",
|
||||
"cmd.model_threads": "列出螺纹",
|
||||
"cmd.motion_delete": "删除运动研究",
|
||||
"cmd.motion_export": "导出运动研究",
|
||||
"cmd.motion_list": "列出运动研究",
|
||||
"cmd.motion_play": "播放运动研究",
|
||||
"cmd.motion_study": "运动研究",
|
||||
"cmd.occurrence_copy": "粘贴(实例)",
|
||||
"cmd.occurrence_ground": "固定",
|
||||
"cmd.occurrence_move": "移动实例",
|
||||
"cmd.occurrence_properties": "实例物理属性",
|
||||
"cmd.parameters_add": "添加用户参数",
|
||||
"cmd.parameters_change": "更改参数",
|
||||
"cmd.parameters_comment": "注释参数",
|
||||
"cmd.parameters_complete": "补全表达式",
|
||||
"cmd.parameters_delete": "删除参数",
|
||||
"cmd.parameters_export": "导出参数",
|
||||
"cmd.parameters_favorite": "收藏参数",
|
||||
"cmd.parameters_graph": "参数关系图",
|
||||
"cmd.parameters_import": "导入参数",
|
||||
"cmd.parameters_list": "列出参数",
|
||||
"cmd.parameters_rename": "重命名参数",
|
||||
"cmd.parameters_users": "参数使用者",
|
||||
"cmd.parts_fastener": "插入紧固件",
|
||||
"cmd.parts_insert": "插入零件",
|
||||
"cmd.parts_library": "标准零件",
|
||||
"cmd.parts_resize": "更改零件尺寸",
|
||||
"cmd.plastic_assign_rule": "指定塑料规则",
|
||||
"cmd.plastic_boss": "凸台",
|
||||
"cmd.plastic_lip": "凸缘",
|
||||
"cmd.plastic_manage_rules": "管理塑料规则",
|
||||
"cmd.plastic_rest": "垫块",
|
||||
"cmd.plastic_rules": "列出塑料规则",
|
||||
"cmd.plastic_snap_fit": "卡扣",
|
||||
"cmd.select_clear": "清除选择",
|
||||
"cmd.sheet_convert": "转换为钣金",
|
||||
"cmd.sheet_export_bend_table": "导出折弯表",
|
||||
"cmd.sheet_export_dxf": "将展开图导出为 DXF",
|
||||
"cmd.sheet_flange": "法兰",
|
||||
"cmd.sheet_flat_pattern": "创建展开图",
|
||||
"cmd.sheet_fold": "折叠",
|
||||
"cmd.sheet_hem": "卷边",
|
||||
"cmd.sheet_manage_rules": "钣金规则",
|
||||
"cmd.sheet_refold": "重新折弯",
|
||||
"cmd.sheet_rules": "列出钣金规则",
|
||||
"cmd.sheet_unfold": "展开",
|
||||
"cmd.sketch_arc_center_point": "中心点圆弧",
|
||||
"cmd.sketch_arc_tangent": "相切圆弧",
|
||||
"cmd.sketch_arc_three_point": "三点圆弧",
|
||||
"cmd.sketch_auto_constrain": "自动约束",
|
||||
"cmd.sketch_auto_constrain_from_datum": "基于基准自动约束",
|
||||
"cmd.sketch_auto_project": "自动投影边到参考",
|
||||
"cmd.sketch_blend_curve": "混合曲线",
|
||||
"cmd.sketch_chain": "连续链",
|
||||
"cmd.sketch_chamfer_distance_angle": "距离和角度倒角",
|
||||
"cmd.sketch_chamfer_equal_distance": "等距倒角",
|
||||
"cmd.sketch_chamfer_two_distance": "双距离倒角",
|
||||
"cmd.sketch_circle_center": "圆心直径圆",
|
||||
"cmd.sketch_circle_three_point": "三点圆",
|
||||
"cmd.sketch_circle_three_tangent": "三切线圆",
|
||||
"cmd.sketch_circle_two_point": "两点圆",
|
||||
"cmd.sketch_circle_two_tangent": "两切线圆",
|
||||
"cmd.sketch_constrainer": "草图约束器",
|
||||
"cmd.sketch_constraint_fix": "固定/解除固定",
|
||||
"cmd.sketch_constraint_midpoint": "中点",
|
||||
"cmd.sketch_construction": "普通/构造",
|
||||
"cmd.sketch_delete": "删除草图图元",
|
||||
"cmd.sketch_dimension_text": "移动尺寸文字",
|
||||
"cmd.sketch_drag": "拖动草图几何",
|
||||
"cmd.sketch_drag_end": "结束草图拖动",
|
||||
"cmd.sketch_draw_dimensioned": "绘制带尺寸的草图图元",
|
||||
"cmd.sketch_edit": "编辑草图",
|
||||
"cmd.sketch_edit_text": "编辑文字",
|
||||
"cmd.sketch_export_dxf": "另存为 DXF",
|
||||
"cmd.sketch_finish_auto_constrain": "使用自动约束完成",
|
||||
"cmd.sketch_fit_curves_to_section": "将曲线拟合到网格截面",
|
||||
"cmd.sketch_glyphs": "约束符号",
|
||||
"cmd.sketch_include_3d": "包含三维几何",
|
||||
"cmd.sketch_insert_dxf": "插入 DXF",
|
||||
"cmd.sketch_insert_svg": "插入 SVG",
|
||||
"cmd.sketch_inspect": "检查草图",
|
||||
"cmd.sketch_intersection_curve": "相交曲线",
|
||||
"cmd.sketch_isoparametric_curve": "等参数曲线",
|
||||
"cmd.sketch_line3d": "三维直线",
|
||||
"cmd.sketch_line_midpoint": "中点线",
|
||||
"cmd.sketch_move": "移动/复制草图图元",
|
||||
"cmd.sketch_move3d": "移动三维点",
|
||||
"cmd.sketch_move_point": "拖动草图点",
|
||||
"cmd.sketch_offset_break": "断开偏移关联",
|
||||
"cmd.sketch_options": "草图选项板选项",
|
||||
"cmd.sketch_point3d": "三维点",
|
||||
"cmd.sketch_polygon_circumscribed": "外切多边形",
|
||||
"cmd.sketch_polygon_edge": "边长多边形",
|
||||
"cmd.sketch_polygon_inscribed": "内接多边形",
|
||||
"cmd.sketch_project_to_surface": "投影到曲面",
|
||||
"cmd.sketch_rectangle_center": "中心矩形",
|
||||
"cmd.sketch_rectangle_rounded": "圆角矩形",
|
||||
"cmd.sketch_rectangle_three_point": "三点矩形",
|
||||
"cmd.sketch_rectangle_two_point": "两点矩形",
|
||||
"cmd.sketch_redefine": "重定义草图平面",
|
||||
"cmd.sketch_slot_arc_center": "中心点圆弧槽",
|
||||
"cmd.sketch_slot_arc_three_point": "三点圆弧槽",
|
||||
"cmd.sketch_slot_center_point": "中心点槽",
|
||||
"cmd.sketch_slot_center_to_center": "中心距槽",
|
||||
"cmd.sketch_slot_overall": "全长槽",
|
||||
"cmd.sketch_snap": "捕捉推断",
|
||||
"cmd.sketch_solve": "解算草图",
|
||||
"cmd.sketch_spline3d": "三维样条曲线",
|
||||
"cmd.sketch_spline_control_point": "控制点样条曲线",
|
||||
"cmd.sketch_spline_control_point_5": "控制点样条曲线(5 次)",
|
||||
"cmd.sketch_spline_fit_point": "拟合点样条曲线",
|
||||
"cmd.sketch_spun_profile": "旋转轮廓",
|
||||
"cmd.sketch_toggle_driven": "切换被驱动尺寸",
|
||||
"cmd.solid_align": "对齐",
|
||||
"cmd.solid_boundary_fill": "边界填充",
|
||||
"cmd.solid_bounding_solid": "包围实体",
|
||||
"cmd.solid_emboss": "压印",
|
||||
"cmd.solid_move": "移动/复制",
|
||||
"cmd.solid_offset_face": "偏移面",
|
||||
"cmd.solid_press_pull": "按压拖动",
|
||||
"cmd.solid_remove": "移除",
|
||||
"cmd.solid_replace_face": "替换面",
|
||||
"cmd.solid_rib": "筋板",
|
||||
"cmd.solid_split_face": "拆分面",
|
||||
"cmd.solid_web": "网格筋",
|
||||
"cmd.surface_patch": "补面",
|
||||
"cmd.surface_stitch": "缝合",
|
||||
"cmd.surface_thicken": "加厚",
|
||||
"cmd.surface_extrude": "曲面拉伸",
|
||||
"cmd.surface_revolve": "曲面旋转",
|
||||
"cmd.surface_sweep": "曲面扫掠",
|
||||
"cmd.surface_loft": "曲面放样",
|
||||
"cmd.surface_ruled": "直纹曲面",
|
||||
"cmd.surface_boundary": "边界曲面",
|
||||
"cmd.surface_offset": "偏移曲面",
|
||||
"cmd.surface_blend": "混合曲面",
|
||||
"cmd.surface_fillet": "曲面圆角",
|
||||
"cmd.surface_untrim": "取消修剪",
|
||||
"cmd.surface_unstitch": "取消缝合",
|
||||
"cmd.surface_reverse_normal": "反转法向",
|
||||
"cmd.inspect_continuity": "连续性分析",
|
||||
"cmd.timeline_compute_all": "全部重新計算",
|
||||
"cmd.timeline_copy": "复制特征",
|
||||
"cmd.timeline_dependents": "特征依存项",
|
||||
"cmd.timeline_inspect": "检查特征",
|
||||
"cmd.timeline_paste": "粘贴特征",
|
||||
"cmd.timeline_redefine": "编辑特征",
|
||||
"cmd.timeline_rename": "重命名",
|
||||
"cmd.timeline_reorder": "重排特征",
|
||||
"cmd.timeline_roll_to": "滚动历史标记",
|
||||
"cmd.timeline_rollback": "移动时间线标记",
|
||||
"cmd.timeline_suppress": "抑制特征",
|
||||
"cmd.view_apply": "还原命名视图",
|
||||
"cmd.view_delete": "删除命名视图",
|
||||
"cmd.view_list": "列出命名视图",
|
||||
"cmd.view_rename": "重命名命名视图",
|
||||
"cmd.view_save": "新建命名视图",
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.select": "↖ 选择",
|
||||
"dialog.selected_count": "↖ 已选择 {count} 个",
|
||||
"dialog.step": "步骤",
|
||||
"dialog.selection_step": "第 {step} 步,共 {total} 步:选择{input}",
|
||||
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
|
||||
"document.save_changes": "保存对 {name} 的更改?",
|
||||
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
|
||||
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
|
||||
"settings.msaa_reset": "{setting}: {requested}× 不受此图形设备支持;已重置为 {reset}× (支持: {supported})。",
|
||||
"settings.msaa_unsupported": "此图形设备不支持",
|
||||
"settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:",
|
||||
"settings.reset_title": "设置已更改",
|
||||
"sketch.degrees_of_freedom": "{count} 个自由度",
|
||||
"sketch.draw_curves": "绘制为 {linetype}",
|
||||
"sketch.make_selection": "将选择转换为 {linetype}",
|
||||
"ui.1_degree_of_freedom": "1 个自由度",
|
||||
"ui.3d_mouse_speed": "3D 鼠标速度",
|
||||
"ui.3d_mouse_windows": "3D 鼠标(Windows)",
|
||||
"ui.3d_sketch": "3D 草图",
|
||||
"ui.about": "关于",
|
||||
"ui.activate": "激活",
|
||||
"ui.active_component": "活动组件",
|
||||
"ui.agent_cursor": "代理光标",
|
||||
"ui.agent_cursor_speed": "代理光标速度",
|
||||
"ui.all": "全部",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度小数位",
|
||||
"ui.animate_view_changes": "视图变化动画",
|
||||
"ui.animation": "动画",
|
||||
"ui.animation_speed": "动画速度",
|
||||
"ui.anti_aliasing": "抗锯齿",
|
||||
"ui.apply": "应用",
|
||||
"ui.arc": "圆弧",
|
||||
"ui.assemble": "装配",
|
||||
"ui.autosave_every": "自动保存间隔",
|
||||
"ui.axes": "轴",
|
||||
"ui.axis": "轴",
|
||||
"ui.base_point": "基点",
|
||||
"ui.bodies": "实体",
|
||||
"ui.box": "长方体",
|
||||
"ui.break": "打断",
|
||||
"ui.cancel": "取消",
|
||||
"ui.canvases": "画布",
|
||||
"ui.center": "中心",
|
||||
"ui.centerline": "中心线",
|
||||
"ui.chain": "链",
|
||||
"ui.chamfer": "倒角",
|
||||
"ui.chamfer_type": "倒角类型",
|
||||
"ui.change_workspace": "更改工作区",
|
||||
"ui.chord_length": "弦长",
|
||||
"ui.curvature_g2": "曲率连续 (G2)",
|
||||
"ui.equivalent_radius": "等效半径",
|
||||
"ui.circle": "圆",
|
||||
"ui.circular_pattern": "圆形阵列",
|
||||
"ui.clear": "清除",
|
||||
"ui.clear_recent": "清除最近",
|
||||
"ui.clear_the_selection": "清除选择",
|
||||
"ui.click_selects_whole_bodies": "单击选择整个实体",
|
||||
"ui.click_then_press_key": "单击,然后按新按键",
|
||||
"ui.close": "关闭",
|
||||
"ui.coil": "螺旋",
|
||||
"ui.coincident": "重合",
|
||||
"ui.collapse": "折叠",
|
||||
"ui.collinear": "共线",
|
||||
"ui.combine": "组合",
|
||||
"ui.coming_later": "即将推出",
|
||||
"ui.concentric": "同心",
|
||||
"ui.configurations": "配置",
|
||||
"ui.conic_curve": "圆锥曲线",
|
||||
"ui.constant": "恒定",
|
||||
"ui.constraints_conflict": "约束冲突",
|
||||
"ui.construct": "构造",
|
||||
"ui.construction": "构造",
|
||||
"ui.context_menu": "上下文菜单",
|
||||
"ui.copy": "复制",
|
||||
"ui.corners": "角点",
|
||||
"ui.corner": "角",
|
||||
"ui.counterbore": "沉头孔",
|
||||
"ui.countersink": "锥形沉孔",
|
||||
"ui.create": "创建",
|
||||
"ui.create_copy": "创建副本",
|
||||
"ui.create_sketch": "创建草图",
|
||||
"ui.current": "当前",
|
||||
"ui.curvature": "曲率",
|
||||
"ui.curves": "曲线",
|
||||
"ui.cut": "剪切",
|
||||
"ui.cylinder": "圆柱体",
|
||||
"ui.dark": "深色",
|
||||
"ui.default_modelling_orientation": "默认建模方向",
|
||||
"ui.degrees_of_freedom": "自由度",
|
||||
"ui.delete": "删除",
|
||||
"ui.depth": "深度",
|
||||
"ui.design": "设计",
|
||||
"ui.diameter": "直径",
|
||||
"ui.dimension": "尺寸",
|
||||
"ui.direction": "方向",
|
||||
"ui.distance": "距离",
|
||||
"ui.distance_1": "距离 1",
|
||||
"ui.distance_2": "距离 2",
|
||||
"ui.document_settings": "文档设置",
|
||||
"ui.don_t_save": "不保存",
|
||||
"ui.draft": "拔模",
|
||||
"ui.drawing": "绘图",
|
||||
"ui.drill_point": "钻尖",
|
||||
"ui.edit": "编辑",
|
||||
"ui.ellipse": "椭圆",
|
||||
"ui.end_radius": "结束半径",
|
||||
"ui.entities": "实体",
|
||||
"ui.equal": "相等",
|
||||
"ui.expand": "展开",
|
||||
"ui.export_ellipsis": "导出...",
|
||||
"ui.export_ellipsis_keymap": "导出…",
|
||||
"ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件",
|
||||
"ui.extend": "延伸",
|
||||
"ui.extent_type": "延伸类型",
|
||||
"ui.extrude": "拉伸",
|
||||
"ui.factor": "系数",
|
||||
"ui.feature_history": "特征历史",
|
||||
"ui.feature_options": "特征选项",
|
||||
"ui.features": "特征",
|
||||
"ui.file": "文件",
|
||||
"ui.fillet": "圆角",
|
||||
"ui.filter_commands_or_keys": "筛选命令或按键",
|
||||
"ui.finish_sketch": "完成草图",
|
||||
"ui.fix_unfix": "固定/取消固定",
|
||||
"ui.flat": "平面",
|
||||
"ui.flip": "翻转",
|
||||
"ui.front": "前视",
|
||||
"ui.fully_constrained": "完全约束",
|
||||
"ui.general": "常规",
|
||||
"ui.graphics": "图形",
|
||||
"ui.ground_shadow": "地面阴影",
|
||||
"ui.heading_add_ins": "加载项",
|
||||
"ui.heading_assemble": "装配",
|
||||
"ui.heading_box": "长方体",
|
||||
"ui.heading_chamfer": "倒角",
|
||||
"ui.heading_circular_pattern": "圆形阵列",
|
||||
"ui.heading_coil": "螺旋",
|
||||
"ui.heading_combine": "组合",
|
||||
"ui.heading_configure": "配置",
|
||||
"ui.heading_constraints": "约束",
|
||||
"ui.heading_construct": "构造",
|
||||
"ui.heading_create": "创建",
|
||||
"ui.heading_create_sketch": "创建草图",
|
||||
"ui.heading_cylinder": "圆柱体",
|
||||
"ui.heading_design": "设计",
|
||||
"ui.heading_draft": "拔模",
|
||||
"ui.heading_edit": "编辑",
|
||||
"ui.heading_extrude": "拉伸",
|
||||
"ui.heading_fillet": "圆角",
|
||||
"ui.heading_finish_sketch": "完成草图",
|
||||
"ui.heading_front": "前视",
|
||||
"ui.heading_hole": "孔",
|
||||
"ui.heading_home": "主页",
|
||||
"ui.heading_insert": "插入",
|
||||
"ui.heading_inspect": "检查",
|
||||
"ui.heading_loft": "放样",
|
||||
"ui.heading_make": "制作",
|
||||
"ui.heading_measure": "测量",
|
||||
"ui.heading_mesh": "网格",
|
||||
"ui.heading_mirror": "镜像",
|
||||
"ui.heading_modify": "修改",
|
||||
"ui.heading_move": "移动",
|
||||
"ui.heading_offset": "偏移",
|
||||
"ui.heading_move_copy": "移动/复制",
|
||||
"ui.heading_offset_plane": "偏移平面",
|
||||
"ui.heading_ok": "确定",
|
||||
"ui.heading_parameters": "参数",
|
||||
"ui.heading_pattern_on_path": "路径阵列",
|
||||
"ui.heading_pipe": "管道",
|
||||
"ui.heading_plane_at_angle": "角度平面",
|
||||
"ui.heading_plastic": "塑料",
|
||||
"ui.heading_prepare": "准备",
|
||||
"ui.heading_rectangular_pattern": "矩形阵列",
|
||||
"ui.heading_revolve": "旋转",
|
||||
"ui.heading_right": "右视",
|
||||
"ui.heading_scale": "缩放",
|
||||
"ui.heading_section_analysis": "截面分析",
|
||||
"ui.heading_select": "选择",
|
||||
"ui.heading_sheet_metal": "钣金",
|
||||
"ui.heading_shell": "抽壳",
|
||||
"ui.heading_sketch": "草图",
|
||||
"ui.heading_sketch_palette": "草图面板",
|
||||
"ui.heading_sketch_scale": "草图缩放",
|
||||
"ui.heading_solid": "实体",
|
||||
"ui.heading_sphere": "球体",
|
||||
"ui.heading_split_body": "分割实体",
|
||||
"ui.heading_surface": "曲面",
|
||||
"ui.heading_sweep": "扫掠",
|
||||
"ui.heading_thread": "螺纹",
|
||||
"ui.heading_top": "上视",
|
||||
"ui.heading_torus": "圆环体",
|
||||
"ui.heading_utilities": "工具",
|
||||
"ui.heading_utility": "实用工具",
|
||||
"ui.help": "帮助",
|
||||
"ui.hide": "隐藏",
|
||||
"ui.hole": "孔",
|
||||
"ui.hole_tap_type": "孔攻丝类型",
|
||||
"ui.hole_type": "孔类型",
|
||||
"ui.home": "主页",
|
||||
"ui.horizontal_vertical": "水平/垂直",
|
||||
"ui.import_ellipsis": "导入…",
|
||||
"ui.insert": "插入",
|
||||
"ui.inspect": "检查",
|
||||
"ui.intersect": "相交",
|
||||
"ui.join": "连接",
|
||||
"ui.joints": "关节",
|
||||
"ui.keyboard_shortcuts": "键盘快捷键",
|
||||
"ui.language": "语言",
|
||||
"ui.length_decimals": "长度小数位",
|
||||
"ui.light": "浅色",
|
||||
"ui.line": "直线",
|
||||
"ui.linetype": "线型",
|
||||
"ui.loft": "放样",
|
||||
"ui.look_at": "朝向",
|
||||
"ui.look_at_anim_desc": "轴视图、注视、主视图与适合窗口",
|
||||
"ui.manufacture": "制造",
|
||||
"ui.measure": "测量",
|
||||
"ui.midpoint": "中点",
|
||||
"ui.mirror": "镜像",
|
||||
"ui.mirror_line": "镜像线",
|
||||
"ui.modify": "修改",
|
||||
"ui.move": "移动",
|
||||
"ui.multiple": "多个",
|
||||
"ui.named_views": "命名视图",
|
||||
"ui.navigation": "导航",
|
||||
"ui.needs_design": "{command}: 请先打开一个设计",
|
||||
"ui.new_body": "新建实体",
|
||||
"ui.new_design": "新建设计",
|
||||
"ui.normal": "法向",
|
||||
"ui.object_type": "对象类型",
|
||||
"ui.objects": "对象",
|
||||
"ui.offer_to_stand_up_y_up_files": "提供站立 Y-up 文件",
|
||||
"ui.offset": "偏移",
|
||||
"ui.offset_plane": "偏移平面",
|
||||
"ui.one_side": "单侧",
|
||||
"ui.open": "打开",
|
||||
"ui.open_ellipsis": "打开...",
|
||||
"ui.open_preferences": "打开首选项",
|
||||
"ui.open_recent": "打开最近",
|
||||
"ui.operation": "操作",
|
||||
"ui.options": "选项",
|
||||
"ui.orbit_around": "环绕",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_points": "点",
|
||||
"ui.palette_profile": "轮廓",
|
||||
"ui.pan_zoom_orbit": "平移、缩放、环绕",
|
||||
"ui.parallel": "平行",
|
||||
"ui.parameters": "参数",
|
||||
"ui.paste": "粘贴",
|
||||
"ui.path": "路径",
|
||||
"ui.pattern_on_path": "路径阵列",
|
||||
"ui.perpendicular": "垂直",
|
||||
"ui.perspective_view": "透视图",
|
||||
"ui.pipe": "管道",
|
||||
"ui.placement": "放置",
|
||||
"ui.plane": "平面",
|
||||
"ui.plane_at_angle": "角度平面",
|
||||
"ui.point": "点",
|
||||
"ui.point_under_the_cursor": "光标下的点",
|
||||
"ui.points": "点",
|
||||
"ui.polygon": "多边形",
|
||||
"ui.preferences": "首选项",
|
||||
"ui.press_a_key": "按一个键…",
|
||||
"ui.profile": "轮廓",
|
||||
"ui.profile_plane": "轮廓平面",
|
||||
"ui.profiles": "轮廓",
|
||||
"ui.project": "投影",
|
||||
"ui.project_face_edges_sketch": "将面的边投影到新草图",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quit": "退出",
|
||||
"ui.radial_context_menu": "环形上下文菜单",
|
||||
"ui.radius": "半径",
|
||||
"ui.radius_type": "半径类型",
|
||||
"ui.rectangle": "矩形",
|
||||
"ui.rectangular_pattern": "矩形阵列",
|
||||
"ui.redo": "重做",
|
||||
"ui.render": "渲染",
|
||||
"ui.renderer": "渲染器",
|
||||
"ui.reset": "重置",
|
||||
"ui.reset_all": "全部重置",
|
||||
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
|
||||
"ui.restore_defaults": "恢复默认值",
|
||||
"ui.reverse_zoom_direction": "反转缩放方向",
|
||||
"ui.revolve": "旋转",
|
||||
"ui.right": "右视",
|
||||
"ui.save": "保存",
|
||||
"ui.save_as": "另存为",
|
||||
"ui.save_as_ellipsis": "另存为...",
|
||||
"ui.scale": "缩放",
|
||||
"ui.section_analysis": "截面分析",
|
||||
"ui.select": "选择",
|
||||
"ui.select_other_hint": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。",
|
||||
"ui.selection": "选择",
|
||||
"ui.shell": "抽壳",
|
||||
"ui.show": "显示",
|
||||
"ui.show_constraints": "约束",
|
||||
"ui.show_dimensions": "尺寸",
|
||||
"ui.show_points": "显示点",
|
||||
"ui.show_profile": "显示轮廓",
|
||||
"ui.show_projected_geometries": "投影几何",
|
||||
"ui.simple": "简单",
|
||||
"ui.simulation": "仿真",
|
||||
"ui.single": "单个",
|
||||
"ui.sketch": "草图",
|
||||
"ui.sketch_dimension": "草图尺寸",
|
||||
"ui.sketch_grid": "草图网格",
|
||||
"ui.sketch_scale": "草图比例",
|
||||
"ui.sketch_status": "草图状态",
|
||||
"ui.sketches": "草图",
|
||||
"ui.slice": "切片",
|
||||
"ui.slot": "槽",
|
||||
"ui.smooth": "平滑",
|
||||
"ui.smooth_wheel_zoom": "平滑滚轮缩放",
|
||||
"ui.smooth_wheel_zoom_tip": "关闭后每个滚轮事件立即生效,不做平滑延迟",
|
||||
"ui.snap": "捕捉",
|
||||
"ui.space_mouse_hint": "同时移动和旋转视图;草图中仅支持平移和缩放。",
|
||||
"ui.space_mouse_navigate": "使用 SpaceNavigator / SpaceMouse 导航",
|
||||
"ui.spacing_2": "间距 2",
|
||||
"ui.spacing": "间距",
|
||||
"ui.sphere": "球体",
|
||||
"ui.spline": "样条曲线",
|
||||
"ui.split_body": "分割实体",
|
||||
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
|
||||
"ui.start": "开始",
|
||||
"ui.start_radius": "起始半径",
|
||||
"ui.sweep": "扫掠",
|
||||
"ui.symmetric": "对称",
|
||||
"ui.symmetry": "对称",
|
||||
"ui.system": "系统",
|
||||
"ui.tangent": "相切",
|
||||
"ui.taper_angle": "锥角",
|
||||
"ui.tapped": "攻丝",
|
||||
"ui.text": "文本",
|
||||
"ui.theme": "主题",
|
||||
"ui.thickness": "厚度",
|
||||
"ui.thread": "螺纹",
|
||||
"ui.timeline_bottom": "底部时间轴",
|
||||
"ui.timeline_left_note": "左侧布局下,回退条显示在零件和场景浏览器的上方。",
|
||||
"ui.timeline_left_sidebar": "左侧边栏",
|
||||
"ui.tip_angle": "尖端角度",
|
||||
"ui.to_object": "到对象",
|
||||
"ui.top": "上视",
|
||||
"ui.torus": "圆环体",
|
||||
"ui.trackpad_pan_tip": "触控板手势用于平移(关闭:像滚轮一样缩放);鼠标滚轮缩放、拇指滚轮横向平移、双指捏合始终缩放",
|
||||
"ui.trim": "修剪",
|
||||
"ui.two_finger_scroll_pans": "双指滚动平移",
|
||||
"ui.two_sides": "双侧",
|
||||
"ui.ui_scale": "界面缩放",
|
||||
"ui.undo": "撤销",
|
||||
"ui.units": "单位",
|
||||
"ui.units_of_new_designs": "新设计的单位",
|
||||
"ui.untitled": "无标题",
|
||||
"ui.up_axis_note": "视图中朝上的轴(轴视图、主视图、注视)。设计本身不变。",
|
||||
"ui.variable": "变量",
|
||||
"ui.view_centre": "视图中心",
|
||||
"ui.visual_shaded_hidden_edges": "带隐藏边的着色",
|
||||
"ui.visual_shaded_with_edges": "带边着色",
|
||||
"ui.visual_style": "视觉样式",
|
||||
"ui.visual_wireframe": "线框",
|
||||
"ui.workspace": "工作区",
|
||||
"ui.y_distance": "Y 距离",
|
||||
"ui.x_distance": "X 距离",
|
||||
"ui.y_up": "Y 朝上",
|
||||
"ui.z_up": "Z 朝上",
|
||||
"ui.zoom_to_cursor_tip": "滚轮向光标所在点缩放(关闭:向视图中心缩放)",
|
||||
"ui.zoom_to_the_cursor": "缩放到光标",
|
||||
"A new configuration, a copy of the active one": "新建配置,为当前配置的副本",
|
||||
"About SolidCraft": "关于 SolidCraft",
|
||||
"Active": "活动",
|
||||
"Active rule": "活动规则",
|
||||
"Add": "添加",
|
||||
"Add Key": "添加按键",
|
||||
"Add Mid Radius": "添加中间半径",
|
||||
"Add Row": "添加行",
|
||||
"All Sketch Points": "所有草图点",
|
||||
"All open designs": "所有打开的设计",
|
||||
"Allowed expressions, separated by semicolons": "允许的表达式,以分号分隔",
|
||||
"Also deletes": "同时删除",
|
||||
"Angle 2": "角度 2",
|
||||
"Animate": "动画",
|
||||
"Appearance name": "外观名称",
|
||||
"Applied Radius": "应用半径",
|
||||
"Apply To": "应用于",
|
||||
"Area": "面积",
|
||||
"Attachment": "附着",
|
||||
"BROWSER": "浏览器",
|
||||
"Bend Line Position": "弯折线位置",
|
||||
"Bend Position": "折弯位置",
|
||||
"Bend Radius": "折弯半径",
|
||||
"Bends": "折弯",
|
||||
"Body": "实体",
|
||||
"Bounding box": "边界框",
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数",
|
||||
"Capture All Positions": "捕获所有位置",
|
||||
"Catch Direction": "卡扣方向",
|
||||
"Cells": "区域",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
"Center of mass": "质心",
|
||||
"Chamfer Type": "倒角类型",
|
||||
"Check collisions": "检查碰撞",
|
||||
"Checked": "已检查",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
|
||||
"Close draft": "关闭草稿",
|
||||
"Collisions": "碰撞",
|
||||
"Colour": "颜色",
|
||||
"Component1": "组件1",
|
||||
"Configuration": "配置",
|
||||
"Configuration rules": "配置规则",
|
||||
"Contact": "接触",
|
||||
"Copy all licences": "复制所有许可证",
|
||||
"Copy diagnostic information": "复制诊断信息",
|
||||
"Copy report": "复制报告",
|
||||
"Corner Relief Shape": "拐角释放形状",
|
||||
"Counterbore depth": "沉孔深度",
|
||||
"Countersink angle": "沉头角度",
|
||||
"Create Issue — review draft": "创建 Issue——审阅草稿",
|
||||
"Customize Toolbar": "自定义工具栏",
|
||||
"Delete all": "全部删除",
|
||||
"Delete rule": "删除规则",
|
||||
"Delete the row": "删除此行",
|
||||
"Delete the set": "删除此集合",
|
||||
"Delete this parameter": "删除此参数",
|
||||
"Density": "密度",
|
||||
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
|
||||
"Discard": "放弃",
|
||||
"Double-click to rename": "双击重命名",
|
||||
"Drive Joint": "驱动接头",
|
||||
"Edit Profile Sketch": "编辑轮廓草图",
|
||||
"Enable rule": "启用规则",
|
||||
"Enabled": "已启用",
|
||||
"Explode": "爆炸",
|
||||
"Extents": "范围",
|
||||
"Face attachment lost": "面附着丢失",
|
||||
"Faces / edges / vertices": "面 / 边 / 顶点",
|
||||
"Favourite": "收藏",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。",
|
||||
"Filter": "筛选",
|
||||
"Find in Browser": "在浏览器中查找",
|
||||
"Flat size": "平板尺寸",
|
||||
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Font": "字体",
|
||||
"Full Length": "全长",
|
||||
"Gap": "间隙",
|
||||
"Grid": "网格",
|
||||
"Height": "高度",
|
||||
"Home view": "主视图",
|
||||
"In This Design": "在此设计中",
|
||||
"Interferes": "发生干涉",
|
||||
"Issue title": "Issue 标题",
|
||||
"Joint Origin1": "接头原点1",
|
||||
"Keep Tools": "保留工具",
|
||||
"Later": "稍后",
|
||||
"Leave it": "保持原样",
|
||||
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "旧版面附着。重新绑定到经验证的面可创建精确附着。",
|
||||
"Length": "长度",
|
||||
"Length units": "长度单位",
|
||||
"Library": "零件库",
|
||||
"Loop": "循环",
|
||||
"Margin": "余量",
|
||||
"Mass": "质量",
|
||||
"Material": "材质",
|
||||
"Mid Radius": "中间半径",
|
||||
"Modeled": "已建模",
|
||||
"More toolbar panels": "更多工具栏面板",
|
||||
"Motion": "运动",
|
||||
"Move Type": "移动类型",
|
||||
"Move earlier": "向前移动",
|
||||
"Move later": "向后移动",
|
||||
"Name": "名称",
|
||||
"No crash reports.": "没有崩溃报告。",
|
||||
"No matching appearances": "没有匹配的外观",
|
||||
"Not available yet": "暂不可用",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "打开 GitHub 草稿",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
|
||||
"Other side": "另一侧",
|
||||
"Parameter values": "参数值",
|
||||
"Per Edge": "每条边",
|
||||
"Pick the tool bodies": "选择工具实体",
|
||||
"Place at the origin instead": "改为放置在原点",
|
||||
"Playback": "播放",
|
||||
"Position": "位置",
|
||||
"Position (0 to 1)": "位置(0 到 1)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1",
|
||||
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
|
||||
"RECENT": "最近使用",
|
||||
"Radius Fit": "半径拟合",
|
||||
"Ratio": "比率",
|
||||
"Recover": "恢复",
|
||||
"Recover unsaved design?": "恢复未保存的设计?",
|
||||
"Remove Tools": "移除工具",
|
||||
"Remove from list": "从列表移除",
|
||||
"Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。",
|
||||
"Remove the column": "移除此列",
|
||||
"Remove the track": "移除轨迹",
|
||||
"Remove this point": "移除此点",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿",
|
||||
"Reset Toolbar": "重置工具栏",
|
||||
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
|
||||
"Result": "结果",
|
||||
"Retake screenshot": "重新截图",
|
||||
"Reverse": "反向",
|
||||
"Reverse Direction": "反向",
|
||||
"Revert": "还原",
|
||||
"Revert All Positions": "还原所有位置",
|
||||
"Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。",
|
||||
"Rim Edge": "轮缘边",
|
||||
"Roll History Marker Here": "将历史标记滚动到此处",
|
||||
"Rule": "规则",
|
||||
"Saved preview. Geometry is being checked before editing.": "已保存预览。编辑前正在检查几何。",
|
||||
"Scale factor": "比例因子",
|
||||
"Search": "搜索",
|
||||
"Search commands": "搜索命令",
|
||||
"Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)",
|
||||
"Select bodies or faces instead": "改为选择实体或面",
|
||||
"Selection filter": "选择筛选",
|
||||
"Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。",
|
||||
"Shape": "形状",
|
||||
"Show Agent Cursor": "显示代理光标",
|
||||
"Show in folder": "在文件夹中显示",
|
||||
"Size": "大小",
|
||||
"Sketch plane has no body-face attachment.": "草图平面没有实体面附着。",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
|
||||
"Speed": "速度",
|
||||
"Stand it up": "将其立起",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
|
||||
"Stand this part up?": "立起此零件?",
|
||||
"Step": "步骤",
|
||||
"Steps": "步数",
|
||||
"Suppressed": "已抑制",
|
||||
"Swap Ends": "交换端点",
|
||||
"Tangent Chain": "相切链",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。",
|
||||
"Then": "操作",
|
||||
"These features will be deleted:": "以下特征将被删除:",
|
||||
"These features will fail:": "以下特征将失败:",
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "此子项是共享的。删除它会从其父项的每个副本中移除。",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。",
|
||||
"To Offset": "偏移",
|
||||
"Tolerance": "公差",
|
||||
"Type": "类型",
|
||||
"Updating the preview": "正在更新预览",
|
||||
"Use fixed plane": "使用固定平面",
|
||||
"VIEW": "视图",
|
||||
"Value": "值",
|
||||
"Volume": "体积",
|
||||
"Wall": "壁厚",
|
||||
"When": "条件",
|
||||
"Width": "宽度",
|
||||
"Will fail": "将失败",
|
||||
"angle, height": "角度、高度",
|
||||
"command name": "命令名称",
|
||||
"expression, e.g. 25 mm": "表达式,例如 25 mm",
|
||||
"fits the face, e.g. M8": "适配该面,例如 M8",
|
||||
"from the rule": "来自规则",
|
||||
"needs two joints": "需要两个接头",
|
||||
"new configuration": "新配置",
|
||||
"new name": "新名称",
|
||||
"new parameter": "新参数",
|
||||
"no interference": "无干涉",
|
||||
"no joint that moves yet": "尚无运动接头",
|
||||
"no picture": "无图片",
|
||||
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
|
||||
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
|
||||
"the parts library is not available": "零件库不可用",
|
||||
"to the next face": "到下一个面",
|
||||
"Revert Position": "恢复位置",
|
||||
"Construction": "构造",
|
||||
"Faces": "面",
|
||||
"Edges": "边",
|
||||
"Vertices": "顶点",
|
||||
"Sketch geometry and profiles": "草图几何和轮廓",
|
||||
"Sketch curves": "草图曲线",
|
||||
"Sketch points": "草图点",
|
||||
"Sketch dimensions": "草图尺寸",
|
||||
"Sketch constraints": "草图约束",
|
||||
"0 turns it off": "0 表示关闭",
|
||||
"Show the pointer an AI agent moves (#35)": "显示 AI 代理移动的指针(#35)",
|
||||
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Flat": "平面",
|
||||
"Save Rule": "保存规则",
|
||||
"Preview Row": "预览行",
|
||||
"Add condition": "添加条件",
|
||||
"Apply to selection": "应用到选择",
|
||||
"Remove override": "移除覆盖",
|
||||
"Copy screenshot": "复制截图",
|
||||
"Remove screenshot": "移除截图",
|
||||
"Use active author placement": "使用活动作者放置",
|
||||
"Reattach selected face": "重新附着所选面",
|
||||
"Use saved fixed plane": "使用已保存的固定平面",
|
||||
"Use selected face as fixed plane": "使用所选面作为固定平面",
|
||||
"View navigator": "视图导航器",
|
||||
"Signed axes": "带符号轴",
|
||||
"Cube": "立方体",
|
||||
"Middle double-click": "中键双击",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。",
|
||||
"cmd.render_backends": "渲染后端",
|
||||
"cmd.view_navigate": "导航视图",
|
||||
"ui.animateJoint": "动画接头",
|
||||
"ui.calibrate": "校准",
|
||||
"ui.customizeToolbar": "自定义工具栏…",
|
||||
"ui.editJointLimits": "编辑接头限制",
|
||||
"ui.exportDxf": "导出 DXF…",
|
||||
"ui.findInTimeline": "在时间线中查找",
|
||||
"ui.findInWindow": "在窗口中查找",
|
||||
"ui.fit": "适合",
|
||||
"ui.groupSelected": "将所选编组",
|
||||
"ui.hideAxes": "隐藏轴",
|
||||
"ui.hideDimension": "隐藏尺寸",
|
||||
"ui.hideOrigin": "隐藏原点",
|
||||
"ui.hidePlanes": "隐藏平面",
|
||||
"ui.hideProfile": "隐藏轮廓",
|
||||
"ui.homeView": "主视图",
|
||||
"ui.isolate": "隔离",
|
||||
"ui.lock": "锁定",
|
||||
"ui.newSketchGroup": "新建草图组",
|
||||
"ui.normalConstruction": "普通 / 构造",
|
||||
"ui.orbit": "环绕",
|
||||
"ui.heading_position": "位置",
|
||||
"ui.pan": "平移",
|
||||
"ui.pinToShortcuts": "固定到快捷方式",
|
||||
"ui.pinToToolbar": "固定到工具栏",
|
||||
"ui.properties": "属性",
|
||||
"ui.removeSection": "移除剖面",
|
||||
"ui.removeFromToolbar": "从工具栏移除",
|
||||
"ui.repeat": "重复",
|
||||
"ui.report_a_bug": "报告缺陷",
|
||||
"ui.restore": "还原",
|
||||
"ui.heading_setup": "设置",
|
||||
"ui.selectOther": "选择其他…",
|
||||
"ui.selectTangentChain": "选择相切链",
|
||||
"ui.showAll": "显示全部",
|
||||
"ui.showAllBodies": "显示所有实体",
|
||||
"ui.showAxes": "显示轴",
|
||||
"ui.showDimension": "显示尺寸",
|
||||
"ui.showOrigin": "显示原点",
|
||||
"ui.showPlanes": "显示平面",
|
||||
"ui.suppress": "抑制",
|
||||
"ui.command_palette_hint": "输入命令,例如 {example} — 或按 S 搜索",
|
||||
"ui.unground": "取消固定",
|
||||
"ui.unisolate": "取消隔离",
|
||||
"ui.unlock": "解锁",
|
||||
"ui.unsuppress": "取消抑制",
|
||||
"ui.updateToCurrentView": "更新为当前视图",
|
||||
"ui.zoom": "缩放",
|
||||
"ui.command_done": "{command} 完成",
|
||||
"timeline.group": "新建特征文件夹",
|
||||
"timeline.ungroup": "移除特征文件夹",
|
||||
"timeline.rename_group": "重命名特征文件夹",
|
||||
"timeline.move_to_group": "将特征移至文件夹",
|
||||
"timeline.move_many": "移动时间线项目",
|
||||
"timeline.reorder_many": "特征重新排序",
|
||||
"ui.show_construction_geometries": "构造几何图元",
|
||||
"ui.total_angle": "总角度",
|
||||
"sketch_text.builtin_font": "{family}(内置)",
|
||||
"sketch_text.font_search": "搜索字体",
|
||||
"sketch_text.loading_fonts": "正在查找已安装字体…",
|
||||
"sketch_text.use_font_file": "使用字体文件 {path}",
|
||||
"sketch_text.bold": "粗体",
|
||||
"sketch_text.italic": "斜体",
|
||||
"sketch_text.align_left": "左对齐",
|
||||
"sketch_text.align_center": "居中",
|
||||
"sketch_text.align_right": "右对齐",
|
||||
"sketch_text.path_hint": "文字所跟随的草图曲线:先单击此处,再选择曲线",
|
||||
"sketch_text.path_clear": "将文字移离其曲线",
|
||||
"Style": "样式",
|
||||
"Alignment": "对齐",
|
||||
"Letter Spacing": "字间距",
|
||||
"Line Spacing": "行间距",
|
||||
"Path": "路径",
|
||||
"sketch_text.on_path": "文字沿路径排列:由曲线放置",
|
||||
"ui.analysis": "分析",
|
||||
"ui.edit_section_analysis": "编辑截面分析",
|
||||
"cmd.material_properties_validate":"验证物理材料数据"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
("Default", DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
("Aluminum", 2.70),
|
||||
("Brass", 8.50),
|
||||
("Copper", 8.96),
|
||||
("Titanium", 4.43),
|
||||
("Cast Iron", 7.20),
|
||||
("ABS Plastic", 1.06),
|
||||
("PLA", 1.24),
|
||||
("Nylon", 1.14),
|
||||
("Polycarbonate", 1.20),
|
||||
("Oak", 0.75),
|
||||
("Glass", 2.50),
|
||||
];
|
||||
|
||||
/// How a physical material looks (sRGB); the default material has no colour of its own.
|
||||
pub fn material_color(name: &str) -> Option<[u8; 3]> {
|
||||
Some(match name {
|
||||
"Steel" => [158, 164, 172],
|
||||
"Stainless Steel" => [186, 191, 198],
|
||||
"Aluminum" => [204, 208, 214],
|
||||
"Brass" => [208, 172, 88],
|
||||
"Copper" => [204, 122, 80],
|
||||
"Titanium" => [150, 148, 158],
|
||||
"Cast Iron" => [104, 104, 110],
|
||||
"ABS Plastic" => [222, 222, 216],
|
||||
"PLA" => [110, 160, 222],
|
||||
"Nylon" => [236, 230, 214],
|
||||
"Polycarbonate" => [196, 216, 232],
|
||||
"Oak" => [190, 146, 92],
|
||||
"Glass" => [170, 208, 218],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
|
||||
struct U {
|
||||
vp: mat4x4<f32>,
|
||||
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
|
||||
screen: vec4<f32>, // viewport width, height (px), depth bias (see `biased_z`), silhouettes on (1)
|
||||
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
|
||||
cap: vec4<f32>, // colour of the inside of cut bodies
|
||||
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
|
||||
pull: vec4<f32>, // draft pull direction
|
||||
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
|
||||
acc_v: vec4<f32>,
|
||||
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
|
||||
};
|
||||
|
||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||
|
||||
fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
|
||||
fn to_linear(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
|
||||
}
|
||||
|
||||
fn out_color(c: vec4<f32>) -> vec4<f32> {
|
||||
if (u.back.w > 0.5) {
|
||||
return vec4<f32>(to_linear(c.rgb), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
struct TOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) n: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let c = solid(i, front);
|
||||
return c;
|
||||
}
|
||||
|
||||
struct CutOut {
|
||||
@location(0) c: vec4<f32>,
|
||||
@builtin(frag_depth) depth: f32,
|
||||
};
|
||||
|
||||
/// The section cap (the inside seen through the cut, drawn on the far walls' back faces where
|
||||
/// they are the visible surface) again, at the depth of the section plane rather than of the
|
||||
/// far wall, so the cut face hides what lies inside the body instead of showing it like a
|
||||
/// pane of glass.
|
||||
@fragment
|
||||
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
|
||||
var o: CutOut;
|
||||
o.c = solid(i, front);
|
||||
o.depth = cap_depth(i.wp, i.pos.z);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
|
||||
/// never behind the wall itself (`z`).
|
||||
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
|
||||
var dir = -u.back.xyz;
|
||||
if (u.eye.w > 0.5) { dir = wp - u.eye.xyz; }
|
||||
let dn = dot(u.clip.xyz, dir);
|
||||
if (abs(dn) < 1e-9) { return z; }
|
||||
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
|
||||
let c = u.vp * vec4<f32>(p, 1.0);
|
||||
if (c.w <= 0.0) { return z; }
|
||||
return clamp(c.z / c.w, 0.0, z);
|
||||
}
|
||||
|
||||
fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
// Derivatives first, in uniform control flow (browsers' WGSL compilers reject them after
|
||||
// a branch that returns; the whole shader module then fails and nothing is drawn).
|
||||
let k = curvature(i);
|
||||
let ndv = dot(normalize(i.n), normalize(u.back.xyz));
|
||||
let fw = fwidth(ndv);
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
}
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
return analysis(i, k);
|
||||
}
|
||||
let c = shade(i);
|
||||
// Silhouettes (styles with edges): where a curved surface turns away from the eye, a line
|
||||
// about 1.5 px wide, on the visible surface only (depth-correct).
|
||||
if (u.screen.w > 0.5 && length(i.n) > 0.5 && fw > 1e-6) {
|
||||
let a = 1.0 - smoothstep(fw * 0.8, fw * 2.0, abs(ndv));
|
||||
return vec4<f32>(mix(c.rgb, out_color(vec4<f32>(0.07, 0.08, 0.10, 1.0)).rgb, a), c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/// How fast the normal turns across the pixel (per mm).
|
||||
fn curvature(i: TOut) -> f32 {
|
||||
let nn = normalize(i.n);
|
||||
let kx = length(dpdx(nn)) / max(length(dpdx(i.wp)), 1e-6);
|
||||
let ky = length(dpdy(nn)) / max(length(dpdy(i.wp)), 1e-6);
|
||||
return max(kx, ky);
|
||||
}
|
||||
|
||||
/// Surface analysis colours: zebra stripes, draft angle bands, curvature map (`k`: the
|
||||
/// surface's curvature here, worked out by the caller).
|
||||
fn analysis(i: TOut, k: f32) -> vec4<f32> {
|
||||
let v = normalize(u.back.xyz);
|
||||
var n = normalize(i.n);
|
||||
if (dot(n, v) < 0.0) { n = -n; }
|
||||
let lit = 0.55 + 0.45 * max(dot(n, v), 0.0);
|
||||
if (u.ana.x < 1.5) {
|
||||
// Reflections of upright light bars standing around the viewer: the stripe follows
|
||||
// the reflected ray's sideways direction (horizontal on screen).
|
||||
let r = reflect(-v, n);
|
||||
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
|
||||
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
|
||||
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
|
||||
let c = select(0.08, 0.95, s > 0.0);
|
||||
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
|
||||
}
|
||||
if (u.ana.x > 4.5) {
|
||||
// Reachable from the access direction: facing it and nothing above.
|
||||
let d = normalize(u.pull.xyz);
|
||||
let size = i32(u.ana.w);
|
||||
let px = vec2<i32>(floor(vec2<f32>(dot(i.wp, u.acc_u.xyz) + u.acc_u.w, dot(i.wp, u.acc_v.xyz) + u.acc_v.w) * u.ana.z));
|
||||
var top = -1e30;
|
||||
if (px.x >= 0 && px.y >= 0 && px.x < size && px.y < size) {
|
||||
top = textureLoad(acc_t, px, 0).x;
|
||||
}
|
||||
let cn = dot(normalize(i.n), d);
|
||||
let facing = cn > 0.02;
|
||||
// Slack of a pixel and a half, more on slopes (the map holds the pixel's highest point).
|
||||
let slack = u.ana.y * (1.0 + sqrt(max(1.0 - cn * cn, 0.0)) / max(cn, 0.05));
|
||||
let clear = dot(i.wp, d) >= top - slack;
|
||||
var c = vec3<f32>(0.85, 0.22, 0.20);
|
||||
if (facing && clear) { c = vec3<f32>(0.20, 0.75, 0.30); }
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
if (u.ana.x > 3.5) {
|
||||
// A studio around the model: sky above, a dark floor below, a bright horizon band and
|
||||
// two soft light panels, seen in a mirror.
|
||||
let r = reflect(-v, n);
|
||||
var c = mix(vec3<f32>(0.32, 0.34, 0.38), vec3<f32>(0.80, 0.86, 0.95), smoothstep(-0.05, 0.6, r.z));
|
||||
c = c + vec3<f32>(0.6) * exp(-abs(r.z) * 40.0);
|
||||
let side = atan2(r.y, r.x);
|
||||
c = c + vec3<f32>(0.9) * smoothstep(0.92, 0.98, cos(side * 2.0)) * smoothstep(0.1, 0.3, r.z) * smoothstep(0.8, 0.6, r.z);
|
||||
if (r.z < -0.02) {
|
||||
// The floor: lighter toward the horizon, with light-panel streaks along it.
|
||||
c = mix(vec3<f32>(0.55, 0.56, 0.60), vec3<f32>(0.20, 0.20, 0.22), clamp(-r.z * 1.6, 0.0, 1.0));
|
||||
c = c + vec3<f32>(0.35) * smoothstep(0.95, 0.99, cos(side * 3.0));
|
||||
}
|
||||
return out_color(vec4<f32>(min(c, vec3<f32>(1.0)), 1.0));
|
||||
}
|
||||
if (u.ana.x < 2.5) {
|
||||
let a = degrees(asin(clamp(dot(normalize(i.n), normalize(u.pull.xyz)), -1.0, 1.0)));
|
||||
var c = vec3<f32>(0.95, 0.80, 0.15);
|
||||
if (a >= u.ana.y) { c = vec3<f32>(0.20, 0.75, 0.30); }
|
||||
if (a <= -u.ana.y) { c = vec3<f32>(0.85, 0.22, 0.20); }
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
// Curvature from how fast the normal turns across the pixel.
|
||||
let t = clamp(k * u.ana.y, 0.0, 1.0);
|
||||
let c = select(mix(vec3<f32>(0.10, 0.80, 0.25), vec3<f32>(0.90, 0.15, 0.10), t * 2.0 - 1.0),
|
||||
mix(vec3<f32>(0.15, 0.35, 0.95), vec3<f32>(0.10, 0.80, 0.25), t * 2.0), t < 0.5);
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return shade(i);
|
||||
}
|
||||
|
||||
fn shade(i: TOut) -> vec4<f32> {
|
||||
// A zero normal marks flat colour (selection fills, translucent planes).
|
||||
if (length(i.n) < 0.5) {
|
||||
return out_color(i.c);
|
||||
}
|
||||
let v = normalize(u.back.xyz);
|
||||
var n = normalize(i.n);
|
||||
if (dot(n, v) < 0.0) { n = -n; }
|
||||
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
|
||||
let diff = max(dot(n, key), 0.0);
|
||||
let fill = max(dot(n, v), 0.0);
|
||||
let h = normalize(key + v);
|
||||
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill;
|
||||
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
||||
}
|
||||
|
||||
/// Clip-space z of `p` (clipped to `c`) pulled `s` depth biases toward the eye (pushed away
|
||||
/// when negative). Orthographic depth is linear in distance, so the bias is a fixed step of it.
|
||||
/// Perspective depth is not: near the far plane a fixed step spans tens of millimetres (edges
|
||||
/// showed through walls), so the point moves toward the eye by a fraction of its distance.
|
||||
fn biased_z(p: vec3<f32>, c: vec4<f32>, s: f32) -> f32 {
|
||||
if (u.eye.w > 0.5) {
|
||||
let q = u.vp * vec4<f32>(mix(p, u.eye.xyz, u.screen.z * 8.0 * s), 1.0);
|
||||
return q.z / max(q.w, 1e-6) * c.w;
|
||||
}
|
||||
return c.z - u.screen.z * s * c.w;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_ghost(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.pos.z = biased_z(p, o.pos, -4.0);
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
struct LOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) c: vec4<f32>,
|
||||
@location(1) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
|
||||
var pa = a;
|
||||
var pb = b;
|
||||
var ca = u.vp * vec4<f32>(a, 1.0);
|
||||
var cb = u.vp * vec4<f32>(b, 1.0);
|
||||
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
|
||||
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
|
||||
// the segment at the plane instead; clip space is linear in the world point.
|
||||
if (ca.z < 0.0 && cb.z > 0.0) {
|
||||
let k = ca.z / (ca.z - cb.z);
|
||||
ca = mix(ca, cb, k);
|
||||
pa = mix(a, b, k);
|
||||
} else if (cb.z < 0.0 && ca.z > 0.0) {
|
||||
let k = cb.z / (cb.z - ca.z);
|
||||
cb = mix(cb, ca, k);
|
||||
pb = mix(b, a, k);
|
||||
}
|
||||
let half = u.screen.xy * 0.5;
|
||||
let sa = ca.xy / max(ca.w, 1e-6) * half;
|
||||
let sb = cb.xy / max(cb.w, 1e-6) * half;
|
||||
var dir = sb - sa;
|
||||
let len = length(dir);
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
|
||||
let nrm = vec2<f32>(-dir.y, dir.x);
|
||||
// Two triangles: corners (t, side).
|
||||
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
|
||||
var ss = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||
let t = ts[vi];
|
||||
let s = ss[vi];
|
||||
let base = select(ca, cb, t > 0.5);
|
||||
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
|
||||
var o: LOut;
|
||||
o.pos = vec4<f32>(base.xy + off / half * base.w, biased_z(select(pa, pb, t > 0.5), base, 1.0), base.w);
|
||||
o.c = c;
|
||||
o.wp = select(pa, pb, t > 0.5);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
struct IOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) a: f32,
|
||||
@location(2) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var img_t: texture_2d<f32>;
|
||||
@group(1) @binding(1) var img_s: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
|
||||
var o: IOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
// Pulled toward the eye like lines, so a decal on a face is not lost in it.
|
||||
o.pos.z = biased_z(p, o.pos, 1.0);
|
||||
o.uv = uv;
|
||||
o.a = a;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_img(i: IOut) -> @location(0) vec4<f32> {
|
||||
let c = textureSample(img_t, img_s, i.uv);
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(vec4<f32>(c.rgb, c.a * i.a));
|
||||
}
|
||||
|
||||
/// Lines the section never cuts (the grid, highlights drawn on top).
|
||||
@fragment
|
||||
fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Original light and dark palette values. Compiled by the UI theme boundary.
|
||||
use crate::theme::Tokens;
|
||||
use egui::Color32;
|
||||
impl Tokens {
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
app_bar: Color32::from_rgb(52, 56, 64),
|
||||
toolbar: Color32::from_rgb(246, 247, 249),
|
||||
panel: Color32::from_rgb(250, 251, 252),
|
||||
panel_header: Color32::from_rgb(236, 238, 242),
|
||||
border: Color32::from_rgb(208, 212, 220),
|
||||
text: Color32::from_rgb(38, 42, 50),
|
||||
text_dim: Color32::from_rgb(110, 116, 128),
|
||||
accent: Color32::from_rgb(38, 120, 218),
|
||||
accent_soft: Color32::from_rgb(214, 230, 250),
|
||||
sketch_accent: Color32::from_rgb(46, 150, 90),
|
||||
error: Color32::from_rgb(214, 54, 54),
|
||||
warning: Color32::from_rgb(222, 150, 30),
|
||||
icon: Color32::from_rgb(70, 78, 92),
|
||||
icon_fill: Color32::from_rgb(178, 196, 222),
|
||||
timeline: Color32::from_rgb(232, 235, 240),
|
||||
hover: Color32::from_rgb(226, 234, 246),
|
||||
sel_face: Color32::from_rgb(76, 127, 203),
|
||||
sel_edge: Color32::from_rgb(0, 127, 255),
|
||||
sel_edge_rim: Color32::from_rgb(127, 191, 255),
|
||||
hover_edge: Color32::from_rgb(0, 0, 0),
|
||||
hover_edge_halo: Color32::from_rgb(203, 203, 203),
|
||||
hover_face_lift: 40,
|
||||
sel_profile: Color32::from_rgb(26, 84, 167),
|
||||
sel_profile_edge: Color32::from_rgb(47, 109, 198),
|
||||
hover_profile: Color32::from_rgba_unmultiplied_const(75, 129, 253, 48),
|
||||
hover_profile_edge: Color32::from_rgb(75, 129, 253),
|
||||
sel_vertex: Color32::from_rgb(70, 110, 170),
|
||||
origin_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 94),
|
||||
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
|
||||
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
|
||||
origin_point: Color32::from_rgb(214, 215, 218),
|
||||
axis_x: Color32::from_rgb(255, 0, 0),
|
||||
axis_y: Color32::from_rgb(0, 255, 0),
|
||||
axis_z: Color32::from_rgb(2, 2, 248),
|
||||
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
|
||||
construction_geom: Color32::from_rgb(232, 140, 48),
|
||||
body: Color32::from_rgb(176, 186, 198),
|
||||
body_edge: Color32::from_rgb(40, 44, 52),
|
||||
manipulator: Color32::from_rgb(40, 110, 220),
|
||||
preview_add: Color32::from_rgb(112, 160, 226),
|
||||
preview_sketch: Color32::from_rgb(32, 190, 96),
|
||||
preview_cut: Color32::from_rgba_unmultiplied_const(232, 84, 44, 110),
|
||||
cube_face: Color32::from_rgb(236, 239, 244),
|
||||
cube_border: Color32::from_rgb(85, 94, 110),
|
||||
cube_text: Color32::from_rgb(38, 42, 50),
|
||||
cube_hover: Color32::from_rgb(190, 220, 252),
|
||||
viewport_top: Color32::from_rgb(240, 243, 247),
|
||||
viewport_bottom: Color32::from_rgb(203, 210, 221),
|
||||
grid: Color32::from_rgb(196, 202, 212),
|
||||
grid_major: Color32::from_rgb(170, 178, 190),
|
||||
body_selected: Color32::from_rgb(110, 170, 235),
|
||||
sketch: Color32::from_rgb(30, 90, 200),
|
||||
sketch_fixed: Color32::from_rgb(20, 20, 20),
|
||||
sketch_point: Color32::BLACK,
|
||||
sketch_locked: Color32::from_rgb(0, 140, 60),
|
||||
dimension: Color32::from_rgb(36, 40, 48),
|
||||
dimension_driven: Color32::from_rgb(120, 126, 138),
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||
dialog_border: Color32::from_rgb(208, 212, 220),
|
||||
chip: Color32::from_rgb(38, 120, 218),
|
||||
heading_bg: Color32::from_rgb(218, 223, 231),
|
||||
subheading_bg: Color32::from_rgb(234, 237, 242),
|
||||
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
|
||||
glyph_bg: Color32::WHITE,
|
||||
glyph_edge: Color32::from_rgb(190, 110, 100),
|
||||
glyph_text: Color32::from_rgb(150, 40, 35),
|
||||
overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235),
|
||||
rubber_band: Color32::from_rgb(30, 120, 230),
|
||||
app_bar_text: Color32::from_rgb(230, 234, 242),
|
||||
timeline_item: Color32::WHITE,
|
||||
timeline_item_error: Color32::from_rgb(250, 220, 220),
|
||||
timeline_marker: Color32::from_rgb(70, 76, 88),
|
||||
field: Color32::WHITE,
|
||||
box_window: Color32::from_rgb(38, 120, 218),
|
||||
box_crossing: Color32::from_rgb(40, 160, 80),
|
||||
}
|
||||
}
|
||||
pub const fn dark() -> Tokens {
|
||||
let mut t = Tokens::light();
|
||||
t.app_bar = Color32::from_rgb(28, 31, 38);
|
||||
t.toolbar = Color32::from_rgb(37, 41, 49);
|
||||
t.panel = Color32::from_rgb(39, 43, 52);
|
||||
t.panel_header = Color32::from_rgb(47, 52, 62);
|
||||
t.border = Color32::from_rgb(63, 69, 82);
|
||||
t.text = Color32::from_rgb(222, 226, 232);
|
||||
t.text_dim = Color32::from_rgb(148, 155, 168);
|
||||
t.accent = Color32::from_rgb(58, 136, 238);
|
||||
t.accent_soft = Color32::from_rgb(42, 70, 110);
|
||||
t.sketch_accent = Color32::from_rgb(110, 200, 140);
|
||||
t.error = Color32::from_rgb(240, 100, 96);
|
||||
t.warning = Color32::from_rgb(236, 170, 70);
|
||||
t.icon = Color32::from_rgb(208, 214, 226);
|
||||
t.icon_fill = Color32::from_rgb(86, 112, 150);
|
||||
t.timeline = Color32::from_rgb(33, 37, 45);
|
||||
t.hover = Color32::from_rgb(54, 61, 74);
|
||||
t.hover_edge_halo = Color32::from_rgb(203, 203, 203);
|
||||
t.origin_point = Color32::from_rgb(214, 215, 218);
|
||||
t.viewport_top = Color32::from_rgb(46, 52, 64);
|
||||
t.viewport_bottom = Color32::from_rgb(46, 52, 64);
|
||||
t.grid = Color32::from_rgb(59, 65, 76);
|
||||
t.grid_major = Color32::from_rgb(73, 79, 90);
|
||||
t.body = Color32::from_rgb(152, 156, 162);
|
||||
t.body_edge = Color32::from_rgb(16, 16, 18);
|
||||
t.sketch = Color32::from_rgb(92, 150, 255);
|
||||
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_point = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_locked = Color32::from_rgb(96, 204, 116);
|
||||
t.dimension = Color32::from_rgb(224, 228, 234);
|
||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||
t.dialog_bg = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_border = Color32::from_rgb(72, 83, 99);
|
||||
t.chip = Color32::from_rgb(59, 101, 168);
|
||||
t.heading_bg = Color32::from_rgb(38, 44, 55);
|
||||
t.subheading_bg = Color32::from_rgb(48, 55, 68);
|
||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||
t.glyph_edge = Color32::from_rgb(200, 130, 120);
|
||||
t.glyph_text = Color32::from_rgb(240, 165, 155);
|
||||
t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240);
|
||||
t.rubber_band = Color32::from_rgb(92, 160, 255);
|
||||
t.timeline_item = Color32::from_rgb(52, 58, 70);
|
||||
t.timeline_item_error = Color32::from_rgb(110, 50, 54);
|
||||
t.timeline_marker = Color32::from_rgb(170, 178, 192);
|
||||
t.field = Color32::from_rgb(30, 34, 41);
|
||||
t.box_window = Color32::from_rgb(70, 150, 255);
|
||||
t.box_crossing = Color32::from_rgb(80, 200, 120);
|
||||
t
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
|
||||
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
@@ -103,28 +103,9 @@ impl Appearances {
|
||||
}
|
||||
|
||||
/// The built-in appearance library: (name, colour, opacity).
|
||||
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
|
||||
("Aluminum - Satin", [204, 208, 214], 1.0),
|
||||
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
|
||||
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
|
||||
("Steel - Satin", [158, 164, 172], 1.0),
|
||||
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
|
||||
("Cast Iron", [104, 104, 110], 1.0),
|
||||
("Brass - Polished", [208, 172, 88], 1.0),
|
||||
("Copper", [204, 122, 80], 1.0),
|
||||
("Titanium", [150, 148, 158], 1.0),
|
||||
("ABS - White", [236, 236, 230], 1.0),
|
||||
("ABS - Black", [36, 36, 38], 1.0),
|
||||
("ABS - Red", [196, 40, 40], 1.0),
|
||||
("ABS - Blue", [40, 90, 186], 1.0),
|
||||
("ABS - Yellow", [236, 196, 40], 1.0),
|
||||
("ABS - Green", [52, 150, 80], 1.0),
|
||||
("Rubber - Black", [28, 28, 28], 1.0),
|
||||
("Glass - Clear", [190, 220, 228], 0.25),
|
||||
("Glass - Smoked", [90, 96, 104], 0.45),
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
#[path = "../../../assets/appearances/library.rs"]
|
||||
mod defaults;
|
||||
pub use defaults::LIBRARY;
|
||||
|
||||
/// A library appearance by name (case-insensitive).
|
||||
pub fn library(name: &str) -> Option<Look> {
|
||||
@@ -263,7 +244,7 @@ pub fn pick_tolerance(b: &crate::ModelBody) -> f64 {
|
||||
}
|
||||
|
||||
/// The point of triangle abc nearest p.
|
||||
fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
pub(crate) fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
let (ab, ac, ap) = (b - a, c - a, p - a);
|
||||
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
|
||||
if d1 <= 0.0 && d2 <= 0.0 {
|
||||
|
||||
@@ -124,6 +124,10 @@ fn identity() -> Mat {
|
||||
IDENTITY
|
||||
}
|
||||
|
||||
/// How deep and how many placements [`Document::placements`] walks.
|
||||
pub const MAX_PLACEMENT_DEPTH: usize = 64;
|
||||
pub const MAX_PLACEMENTS: usize = 100_000;
|
||||
|
||||
fn map_plane(m: &Mat, p: &Plane) -> Plane {
|
||||
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
|
||||
Plane::new(o, x, y).unwrap_or(*p)
|
||||
@@ -221,20 +225,22 @@ impl FeatureKind {
|
||||
pt(&mut t.point);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. }
|
||||
| FeatureKind::SurfaceRevolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||||
pt(origin);
|
||||
dir(d);
|
||||
}
|
||||
FeatureKind::Fillet { edges, style, .. } => {
|
||||
edges.iter_mut().for_each(pt);
|
||||
if let crate::FilletStyle::Variable { start, .. } = style {
|
||||
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::Shell { faces, .. }
|
||||
| FeatureKind::OffsetFace { faces, .. }
|
||||
| FeatureKind::DeleteFace { faces, .. }
|
||||
| FeatureKind::SurfaceOffset { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::Draft { faces, neutral, pull, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, neutral);
|
||||
@@ -265,7 +271,10 @@ impl FeatureKind {
|
||||
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, .. } => pt(face),
|
||||
FeatureKind::SheetConvert { face, bends, .. } => {
|
||||
pt(face);
|
||||
bends.iter_mut().for_each(pt);
|
||||
}
|
||||
FeatureKind::SheetFold { a, b, fixed, .. } => {
|
||||
pt(a);
|
||||
pt(b);
|
||||
@@ -318,6 +327,10 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::Revolve { .. }
|
||||
| FeatureKind::Extrude { .. }
|
||||
| FeatureKind::SurfaceExtrude { .. }
|
||||
| FeatureKind::SurfaceRevolve { .. }
|
||||
| FeatureKind::SurfaceSweep { .. }
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::Combine { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
@@ -333,6 +346,7 @@ impl FeatureKind {
|
||||
| FeatureKind::MeshConvert { .. }
|
||||
| FeatureKind::BodyTransfer { .. } => {}
|
||||
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
|
||||
map_plane_ref(m, plane);
|
||||
if let Some(t) = tool {
|
||||
@@ -366,13 +380,18 @@ impl Document {
|
||||
}
|
||||
|
||||
/// Every placement of each component: (component, occurrence path ids, world transform).
|
||||
/// A branch stops past [`MAX_PLACEMENT_DEPTH`] levels, the walk past [`MAX_PLACEMENTS`] placements.
|
||||
pub fn placements(&self) -> Vec<(u64, Vec<u64>, Mat)> {
|
||||
let mut out = vec![(0, Vec::new(), IDENTITY)];
|
||||
let mut stack: Vec<(u64, Vec<u64>, Mat)> = vec![(0, Vec::new(), IDENTITY)];
|
||||
while let Some((comp, path, m)) = stack.pop() {
|
||||
if path.len() > 64 || out.len() > 100_000 {
|
||||
if out.len() > MAX_PLACEMENTS {
|
||||
break;
|
||||
}
|
||||
// Only this branch stops past the depth limit; the others are still walked.
|
||||
if path.len() > MAX_PLACEMENT_DEPTH {
|
||||
continue;
|
||||
}
|
||||
for o in self.occurrences.iter().filter(|o| o.parent == comp) {
|
||||
let mut p = path.clone();
|
||||
p.push(o.id);
|
||||
|
||||
@@ -31,11 +31,14 @@ pub fn validate_body_transfer(doc: &Document, bodies: &[String], from: u64, to:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the bodies in `st`, which the evaluator discards when this fails (so it is changed in
|
||||
/// place rather than copied: a copy of every body for each moved one was most of the cost).
|
||||
pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], from: u64, to: u64, transform: Mat) -> Result<()> {
|
||||
validate_body_transfer(doc, bodies, from, to, &transform)?;
|
||||
let mut next = st.clone();
|
||||
let mut moved = Vec::with_capacity(bodies.len());
|
||||
for name in bodies {
|
||||
let old = st.body(name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
let (slot, old) =
|
||||
st.bodies.iter().enumerate().find(|(_, b)| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
if doc.model_body_component(old) != from {
|
||||
return Err(DocError::Invalid(format!("body `{name}` no longer belongs to transfer source component {from}")));
|
||||
}
|
||||
@@ -47,12 +50,14 @@ pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], f
|
||||
body.source_transform = crate::mat_mul(&transform, &old.source_transform);
|
||||
body.imported = old.imported.clone();
|
||||
body.names = crate::naming::NameCell::same(old);
|
||||
let slot = next.bodies.iter_mut().find(|b| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
|
||||
*slot = body;
|
||||
moved.push((slot, body));
|
||||
}
|
||||
crate::body_transfer_sheet::remap_sheet_metadata(&mut next, bodies, &transform)?;
|
||||
*st = next;
|
||||
Ok(())
|
||||
for (slot, body) in moved {
|
||||
if let Some(b) = st.bodies.get_mut(slot) {
|
||||
*b = body;
|
||||
}
|
||||
}
|
||||
crate::body_transfer_sheet::remap_sheet_metadata(st, bodies, &transform)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -47,19 +47,22 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Extrude { sketch, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch, .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch, .. }
|
||||
| FeatureKind::Revolve { sketch, .. }
|
||||
| FeatureKind::Emboss { sketch, .. }
|
||||
| FeatureKind::SheetBase { sketch, .. }
|
||||
| FeatureKind::SheetContour { sketch, .. }
|
||||
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
||||
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } => {
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
|
||||
sk(sketch);
|
||||
sk(path_sketch);
|
||||
}
|
||||
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
|
||||
FeatureKind::SheetFold { sketch: Some(s), .. } => sk(s),
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::Hole { points: Some(p), .. } => sk(&mut p.sketch),
|
||||
FeatureKind::Pattern { features, pattern, .. } => {
|
||||
feats(features);
|
||||
@@ -243,6 +246,92 @@ 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::SurfaceOffset { 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::Unstitch { body, .. }
|
||||
| K::ReverseNormal { 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(
|
||||
@@ -298,81 +387,7 @@ impl crate::Feature {
|
||||
for e in self.kind.point_expr_mut().into_iter().flatten() {
|
||||
*e = clone_expression(e, parameters);
|
||||
}
|
||||
match &mut self.kind {
|
||||
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 &mut self.kind {
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
self.kind.remap_bodies(bodies);
|
||||
match &mut self.kind {
|
||||
K::Sketch { plane, sketch } => {
|
||||
remap_plane(plane, ids, names, bodies, 0)?;
|
||||
@@ -383,7 +398,7 @@ impl crate::Feature {
|
||||
}
|
||||
for link in &mut sketch.links {
|
||||
remap_link(&mut link.source, ids, names, bodies, 0);
|
||||
if let Some(n) = &mut link.face_name {
|
||||
for n in link.face_name.iter_mut().chain(&mut link.edge_name) {
|
||||
*n = remap_topology(n, ids, bodies);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +421,80 @@ impl<'a> ExactSketchCurves<'a> {
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`find`](Self::find), else the one sketch line a generated straight segment runs along
|
||||
/// and overlaps: a profile a feature grew to overlap a flush face (its neighbours lengthened
|
||||
/// past the sketch's corners) still names its sides by the lines they were drawn on. Two
|
||||
/// candidates stay ambiguous.
|
||||
pub(crate) fn find_along(&self, generated: &Seg2) -> Option<String> {
|
||||
if let Some(id) = self.find(generated) {
|
||||
return Some(id);
|
||||
}
|
||||
let Seg2::Line { a: c, b: d } = *generated else { return None };
|
||||
let mut found: Option<Option<&str>> = None;
|
||||
for (id, linked, originals) in &self.segments {
|
||||
if *linked {
|
||||
continue;
|
||||
}
|
||||
let along = originals.iter().any(|original| {
|
||||
let Seg2::Line { a, b } = *original 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
|
||||
});
|
||||
if along {
|
||||
found = Some(if found.is_some() { None } else { Some(*id) });
|
||||
}
|
||||
}
|
||||
found.flatten().map(str::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -581,6 +655,14 @@ pub fn lineage_frame(body: &ModelBody, plane: &solidcraft_geom::Plane) -> Option
|
||||
solidcraft_geom::Plane::new(origin, x, n.cross(x))
|
||||
}
|
||||
|
||||
/// Whether Split Face split `body`'s faces on `plane` (a split line lies on it).
|
||||
fn split_on_purpose(body: &ModelBody, plane: &solidcraft_geom::Plane) -> bool {
|
||||
let tolerance = 1e-6 * (1. + body.body.size());
|
||||
let on_plane =
|
||||
|line: &Vec<solidcraft_geom::Vec3>| !line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance);
|
||||
body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane))
|
||||
}
|
||||
|
||||
/// Whether two exact planes are one oriented plane (pieces of one face).
|
||||
fn same_oriented_plane(a: &solidcraft_geom::Plane, b: &solidcraft_geom::Plane, scale: f64) -> bool {
|
||||
a.normal().dot(b.normal()) >= 1. - 1e-8 && (b.origin - a.origin).dot(a.normal()).abs() <= 1e-7 * (1. + scale)
|
||||
@@ -599,7 +681,9 @@ impl Document {
|
||||
|
||||
/// A target for flat face `index` of `body` by its exact history name
|
||||
/// ([`FaceCertificate::Lineage`]), when that name follows exact history, names a feature of
|
||||
/// the design and finds this face (with its other pieces, all on one oriented plane).
|
||||
/// the design and finds this face (with its other pieces, all on one oriented plane). Where
|
||||
/// the face's pieces no longer share a plane (Offset Face moved one of them), the piece by
|
||||
/// its own name: the face's name qualified by the faces around it (`F2:top#[F2:side:l4,…]`).
|
||||
pub(crate) fn lineage_face_target(
|
||||
&self,
|
||||
body: &ModelBody,
|
||||
@@ -610,17 +694,24 @@ impl Document {
|
||||
if !names.verified(index) {
|
||||
return None;
|
||||
}
|
||||
let name = crate::naming::strip_piece(names.faces.get(index)?).to_string();
|
||||
let origin = name.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
|
||||
let full = names.faces.get(index)?;
|
||||
let stem = crate::naming::strip_piece(full);
|
||||
let origin = stem.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
|
||||
let inverse = crate::mat_inverse(&body.source_transform)?;
|
||||
let target = crate::config::ConfigFaceTarget {
|
||||
body: body.definition_name().into(),
|
||||
origin_feature: origin,
|
||||
name,
|
||||
point: crate::apply_point(&inverse, point),
|
||||
certificate: Some(FaceCertificate::Lineage),
|
||||
};
|
||||
self.configuration_face_indices(body, &target).ok()?.contains(&index).then_some(target)
|
||||
let mut candidates = vec![stem];
|
||||
if full != stem {
|
||||
candidates.push(full);
|
||||
}
|
||||
candidates.into_iter().find_map(|name| {
|
||||
let target = crate::config::ConfigFaceTarget {
|
||||
body: body.definition_name().into(),
|
||||
origin_feature: origin,
|
||||
name: name.to_string(),
|
||||
point: crate::apply_point(&inverse, point),
|
||||
certificate: Some(FaceCertificate::Lineage),
|
||||
};
|
||||
self.configuration_face_indices(body, &target).ok()?.contains(&index).then_some(target)
|
||||
})
|
||||
}
|
||||
|
||||
/// The faces of `body` an exact history name finds: those named `name` or a piece of it, else
|
||||
@@ -628,8 +719,20 @@ impl Document {
|
||||
/// follow exact history and lie on one oriented plane.
|
||||
pub(crate) fn lineage_face_indices(&self, body: &ModelBody, name: &str) -> Result<Vec<usize>> {
|
||||
let names = crate::naming::names(body);
|
||||
let mut indices: Vec<usize> =
|
||||
names.faces.iter().enumerate().filter(|(_, face)| crate::naming::strip_piece(face) == name).map(|(index, _)| index).collect();
|
||||
// `face` is `name` itself or a piece of it (a piece of a piece, …).
|
||||
let piece_of = |face: &str| {
|
||||
let mut n = face;
|
||||
loop {
|
||||
if n == name {
|
||||
return true;
|
||||
}
|
||||
match crate::piece_names::parent_piece(n) {
|
||||
Some(parent) => n = parent,
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut indices: Vec<usize> = names.faces.iter().enumerate().filter(|(_, face)| piece_of(face)).map(|(index, _)| index).collect();
|
||||
if indices.is_empty() {
|
||||
indices =
|
||||
names.info.iter().enumerate().filter(|(_, info)| info.aliases.iter().any(|alias| alias == name)).map(|(index, _)| index).collect();
|
||||
@@ -638,6 +741,16 @@ impl Document {
|
||||
return Err(DocError::Unknown(format!("face `{name}` on `{}` no longer exists", body.name)));
|
||||
}
|
||||
self.coplanar_pieces(body, &indices, |index| names.verified(index))?;
|
||||
// A piece by its own name stands for itself, which a piece of a face split on purpose
|
||||
// (Split Face) never does: that needs a fixed plane.
|
||||
if crate::naming::strip_piece(name) != name
|
||||
&& let Some(plane) = indices.first().and_then(|index| body.body.planar_face(*index))
|
||||
&& split_on_purpose(body, &plane)
|
||||
{
|
||||
return Err(DocError::Invalid(
|
||||
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
|
||||
));
|
||||
}
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
@@ -658,16 +771,12 @@ impl Document {
|
||||
Some(_) => return Err(DocError::Invalid("attached face pieces no longer share one oriented plane".into())),
|
||||
}
|
||||
}
|
||||
if let (Some(plane), true) = (first, indices.len() > 1) {
|
||||
let tolerance = 1e-6 * (1. + scale);
|
||||
let on_plane = |line: &Vec<solidcraft_geom::Vec3>| {
|
||||
!line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance)
|
||||
};
|
||||
if body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane)) {
|
||||
return Err(DocError::Invalid(
|
||||
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
|
||||
));
|
||||
}
|
||||
if let (Some(plane), true) = (first, indices.len() > 1)
|
||||
&& split_on_purpose(body, &plane)
|
||||
{
|
||||
return Err(DocError::Invalid(
|
||||
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,7 +124,12 @@ impl Document {
|
||||
target.validate()?;
|
||||
let prefix = format!("F{}:", target.origin_feature);
|
||||
let role = target.name.strip_prefix(&prefix).ok_or_else(|| DocError::Invalid("face lineage does not match its feature".into()))?;
|
||||
if crate::naming::strip_piece(&target.name) != target.name {
|
||||
// An exact history name may name a piece by the faces around it (`F2:top#[F2:side:l4,…]`),
|
||||
// never by a number (an older design's piece, or a tie the faces around it left).
|
||||
let numbered =
|
||||
target.name.match_indices('#').any(|(i, _)| target.name.get(i + 1..).is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())));
|
||||
let lineage_piece = matches!(target.certificate, Some(super::FaceCertificate::Lineage)) && !numbered;
|
||||
if crate::naming::strip_piece(&target.name) != target.name && !lineage_piece {
|
||||
return Err(DocError::Invalid("configure a face lineage, not a numbered split piece".into()));
|
||||
}
|
||||
let feature = self.feature(target.origin_feature).ok_or_else(|| DocError::Unknown("face origin feature is gone".into()))?;
|
||||
|
||||
@@ -485,7 +485,7 @@ pub enum FeatureKind {
|
||||
depth: Option<String>,
|
||||
#[serde(default)]
|
||||
hole: HoleKind,
|
||||
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch);
|
||||
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch, into the material: against the sketch normal, or along it when only that side has material);
|
||||
/// the holes follow the points when the sketch changes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
points: Option<SketchPoints>,
|
||||
@@ -607,6 +607,10 @@ pub enum FeatureKind {
|
||||
faces: Vec<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
/// Close the gap by extending the neighbouring faces (the default); `false` leaves it
|
||||
/// open, the faces left a surface body.
|
||||
#[serde(default = "yes", skip_serializing_if = "is_yes")]
|
||||
heal: bool,
|
||||
},
|
||||
/// A box around bodies, grown by a margin.
|
||||
BoundingSolid {
|
||||
@@ -782,12 +786,17 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
refold: bool,
|
||||
},
|
||||
/// Convert a plate body (constant thickness from the face at `face`) to sheet metal.
|
||||
/// Convert a body to sheet metal from its fixed face at `face`: a plate of constant
|
||||
/// thickness or, with `bends`, a solid or shelled body whose faces reached from the fixed
|
||||
/// face across those edges become the sheet.
|
||||
SheetConvert {
|
||||
body: String,
|
||||
face: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
rule: Option<String>,
|
||||
/// Points on the sharp edges that become bends.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
bends: Vec<Vec3>,
|
||||
},
|
||||
/// Plastic boss: a round post on a face (draft, root fillet, hole, ribs).
|
||||
Boss {
|
||||
@@ -920,6 +929,77 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
},
|
||||
/// A surface body swept straight along the sketch normal from open curve chains and the
|
||||
/// boundaries of profiles: walls only, no caps.
|
||||
SurfaceExtrude {
|
||||
sketch: u64,
|
||||
/// Profiles whose boundary loops are swept (none: only `curves`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
profiles: Option<ProfileSel>,
|
||||
/// Sketch curve ids, grouped into chains of curves that meet end to end.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
curves: Vec<String>,
|
||||
extent: Extent,
|
||||
},
|
||||
/// A body taken apart: each face at `faces` (every face when empty) its own surface body,
|
||||
/// the faces left one surface body under the body's name. A solid comes apart into surfaces.
|
||||
Unstitch {
|
||||
body: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
faces: Vec<Vec3>,
|
||||
},
|
||||
/// A surface body with the faces at `faces` (every face when empty) turned over.
|
||||
ReverseNormal {
|
||||
body: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
faces: Vec<Vec3>,
|
||||
},
|
||||
/// A surface body turned about an axis from open curve chains and the boundaries of
|
||||
/// profiles: walls only, no caps (a solid where a full turn closes them).
|
||||
SurfaceRevolve {
|
||||
sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
curves: Vec<String>,
|
||||
axis: AxisRef,
|
||||
angle: String,
|
||||
/// Two Sides: the angle on the other side.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
angle2: Option<String>,
|
||||
/// Symmetric: `angle` split evenly either side of the sketch.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
symmetric: bool,
|
||||
},
|
||||
/// A surface body swept along a chain of sketch curves from open curve chains and the
|
||||
/// boundaries of profiles: walls only, no caps.
|
||||
SurfaceSweep {
|
||||
sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
curves: Vec<String>,
|
||||
path_sketch: u64,
|
||||
path: Vec<String>,
|
||||
},
|
||||
/// A surface body lofted through sections in order, each one open or closed chain of
|
||||
/// sketch curves: walls only, no caps.
|
||||
SurfaceLoft {
|
||||
sections: Vec<SheetSection>,
|
||||
},
|
||||
/// The faces at `faces` copied out at a signed `distance` along their normals as a new
|
||||
/// surface body (0 copies them); `chain` takes each pick's whole smooth chain of faces;
|
||||
/// fitted faces within `tolerance`.
|
||||
SurfaceOffset {
|
||||
faces: Vec<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
distance: String,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
chain: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tolerance: Option<String>,
|
||||
},
|
||||
/// A planar surface body extended outward by `distance` past the edges through `edges`.
|
||||
SurfaceExtend {
|
||||
body: String,
|
||||
@@ -952,8 +1032,9 @@ pub enum FeatureKind {
|
||||
Import {
|
||||
/// Source file name (display only).
|
||||
file: String,
|
||||
/// The STEP (ISO 10303-21) text.
|
||||
step: String,
|
||||
/// The STEP (ISO 10303-21) text, shared by every copy of the design (undo states,
|
||||
/// previews, scratch sessions): a large file is held once, not once per edit.
|
||||
step: std::sync::Arc<str>,
|
||||
/// The file's product/assembly tree, kept for components.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
components: Vec<solidcraft_kernel::StepNode>,
|
||||
@@ -979,6 +1060,9 @@ pub enum FilletStyle {
|
||||
Constant,
|
||||
/// `radius` is the chord: the width across the blend.
|
||||
Chord,
|
||||
/// G2 quintic blend of planar faces along a straight edge. `radius` sets the
|
||||
/// equivalent circular setback, not a constant curvature radius.
|
||||
Curvature,
|
||||
/// `radius` at the end of each edge nearest `start`, `radius2` at the other; `mid` radii at
|
||||
/// positions between (0 to 1 from the start end); straight between them, or `smooth`.
|
||||
Variable {
|
||||
@@ -989,16 +1073,23 @@ pub enum FilletStyle {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
smooth: bool,
|
||||
},
|
||||
/// The radius along each edge as a clamped B-spline of the position from the end nearest
|
||||
/// `start` (0) to the other (1): `radius` is its first control radius and `radii` the rest,
|
||||
/// its degree what `knots` leave (knots = control radii + degree + 1).
|
||||
Spline { start: Vec3, knots: Vec<f64>, radii: Vec<String> },
|
||||
}
|
||||
|
||||
/// A fillet radius too large for the geometry around its edges: refused (`Exact`, the default),
|
||||
/// or reduced to the largest that works for all of them (`Reduce`), with a warning.
|
||||
/// reduced to the largest that works for all of them (`Reduce`), or tapered along each edge it
|
||||
/// doesn't fit to the room there, down to zero where a face closes to a point (`Taper`); a
|
||||
/// warning says what was done.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilletFit {
|
||||
#[default]
|
||||
Exact,
|
||||
Reduce,
|
||||
Taper,
|
||||
}
|
||||
|
||||
impl FilletFit {
|
||||
@@ -1011,6 +1102,13 @@ impl FilletStyle {
|
||||
pub fn is_constant(&self) -> bool {
|
||||
*self == FilletStyle::Constant
|
||||
}
|
||||
|
||||
/// The spline of a [`FilletStyle::Spline`] with its control radii `values` (the first is
|
||||
/// the feature's `radius`).
|
||||
pub fn spline(knots: &[f64], values: Vec<f64>) -> solidcraft_kernel::RadiusSpline {
|
||||
let degree = knots.len().saturating_sub(values.len() + 1);
|
||||
solidcraft_kernel::RadiusSpline { degree, knots: knots.to_vec(), radii: values }
|
||||
}
|
||||
}
|
||||
|
||||
/// A face picked by a point on it, on a named body.
|
||||
@@ -1037,6 +1135,9 @@ pub struct MeshData {
|
||||
pub struct SketchPoints {
|
||||
pub sketch: u64,
|
||||
pub ids: Vec<String>,
|
||||
/// Drill the other way: away from the side the holes would go into by themselves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub flip: bool,
|
||||
}
|
||||
|
||||
/// A component: a node of the design tree that owns sketches, features and bodies. The root
|
||||
@@ -1060,6 +1161,17 @@ pub struct LoftSection {
|
||||
pub point: Option<String>,
|
||||
}
|
||||
|
||||
/// A section of a surface loft: one chain from a sketch (one profile's boundary, or curves
|
||||
/// meeting end to end).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SheetSection {
|
||||
pub sketch: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profiles: Option<ProfileSel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub curves: Vec<String>,
|
||||
}
|
||||
|
||||
/// Coil cross-sections.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -1206,14 +1318,17 @@ impl FeatureKind {
|
||||
use FeatureKind as K;
|
||||
match self {
|
||||
K::Extrude { sketch, .. }
|
||||
| K::SurfaceExtrude { sketch, .. }
|
||||
| K::SurfaceRevolve { sketch, .. }
|
||||
| K::Revolve { sketch, .. }
|
||||
| K::Emboss { sketch, .. }
|
||||
| K::Rib { sketch, .. }
|
||||
| K::SheetBase { sketch, .. }
|
||||
| K::SheetContour { sketch, .. } => vec![*sketch],
|
||||
K::Sweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
||||
K::Sweep { sketch, path_sketch, .. } | K::SurfaceSweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
||||
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
|
||||
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
|
||||
K::SurfaceLoft { sections } => sections.iter().map(|s| s.sketch).collect(),
|
||||
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
|
||||
K::Hole { points, .. } => points.iter().map(|p| p.sketch).collect(),
|
||||
K::Sketch { .. }
|
||||
@@ -1236,6 +1351,7 @@ impl FeatureKind {
|
||||
| K::Scale { .. }
|
||||
| K::OffsetFace { .. }
|
||||
| K::DeleteFace { .. }
|
||||
| K::SurfaceOffset { .. }
|
||||
| K::BoundingSolid { .. }
|
||||
| K::ReplaceFace { .. }
|
||||
| K::Align { .. }
|
||||
@@ -1254,6 +1370,8 @@ impl FeatureKind {
|
||||
| K::Thicken { .. }
|
||||
| K::SurfaceExtend { .. }
|
||||
| K::SurfaceTrim { .. }
|
||||
| K::Unstitch { .. }
|
||||
| K::ReverseNormal { .. }
|
||||
| K::Move { .. }
|
||||
| K::Import { .. }
|
||||
| K::MeshImport { .. }
|
||||
@@ -1313,6 +1431,7 @@ impl FeatureKind {
|
||||
FeatureKind::Scale { .. } => "ScaleFeature",
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFacesFeature",
|
||||
FeatureKind::DeleteFace { .. } => "DeleteFaceFeature",
|
||||
FeatureKind::SurfaceOffset { .. } => "OffsetSurfaceFeature",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
|
||||
FeatureKind::Pipe { .. } => "PipeFeature",
|
||||
FeatureKind::Emboss { .. } => "EmbossFeature",
|
||||
@@ -1337,10 +1456,16 @@ impl FeatureKind {
|
||||
FeatureKind::Align { .. } => "AlignFeature",
|
||||
FeatureKind::Remove { .. } => "RemoveFeature",
|
||||
FeatureKind::Patch { .. } => "PatchFeature",
|
||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoftFeature",
|
||||
FeatureKind::Stitch { .. } => "StitchFeature",
|
||||
FeatureKind::Thicken { .. } => "ThickenFeature",
|
||||
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
|
||||
FeatureKind::SurfaceExtend { .. } => "ExtendFeature",
|
||||
FeatureKind::Unstitch { .. } => "UnstitchFeature",
|
||||
FeatureKind::ReverseNormal { .. } => "ReverseNormalFeature",
|
||||
FeatureKind::Import { .. } => "BaseFeature",
|
||||
FeatureKind::MeshImport { .. } => "MeshFeature",
|
||||
FeatureKind::MeshConvert { .. } => "MeshConvertFeature",
|
||||
@@ -1379,6 +1504,7 @@ impl FeatureKind {
|
||||
FeatureKind::Scale { .. } => "Scale",
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFace",
|
||||
FeatureKind::DeleteFace { .. } => "DeleteFace",
|
||||
FeatureKind::SurfaceOffset { .. } => "OffsetSurface",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
|
||||
FeatureKind::Pipe { .. } => "Pipe",
|
||||
FeatureKind::Emboss { .. } => "Emboss",
|
||||
@@ -1403,10 +1529,16 @@ impl FeatureKind {
|
||||
FeatureKind::Align { .. } => "Align",
|
||||
FeatureKind::Remove { .. } => "Remove",
|
||||
FeatureKind::Patch { .. } => "Patch",
|
||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
|
||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoft",
|
||||
FeatureKind::Stitch { .. } => "Stitch",
|
||||
FeatureKind::Thicken { .. } => "Thicken",
|
||||
FeatureKind::SurfaceTrim { .. } => "Trim",
|
||||
FeatureKind::SurfaceExtend { .. } => "Extend",
|
||||
FeatureKind::Unstitch { .. } => "Unstitch",
|
||||
FeatureKind::ReverseNormal { .. } => "ReverseNormal",
|
||||
FeatureKind::Import { .. } => "Import",
|
||||
FeatureKind::MeshImport { .. } => "Mesh",
|
||||
FeatureKind::MeshConvert { .. } => "ConvertMesh",
|
||||
@@ -1423,8 +1555,14 @@ impl FeatureKind {
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
| FeatureKind::Emboss { sketch, profiles, .. }
|
||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sk), profiles, .. } => vec![(*sk, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
|
||||
vec![(*sk, profiles)]
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().filter_map(|s| Some((s.sketch, s.profiles.as_mut()?))).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1437,10 +1575,16 @@ impl FeatureKind {
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
| FeatureKind::Emboss { sketch, profiles, .. }
|
||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sketch), profiles, .. } => vec![(*sketch, profiles)],
|
||||
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
|
||||
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
|
||||
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
|
||||
vec![(*sketch, profiles)]
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } => {
|
||||
sections.iter().filter(|section| section.point.is_none()).map(|section| (section.sketch, §ion.profiles)).collect()
|
||||
}
|
||||
FeatureKind::SurfaceLoft { sections } => sections.iter().filter_map(|s| Some((s.sketch, s.profiles.as_ref()?))).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1451,6 +1595,7 @@ impl FeatureKind {
|
||||
| FeatureKind::Draft { faces, .. }
|
||||
| FeatureKind::OffsetFace { faces, .. }
|
||||
| FeatureKind::DeleteFace { faces, .. }
|
||||
| FeatureKind::SurfaceOffset { faces, .. }
|
||||
| FeatureKind::SplitFace { faces, .. }
|
||||
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
|
||||
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
|
||||
@@ -1460,6 +1605,8 @@ impl FeatureKind {
|
||||
FeatureKind::Thread { face, .. } => vec![face],
|
||||
FeatureKind::Emboss { face: Some(face), .. } => vec![&mut face.at],
|
||||
FeatureKind::Boss { position, .. } | FeatureKind::SnapFit { position, .. } | FeatureKind::Rest { position, .. } => vec![position],
|
||||
// With bend edges the fixed face is found by name (a plate keeps its point).
|
||||
FeatureKind::SheetConvert { face, bends, .. } if !bends.is_empty() => vec![face],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1483,7 +1630,7 @@ impl FeatureKind {
|
||||
| FeatureKind::ConstructionPlane { plane }
|
||||
| FeatureKind::Split { plane, .. }
|
||||
| FeatureKind::SplitFace { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::Extrude { extent, .. } => {
|
||||
FeatureKind::Extrude { extent, .. } | FeatureKind::SurfaceExtrude { extent, .. } => {
|
||||
v.push(&extent.distance);
|
||||
if let Some(d) = &extent.distance2 {
|
||||
v.push(d);
|
||||
@@ -1498,15 +1645,19 @@ impl FeatureKind {
|
||||
v.push(d);
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { angle, angle2, .. } => {
|
||||
FeatureKind::Revolve { angle, angle2, .. } | FeatureKind::SurfaceRevolve { angle, angle2, .. } => {
|
||||
v.push(angle);
|
||||
v.extend(angle2.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Fillet { radius, style, .. } => {
|
||||
v.push(radius);
|
||||
if let FilletStyle::Variable { radius2, mid, .. } = style {
|
||||
v.push(radius2);
|
||||
v.extend(mid.iter().map(|(_, r)| r.as_str()));
|
||||
match style {
|
||||
FilletStyle::Variable { radius2, mid, .. } => {
|
||||
v.push(radius2);
|
||||
v.extend(mid.iter().map(|(_, r)| r.as_str()));
|
||||
}
|
||||
FilletStyle::Spline { radii, .. } => v.extend(radii.iter().map(String::as_str)),
|
||||
FilletStyle::Constant | FilletStyle::Chord | FilletStyle::Curvature => {}
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
@@ -1542,7 +1693,7 @@ impl FeatureKind {
|
||||
},
|
||||
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::Shell { thickness, .. } => v.push(thickness),
|
||||
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } => {}
|
||||
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } | FeatureKind::SurfaceSweep { .. } | FeatureKind::SurfaceLoft { .. } => {}
|
||||
FeatureKind::Draft { angle, neutral, .. } => {
|
||||
v.push(angle);
|
||||
plane_exprs(neutral, &mut v);
|
||||
@@ -1620,11 +1771,18 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
|
||||
FeatureKind::Align { .. } | FeatureKind::Remove { .. } | FeatureKind::BoundaryFill { .. } | FeatureKind::DeleteFace { .. } => {}
|
||||
FeatureKind::SurfaceOffset { distance, tolerance, .. } => {
|
||||
v.push(distance);
|
||||
if let Some(t) = tolerance {
|
||||
v.push(t);
|
||||
}
|
||||
}
|
||||
FeatureKind::Stitch { tolerance, .. } => v.push(tolerance),
|
||||
FeatureKind::MeshConvert { tolerance, .. } => v.push(tolerance),
|
||||
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
|
||||
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::SurfaceExtend { distance, .. } => v.push(distance),
|
||||
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
|
||||
FeatureKind::Patch { .. } => {}
|
||||
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } | FeatureKind::BodyTransfer { .. } => {}
|
||||
}
|
||||
@@ -1681,6 +1839,14 @@ fn all_profiles() -> ProfileSel {
|
||||
ProfileSel::All
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_yes(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn zero_expr() -> String {
|
||||
"0".into()
|
||||
}
|
||||
@@ -1777,6 +1943,10 @@ pub struct Document {
|
||||
/// Browser groups (folders of bodies, sketches or construction planes).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub browser_groups: Vec<BrowserGroup>,
|
||||
/// Named folders used to organise features in the timeline. They do not affect evaluation
|
||||
/// order; feature ids remain the source of truth when features are renamed or reordered.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub timeline_groups: Vec<TimelineGroup>,
|
||||
/// Browser item order per component folder (`"<component>/<folder>"` → item keys).
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub browser_order: std::collections::BTreeMap<String, Vec<String>>,
|
||||
@@ -1843,6 +2013,66 @@ pub struct BrowserGroup {
|
||||
pub items: Vec<String>,
|
||||
}
|
||||
|
||||
/// A saved feature-history folder. Folders may nest, and a feature can belong to at most one folder.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TimelineGroup {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
/// The containing feature folder. `None` places the folder at the timeline root.
|
||||
#[serde(default)]
|
||||
pub parent: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub features: Vec<u64>,
|
||||
}
|
||||
|
||||
/// The first feature-history folder whose own and nested features do not form one
|
||||
/// uninterrupted run in the timeline.
|
||||
pub fn non_contiguous_timeline_group(features: &[Feature], groups: &[TimelineGroup]) -> Option<u64> {
|
||||
let positions: std::collections::BTreeMap<u64, usize> = features.iter().enumerate().map(|(index, feature)| (feature.id, index)).collect();
|
||||
let mut stats = std::collections::BTreeMap::new();
|
||||
let mut children = std::collections::BTreeMap::new();
|
||||
for group in groups {
|
||||
let mut count = 0usize;
|
||||
let mut first = usize::MAX;
|
||||
let mut last = 0usize;
|
||||
for index in group.features.iter().filter_map(|feature| positions.get(feature)) {
|
||||
count = count.saturating_add(1);
|
||||
first = first.min(*index);
|
||||
last = last.max(*index);
|
||||
}
|
||||
stats.insert(group.id, (count, first, last));
|
||||
children.entry(group.id).or_insert(0usize);
|
||||
if let Some(parent) = group.parent {
|
||||
let child_count = children.entry(parent).or_insert(0);
|
||||
*child_count = child_count.saturating_add(1);
|
||||
}
|
||||
}
|
||||
let parents: std::collections::BTreeMap<u64, Option<u64>> = groups.iter().map(|group| (group.id, group.parent)).collect();
|
||||
let mut pending: Vec<u64> = groups.iter().filter(|group| children.get(&group.id) == Some(&0)).map(|group| group.id).collect();
|
||||
while let Some(id) = pending.pop() {
|
||||
let (count, first, last) = stats.get(&id).copied().unwrap_or_default();
|
||||
if count > 0 && last.saturating_sub(first).saturating_add(1) != count {
|
||||
return Some(id);
|
||||
}
|
||||
if let Some(parent) = parents.get(&id).copied().flatten() {
|
||||
if let Some((parent_count, parent_first, parent_last)) = stats.get_mut(&parent) {
|
||||
*parent_count = parent_count.saturating_add(count);
|
||||
if count > 0 {
|
||||
*parent_first = (*parent_first).min(first);
|
||||
*parent_last = (*parent_last).max(last);
|
||||
}
|
||||
}
|
||||
if let Some(remaining) = children.get_mut(&parent) {
|
||||
*remaining = remaining.saturating_sub(1);
|
||||
if *remaining == 0 {
|
||||
pending.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn default_format() -> String {
|
||||
crate::format::format_string(crate::format::FORMAT)
|
||||
}
|
||||
@@ -1883,6 +2113,7 @@ impl Document {
|
||||
canvases: Vec::new(),
|
||||
param_comments: Default::default(),
|
||||
browser_groups: Vec::new(),
|
||||
timeline_groups: Vec::new(),
|
||||
browser_order: Default::default(),
|
||||
named_views: Vec::new(),
|
||||
visibility: Visibility::default(),
|
||||
@@ -2067,6 +2298,40 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// The deepest timeline folder spanning the insertion slot between `marker - 1` and
|
||||
/// `marker`. A slot at a folder's outer boundary is not inside that folder.
|
||||
fn timeline_group_at_insertion(&self, marker: usize) -> Option<u64> {
|
||||
let before = self.features.get(marker.checked_sub(1)?)?.id;
|
||||
let after = self.features.get(marker)?.id;
|
||||
let direct = |feature| self.timeline_groups.iter().find(|group| group.features.contains(&feature)).map(|group| group.id);
|
||||
let parents: std::collections::BTreeMap<u64, Option<u64>> = self.timeline_groups.iter().map(|group| (group.id, group.parent)).collect();
|
||||
|
||||
let mut before_ancestors = std::collections::BTreeSet::new();
|
||||
let mut current = direct(before);
|
||||
let mut steps = 0usize;
|
||||
while let Some(id) = current {
|
||||
if !before_ancestors.insert(id) || steps >= self.timeline_groups.len() {
|
||||
return None;
|
||||
}
|
||||
current = parents.get(&id).copied().flatten();
|
||||
steps = steps.saturating_add(1);
|
||||
}
|
||||
|
||||
current = direct(after);
|
||||
steps = 0;
|
||||
while let Some(id) = current {
|
||||
if before_ancestors.contains(&id) {
|
||||
return Some(id);
|
||||
}
|
||||
if steps >= self.timeline_groups.len() {
|
||||
return None;
|
||||
}
|
||||
current = parents.get(&id).copied().flatten();
|
||||
steps = steps.saturating_add(1);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Insert a feature at the marker (or the end) and return its id.
|
||||
pub fn add_feature(&mut self, kind: FeatureKind, name: Option<&str>) -> Result<u64> {
|
||||
if self.features.len() >= MAX_FEATURES {
|
||||
@@ -2098,6 +2363,7 @@ impl Document {
|
||||
shell_piece_order: false,
|
||||
kind,
|
||||
};
|
||||
let insertion_group = self.marker.and_then(|marker| self.timeline_group_at_insertion(marker));
|
||||
match self.marker {
|
||||
Some(m) if m < self.features.len() => {
|
||||
self.features.insert(m, f);
|
||||
@@ -2105,6 +2371,14 @@ impl Document {
|
||||
}
|
||||
_ => self.features.push(f),
|
||||
}
|
||||
if let Some(group_id) = insertion_group {
|
||||
let positions: std::collections::BTreeMap<u64, usize> =
|
||||
self.features.iter().enumerate().map(|(index, feature)| (feature.id, index)).collect();
|
||||
if let Some(group) = self.timeline_groups.iter_mut().find(|group| group.id == group_id) {
|
||||
group.features.push(id);
|
||||
group.features.sort_by_key(|feature| positions.get(feature).copied().unwrap_or(usize::MAX));
|
||||
}
|
||||
}
|
||||
self.name_feature_inputs(id);
|
||||
Ok(id)
|
||||
}
|
||||
@@ -2119,6 +2393,9 @@ impl Document {
|
||||
.rules
|
||||
.retain(|rule| !rule.references_features(&gone) && !rule.parameter_references().iter().any(|name| gone_params.contains(name)));
|
||||
self.features.retain(|f| !gone.contains(&f.id));
|
||||
for group in &mut self.timeline_groups {
|
||||
group.features.retain(|feature| !gone.contains(feature));
|
||||
}
|
||||
// Configuration columns of deleted features go with them.
|
||||
let keep: Vec<bool> = self
|
||||
.configs
|
||||
@@ -2402,45 +2679,9 @@ impl Document {
|
||||
}
|
||||
|
||||
/// Density of the default material (g/cm³), as Fusion's default.
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
("Default", DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
("Aluminum", 2.70),
|
||||
("Brass", 8.50),
|
||||
("Copper", 8.96),
|
||||
("Titanium", 4.43),
|
||||
("Cast Iron", 7.20),
|
||||
("ABS Plastic", 1.06),
|
||||
("PLA", 1.24),
|
||||
("Nylon", 1.14),
|
||||
("Polycarbonate", 1.20),
|
||||
("Oak", 0.75),
|
||||
("Glass", 2.50),
|
||||
];
|
||||
|
||||
/// How a physical material looks (sRGB); the default material has no colour of its own.
|
||||
pub fn material_color(name: &str) -> Option<[u8; 3]> {
|
||||
Some(match name {
|
||||
"Steel" => [158, 164, 172],
|
||||
"Stainless Steel" => [186, 191, 198],
|
||||
"Aluminum" => [204, 208, 214],
|
||||
"Brass" => [208, 172, 88],
|
||||
"Copper" => [204, 122, 80],
|
||||
"Titanium" => [150, 148, 158],
|
||||
"Cast Iron" => [104, 104, 110],
|
||||
"ABS Plastic" => [222, 222, 216],
|
||||
"PLA" => [110, 160, 222],
|
||||
"Nylon" => [236, 230, 214],
|
||||
"Polycarbonate" => [196, 216, 232],
|
||||
"Oak" => [190, 146, 92],
|
||||
"Glass" => [170, 208, 218],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
#[path = "../../../assets/materials/library.rs"]
|
||||
mod materials;
|
||||
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
|
||||
|
||||
/// A physical material defined in a design (`material.create`): its name, its density and,
|
||||
/// optionally, the look it gives bodies without an appearance of their own.
|
||||
|
||||
@@ -300,6 +300,17 @@ fn replace_sheet_body(mb: &mut ModelBody, body: Body) {
|
||||
*mb = next;
|
||||
}
|
||||
|
||||
/// Put a converted sheet in the place of the body it was made from.
|
||||
fn put_sheet(st: &mut ModelState, sh: sheet::SheetBody) -> Result<()> {
|
||||
let solid = sh.solid()?;
|
||||
if let Some(b) = st.bodies.iter_mut().find(|b| b.name == sh.body) {
|
||||
replace_sheet_body(b, solid);
|
||||
}
|
||||
st.sheets.retain(|s| s.body != sh.body);
|
||||
st.sheets.push(sh);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rebuild_sheet(st: &mut ModelState, idx: usize) -> Result<()> {
|
||||
let sheet = st.sheets.get(idx).cloned().ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
let solid = sheet.solid()?;
|
||||
@@ -334,26 +345,26 @@ fn on_sheet_material(st: &ModelState, i: usize, p: Vec3) -> bool {
|
||||
if mesh.triangles.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`).
|
||||
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`), and a quarter of the
|
||||
// thickness: on a large thin sheet the first steps from a point on its face clean through
|
||||
// it, so a point on the face would touch nothing (#685).
|
||||
let step = 2.0 * (mb.body.size() * 1e-3).max(1e-3);
|
||||
let offsets = [-step, 0.0, step];
|
||||
let thin = st.sheets.get(i).map_or(step, |sh| (sh.t * 0.25).min(step));
|
||||
let offsets = [-step, -thin, 0.0, thin, step];
|
||||
offsets.iter().any(|&dx| offsets.iter().any(|&dy| offsets.iter().any(|&dz| mesh.contains(p + Vec3::new(dx, dy, dz)))))
|
||||
}
|
||||
|
||||
/// A region mirrored in the sketch x axis (y → −y); curves other than lines and arcs as polylines.
|
||||
/// A region mirrored in the sketch x axis (y → −y), exactly: free-form curves through their
|
||||
/// control points.
|
||||
fn mirror_y(r: &Region2) -> Region2 {
|
||||
let f = |p: Vec2| Vec2::new(p.x, -p.y);
|
||||
let m = |l: &Loop2| {
|
||||
let segs: Vec<Seg2> = l
|
||||
.segs
|
||||
.iter()
|
||||
.flat_map(|s| match *s {
|
||||
Seg2::Line { a, b } => vec![Seg2::Line { a: f(a), b: f(b) }],
|
||||
Seg2::Arc { center, radius, start, sweep } => vec![Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep }],
|
||||
other => {
|
||||
let pts: Vec<Vec2> = (0..=16).map(|k| f(other.point_at(k as f64 / 16.0))).collect();
|
||||
pts.windows(2).map(|w| Seg2::Line { a: w[0], b: w[1] }).collect()
|
||||
}
|
||||
.map(|s| match *s {
|
||||
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep },
|
||||
other => sheet::map_controls(&other, f),
|
||||
})
|
||||
.collect();
|
||||
Loop2 { segs }.ccw()
|
||||
@@ -549,7 +560,34 @@ pub(super) fn sheet_eval(
|
||||
}
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetConvert { body, face, rule } => {
|
||||
// With bend edges: a solid or shelled body whose faces become the sheet.
|
||||
FeatureKind::SheetConvert { body, face, rule, bends } if !bends.is_empty() => {
|
||||
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let rv = doc.rule_values(vals, &r)?;
|
||||
// The bend edges by name first; a name that is gone keeps its point, with a warning.
|
||||
let mut picks = Vec::with_capacity(bends.len());
|
||||
for (i, p) in bends.iter().enumerate() {
|
||||
let name = f.edge_names.get(i).filter(|n| !n.is_empty());
|
||||
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, *p))) {
|
||||
Some((n, Ok(hit))) => {
|
||||
if let Some(note) = hit.note(n) {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
hit.point
|
||||
}
|
||||
Some((n, Err(_))) => {
|
||||
append_warning(warning, format!("edge `{n}` no longer exists; using the edge at the picked point"));
|
||||
*p
|
||||
}
|
||||
None => *p,
|
||||
});
|
||||
}
|
||||
let mut sh = sheet::convert_body(&mb, &r.name, &rv, *face, &picks)?;
|
||||
tag_flanges(&mut sh, 0, f.id);
|
||||
put_sheet(st, sh)
|
||||
}
|
||||
FeatureKind::SheetConvert { body, face, rule, .. } => {
|
||||
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
|
||||
let tol = (mb.body.size() * 1e-3).max(1e-3);
|
||||
@@ -558,30 +596,31 @@ pub(super) fn sheet_eval(
|
||||
let ds: Vec<f64> = mesh.positions.iter().map(|p| (*p - *face).dot(n)).collect();
|
||||
let (lo, hi) = (ds.iter().cloned().fold(f64::MAX, f64::min), ds.iter().cloned().fold(f64::MIN, f64::max));
|
||||
let t = hi - lo;
|
||||
// The face outline: boundary edges of the triangles on that face's plane.
|
||||
// The face outline: boundary edges of the triangles on that face's plane, where it runs
|
||||
// along a round wall an arc of the wall's circle (not chords: the plate check below
|
||||
// compares its area with the exact volume).
|
||||
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
|
||||
let mut loops = face_loops(&mesh, &plane, *face, n, lo, tol);
|
||||
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
|
||||
let mut walls = std::collections::HashMap::new();
|
||||
let mut wall = |f: u32| *walls.entry(f).or_insert_with(|| round_wall(&mb.body, &plane, n, f));
|
||||
let mut loops: Vec<Loop2> =
|
||||
face_outline(&mesh, &plane, *face, n, lo, tol).iter().map(|l| outline_loop(l, &mut wall, tol * 1e-2)).collect();
|
||||
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
|
||||
let mut it = loops.into_iter();
|
||||
let outer = Loop2::polygon(&it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?).ccw();
|
||||
let region = Region2 { outer, holes: it.map(|l| Loop2::polygon(&l).ccw().reversed()).collect() };
|
||||
let outer = it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?.ccw();
|
||||
let region = Region2 { outer, holes: it.map(|l| l.ccw().reversed()).collect() };
|
||||
let area = region.area().abs();
|
||||
let vol = kernel::measure(&mb.body)?.volume;
|
||||
if (area * t - vol).abs() > 1e-3 * vol.max(1.0) {
|
||||
return Err(DocError::Invalid("not supported yet: converting bodies that are not a plate of even thickness".into()));
|
||||
return Err(DocError::Invalid(
|
||||
"not supported yet: converting bodies that are not a plate of even thickness without bend edges (`bends`)".into(),
|
||||
));
|
||||
}
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let mut rv = doc.rule_values(vals, &r)?;
|
||||
rv.thickness = t;
|
||||
let mut sh = sheet::base_flange(body, &r.name, &rv, plane, Region2 { outer: region.outer.clone(), holes: Vec::new() });
|
||||
sh.holes = region.holes.iter().map(|h| Region2 { outer: h.ccw(), holes: Vec::new() }).collect();
|
||||
let solid = sh.solid()?;
|
||||
if let Some(b) = st.bodies.iter_mut().find(|b| &b.name == body) {
|
||||
replace_sheet_body(b, solid);
|
||||
}
|
||||
st.sheets.retain(|s| &s.body != body);
|
||||
st.sheets.push(sh);
|
||||
Ok(())
|
||||
put_sheet(st, sh)
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("{} is not a sheet metal feature", f.name))),
|
||||
}
|
||||
@@ -695,9 +734,17 @@ fn any_eligible_sheet(f: &Feature, st: &ModelState, targets: &[String]) -> bool
|
||||
|
||||
/// A Box or Cylinder cut (or intersect) whose tool is a straight prism along the normal of
|
||||
/// the sheet it meets: machined like the equivalent extrude of its footprint, so the sheet
|
||||
/// keeps it as a flat cut-out or pocket. Returns the sheet bodies it handled; none when the
|
||||
/// keeps it as a flat cut-out or pocket. A hole's drill (`drill`) may also be square to a
|
||||
/// folded panel of the sheet, a flange's. Returns the sheet bodies it handled; none when the
|
||||
/// primitive is not such a prism (the generic preflight then refuses real 3D machining).
|
||||
pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body, prism: PrimitivePrism, operation: Operation) -> Result<Vec<String>> {
|
||||
pub(super) fn sheet_primitive_cut(
|
||||
f: &Feature,
|
||||
st: &mut ModelState,
|
||||
tool: &Body,
|
||||
prism: PrimitivePrism,
|
||||
operation: Operation,
|
||||
drill: bool,
|
||||
) -> Result<Vec<String>> {
|
||||
if !matches!(operation, Operation::Cut | Operation::Intersect) || !any_eligible_sheet(f, st, &[]) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -707,7 +754,14 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
|
||||
if let Some(mb) = st.body(&sh.body)
|
||||
&& kernel::boolean(&mb.body, tool, kernel::BoolOp::Intersect)?.is_some()
|
||||
{
|
||||
normal = Some(sh.frame.normal());
|
||||
normal = Some(match prism {
|
||||
PrimitivePrism::Cylinder { axis, .. }
|
||||
if drill && !sh.flat && Plane::from_normal(Vec3::ZERO, axis).is_some_and(|p| !sh.parallel_panels(&p).is_empty()) =>
|
||||
{
|
||||
axis
|
||||
}
|
||||
_ => sh.frame.normal(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -717,7 +771,47 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
|
||||
sheet_prism_cut(f, st, &plane, std::slice::from_ref(®ion), operation, &tools, &[], None, false)
|
||||
}
|
||||
|
||||
/// A hole's straight drill along a panel of a sheet (into its rim), kept as a bore of each
|
||||
/// panel it meets (see [`crate::sheet::SheetBore`]), so the sheet still unfolds. Returns the
|
||||
/// sheet bodies it machined; None, with the model as it was, when the drill is not a plain
|
||||
/// cylinder or a sheet it meets can't keep it that way (it runs into a bend, or crosses a
|
||||
/// panel at an angle).
|
||||
pub(super) fn sheet_bore_cut(f: &Feature, st: &mut ModelState, prism: PrimitivePrism) -> Result<Option<Vec<String>>> {
|
||||
let PrimitivePrism::Cylinder { base, axis, radius, height } = prism else { return Ok(None) };
|
||||
let mut plans = Vec::new();
|
||||
for original in &st.sheets {
|
||||
if !sheet_eligible(f, st, &[], original) {
|
||||
continue;
|
||||
}
|
||||
let mut sh = original.clone();
|
||||
match sh.add_bore(base, axis, radius, height) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => continue,
|
||||
// Refused here, the hole keeps the message of the machining it was first tried as.
|
||||
Err(DocError::Invalid(_)) => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let body = sh.solid()?;
|
||||
plans.push((sh, body));
|
||||
}
|
||||
if plans.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut handled = Vec::new();
|
||||
for (sh, body) in plans {
|
||||
if let Some(mb) = st.bodies.iter_mut().find(|b| b.name == sh.body) {
|
||||
replace_sheet_body(mb, body);
|
||||
}
|
||||
handled.push(sh.body.clone());
|
||||
if let Some(slot) = st.sheets.iter_mut().find(|s| s.body == sh.body) {
|
||||
*slot = sh;
|
||||
}
|
||||
}
|
||||
Ok(Some(handled))
|
||||
}
|
||||
|
||||
/// A primitive tool's exact geometry, for [`sheet_primitive_cut`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum PrimitivePrism {
|
||||
/// An axis-aligned box between two corners.
|
||||
Box { lo: Vec3, hi: Vec3 },
|
||||
@@ -918,9 +1012,13 @@ fn sheet_prism_cut(
|
||||
}
|
||||
modeled = true;
|
||||
} else if z1 > z0 {
|
||||
// Each tool meets only the panel's cut-outs near it: arranging every one of a
|
||||
// perforation's holes against every new hole would grow with their square (#732).
|
||||
let near = sheet::NearHoles::new(&panel)?;
|
||||
for flat in mapped {
|
||||
let local = near.around(&flat)?;
|
||||
let clipped =
|
||||
sheet::material_boolean(std::slice::from_ref(&panel), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
||||
sheet::material_boolean(std::slice::from_ref(&local), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
||||
if !clipped.is_empty() && !depth_resolved {
|
||||
return Err(DocError::Invalid(
|
||||
"sheet machining depth is below the supported 0.000001 mm extrusion resolution; increase the layer depth".into(),
|
||||
@@ -978,50 +1076,78 @@ fn sheet_prism_cut(
|
||||
/// Outline loops (in `plane` coordinates) of the mesh triangles lying in the plane at distance
|
||||
/// `level` from `at` along `n`: the edges used by one of them only, chained.
|
||||
fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
let on: Vec<[u32; 3]> = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|t| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol)))
|
||||
.collect();
|
||||
face_outline(mesh, plane, at, n, level, tol).into_iter().map(|l| l.into_iter().map(|(p, _)| p).collect()).collect()
|
||||
}
|
||||
|
||||
/// [`face_loops`] with each point's B-rep face across the outline from it to the next point (the
|
||||
/// wall the outline runs along there): the face off the plane that meets both points.
|
||||
fn face_outline(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<(Vec2, Option<u32>)>> {
|
||||
let on_plane = |t: &[u32; 3]| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol));
|
||||
let on: Vec<[u32; 3]> = mesh.triangles.iter().copied().filter(on_plane).collect();
|
||||
// Boundary edges by position (meshes may repeat vertices per triangle).
|
||||
let pos = |k: u32| mesh.positions.get(k as usize).copied().unwrap_or_default();
|
||||
let q = |p: Vec3| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64, (p.z * 1e6).round() as i64);
|
||||
let key = |a: Vec3, b: Vec3| {
|
||||
let (ka, kb) = (q(a), q(b));
|
||||
(ka.min(kb), ka.max(kb))
|
||||
};
|
||||
let mut count: std::collections::HashMap<((i64, i64, i64), (i64, i64, i64)), i32> = Default::default();
|
||||
let edges: Vec<(Vec3, Vec3)> = on.iter().flat_map(|t| [(pos(t[0]), pos(t[1])), (pos(t[1]), pos(t[2])), (pos(t[2]), pos(t[0]))]).collect();
|
||||
for (a, b) in &edges {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
*count.entry((ka.min(kb), ka.max(kb))).or_insert(0) += 1;
|
||||
*count.entry(key(*a, *b)).or_insert(0) += 1;
|
||||
}
|
||||
let mut boundary: Vec<(Vec3, Vec3)> = edges
|
||||
.into_iter()
|
||||
.filter(|(a, b)| {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
count.get(&(ka.min(kb), ka.max(kb))) == Some(&1)
|
||||
})
|
||||
.collect();
|
||||
// The faces off the plane at each point on it, by position to a millionth of a millimetre
|
||||
// either way (a neighbouring face's mesh may round a shared point differently).
|
||||
let mut touch: std::collections::HashMap<(i64, i64, i64), Vec<u32>> = Default::default();
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face).filter(|(t, _)| !on_plane(t)) {
|
||||
for p in t.iter().map(|k| pos(*k)).filter(|p| ((*p - at).dot(n) - level).abs() < tol) {
|
||||
let faces = touch.entry(q(p)).or_default();
|
||||
if !faces.contains(f) {
|
||||
faces.push(*f);
|
||||
}
|
||||
}
|
||||
}
|
||||
let near = |p: Vec3| {
|
||||
let (x, y, z) = q(p);
|
||||
let cells = (-1..=1).flat_map(|i| (-1..=1).flat_map(move |j| (-1..=1).map(move |k| (x + i, y + j, z + k))));
|
||||
let mut faces: Vec<u32> = cells.filter_map(|c| touch.get(&c)).flatten().copied().collect();
|
||||
faces.sort_unstable();
|
||||
faces.dedup();
|
||||
faces
|
||||
};
|
||||
let across = |a: Vec3, b: Vec3| {
|
||||
let at_b = near(b);
|
||||
near(a).into_iter().find(|f| at_b.contains(f))
|
||||
};
|
||||
let mut boundary: Vec<(Vec3, Vec3)> = edges.into_iter().filter(|(a, b)| count.get(&key(*a, *b)) == Some(&1)).collect();
|
||||
// Chain into loops (plane coordinates).
|
||||
let mut loops: Vec<Vec<Vec2>> = Vec::new();
|
||||
let mut loops: Vec<Vec<(Vec2, Option<u32>)>> = Vec::new();
|
||||
for _ in 0..10_000 {
|
||||
let Some((s0, mut cur)) = boundary.pop() else { break };
|
||||
let mut lp = vec![s0];
|
||||
let mut lp = vec![(s0, across(s0, cur))];
|
||||
for _ in 0..boundary.len() + 1 {
|
||||
if q(cur) == q(s0) {
|
||||
break;
|
||||
}
|
||||
lp.push(cur);
|
||||
match boundary.iter().position(|(a, _)| q(*a) == q(cur)) {
|
||||
Some(k) => cur = boundary.remove(k).1,
|
||||
None => break,
|
||||
Some(k) => {
|
||||
let b = boundary.remove(k).1;
|
||||
lp.push((cur, across(cur, b)));
|
||||
cur = b;
|
||||
}
|
||||
None => {
|
||||
lp.push((cur, None));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let raw: Vec<Vec2> = lp.iter().map(|p| plane.to_local(*p)).collect();
|
||||
let raw: Vec<(Vec2, Option<u32>)> = lp.iter().map(|(p, f)| (plane.to_local(*p), *f)).collect();
|
||||
// Drop points in the middle of straight runs.
|
||||
let m = raw.len();
|
||||
let pts: Vec<Vec2> = (0..m)
|
||||
let pts: Vec<(Vec2, Option<u32>)> = (0..m)
|
||||
.filter_map(|i| {
|
||||
let (p, c, n) = (*raw.get((i + m - 1) % m)?, *raw.get(i)?, *raw.get((i + 1) % m)?);
|
||||
((c - p).cross(n - c).abs() > 1e-9 * (c - p).len().max(1e-9) * (n - c).len().max(1e-9)).then_some(c)
|
||||
let (p, c, n) = (raw.get((i + m - 1) % m)?.0, *raw.get(i)?, raw.get((i + 1) % m)?.0);
|
||||
((c.0 - p).cross(n - c.0).abs() > 1e-9 * (c.0 - p).len().max(1e-9) * (n - c.0).len().max(1e-9)).then_some(c)
|
||||
})
|
||||
.collect();
|
||||
if pts.len() >= 3 {
|
||||
@@ -1031,6 +1157,62 @@ fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, le
|
||||
loops
|
||||
}
|
||||
|
||||
/// The circle (centre in `plane` coordinates, radius) of face `f` when it is a round wall
|
||||
/// standing square on the plane: a cylinder about the plane's normal `n`.
|
||||
fn round_wall(body: &Body, plane: &Plane, n: Vec3, f: u32) -> Option<(Vec2, f64)> {
|
||||
match kernel::face_surface(body, f as usize)? {
|
||||
kernel::FaceSurface::Cylinder { origin, axis, radius } if axis.cross(n).len() < 1e-6 => Some((plane.to_local(origin), radius)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop of [`face_outline`] as segments: each run along one round wall (`wall` gives a face's
|
||||
/// circle) is an arc of that circle, the rest stays straight. Coaxial cylinders of one radius are
|
||||
/// one wall, however many faces it comes in (many systems write a round hole's wall as two
|
||||
/// half-cylinders), so such a hole is one circle. `eps`: how far off its circle a point may be.
|
||||
fn outline_loop(pts: &[(Vec2, Option<u32>)], wall: &mut impl FnMut(u32) -> Option<(Vec2, f64)>, eps: f64) -> Loop2 {
|
||||
use std::f64::consts::{PI, TAU};
|
||||
let m = pts.len();
|
||||
let point = |i: usize| pts.get(i % m).map(|x| x.0).unwrap_or_default();
|
||||
// The circle each segment runs along: its wall's, when both its ends lie on it.
|
||||
let circles: Vec<Option<(Vec2, f64)>> = (0..m)
|
||||
.map(|i| {
|
||||
let (c, r) = wall(pts.get(i)?.1?)?;
|
||||
((point(i).dist(c) - r).abs() <= eps && (point(i + 1).dist(c) - r).abs() <= eps).then_some((c, r))
|
||||
})
|
||||
.collect();
|
||||
let circle = |i: usize| circles.get(i % m).copied().flatten();
|
||||
let same = |i: usize, (c, r): (Vec2, f64)| circle(i).is_some_and(|(d, s)| d.dist(c) <= eps && (s - r).abs() <= eps);
|
||||
// Start where no arc runs on across the start (anywhere when the whole loop is one circle).
|
||||
let start = (0..m).find(|i| circle(*i + m - 1).is_none_or(|c| !same(*i, c))).unwrap_or(0);
|
||||
let mut segs = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < m {
|
||||
let k = start + i;
|
||||
let Some((c, r)) = circle(k) else {
|
||||
segs.push(Seg2::Line { a: point(k), b: point(k + 1) });
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
let mut j = i + 1;
|
||||
while j < m && same(start + j, (c, r)) {
|
||||
j += 1;
|
||||
}
|
||||
let sweep: f64 = (i..j)
|
||||
.map(|s| {
|
||||
let d = (point(start + s + 1) - c).angle() - (point(start + s) - c).angle();
|
||||
(d + PI).rem_euclid(TAU) - PI
|
||||
})
|
||||
.sum();
|
||||
// Arcs of up to half a turn each.
|
||||
let pieces = ((sweep.abs() / PI - 1e-9).ceil() as usize).clamp(1, 4);
|
||||
let (a0, step) = ((point(k) - c).angle(), sweep / pieces as f64);
|
||||
segs.extend((0..pieces).map(|p| Seg2::Arc { center: c, radius: r, start: a0 + step * p as f64, sweep: step }));
|
||||
i = j;
|
||||
}
|
||||
Loop2 { segs }
|
||||
}
|
||||
|
||||
/// The body a placed plastic feature joins: by name, else the one under the point.
|
||||
fn body_under(st: &ModelState, body: &Option<String>, p: Vec3) -> Result<String> {
|
||||
let i = body_at(st, body, &[p])?;
|
||||
@@ -1117,11 +1299,12 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let base = Plane::new(*position - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
|
||||
let r_in = r0 + delta * dr.tan();
|
||||
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, r_in), holes: Vec::new() };
|
||||
let mut tools = if dr.abs() > 1e-12 {
|
||||
let post = if dr.abs() > 1e-12 {
|
||||
vec![kernel::extrude_tapered(&base, &disc, h + delta, 1.0, -dr)?]
|
||||
} else {
|
||||
kernel::extrude(&base, &[disc], 0.0, h + delta)?
|
||||
};
|
||||
let mut tools: Vec<Traced> = post.into_iter().map(|b| crate::lineage::part_tool(b, "post")).collect();
|
||||
if let Some(n) = ribs {
|
||||
let n = val(vals, n, Kind::Unitless)?.round();
|
||||
if !(0.0..=64.0).contains(&n) {
|
||||
@@ -1156,19 +1339,19 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
]),
|
||||
holes: Vec::new(),
|
||||
};
|
||||
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?);
|
||||
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?.into_iter().map(|b| crate::lineage::part_tool(b, &format!("rib{k}"))));
|
||||
}
|
||||
}
|
||||
apply_op(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
|
||||
apply_traced(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
|
||||
// The root fillet: rounds the circle where the post meets the face.
|
||||
if rf > 1e-9 {
|
||||
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("boss body".into())) };
|
||||
let mut warn = None;
|
||||
let edges = resolve_edges(&mb.body, &[*position + x * r0], &[], &mut warn)?;
|
||||
let nb = kernel::fillet(&mb.body, &edges, rf)?;
|
||||
let nb = mb.traced().then(kernel::fillet(&mb.body, &edges, rf)?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, nb, mb.feature);
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
}
|
||||
}
|
||||
if let Some(hd) = hole_diameter {
|
||||
@@ -1182,7 +1365,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
}
|
||||
let hp = Plane::new(*position, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
|
||||
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
|
||||
apply_op(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
|
||||
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
|
||||
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1205,8 +1389,32 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let mut loops = face_loops(&mesh, &plane, *face, n, 0.0, tol);
|
||||
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
|
||||
let ccw = |l: &[Vec2]| if poly_area(l) < 0.0 { l.iter().rev().copied().collect::<Vec<_>>() } else { l.to_vec() };
|
||||
let (Some(outer), Some(inner)) = (loops.first().map(|l| ccw(l)), loops.get(1).map(|l| ccw(l))) else {
|
||||
return Err(DocError::Invalid("pick the rim face of a shelled body (a face with an inner and an outer edge)".into()));
|
||||
// The inner edge is an opening (the cavity under a rim): not the foot of a boss or a
|
||||
// wall standing on the face, whose side the band would run against.
|
||||
let opening = |l: &[Vec2]| {
|
||||
let eps = (tol * 4.0).max(1e-3);
|
||||
l.iter().zip(l.iter().cycle().skip(1)).filter_map(|(a, b)| Some(((*a + *b) * 0.5, (*b - *a).normalized()?))).take(3).any(|(m, t)| {
|
||||
// Just over the face, inside the loop (to the left of a ccw loop's edges).
|
||||
let q = plane.to_world(m + Vec2::new(-t.y, t.x) * eps) + n * eps;
|
||||
!mesh.contains(q)
|
||||
})
|
||||
};
|
||||
let outer = loops.first().map(|l| ccw(l));
|
||||
let openings: Vec<Vec<Vec2>> = loops.iter().skip(1).map(|l| ccw(l)).filter(|l| opening(l)).collect();
|
||||
// A rim runs round one opening, the cavity; smaller ones (a screw hole) may sit in it.
|
||||
// Openings alike in size (a plate's holes) leave no edge that is the rim's.
|
||||
let alike = match openings.as_slice() {
|
||||
[a, b, ..] => poly_area(b).abs() > 0.5 * poly_area(a).abs(),
|
||||
_ => false,
|
||||
};
|
||||
let (Some(outer), Some(inner), false) = (outer, openings.first().cloned(), alike) else {
|
||||
let what = match (loops.len(), openings.len()) {
|
||||
(0 | 1, _) => "pick the rim face of a shelled body (a face with an inner and an outer edge)".to_string(),
|
||||
(_, 0) => "pick the rim face of a shelled body: this face's inner edges are where other features stand on it, not an opening"
|
||||
.to_string(),
|
||||
(_, k) => format!("pick the rim face of a shelled body (round one opening): this face has {k} openings alike in size"),
|
||||
};
|
||||
return Err(DocError::Invalid(what));
|
||||
};
|
||||
let bw = if *groove { w + g } else { w };
|
||||
// A lip: the band raised from the rim's own faces (its walls continue the walls
|
||||
@@ -1215,7 +1423,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
&& let Ok(b) = kernel::raise_band(&mb.body, *face, w, h, *outside)
|
||||
&& let Some(slot) = st.bodies.iter_mut().find(|x| x.name == target)
|
||||
{
|
||||
*slot = ModelBody::new(target.clone(), b, slot.feature);
|
||||
*slot = ModelBody::traced_new(target.clone(), Traced::step(b, &[&mb.traced()]), slot.feature);
|
||||
return Ok(());
|
||||
}
|
||||
// The band along the chosen edge, inside the rim face. A groove's cutter reaches past
|
||||
@@ -1234,11 +1442,11 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
};
|
||||
let delta = (h * 0.05).clamp(1e-3, 0.5);
|
||||
if *groove {
|
||||
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?;
|
||||
apply_op(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
|
||||
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?.into_iter().map(|b| crate::lineage::part_tool(b, "groove")).collect();
|
||||
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
|
||||
} else {
|
||||
let tool = kernel::extrude(&plane, &[band], -delta, h)?;
|
||||
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
let tool = kernel::extrude(&plane, &[band], -delta, h)?.into_iter().map(|b| crate::lineage::part_tool(b, "lip")).collect();
|
||||
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
}
|
||||
}
|
||||
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
|
||||
@@ -1261,7 +1469,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let sgn = if pl.normal().dot(y) >= 0.0 { 1.0 } else { -1.0 };
|
||||
let (lo, hi) = if sgn > 0.0 { (0.0, w) } else { (-w, 0.0) };
|
||||
let tool = kernel::extrude(&pl, &[Region2 { outer: lp, holes: Vec::new() }], lo, hi)?;
|
||||
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "arm")).collect();
|
||||
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
}
|
||||
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
|
||||
let target = body_under(st, body, *position)?;
|
||||
@@ -1310,7 +1519,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
} else {
|
||||
kernel::extrude(&base, &[region], 0.0, h + delta)?
|
||||
};
|
||||
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "rest")).collect();
|
||||
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("{} is not a plastic feature", f.name))),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Evaluation of the surface features: Patch, Stitch, Thicken and Trim (kernel `surfaces`).
|
||||
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
|
||||
//! Revolve, Sweep and Loft, Offset, Unstitch and Reverse Normal (kernel `surfaces`,
|
||||
//! `surface_offset`).
|
||||
|
||||
use super::*;
|
||||
use solidcraft_geom::Seg2;
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
match &f.kind {
|
||||
@@ -84,6 +87,225 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
|
||||
return Err(DocError::Invalid("not supported yet: a surface extrude through all, to or from an object, or with a taper".into()));
|
||||
}
|
||||
let d = val(vals, &extent.distance, Kind::Length)?;
|
||||
let off = extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
|
||||
let (a, b) = extent_sides(vals, extent, d, extent.direction)?;
|
||||
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
|
||||
let body = kernel::extrude_sheet(&ss.plane, &chains, a + off, b + off)?;
|
||||
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
let (start, sweep) = match (angle2, symmetric) {
|
||||
(Some(a2), _) => {
|
||||
let a2 = val(vals, a2, Kind::Angle)?;
|
||||
(-a2, a + a2)
|
||||
}
|
||||
(None, true) => (-a / 2.0, a),
|
||||
(None, false) => (0.0, a),
|
||||
};
|
||||
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
|
||||
let body = kernel::revolve_sheet(&ss.plane, &chains, o, d, start, sweep)?;
|
||||
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Unstitch { body, faces } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
|
||||
let source = mb.traced();
|
||||
// The faces left keep the body's name; each face taken out is a new surface body.
|
||||
match rest {
|
||||
Some(rest) => {
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), source.then(rest), mb.feature);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
st.bodies.remove(i);
|
||||
}
|
||||
}
|
||||
for (k, piece) in pieces.into_iter().enumerate() {
|
||||
let name = new_name(st, f, k);
|
||||
st.bodies.push(ModelBody::traced_new(name, source.then(piece), f.id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::ReverseNormal { body, faces } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
|
||||
let ps =
|
||||
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
|
||||
let profile;
|
||||
let ss = if !ps.sketch.wires.is_empty() {
|
||||
profile = profile_in_frame(doc, ss, f.component)?;
|
||||
&profile
|
||||
} else {
|
||||
ss
|
||||
};
|
||||
let mapped = path_in_frame(doc, ps, f.component)?;
|
||||
let ps = mapped.as_ref();
|
||||
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
|
||||
// The path runs on from the end nearest the middle of the curves.
|
||||
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
|
||||
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
|
||||
let body = match path_points(ps, path, start)? {
|
||||
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
|
||||
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
|
||||
};
|
||||
let traced = crate::lineage::ChainRoles { sketch: ss, chains: &chains }.tool(body);
|
||||
push_sheet(st, f, traced);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceLoft { sections } => {
|
||||
let mut secs = Vec::with_capacity(sections.len());
|
||||
let mut sketches = Vec::with_capacity(sections.len());
|
||||
for sec in sections {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(sec.sketch)
|
||||
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?,
|
||||
f.component,
|
||||
)?;
|
||||
let profiles = match (&sec.profiles, sec.curves.is_empty()) {
|
||||
(None, true) => Some(&ProfileSel::All),
|
||||
(p, _) => p.as_ref(),
|
||||
};
|
||||
let mut chains = sheet_chains(&ss, profiles, &sec.curves)?.into_iter();
|
||||
let (Some(chain), None) = (chains.next(), chains.next()) else {
|
||||
return Err(DocError::Invalid(
|
||||
"each loft section must be one chain: one profile without holes, or curves meeting end to end".into(),
|
||||
));
|
||||
};
|
||||
secs.push((ss.plane, chain));
|
||||
sketches.push(ss);
|
||||
}
|
||||
let body = kernel::loft_sheet(&secs)?;
|
||||
// Each wall named by the section curves it runs between, as a loft's sides are.
|
||||
let loops: Vec<solidcraft_geom::Loop2> = secs.iter().map(|(_, c)| solidcraft_geom::Loop2 { segs: c.clone() }).collect();
|
||||
let traced = crate::lineage::SectionRoles { sections: sketches.iter().zip(&loops).collect() }.tool(body);
|
||||
push_sheet(st, f, traced);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceOffset { faces, body, distance, chain, tolerance } => {
|
||||
let d = val(vals, distance, Kind::Length)?;
|
||||
let tol = match tolerance {
|
||||
Some(t) => val(vals, t, Kind::Length)?,
|
||||
None => 0.01,
|
||||
};
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let picks = if *chain { smooth_chain(&mb, faces) } else { faces.clone() };
|
||||
let mut picked: Vec<usize> = Vec::new();
|
||||
for p in &picks {
|
||||
let fi =
|
||||
crate::appearance::face_index_at(&mb, *p).ok_or_else(|| DocError::Invalid(format!("Offset: no face of {} at {p:?}", mb.name)))?;
|
||||
if !picked.contains(&fi) {
|
||||
picked.push(fi);
|
||||
}
|
||||
}
|
||||
let made = Traced::step(kernel::offset_surface(&mb.body, &picked, d, tol)?, &[&mb.traced()]);
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::traced_new(name, made, f.id));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DocError::Invalid("not a surface feature".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface builder's body, added to the model with what its faces came from.
|
||||
fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced) {
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
|
||||
}
|
||||
|
||||
/// What a surface builder sweeps: the boundary loops of the picked profiles, then the picked
|
||||
/// curves chained end to end.
|
||||
fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
let mut chains: Vec<Vec<Seg2>> = Vec::new();
|
||||
if let Some(sel) = profiles {
|
||||
for r in solidcraft_sketch::merge_regions(&select_profiles(ss, sel)?) {
|
||||
chains.extend(std::iter::once(r.outer).chain(r.holes).map(|lp| lp.segs));
|
||||
}
|
||||
}
|
||||
chains.extend(curve_chains(ss, curves)?);
|
||||
if chains.is_empty() {
|
||||
return Err(DocError::Invalid("pick the profiles or curves".into()));
|
||||
}
|
||||
Ok(chains)
|
||||
}
|
||||
|
||||
/// Most sketch curves one feature chains.
|
||||
const MAX_CHAIN_CURVES: usize = 1000;
|
||||
|
||||
/// The sketch curves `ids` (free-form ones exactly where they can be) grouped into chains of segments that meet end to end, each oriented
|
||||
/// to run one way. Curves are joined greedily at their free ends: where three meet, one of them
|
||||
/// starts a chain of its own.
|
||||
fn curve_chains(ss: &SolvedSketch, ids: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
if ids.len() > MAX_CHAIN_CURVES {
|
||||
return Err(DocError::Invalid(format!("at most {MAX_CHAIN_CURVES} curves")));
|
||||
}
|
||||
let mut pieces: std::collections::VecDeque<Vec<Seg2>> = ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
|
||||
Ok(ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci)))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect();
|
||||
let reversed = |p: Vec<Seg2>| -> Vec<Seg2> { p.iter().rev().map(Seg2::reversed).collect() };
|
||||
let near = |p: Vec2, q: Vec2| p.dist(q) <= 1e-6;
|
||||
let mut chains = Vec::new();
|
||||
while let Some(mut chain) = pieces.pop_front() {
|
||||
while let (Some(head), Some(tail)) = (chain.first().map(Seg2::start), chain.last().map(Seg2::end)) {
|
||||
if near(head, tail) {
|
||||
break;
|
||||
}
|
||||
let ends = |p: &Vec<Seg2>| p.first().map(Seg2::start).zip(p.last().map(Seg2::end));
|
||||
let Some(i) = pieces.iter().position(|p| ends(p).is_some_and(|(s, e)| near(s, tail) || near(e, tail) || near(s, head) || near(e, head)))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let Some(p) = pieces.remove(i) else { break };
|
||||
let Some((s, e)) = ends(&p) else { continue };
|
||||
if near(s, tail) {
|
||||
chain.extend(p);
|
||||
} else if near(e, tail) {
|
||||
chain.extend(reversed(p));
|
||||
} else if near(e, head) {
|
||||
chain = p.into_iter().chain(chain).collect();
|
||||
} else {
|
||||
chain = reversed(p).into_iter().chain(chain).collect();
|
||||
}
|
||||
}
|
||||
chains.push(chain);
|
||||
}
|
||||
Ok(chains)
|
||||
}
|
||||
@@ -350,6 +350,17 @@ pub fn is_literal(s: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the expression a bare number without a unit, like `20` or `-1.5`?
|
||||
pub fn is_bare_number(s: &str) -> bool {
|
||||
is_literal(s) && s.trim().trim_start_matches(['-', '+']).trim().parse::<f64>().is_ok()
|
||||
}
|
||||
|
||||
/// The length unit of a design with these units: `mm` unless they name another length unit.
|
||||
pub fn length_unit_or_mm(units: &str) -> &str {
|
||||
let u = units.trim();
|
||||
if unit_info(u).is_some_and(|(k, _)| k == Kind::Length) { u } else { "mm" }
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
toks: Vec<Tok>,
|
||||
pos: usize,
|
||||
@@ -942,5 +953,7 @@ mod tests {
|
||||
assert_eq!(rename_reference("10 mm", "mm", "x"), "10 mm");
|
||||
assert!(is_literal("20") && is_literal("-1.5 in") && is_literal("3 deg"));
|
||||
assert!(!is_literal("d1") && !is_literal("2 * 3") && !is_literal("2 width"));
|
||||
assert!(is_bare_number(" -1.5 ") && is_bare_number("+2") && !is_bare_number("20 mm") && !is_bare_number("(2)"));
|
||||
assert_eq!((length_unit_or_mm(" in "), length_unit_or_mm("deg"), length_unit_or_mm("")), ("in", "mm", "mm"));
|
||||
}
|
||||
}
|
||||
@@ -73,13 +73,78 @@
|
||||
//! certificate: the target's `name`, e.g. a cut wall `F5:side:l1` or a pattern copy
|
||||
//! `F4:top@F6.2`), where no construction certificate names it alone. Anchors with
|
||||
//! construction certificates resolve as before.
|
||||
//! - 17: a `lineage` face name can be a piece's own, qualified by the faces around it
|
||||
//! (`F2:top#[F2:side:l4,F4:side:l4]`), where Offset Face moved one piece of a face off the
|
||||
//! others' plane; never a numbered piece. Sheet face names can also name a rim, the sheet's
|
||||
//! thickness along a panel's straight edge (`sheet:<body>:<panel>:rim.<edge>`, the edge a base
|
||||
//! sketch line's id, `edge<k>`, or a flange's `far`, `side0` or `side1`), or a flat end of a
|
||||
//! bend (`rim.bend0`, `rim.bend1`). Version 16 designs need no transformation; the version
|
||||
//! stamp makes 0.8.0 (which reads up to 16) ask for an update instead of meeting names it
|
||||
//! can't resolve.
|
||||
//! Convert to Sheet Metal can take bend edges (`bends` in the feature: points on the sharp
|
||||
//! edges of a solid or shelled body that become bends). A conversion without `bends` is the
|
||||
//! plate conversion it always was and saves as before; 0.8.0 would drop the bend edges and
|
||||
//! refuse the body as no plate.
|
||||
//! A sketch's link to a projected edge can also keep the edge's persistent name (`edge_name`,
|
||||
//! beside `face_name`) and is then found by it, wherever the edge went. A link without it is
|
||||
//! the edge nearest its point, as it always was, and takes the name when its sketch is next
|
||||
//! edited.
|
||||
//! A fillet's `fit` can be `taper` (the radius follows the room along each edge it doesn't
|
||||
//! fit). Fillets with `exact` or `reduce` save as before; 0.8.0 would refuse the unknown fit.
|
||||
//! Feature-history folders are also first saved in 17 (`timeline_groups`: stable folder id,
|
||||
//! name, optional parent folder and feature ids). Version 16 designs have no saved timeline
|
||||
//! folders and open with the empty default.
|
||||
//! A fillet's `type` can be `spline` (the radius along each edge a B-spline: `start`, `knots`,
|
||||
//! and the control radii after `radius` in `radii`). Other fillets save as before; 0.8.0 would
|
||||
//! refuse the unknown type.
|
||||
//! Under the same version, a sheet metal rule's corner relief shape can also be `tear` (no
|
||||
//! relief: each bend is cut straight from the bends' corner to where its flange's end
|
||||
//! starts); a rule that says `notch`, `round` or `rectangular` builds what it did.
|
||||
//! A sketch's view options can also say `hide_construction` (the Sketch Palette's
|
||||
//! Construction Geometries); a sketch that shows its construction geometry saves as before.
|
||||
//! A `SurfaceExtrude` feature (Surface Extrude: `sketch`, optional `profiles`, `curves` and
|
||||
//! `extent`, walls only) is first saved in 17; 0.8.0 would refuse the unknown feature.
|
||||
//! - 18: surface features 0.9.0 doesn't know: `Unstitch` (a body's faces taken out as surface
|
||||
//! bodies) and `ReverseNormal` (a surface body's faces turned over), each a `body` and
|
||||
//! optional `faces` points. Version 17 designs need no transformation; the version stamp makes
|
||||
//! 0.9.0 (which reads up to 17) ask for an update instead of meeting a feature it can't read.
|
||||
//! Under the same version, a sketch text's link source can carry a `style`: `bold` and
|
||||
//! `italic` (the font's bold or italic face), `align` (`left`, `center` or `right`),
|
||||
//! `letter_spacing` (mm), `line_spacing` (times the font's line distance), `path` (the id of a
|
||||
//! curve of the same sketch the baseline follows) and `reverse`. A text without `style` is set
|
||||
//! as it always was; 0.9.0 would drop the style and set the text plain, off its curve.
|
||||
//! Under the same version, Delete Face can leave the gap open (`heal: false` on the feature:
|
||||
//! the faces left are a surface body); a Delete Face without it heals, as it always did, and
|
||||
//! saves as before. 0.9.0 would heal a gap the design leaves open.
|
||||
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
|
||||
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
|
||||
//! A `SurfaceSweep` feature (Surface Sweep: `sketch`, optional `profiles` and `curves`,
|
||||
//! `path_sketch`, `path`) is also first saved in 18.
|
||||
//! A `SurfaceLoft` feature (Surface Loft: `sections`, each a `sketch` with optional `profiles`
|
||||
//! and `curves`) is also first saved in 18.
|
||||
//! A `SurfaceOffset` feature (Offset Surface: `faces` points, optional `body`, `distance`,
|
||||
//! `chain` and `tolerance`) is also first saved in 18.
|
||||
//! Nothing new is saved for holes, but a hole on a face is now drilled into the material: a
|
||||
//! saved `direction` that leaves the face the hole starts on (designs up to 17 could save one,
|
||||
//! taken from another face whose plane passed near the point, such as the far side of a thin
|
||||
//! plate or a face of another body at the same height) is mirrored into that face when the
|
||||
//! design is evaluated. Holes whose direction enters their face build as before.
|
||||
//! Holes at sketch points can be flipped (`flip: true` in the hole's `points`): they drill away
|
||||
//! from the side they would go into by themselves. Holes without it save as before; 0.9.0 would
|
||||
//! drop the flip and drill the other way.
|
||||
|
||||
//! - 19: editable associative offset-distance dimensions. Older offsets retain their saved
|
||||
//! fixed distances; migration does not invent dimensions or change geometry.
|
||||
//! Under the same version, a fillet's style can be `curvature` (a G2 quintic blend; `radius`
|
||||
//! is the equivalent circular setback). Fillets of the other styles save as before; 0.9.0
|
||||
//! would refuse the unknown style.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The format this build writes.
|
||||
pub const FORMAT: u32 = 16;
|
||||
pub const FORMAT: u32 = 19;
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solidcraft/{n}")
|
||||
@@ -96,7 +161,7 @@ pub fn version_of(s: &str) -> Option<u32> {
|
||||
|
||||
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
|
||||
type JsonStep = fn(&mut Value) -> Result<()>;
|
||||
const JSON_STEPS: [JsonStep; 15] = [
|
||||
const JSON_STEPS: [JsonStep; 18] = [
|
||||
|_| Ok(()),
|
||||
v2_to_v3,
|
||||
v3_to_v4,
|
||||
@@ -112,6 +177,9 @@ const JSON_STEPS: [JsonStep; 15] = [
|
||||
v13_to_v14,
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
];
|
||||
|
||||
/// Stored face and edge names were worked out from geometry before format 14; they get the
|
||||
@@ -345,9 +413,43 @@ pub fn read(s: &str) -> Result<Document> {
|
||||
if d.features.len() > crate::document::MAX_FEATURES
|
||||
|| d.params.len() > crate::document::MAX_PARAMS
|
||||
|| d.components.len() > crate::document::MAX_FEATURES
|
||||
|| d.timeline_groups.len() > crate::document::MAX_FEATURES
|
||||
|| d.timeline_groups.iter().any(|group| group.features.len() > crate::document::MAX_FEATURES)
|
||||
{
|
||||
return Err(DocError::Invalid("document too large".into()));
|
||||
}
|
||||
let feature_ids: std::collections::BTreeSet<_> = d.features.iter().map(|feature| feature.id).collect();
|
||||
let mut group_ids = std::collections::BTreeSet::new();
|
||||
let mut grouped_features = std::collections::BTreeSet::new();
|
||||
for group in &d.timeline_groups {
|
||||
if group.name.trim().is_empty() || group.name.len() > 128 || !group_ids.insert(group.id) {
|
||||
return Err(DocError::Invalid("damaged design: invalid timeline folder".into()));
|
||||
}
|
||||
if group.parent.is_some_and(|parent| parent == group.id) {
|
||||
return Err(DocError::Invalid("damaged design: timeline folder cannot contain itself".into()));
|
||||
}
|
||||
if group.features.iter().any(|feature| !feature_ids.contains(feature) || !grouped_features.insert(*feature)) {
|
||||
return Err(DocError::Invalid("damaged design: invalid feature in a timeline folder".into()));
|
||||
}
|
||||
}
|
||||
for group in &d.timeline_groups {
|
||||
if let Some(parent) = group.parent {
|
||||
if !group_ids.contains(&parent) {
|
||||
return Err(DocError::Invalid("damaged design: timeline folder has an unknown parent".into()));
|
||||
}
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut current = Some(parent);
|
||||
while let Some(id) = current {
|
||||
if !seen.insert(id) {
|
||||
return Err(DocError::Invalid("damaged design: cyclic timeline folders".into()));
|
||||
}
|
||||
current = d.timeline_groups.iter().find(|candidate| candidate.id == id).and_then(|candidate| candidate.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(id) = crate::non_contiguous_timeline_group(&d.features, &d.timeline_groups) {
|
||||
return Err(DocError::Invalid(format!("damaged design: timeline folder {id} is not contiguous")));
|
||||
}
|
||||
for o in &d.occurrences {
|
||||
if !rigid(&o.transform) {
|
||||
return Err(DocError::Invalid(format!("damaged design: occurrence `{}` has an invalid placement", o.name)));
|
||||
|
||||