Compare commits

..

44 Commits

Author SHA1 Message Date
bherbruck b5728f284b Release 0.24.1 2026-10-27 08:34:27 -04:00
bherbruck 5965439a15 Release 0.24.0 2026-10-26 19:23:21 -04:00
bherbruck 586a358ed4 Release 0.23.1 2026-10-26 07:07:21 -04:00
bherbruck 7770f8982d Release 0.23.0 2026-10-25 18:18:39 -04:00
bherbruck 72ad6383ef Release 0.22.1 2026-10-25 10:38:39 -04:00
bherbruck 0537623a75 Release 0.22.0 2026-10-24 18:53:43 -04:00
bherbruck 65d288227e Release 0.21.0 (rc.2's code, version only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:51:08 -04:00
bherbruck fedb3bcdb1 Release 0.21.0-rc.2 2026-10-23 17:14:38 -04:00
bherbruck 097b8b7550 Release 0.21.0-rc.1
The tree is main at f120285c9 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:48:05 -04:00
bherbruck af7ca3a8a2 Release 0.21.0-rc.1
The tree is main at b31fddb98 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:37:43 -04:00
bherbruck 27ddb725c7 Release 0.21.0-rc.1
The tree is main at 2bf3d4e11 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 12:10:02 -04:00
bherbruck a06bdf5cfc Release 0.20.0
The tree is main at 030187452 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-22 18:34:40 -04:00
bherbruck 33fdcdb3a0 Release 0.19.0
The tree is main at 5899e5b6c exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 21:13:33 -04:00
bherbruck c07ab5f8f9 Release 0.18.4
The tree is main at 728f753b8 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 12:26:34 -04:00
bherbruck fea32f8aac Release 0.18.3
The tree is main at 034aa60d4 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 08:46:37 -04:00
bherbruck 4927214976 Release 0.18.2
The tree is main at 57bcc4083 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 22:09:18 -04:00
bherbruck b621819c0c Release 0.18.1
The tree is main at 522ae7755 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 17:39:11 -04:00
bherbruck ec552a5300 Release 0.18.0
The tree is main at c3151fb7f exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 16:28:59 -04:00
bherbruck e0cc740cc3 Release 0.17.0
The tree is main at 511faedcf exactly; the 0.16.x release commits are kept as a parent so release only moves forward.
2026-10-20 11:41:48 -04:00
bherbruck 5de48e916d Release 0.16.2 2026-10-19 00:39:30 +00:00
bherbruck 80af7bbe3e Release 0.16.2
Version 0.16.2: 0.16.1 with the rigid orbit fix (#2753). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:38:53 +00:00
bherbruck 191e983423 Fix orbit about the picked point (regression in 0.16.0) (#2753)
Orbiting about the point under the cursor (or the origin) turned the camera
about its target and then panned to put the pivot back. The pan works at the
target's depth with the parallel-view scale, so in a perspective view the
pivot slid off its pixel and the model swung about the camera.

The camera now turns rigidly about the pivot: Camera::orbit_about keeps the
pivot's coordinates in the camera's frame and rebuilds the target from the
new orientation, so the pivot stays exactly on its pixel in both projections.
The Shift snap spring and view.navigate's pivot orbit use the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e88779218)
2026-10-19 00:38:52 +00:00
bherbruck 2b9445b049 Release 0.16.1 2026-10-18 20:16:21 +00:00
bherbruck 7ddf48ca16 Release 0.16.1
Version 0.16.1: 0.16.0 with the orbit fix (reverts #2142). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:04 +00:00
bherbruck d2f6848377 Revert #2142: orbit about the picked point like 0.15.0
#2142 made orbiting about the point under the cursor turn the camera in place instead of turning the part about that point. Restore the 0.15.0 behaviour.

This reverts commit cac4bccea8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:00 +00:00
bherbruck 0b6d6fecd8 Release 0.16.0 2026-10-18 18:16:11 +00:00
bherbruck fa691707c2 Release 0.15.0 2026-10-17 13:54:24 +00:00
bherbruck b9a55c74e2 Release 0.15.0
Version 0.15.0; the pending design-format changes are folded into format 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:53:44 +00:00
bherbruck 2bb9c81069 Release 0.14.0 2026-10-16 09:19:14 +00:00
bherbruck cedb51b8ea Release 0.14.0
Version 0.14.0; the pending design-format changes are folded into format 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:19:10 +00:00
bherbruck 66d13059dd Release 0.13.1 2026-10-15 21:56:43 +00:00
bherbruck 800ea40c0d Release 0.13.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:56:35 +00:00
bherbruck 20442eecd3 Agent chat on Windows: start npm agents through their .cmd shim
npm puts a Unix shell script with the bare name (codex-acp,
claude-code-acp, gemini) beside the .cmd shim. The lookup tried the bare
name first, so every npm-installed agent failed with "not a valid Win32
application (os error 193)". Windows now only considers .exe/.cmd/.bat,
and a command saved with the bare path runs the shim beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 60ea8c69d2)
2026-10-15 21:51:01 +00:00
bherbruck 22d1cd7032 Release 0.13.0 2026-10-15 20:34:13 +00:00
bherbruck 6a0dc18655 Fix the Windows 32-bit and macOS release builds
On 32-bit Windows SetWindowLongPtrW is SetWindowLongW, which takes and
returns i32, so the window-procedure hook didn't compile for i686.
The macOS app bundle now signs the bundled solidcraft-cli before the
bundle itself; codesign refuses a bundle with an unsigned helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94e76cd97e)
2026-10-15 20:34:12 +00:00
bherbruck e006dbf1af Release 0.13.0
The tree is the 0.13.0 version commit (8241a750fa) on main exactly; the previous release is kept as a parent so release only moves forward.
2026-10-15 19:59:50 +00:00
bherbruck 8241a750fa Release 0.13.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:59:42 +00:00
bherbruck 41779cf7bb Release 0.12.0: release branch follows main again after the 0.11.1 patch
The tree is main at c36a6f9f exactly; the 0.11.1 patch commit is kept as a parent so release only moves forward.
2026-10-15 13:32:01 +00:00
bherbruck b01b9689c8 Release 0.11.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:41:41 +00:00
bherbruck 4426da6f5d Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU (#1190)
* Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU

wgpu started Vulkan, D3D12 and OpenGL together and preferred Vulkan, so
every Windows start loaded all installed Vulkan drivers. An old Intel
Vulkan driver (igvk64.dll) crashed 0.10.1 at startup inside the driver.

The app now probes D3D12, then Vulkan, then OpenGL, one backend at a
time, and starts only the first with a hardware adapter (WARP when none
has one). WGPU_BACKEND still wins; graphics.json's "backend" sets the
order by hand; the software renderer (safe mode, the retry) uses D3D12
alone. A new Windows CI job starts the real app under several settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: a test filter YAML parses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: the startup smoke test writes graphics.json where the app reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: run the WARP shader tests one at a time

WARP's shader JIT (d3d10warp.dll) crashed twice in 124 runs of the gpu
tests with four test threads, with a null read inside its own optimizer
while several devices compiled compute pipelines at once. Serial runs
did not crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95ea05e7c8)
2026-10-14 20:41:37 +00:00
bherbruck 5ead82e627 Release 0.11.0: release branch follows main again after the 0.10.1 patch
The tree is main at cccbe125 exactly; c29b0d2f (0.10.1) is kept as a parent so release only moves forward.
2026-10-14 20:07:41 +00:00
bherbruck c29b0d2f13 Release 0.10.1 2026-10-14 14:41:19 +00:00
bherbruck 480fd8d517 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:41:16 +00:00
bherbruck 4c09fa5355 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
424 changed files with 2735 additions and 45096 deletions
-3
View File
@@ -1,6 +1,3 @@
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
.gitattributes text eol=lf
/assets/icons/ui/design/requested_icons.txt text eol=lf
# SolidCraft designs (.sc): a JSON document or a ZIP container, never Scala source, so they
# stay out of GitHub's language statistics. Git's own text/binary detection handles both kinds.
*.sc -linguist-detectable
+1 -1
View File
@@ -19,7 +19,7 @@ body:
attributes:
label: Steps to reproduce
description: |
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.sc --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
placeholder: |
1. Sketch a circle on the XY plane
+7 -8
View File
@@ -9,10 +9,9 @@ parameter to watch the timeline rebuild. [Documentation](https://docs.solidcraft
[ROADMAP](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations) lists
the known limitations. Designs from every earlier version open in this one.
**New app ID.** The app ID is now `io.tangencylabs.solidcraft` (it was `ai.storyteller.solidcraft`,
and `ai.storyteller.solvecraft` up to 0.7): `flatpak update` offers to replace the old app, the
`.deb`, `.rpm` and AppImage remove the old menu entry, and settings stay. On macOS the bundle ID
changed too, so macOS asks once more for the permissions it had granted.
**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
@@ -43,16 +42,16 @@ installing SolidCraft; keep both remotes in the same user installation:
```sh
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists solidcraft https://docs.solidcraft.io/flatpak/solidcraft.flatpakrepo
flatpak install --user solidcraft io.tangencylabs.solidcraft
flatpak run io.tangencylabs.solidcraft
flatpak install --user solidcraft ai.storyteller.solidcraft
flatpak run ai.storyteller.solidcraft
```
A remote added from the old `bherbruck.github.io` address no longer finds updates: run
`flatpak remote-modify --user --url=https://docs.solidcraft.io/flatpak/repo/ solidcraft`, then
`flatpak update --user`.
Flatpak installs under an old ID (`ai.storyteller.solidcraft`, or `ai.storyteller.solvecraft` from 0.7 or
earlier): `flatpak update --user` offers to replace it with `io.tangencylabs.solidcraft`, and settings move along.
Flatpak installs from 0.7 or earlier use the old ID `ai.storyteller.solvecraft`: `flatpak update --user`
offers to replace it with `ai.storyteller.solidcraft`, and settings move along.
Single-file `.flatpak` bundles include the runtime repository address; missing runtimes
still need an internet connection. [Linux installation details](https://docs.solidcraft.io/guide/getting-started.html#linux-flatpak).
+8 -52
View File
@@ -98,16 +98,15 @@ jobs:
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
# To ask whether the merge queue already tested a commit pushed to main, and which labels the
# pull requests of a merge group carry (the render-tests opt-in).
pull-requests: read
# To ask whether the merge queue already tested a commit pushed to main.
actions: read
outputs:
native: ${{ steps.paths.outputs.native }}
kernel: ${{ steps.paths.outputs.kernel }}
goldens: ${{ steps.paths.outputs.goldens }}
windows: ${{ steps.paths.outputs.windows }}
# The GPU path tests run (see the rule where it is computed). The job's `if` and the `ci` job's check both read this, so they cannot disagree.
# The GPU path tests run: native, and the whole workspace or solidcraft-gpu selected. The
# job's `if` and the `ci` job's check both read this, so they cannot disagree.
gpu: ${{ steps.paths.outputs.gpu }}
full: ${{ steps.paths.outputs.full }}
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
@@ -132,7 +131,6 @@ jobs:
GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
RENDER_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'render-tests') }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
APPROVED: ${{ contains(github.event.pull_request.labels.*.name, 'approved-for-ci') }}
@@ -142,7 +140,6 @@ jobs:
kernel=false
goldens=false
windows=false
gpu_paths=false
full=true
packages=
# Whether the NUL-separated paths in $1 reach any code: scripts/ci/affected.py selects
@@ -171,29 +168,6 @@ jobs:
windows=true
fi
}
# Whether the NUL-separated paths in $1 are render code: the render crate, the path
# tracer and its shaders in the GPU crate, the render workspace UI, the render assets.
# Core, kernel and CI changes do not trigger the path tests (about 56 runner-minutes):
# the tracer reads the scene through interfaces the other tests cover, and the nightly
# runs them for everything.
gpu_paths() {
while IFS= read -r -d '' path; do
case "$path" in
crates/render/*|crates/gpu/src/path/*|crates/ui-egui/src/render_workspace*|assets/render/*) gpu_paths=true ;;
esac
done < "$1"
}
# Opt-in for a big render change: a pull request labelled render-tests, or a merge group
# holding one (its pull requests are the (#N) of its squash commits), runs them whatever
# the paths say.
group_has_render_label() {
for pr in $(git log --format=%s "$1" | grep -o '(#[0-9]*)$' | tr -d '(#)'); do
if gh api "repos/$REPO/issues/$pr" --jq '.labels[].name' | grep -qx render-tests; then
return 0
fi
done
return 1
}
if [ "$EVENT_NAME" = push ]; then
# The queue lands the very commit its merge_group run tested. If that run passed,
# testing the push again proves nothing new; otherwise (a merge that skipped the
@@ -227,20 +201,14 @@ jobs:
# the nightly runs it whatever changed. A group that can't be compared runs it.
if git diff --no-renames --name-only -z "$GROUP_BASE_SHA...$GROUP_HEAD_SHA" > "$RUNNER_TEMP/ci-paths"; then
windows_paths "$RUNNER_TEMP/ci-paths" "$GROUP_BASE_SHA...$GROUP_HEAD_SHA"
gpu_paths "$RUNNER_TEMP/ci-paths"
if [ "$gpu_paths" = false ] && group_has_render_label "$GROUP_BASE_SHA..$GROUP_HEAD_SHA"; then
gpu_paths=true
fi
else
windows=true
gpu_paths=true
fi
echo "windows=$windows gpu_paths=$gpu_paths for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
echo "windows=$windows for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
elif [ "$EVENT_NAME" != pull_request ]; then
# The nightly run and Run workflow: everything, Windows included.
native=true
windows=true
gpu_paths=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"
@@ -300,24 +268,12 @@ jobs:
goldens=false
fi
fi
# The GPU path tests (their own job, about 56 runner-minutes) are for render changes only:
# - on a pull request, when it carries the render-tests label (opt-in for a big render
# change) or touches render code (gpu_paths); no other pull request runs them;
# - in the merge queue, when the group's diff touches render code or one of its pull
# requests carries render-tests;
# - in the nightly and by hand, always, so anything the above skipped is caught within a day;
# - not on a push to main: the queue tested that commit, or the nightly will.
# The GPU path tests (their own job) run with the other tests, when solidcraft-gpu is
# among the packages they test.
gpu=false
if [ "$native" = true ]; then
case "$EVENT_NAME" in
pull_request)
gpu_paths "$RUNNER_TEMP/ci-paths"
gpu=$gpu_paths
if [ "$RENDER_LABEL" = true ]; then gpu=true; fi ;;
merge_group) gpu=$gpu_paths ;;
push) gpu=false ;;
*) gpu=true ;;
esac
case " $packages " in *" solidcraft-gpu "*) gpu=true ;; esac
if [ "$full" = true ]; then gpu=true; fi
fi
{
echo "native=$native"
+7 -9
View File
@@ -55,15 +55,13 @@ jobs:
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
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/io.tangencylabs.solidcraft.desktop
# The window's X11 class is its app id; the old ids stay only as the rename shims.
grep -qx 'StartupWMClass=io.tangencylabs.solidcraft' assets/platforms/linux/io.tangencylabs.solidcraft.desktop
grep -q '<id>ai.storyteller.solidcraft</id>' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in
grep -qx 'X-Flatpak-RenamedFrom=ai.storyteller.solidcraft.desktop;ai.storyteller.solvecraft.desktop;' assets/platforms/linux/io.tangencylabs.solidcraft.desktop
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' 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/' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/io.tangencylabs.solidcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/io.tangencylabs.solidcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/io.tangencylabs.solidcraft.bundle.yml')); assert m['id'] == 'io.tangencylabs.solidcraft' and m['command'] == 'solidcraft'"
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'"
python3 -m py_compile packaging/linux/snap-smoke.py
+1 -1
View File
@@ -297,7 +297,7 @@ jobs:
export CARGO_TARGET_DIR=/var/tmp/solidcraft-target CARGO_BUILD_JOBS=4
bash packaging/freebsd/package.sh
tar -tzf dist/release/solidcraft-*-freebsd-x86_64.tar.gz \
| grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/io\.tangencylabs\.solidcraft\.desktop$|/share/doc/solidcraft/THIRD-PARTY-LICENSES\.txt$'
| grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/ai\.storyteller\.solvecraft\.desktop$|/share/doc/solidcraft/THIRD-PARTY-LICENSES\.txt$'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-freebsd-x86_64
+1
View File
@@ -45,3 +45,4 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
+11 -30
View File
@@ -128,14 +128,14 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | 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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/io.tangencylabs.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/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 drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | 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 |
@@ -171,9 +171,9 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/zh-TW.json | SolidCraft contributors | Original Traditional Chinese (Taiwan) translation of SolidCraft's English catalog; no commercial CAD text used | 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/io.tangencylabs.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/io.tangencylabs.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | 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/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
@@ -429,20 +429,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_environment.svg | SolidCraft contributors | original Studio icon master (64 grid): dome with a grey cube inside | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_shaded.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_wireframe.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_hidden_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_translucent.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_faces_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_bodies.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_features.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_parts.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_sketch.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/display.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/grid.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ground_shadow.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ambient_occlusion.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -592,11 +578,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/icons/ui/design/svg/model/cone.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/locales/ar.json | Abdullah (A-AL-ANAZI) | Original Arabic translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/he.json | SolidCraft contributors | Original Hebrew translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/styles/ui/themes/cga.json | SolidCraft contributors | original SolidCraft theme file using the 16 colours of the IBM Color Graphics Adapter (black, cyan, magenta, and white of its high-intensity palette 1 for the interface) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit-dark.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-customisation.json` | SolidCraft contributors | Original navigation bar editor translations | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-slots.json` | SolidCraft contributors | Original navigation slot translations | MIT OR Apache-2.0 |
## Code adapted from sibling projects
-7
View File
@@ -136,7 +136,6 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
command without them, and will fail it once required), engine tests that measure the result (volume, area,
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
A command on the toolbar also ships with its help tooltip (step 6 of Adding a command).
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
`navigation::handle` (`crates/ui-egui/src/navigation.rs`) for its camera input, with
@@ -284,12 +283,6 @@ and a fresh CI run:
- **Commands without schemas** (`crates/engine/src/schema/unconverted.txt`): sorted, one id per
line, and it only shrinks. Converting a command removes its line; a new command is never added
to it.
- **Toolbar commands without help** (`BACKLOG` in `crates/ui-egui/src/command_help.rs`): sorted,
one id per line, and it only shrinks. Writing a command's help removes its line; a new command
is never added to it.
- **`ui.prefs` settings** (`docs/control-protocol.md`): one line per setting, sorted by name, in
the list under the protocol table; add a new setting on its own line rather than editing a shared
row.
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
checks it).
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
Generated
+17 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4242,11 +4242,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.25.1"
version = "0.24.1"
[[package]]
name = "solidcraft-cli"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4258,7 +4258,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4267,7 +4267,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"log",
"serde",
@@ -4284,7 +4284,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4305,7 +4305,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"serde",
"serde_json",
@@ -4314,7 +4314,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"image",
"log",
@@ -4334,7 +4334,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4358,7 +4358,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"log",
"proptest",
@@ -4378,7 +4378,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4387,7 +4387,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"image",
"moxcms",
@@ -4401,7 +4401,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"serde",
"serde_json",
@@ -4412,14 +4412,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"arboard",
"egui",
@@ -4449,7 +4449,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"eframe",
"egui",
@@ -5994,7 +5994,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.25.1"
version = "0.24.1"
dependencies = [
"proc-macro2",
"serde",
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.25.1"
version = "0.24.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
-44
View File
@@ -1,44 +0,0 @@
# Disclaimer
This disclaimer covers the software, models, data, documentation and every other file in this
repository and in the releases built from it (together, "the files").
## No warranty
The files are provided "as is", without warranty of any kind, express or implied, including
warranties of accuracy, merchantability, fitness for a particular purpose and non-infringement.
Results produced with the files, such as geometry, meshes, analyses and simulations, can be
wrong. A qualified person must check them before anyone relies on them for a design,
manufacturing or safety decision.
## No liability
To the fullest extent permitted by law, Tangency Labs and the contributors are not liable for any
claim, damage, loss or other liability arising from the files or from their use, modification or
distribution, whether in contract, tort or otherwise.
## No indemnity
Neither Tangency Labs nor any contributor indemnifies, defends or holds harmless anyone who uses,
modifies or distributes the files, including against claims that the files infringe someone
else's rights.
## Harmful or unlawful use
Tangency Labs and the contributors do not authorise, endorse or accept any responsibility for use
of the files to harm people, animals, property or the environment, or for any unlawful use.
Whoever uses the files is solely responsible for how they use them, for what they make with them
and for complying with the laws that apply to them. Tangency Labs and the contributors must not be
named or held out as responsible for any such use.
## No endorsement
Using, modifying or distributing the files does not mean that Tangency Labs or any contributor
endorses you, your product or your results. Do not use the name of Tangency Labs or of a
contributor to suggest such an endorsement without written permission.
## Relation to the licence
This disclaimer adds to the warranty and liability terms of the licence the files are distributed
under. It does not restrict any right that licence grants; where the two differ, the licence
governs.
+3 -10
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" width="96" height="96" alt="SolidCraft" />
<img src="assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png" width="96" height="96" alt="SolidCraft" />
</p>
<h1 align="center">SolidCraft</h1>
@@ -74,7 +74,7 @@ other is a tracked bug.
- **Parameters and configurations** with unit-aware expressions.
- **Assemblies** with joints, motion studies, contact sets, interference and mass properties.
- **Sheet metal** with flanges, hems, fold/unfold and flat patterns, and plastic enclosure features.
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.sc` designs (`.solidcraft` files from earlier releases still open).
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.solidcraft` designs.
- **Desktop app, browser app, headless CLI and MCP server**, all running the same commands.
The full list is in [docs/capabilities.md](docs/capabilities.md) and [docs/features.md](docs/features.md);
@@ -121,18 +121,11 @@ the clean-room and licence rules, and where help is wanted most. Open work is in
SolidCraft's continuous integration runs on [Blacksmith](https://www.blacksmith.sh), which
sponsors its CI minutes.
## Disclaimer
The files in this repository are provided as is, without warranty, liability or indemnity, and
Tangency Labs and the contributors accept no responsibility for any harmful or unlawful use of
them: read [DISCLAIMER.md](DISCLAIMER.md).
## Licence
MIT OR Apache-2.0, at your option. Developed under the
[clean-room contribution rules](AGENTS.md); third-party code and assets retain their
documented licences and attribution. The licence covers the code, not the name and logo: see
[TRADEMARKS.md](TRADEMARKS.md) for how to name a fork or a product built on SolidCraft.
documented licences and attribution.
SolidCraft is independent. Behaviour is designed from public documentation, general CAD
conventions and user feedback. Reference tests use analytic values and open-source tools.
-36
View File
@@ -1,36 +0,0 @@
# SolidCraft name and logo
The code is yours to use under MIT OR Apache-2.0 (see [NOTICE](NOTICE)). The name is not part of
that licence: "SolidCraft" and the SolidCraft logo identify this project and the builds its
maintainers publish, and Tangency Labs, which maintains SolidCraft, uses them as its trademarks.
Neither licence grants rights to them.
This page says how you can use the name. It is meant to be easy to follow: most uses are fine.
## Fine without asking
- Saying what your work is built on: "based on SolidCraft", "a fork of SolidCraft", "works with
SolidCraft", "SolidCraft plugin for …".
- Writing, teaching, reviewing or making videos about SolidCraft, including in titles.
- Redistributing unmodified official builds under the SolidCraft name, for example in a Linux
distribution or package manager.
- Using the logo to link to the project or to show that something supports SolidCraft.
- User groups, meetups and community spaces about SolidCraft, as long as they don't look official.
## Use your own name
- A fork, a modified build or a product built on SolidCraft needs its own name and logo. "SolidCraft"
may appear in its description ("based on SolidCraft"), not as its name or the main part of it.
- Don't use names or logos that could be mistaken for SolidCraft, such as "SolidCraft Pro",
"SolidKraft" or a lightly changed version of the logo.
- Don't imply that SolidCraft's maintainers made, endorse or support your product or service unless
they do.
- Don't use the name in domain names, app store listings or social media accounts in a way that
looks official.
## Questions
If you're not sure whether a use is fine, open an issue or ask on [Discord](https://discord.gg/FtU4NQNVDH).
For anything this page doesn't allow, ask first; we're generally happy to say yes.
Other companies' trademarks named in this repository are covered in [NOTICE](NOTICE).
@@ -68,7 +68,7 @@ fn run() -> Result<(), String> {
let generation_start = Instant::now();
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
let generation_seconds = generation_start.elapsed().as_secs_f64();
let design_path = output.with_extension(solidcraft_engine::io::DESIGN_EXT);
let design_path = output.with_extension("solidcraft");
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
let bodies: Vec<Value> = scene
+2 -5
View File
@@ -8,7 +8,7 @@
//! no fixture.
//!
//! ```text
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.sc]
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.solidcraft]
//! ```
//! prints one JSON line per hundred commands (mean and max milliseconds, bodies so far), then the
//! build total, then the phases `bench-open` prints for the saved design.
@@ -205,10 +205,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
);
let path = match flag("--save") {
Some(p) => p.clone(),
None => std::env::temp_dir()
.join(format!("bench-agent-{}.{}", std::process::id(), solidcraft_engine::io::DESIGN_EXT))
.to_string_lossy()
.into_owned(),
None => std::env::temp_dir().join(format!("bench-agent-{}.solidcraft", std::process::id())).to_string_lossy().into_owned(),
};
s.execute("file.save", &json!({"path": path})).map_err(|e| e.to_string())?;
let r = crate::cmd_bench_open(std::slice::from_ref(&path));
+1 -1
View File
@@ -419,7 +419,7 @@ fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: In
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
// A turn can start and end between two polls: then the conversation has its answer.
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
if idle && (seen_busy || answered || matches!(state, "exited" | "failed" | "auth" | "off") || began.elapsed() >= START_GRACE) {
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
return Ok((chat, false));
}
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
+14 -14
View File
@@ -1,10 +1,10 @@
//! SolidCraft headless command line.
//!
//! ```text
//! solidcraft-cli run <script.json|design.sc|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
//! solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.sc> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
@@ -56,19 +56,19 @@ struct Command {
}
/// `run`'s lines in the usage.
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
/// `eval`'s lines in the usage.
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.solidcraft|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
/// `script`'s lines in the usage.
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
/// `bench-agent`'s lines in the usage.
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.sc] [--script-out script.json]\n";
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
/// `bench-assistant`'s lines in the usage.
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
/// `snapshot`'s lines in the usage.
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.sc|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
/// `render`'s lines in the usage.
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
/// `exec`'s lines in the usage.
@@ -83,7 +83,7 @@ const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
/// `mcp`'s lines in the usage.
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
/// `crashes`'s lines in the usage.
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
/// `licences`'s lines in the usage.
@@ -817,7 +817,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
/// Separate file reading, JSON decoding/repair, timeline evaluation and display meshing.
pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.sc | part.step> [--parse-only]")?;
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]")?;
let t = std::time::Instant::now();
let step = solidcraft_engine::io::is_step_path(path);
let limit = if step { solidcraft_engine::io::MAX_STEP_BYTES } else { solidcraft_engine::io::MAX_DESIGN_BYTES };
@@ -863,13 +863,13 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "place_ms": place_ms, "bodies": st.bodies.len(), "triangles": triangles})
);
let flag = |name: &str| args.iter().position(|a| a == name);
// `--save PATH`: save the opened design without its model cache (to PATH.plain.sc) and
// `--save PATH`: save the opened design without its model cache (to PATH.plain.solidcraft) and
// as the app does, with it, then read the cache back.
let s = match flag("--save").and_then(|i| args.get(i + 1)) {
Some(out) => {
let mut s = s;
// Without the cache first, for comparison.
let plain = format!("{out}.plain.{}", solidcraft_engine::io::DESIGN_EXT);
let plain = format!("{out}.plain.solidcraft");
let t = std::time::Instant::now();
s.execute("file.save_as", &json!({"path": plain, "cache": false})).map_err(|e| e.to_string())?;
let plain_ms = t.elapsed().as_secs_f64() * 1000.0;
+3 -3
View File
@@ -92,7 +92,7 @@ fn designs() -> Vec<(String, PathBuf)> {
add("regressions", root.join("examples/regressions"), "json");
add("demo", root.join("examples/demo"), "json");
add("typical", root.join("tests/action_matrix/designs"), "json");
add("demo", root.join("tests/action_matrix/designs"), solidcraft_engine::io::DESIGN_EXT);
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
let mut cases: Vec<PathBuf> =
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
cases.sort();
@@ -105,7 +105,7 @@ fn designs() -> Vec<(String, PathBuf)> {
/// Build a design from its file.
fn load(path: &Path) -> Result<Session, String> {
let mut s = Session::default();
if solidcraft_engine::io::is_design_container_path(path) {
if path.extension().is_some_and(|x| x == "solidcraft") {
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
return Ok(s);
}
@@ -1558,7 +1558,7 @@ fn sketches_on_loft_and_sweep_faces() {
#[test]
fn press_pull_on_the_pi_case_floor() {
reproduces(
Some("tests/action_matrix/designs/pi-case.sc"),
Some("tests/action_matrix/designs/pi-case.solidcraft"),
json!([{"command": "solid.press_pull", "params": {"face": [30, 20, 0], "distance": 1}}]),
);
}
@@ -75,15 +75,12 @@ fn designs() -> Vec<(String, Session)> {
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| {
(solidcraft_engine::io::is_design_container_path(p) || p.extension().is_some_and(|x| x == "json"))
&& !p.to_string_lossy().ends_with(".expected.json")
})
.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 solidcraft_engine::io::is_design_container_path(&p) {
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
open(&p)
} else {
let mut s = Session::default();
@@ -153,7 +150,7 @@ fn pi_case_faces_per_step_at_both_heights() {
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.sc")).unwrap()).unwrap();
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() {
+1 -6
View File
@@ -145,7 +145,7 @@ fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting(
#[test]
fn cli_loads_both_design_encodings_by_contents() {
let dir = Scratch::new();
for extension in ["sc", "solidcraft", "solvecraft", "json"] {
for extension in ["solidcraft", "solvecraft", "json"] {
let saved = dir.0.join(format!("box.{extension}"));
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&saved).output().unwrap());
let bytes = std::fs::read(&saved).unwrap();
@@ -412,11 +412,6 @@ fn legacy_extension_and_format_tag_still_open() {
assert_eq!(body_count(&resaved), 2);
let reopened = solidcraft_engine::io::read_design(&std::fs::read(&resaved).unwrap()).unwrap();
assert!(reopened.to_json().contains("\"solidcraft/"));
// `.solidcraft` (before `.sc`) opens as `--in` and as the design to evaluate; `.sc` too.
let current = dir.0.join("current.sc");
json_output(cli().args(["run", "--in"]).arg(&resaved).arg(dir.script()).arg("--save").arg(&current).output().unwrap());
assert_eq!(body_count(&current), 3);
assert!(solidcraft_engine::io::is_design_container(&std::fs::read(&current).unwrap()), "a .sc file is a design container");
}
/// `snapshot` starts from `--in` as `run` does (its script runs on that design), and refuses an
@@ -1,5 +1,5 @@
//! Straight edges stay one edge after many cuts (#1049). The owner's design
//! (`tests/action_matrix/designs/crazy-geom-cuts.sc`, saved by 0.10.0): five extrudes,
//! (`tests/action_matrix/designs/crazy-geom-cuts.solidcraft`, saved by 0.10.0): five extrudes,
//! joins and cuts, over four sketches, three of them on faces of the body with the faces'
//! edges projected. Cuts used to leave the straight edges of the walls they sliced split at
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
@@ -14,7 +14,7 @@ use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::Vec3;
fn design() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.solidcraft")
}
fn open(path: &Path) -> Session {
+5 -5
View File
@@ -27,12 +27,12 @@
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
<meta name="theme-color" content="#343840">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/io.tangencylabs.solidcraft.png">
<link rel="apple-touch-icon" href="icons/256x256/io.tangencylabs.solidcraft.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/ai.storyteller.solidcraft.png">
<link rel="apple-touch-icon" href="icons/256x256/ai.storyteller.solidcraft.png">
<link data-trunk rel="copy-file" href="manifest.webmanifest" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/32x32" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/256x256" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/512x512" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png" data-target-path="icons/32x32" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png" data-target-path="icons/256x256" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png" data-target-path="icons/512x512" />
</head>
<body>
<canvas id="solidcraft_canvas"></canvas>
+3 -3
View File
@@ -9,8 +9,8 @@
"theme_color": "#343840",
"categories": ["productivity", "utilities"],
"icons": [
{ "src": "icons/32x32/io.tangencylabs.solidcraft.png", "sizes": "32x32", "type": "image/png" },
{ "src": "icons/256x256/io.tangencylabs.solidcraft.png", "sizes": "256x256", "type": "image/png" },
{ "src": "icons/512x512/io.tangencylabs.solidcraft.png", "sizes": "512x512", "type": "image/png" }
{ "src": "icons/32x32/ai.storyteller.solidcraft.png", "sizes": "32x32", "type": "image/png" },
{ "src": "icons/256x256/ai.storyteller.solidcraft.png", "sizes": "256x256", "type": "image/png" },
{ "src": "icons/512x512/ai.storyteller.solidcraft.png", "sizes": "512x512", "type": "image/png" }
]
}
+8 -55
View File
@@ -147,21 +147,6 @@ pub fn accept_list(extensions: &[&str]) -> String {
list.join(",")
}
/// The `accept` of a file input for `extensions` on this device (`apple_touch`: an iPhone or
/// iPad). The picker there offers only files of a type the system knows from the list and greys
/// out the rest, `.solidcraft` designs among them, since no system type has that extension
/// (#6977). So it offers every file there, and the opener checks what it gets.
pub fn input_accept(extensions: &[&str], apple_touch: bool) -> String {
if apple_touch { String::new() } else { accept_list(extensions) }
}
/// The browser runs on an iPhone or iPad (every browser there is WebKit), from its
/// `navigator.userAgent` and `navigator.maxTouchPoints`. iPadOS presents itself as a Mac; only its
/// touch screen tells them apart.
pub fn apple_touch(user_agent: &str, touch_points: i32) -> bool {
["iPhone", "iPad", "iPod"].iter().any(|d| user_agent.contains(d)) || (user_agent.contains("Macintosh") && touch_points > 1)
}
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
@@ -169,15 +154,10 @@ pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
}
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension). A
/// design is `.sc` unless the name already has an older design extension, which it keeps.
pub fn save_type(name: &str) -> (&'static str, &'static str, String) {
let ext = std::path::Path::new(name).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
match ext.as_deref() {
Some("json") => ("Plain JSON design", "application/json", ".json".into()),
Some(e) if solidcraft_engine::io::is_design_ext(e) => ("SolidCraft design", "application/octet-stream", format!(".{e}")),
_ => ("SolidCraft design", "application/octet-stream", format!(".{}", solidcraft_engine::io::DESIGN_EXT)),
}
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
if json { ("Plain JSON design", "application/json", ".json") } else { ("SolidCraft design", "application/octet-stream", ".solidcraft") }
}
/// Is this a cancelled dialog (the browser's `AbortError`)? Not an error to show.
@@ -269,7 +249,7 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"suggestedName".into(), &name.into());
let (description, mime, extension) = save_type(name);
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, &extension));
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, extension));
options
}
@@ -354,9 +334,7 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
let extensions = Array::new();
let designs = solidcraft_engine::io::DESIGN_EXTS.iter().map(|e| format!(".{e}"));
let others = [".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"].map(String::from);
for e in designs.chain(others) {
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
extensions.push(&e.into());
}
let accept = Object::new();
@@ -634,8 +612,6 @@ mod tests {
assert_eq!(route("/disk/18f-2a/Bracket.solidcraft"), WriteTo::Disk);
assert_eq!(route("/downloads/part.step"), WriteTo::Download);
assert_eq!(route("/upload/part.step"), WriteTo::Memory);
// The temporary folder (a screenshot without a path, #7192) stays in memory.
assert_eq!(route(&format!("{}/solidcraft-shot.png", solidcraft_engine::io::vfs::BROWSER_TEMP)), WriteTo::Memory);
}
#[test]
@@ -674,7 +650,6 @@ mod tests {
#[test]
fn only_design_opens_use_the_disk_dialog() {
assert!(opens_design(&[]));
assert!(opens_design(&["sc"]));
assert!(opens_design(&["solidcraft"]));
assert!(opens_design(&["solvecraft"]));
assert!(!opens_design(&["step", "stp"]));
@@ -696,33 +671,11 @@ mod tests {
assert_eq!(upload_limit(&["dxf"]), None);
}
/// An iPhone or iPad greys out a `.solidcraft` design the `accept` list names, as it knows no
/// type for that extension, so its inputs offer every file; other devices keep the list (#6977).
#[test]
fn file_inputs_on_an_iphone_or_ipad_offer_every_file() {
let iphone =
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1";
let mac = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15";
let chrome_ios = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.156 Mobile/15E148 Safari/604.1";
let chrome = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36";
assert!(apple_touch(iphone, 5));
assert!(apple_touch(chrome_ios, 5), "every browser on iOS has the same picker");
assert!(apple_touch(mac, 5), "an iPad presents itself as a Mac");
assert!(!apple_touch(mac, 0), "a Mac has no touch screen");
assert!(!apple_touch(chrome, 10), "a touch screen elsewhere filters as before");
assert!(!apple_touch("", 0), "hostile input is just not an iPhone");
let designs = ["solidcraft", "json", "step"];
assert_eq!(input_accept(&designs, true), "");
assert_eq!(input_accept(&designs, false), ".solidcraft,.json,.step");
}
#[test]
fn the_save_dialog_offers_the_type_of_the_name() {
assert_eq!(save_type("Plate.sc").2, ".sc");
assert_eq!(save_type("Plate.JSON").2, ".json");
assert_eq!(save_type("Plate").2, ".sc");
// A design opened under an older extension keeps it.
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
assert_eq!(save_type("Plate.JSON").2, ".json");
assert_eq!(save_type("Plate").2, ".solidcraft");
}
#[test]
+12 -25
View File
@@ -24,13 +24,11 @@ pub const OPFS: &str = disk::OPFS_PREFIX;
pub const DOWNLOADS: &str = disk::DOWNLOADS_PREFIX;
/// Files brought in from the computer.
pub const UPLOAD: &str = "/upload/";
/// The autosave of unsaved work (in OPFS: `recovery.sc` and `recovery.json`), named with
/// `solidcraft_engine::io::DESIGN_EXT`.
const RECOVERY: &str = "recovery.sc";
/// The autosaves earlier releases kept, newest first, one per entry of
/// `solidcraft_engine::io::LEGACY_DESIGN_EXTS` (compatibility shim, docs/compatibility.md): the
/// newest is recovered when there is no current one.
const LEGACY_RECOVERY: [&str; 2] = ["recovery.solidcraft", "recovery.solvecraft"];
/// The autosave of unsaved work (in OPFS: `recovery.solidcraft` and `recovery.json`).
const RECOVERY: &str = "recovery.solidcraft";
/// The autosave earlier releases kept (compatibility shim, docs/compatibility.md): recovered
/// when there is no current one.
const LEGACY_RECOVERY: &str = "recovery.solvecraft";
const RECOVERY_META: &str = "recovery.json";
/// What to do with a file brought in from the computer.
@@ -284,10 +282,9 @@ pub fn start(files: &Rc<Files>) {
}
let mut entries = Vec::new();
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
for (name, bytes, modified) in all {
if LEGACY_RECOVERY.contains(&name.as_str()) {
if legacy == Some(name.as_str()) {
if name == LEGACY_RECOVERY {
if !has_recovery {
vfs::insert(&format!("{OPFS}{RECOVERY}"), bytes);
}
continue;
@@ -427,9 +424,7 @@ pub fn autosave(files: &Rc<Files>, doc: Option<(&solidcraft_engine::doc::Documen
}
None => spawn_local(async move {
let _ = remove_file(RECOVERY).await;
for legacy in LEGACY_RECOVERY {
let _ = remove_file(legacy).await;
}
let _ = remove_file(LEGACY_RECOVERY).await;
let _ = remove_file(RECOVERY_META).await;
}),
}
@@ -518,9 +513,9 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
input.set_type("file");
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
let mut extensions = solidcraft_engine::io::formats::open_extensions(true);
extensions.extend(["dxf", "svg"]);
input.set_accept(&disk::input_accept(&extensions, apple_touch()));
let accept =
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
input.set_accept(&accept);
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
let _ = input.set_attribute("style", "display:none");
if let Some(body) = doc.body() {
@@ -551,7 +546,7 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
/// `None` when the dialog was closed or the file could not be read.
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
// Shown now, in the click: a file dialog needs the user's gesture.
let chosen = choose_file(&disk::input_accept(extensions, apple_touch()));
let chosen = choose_file(&disk::accept_list(extensions));
let limit = disk::upload_limit(extensions);
let files = files.clone();
async move {
@@ -578,14 +573,6 @@ pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::
}
}
/// The page runs on an iPhone or iPad, whose file inputs offer every file (`disk::input_accept`).
fn apple_touch() -> bool {
web_sys::window().is_some_and(|w| {
let nav = w.navigator();
disk::apple_touch(&nav.user_agent().unwrap_or_default(), nav.max_touch_points())
})
}
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
+4 -9
View File
@@ -72,10 +72,10 @@ impl Download {
}
let Some((name, bytes)) = self.pending.take() else { return };
// The extension selects the design loader even when the URL has no extension.
let path = &format!("/linked/design.{}", solidcraft_engine::io::DESIGN_EXT);
let path = "/linked/design.solidcraft";
vfs::insert(path, bytes);
app.open_path(path);
if app.session.path.as_deref() == Some(path.as_str()) {
if app.session.path.as_deref() == Some(path) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
@@ -90,14 +90,9 @@ async fn fetch(value: &str) -> Reply {
let window = web_sys::window().ok_or("No browser window")?;
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
let url = design_url(value, &page_origin)?;
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design");
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
// `arm.sc` (or a legacy `arm.solidcraft`) is named `arm`.
let stem = match decoded.rsplit_once('.') {
Some((stem, ext)) if !stem.is_empty() && solidcraft_engine::io::is_design_ext(ext) => stem,
_ => decoded.as_str(),
};
let name: String = stem.chars().take(255).collect();
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
+7 -17
View File
@@ -417,6 +417,7 @@ impl Lines {
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
_counted: Subscriber,
}
impl Subscription {
@@ -434,13 +435,10 @@ impl Subscription {
let stop = Arc::new(AtomicBool::new(false));
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
let (l, s) = (lines.clone(), stop.clone());
// The connection's seat on the watch rides the events thread: it leaves when the thread
// does, so the next failed write to a closed client retires it even while the connection's
// own thread is slow to see the end of the stream (#7226).
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, counted)).ok()?;
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
let sent = send(&mut &lines.socket, reply);
drop(first);
Some((Subscription { lines, stop }, sent))
Some((Subscription { lines, stop, _counted: counted }, sent))
}
}
@@ -460,10 +458,8 @@ impl Drop for Subscription {
}
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
/// last one it was told of (`seen`), until `stop`. Owns the connection's seat on the watch
/// (`counted`), so the seat leaves exactly when this thread does: on `stop`, or on the first
/// write a closed client refuses (#7226).
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), _counted: Subscriber) {
/// last one it was told of (`seen`), until `stop`.
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
while !stop.load(Ordering::Relaxed) {
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
@@ -870,17 +866,11 @@ mod tests {
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
// Closed, it is no longer counted. Each publish asks the events thread for one more line,
// and its write to the closed connection is what retires the seat; that keeps the wait off
// the connection's own thread, which anything else this process is doing may hold up past
// the deadline (#7226).
// Closed, it is no longer counted.
drop((out, lines));
let mut revision = 11;
let deadline = Instant::now() + Duration::from_secs(10);
while WATCH.subscribed() && Instant::now() < deadline {
WATCH.publish("d-1", revision);
revision += 1;
std::thread::sleep(Duration::from_millis(20));
std::thread::sleep(Duration::from_millis(10));
}
assert!(!WATCH.subscribed());
}
+18 -126
View File
@@ -1,6 +1,6 @@
//! SolidCraft desktop app.
//!
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.sc | part.step]`
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]`
//!
//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and
//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md.
@@ -92,8 +92,6 @@ struct App {
native_menu: Option<native_menu::NativeMenu>,
/// A device's own app moving the view (Preferences › Navigation, off by default).
nav_bridge: nav_bridge::NavBridge,
/// Where the next file dialog opens: it follows the open design (#7756).
dialog_folder: DialogFolder,
}
impl eframe::App for App {
@@ -124,7 +122,6 @@ impl eframe::App for App {
// Automation runs (a control port was asked for) leave the user's AI apps alone.
if !self.take_focus_unwanted {
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
solidcraft_ui_egui::agent_chat::begin(&mut self.solve);
}
if let Some(dir) = self.starting.take() {
startup::started(&dir);
@@ -149,7 +146,6 @@ impl eframe::App for App {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
self.dialog_folder.design(self.solve.session.path.as_deref());
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
self.space_mouse.tick(&mut self.solve, ctx);
@@ -244,60 +240,6 @@ impl App {
}
}
/// Where the next file dialog opens (#7756): the folder of the file last chosen in one, or of the
/// design last opened or saved, whichever came last. Linux dialogs otherwise open in the home
/// folder every time; macOS and Windows remember a folder of their own, which this agrees with.
#[derive(Clone, Default)]
struct DialogFolder(std::sync::Arc<std::sync::Mutex<DialogFolderState>>);
#[derive(Default)]
struct DialogFolderState {
folder: Option<std::path::PathBuf>,
/// The open design's path when last looked at, to notice another design opened or saved.
design: Option<String>,
}
impl DialogFolder {
/// A file was chosen in a dialog: the next one opens in its folder.
fn chosen(&self, file: &std::path::Path) {
if let (Some(folder), Ok(mut state)) = (file.parent().filter(|f| !f.as_os_str().is_empty()), self.0.lock()) {
state.folder = Some(folder.to_path_buf());
}
}
/// The open design's path, every frame: a design opened or saved somewhere else moves the
/// next dialog to its folder; the same design again leaves a folder chosen since alone.
fn design(&self, path: Option<&str>) {
let Ok(mut state) = self.0.lock() else { return };
if state.design.as_deref() == path {
return;
}
state.design = path.map(str::to_string);
if let Some(folder) = path.and_then(|p| std::path::Path::new(p).parent()).filter(|f| !f.as_os_str().is_empty()) {
state.folder = Some(folder.to_path_buf());
}
}
/// A dialog's answer as a path, its folder remembered for the next dialog.
fn answer(&self, picked: Option<rfd::FileHandle>) -> Option<String> {
let picked = picked?;
self.chosen(picked.path());
Some(picked.path().to_string_lossy().into_owned())
}
fn folder(&self) -> Option<std::path::PathBuf> {
self.0.lock().ok().and_then(|state| state.folder.clone())
}
/// The dialog, opening in the remembered folder when there is one.
fn start(&self, dialog: rfd::AsyncFileDialog) -> rfd::AsyncFileDialog {
match self.folder() {
Some(folder) => dialog.set_directory(folder),
None => dialog,
}
}
}
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
/// case, plus FreeCAD's own mixed casing.
fn case_variants(exts: &[&str]) -> Vec<String> {
@@ -347,16 +289,15 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
raw.safe_area_insets = Some(insets);
}
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
fn services(cc: &eframe::CreationContext<'_>) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
let program_parent = parent.clone();
let file_parent = parent.clone();
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
Services {
pick_open: Some(Box::new(move |first: &[&str]| {
// The file type the command reads comes first, so the picker opens on it (#1801).
let mut dialog = open_folder.start(open_parent.clone());
let mut dialog = open_parent.clone();
if !first.is_empty() {
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
dialog = dialog.add_filter(label, &case_variants(first));
@@ -369,8 +310,7 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
dialog = dialog.add_filter(label, &case_variants(&exts));
}
let future = dialog.add_filter("All files", &["*"]).pick_file();
let folder = open_folder.clone();
Box::pin(async move { folder.answer(future.await) })
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
// One kind of file (the agent chat's attachments): only the types it names, as given and in
// lower and upper case (Linux dialogs match case; macOS allows just the listed types).
@@ -385,9 +325,8 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
}
let all: Vec<&str> = all.iter().map(String::as_str).collect();
// Any other file attaches by reference, so the list is not the only choice.
let future = file_folder.start(file_parent.clone()).add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
let folder = file_folder.clone();
Box::pin(async move { folder.answer(future.await) })
let future = file_parent.clone().add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
pick_program: Some(Box::new(move || {
let dialog = program_parent.clone().set_title("Choose your slicer");
@@ -396,7 +335,7 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
pick_save: Some(Box::new(move |name: &str, exts: &[&str]| {
let dialog = save_folder.start(parent.clone()).set_file_name(name);
let dialog = parent.clone().set_file_name(name);
let dialog = if exts == [io::DESIGN_EXT, "json"] {
if name.ends_with(".json") {
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
@@ -407,8 +346,7 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
dialog.add_filter("File", exts)
};
let future = dialog.save_file();
let folder = save_folder.clone();
Box::pin(async move { folder.answer(future.await) })
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
@@ -615,26 +553,26 @@ fn graphics_path() -> Option<std::path::PathBuf> {
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
/// settings of earlier releases into the new folder. They kept `graphics.json` (and, on Linux
/// and macOS, the recovery folder) under the old product name, and eframe's `app.ron` under the
/// window's old app id. In the Flatpak sandbox, earlier releases kept everything under an
/// old app id's `~/.var/app` folder; those copies come first, newest first. The old folders
/// window's old app id. In the Flatpak sandbox, releases before 0.8 kept everything under the
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
/// are never changed.
fn adopt_legacy_settings() {
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
let old_flatpak = solidcraft_engine::io::legacy_flatpak_paths(&current);
if let Err(e) = adopt_settings(&current, &old_flatpak, &legacy, legacy_eframe.as_deref()) {
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(&current);
if let Err(e) = adopt_settings(&current, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
log::warn!("settings: could not copy the previous release's settings into {}: {e}", current.display());
}
}
fn adopt_settings(
current: &std::path::Path,
old_flatpak: &[std::path::PathBuf],
old_flatpak: Option<&std::path::Path>,
legacy: &std::path::Path,
legacy_eframe: Option<&std::path::Path>,
) -> std::io::Result<()> {
let fresh = !current.exists();
for old in old_flatpak {
if let Some(old) = old_flatpak {
solidcraft_engine::io::adopt_legacy_dir(current, old)?;
}
solidcraft_engine::io::adopt_legacy_dir(current, legacy)?;
@@ -750,14 +688,11 @@ fn main() -> eframe::Result {
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
let mut files = Vec::new();
let mut sample = false;
// `--theme-file PATH`: follow that theme file for this run without saving it (#7573).
let mut theme_file: Option<String> = None;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
"--sample" => sample = true,
"--theme-file" => theme_file = args.next(),
"--safe-mode" => safe_mode = true,
"--version" | "-V" => {
println!("solidcraft {}", env!("CARGO_PKG_VERSION"));
@@ -777,9 +712,7 @@ fn main() -> eframe::Result {
return Ok(());
}
"--help" | "-h" => {
println!(
"usage: solidcraft [--control PORT] [--sample] [--theme-file THEME.json] [--crash-log] [--safe-mode] [design.sc | part.step]"
);
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]");
return Ok(());
}
_ => files.push(a),
@@ -907,8 +840,7 @@ fn main() -> eframe::Result {
solidcraft_ui_egui::fonts::setup_bundled_fonts(&cc.egui_ctx);
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
mark("fonts");
let dialog_folder = DialogFolder::default();
let mut app = SolveApp::new(Session::default(), services(cc, &dialog_folder));
let mut app = SolveApp::new(Session::default(), services(cc));
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
app.custom_titlebar = custom_titlebar;
#[cfg(target_os = "windows")]
@@ -931,11 +863,6 @@ fn main() -> eframe::Result {
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
if let Some(path) = &theme_file
&& let Err(e) = solidcraft_ui_egui::prefs::follow_theme_file_for_run(&mut app, path)
{
app.set_status(format!("--theme-file: {e}"), true);
}
mark("app state and prefs");
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
lost: rs.device_lost.clone(),
@@ -1066,7 +993,6 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
native_menu,
nav_bridge,
dialog_folder,
}))
}),
);
@@ -1245,40 +1171,6 @@ mod backend_tests {
}
}
#[cfg(test)]
mod dialog_folder_tests {
use super::DialogFolder;
use std::path::{Path, PathBuf};
/// #7756: a file dialog opens where the last one left off, not in the home folder.
#[test]
fn the_next_dialog_opens_in_the_folder_of_the_file_chosen_last() {
let folder = DialogFolder::default();
assert_eq!(folder.folder(), None, "before any choice the system decides");
folder.chosen(Path::new("/work/prints/bracket.stl"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/prints")));
// A bare file name has no folder to remember.
folder.chosen(Path::new("bracket.step"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/prints")));
}
/// Save As starts beside the open design (#7756), and a folder chosen since wins until another
/// design is opened or saved.
#[test]
fn the_open_design_moves_the_folder_only_when_it_changes() {
let folder = DialogFolder::default();
folder.design(Some("/work/lid/lid.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/lid")));
folder.chosen(Path::new("/exports/lid.3mf"));
folder.design(Some("/work/lid/lid.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/exports")), "the same design again leaves the export folder");
folder.design(None);
assert_eq!(folder.folder(), Some(PathBuf::from("/exports")), "a new, unsaved design has no folder of its own");
folder.design(Some("/work/base/base.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/base")));
}
}
#[cfg(test)]
mod viewport_tests {
#[cfg(target_os = "macos")]
@@ -1388,14 +1280,14 @@ mod msaa_tests {
std::fs::create_dir_all(&legacy_eframe).unwrap();
std::fs::write(legacy.join("graphics.json"), r#"{"msaa":8}"#).unwrap();
std::fs::write(legacy_eframe.join("app.ron"), "(prefs)").unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("graphics.json")).unwrap(), r#"{"msaa":8}"#);
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(prefs)");
assert!(legacy.join("graphics.json").is_file() && legacy_eframe.join("app.ron").is_file());
// Later launches leave the current settings alone.
std::fs::write(current.join("app.ron"), "(mine)").unwrap();
std::fs::remove_file(current.join("graphics.json")).unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(mine)");
assert!(!current.join("graphics.json").exists());
let _ = std::fs::remove_dir_all(&d);
+2 -7
View File
@@ -146,7 +146,8 @@ impl NativeMenu {
}
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
action.menu_enabled(app, ctx.egui_wants_keyboard_input())
action.enabled(app)
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
@@ -156,12 +157,6 @@ impl NativeMenu {
item.set_enabled(enabled);
}
}
for (item, action) in &self.help_actions {
let enabled = solidcraft_ui_egui::help::available(app, action);
if item.is_enabled() != enabled {
item.set_enabled(enabled);
}
}
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
&& let Err(error) = self.update_recent(app)
{
+5 -22
View File
@@ -543,16 +543,9 @@ mod tests {
#[test]
fn probing_a_backend_alone_never_panics() {
// Whatever this machine has (often nothing): an answer, not a crash. On Windows the probe
// goes the way the app's does, stopping at the first backend with a GPU: a machine with
// D3D12 never loads its OpenGL driver into the test process, and AMD's (`atio6axx.dll`)
// aborted the process as it exited (#1156).
if cfg!(target_os = "windows") {
let _ = choose_backends(true, false, false, None, has_hardware_adapter);
} else {
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
}
// Whatever this machine has (often nothing): an answer, not a crash.
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
}
}
@@ -565,18 +558,8 @@ mod tests {
#[test]
fn the_software_adapter_is_preferred_and_its_absence_is_an_error_not_a_panic() {
assert!(pick_cpu_adapter(&[], None).unwrap_err().contains("none"));
// The adapters the software renderer chooses among: it starts D3D12 alone on Windows
// (WARP), not every backend, so the test doesn't load the OpenGL driver either (#1156).
let options = crate::wgpu_options(true, None);
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &options.wgpu_setup else { panic!("the software renderer makes its own device") };
let backends = setup.instance_descriptor.backends;
if cfg!(target_os = "windows") && std::env::var_os(WGPU_BACKEND_ENV).is_none() {
assert_eq!(backends, eframe::wgpu::Backends::DX12);
}
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle_from_env();
desc.backends = backends;
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
let adapters = pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all()));
if adapters.is_empty() {
return;
}
+1 -1
View File
@@ -76,7 +76,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
| 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 `io.tangencylabs.solidcraft` |
| 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 and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/`, uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
-3
View File
@@ -27,9 +27,6 @@ error = { master = "status_error", accent = "surface" }
warning = { master = "status_warning", accent = "mesh" }
ground = "c_fix"
# Visual style: Shaded with edges is the generic style icon.
style_shaded_edges = "style"
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
sketch_break = { master = "break", accent = "sketch" }
sketch_cancel = { master = "close", accent = "sketch" }
@@ -1,8 +1,6 @@
accessibility false
activate false
agent_chat false
align false
ambient_occlusion false
appearance false
arc3 false
arc_center false
@@ -85,8 +83,6 @@ decal false
delete false
delete_face false
dimension false
display false
dots false
draft false
draft_analysis false
drive false
@@ -114,9 +110,7 @@ folder false
frame_member false
gear false
graph false
grid false
ground false
ground_shadow false
help false
hem false
hole false
@@ -216,11 +210,6 @@ section false
select false
send false
settings false
settings_keyboard false
settings_monitor false
settings_mouse false
settings_palette false
settings_plug false
sheet_rules false
shell false
shield false
@@ -258,11 +247,6 @@ star false
status_error false
status_recovery false
style false
style_hidden_edges false
style_shaded false
style_shaded_edges false
style_translucent false
style_wireframe false
suppress false
surface false
surface_blend false
-15
View File
@@ -6,7 +6,6 @@
"agent_chat": "4a9ae0e7e2c360b4858b3175674c93d096705c92318fecd481e849f3ae650bf4",
"agent_link": "2f20d89a0e3e9b28744910dddf7f282a1099ccd91aa160c9841c5b1d9ea9ed0f",
"align": "05615db7dd9e4be56ae898e59a68c548b8a8447794442a9a394940651204bc4f",
"ambient_occlusion": "973f8ef2326aeaf0face576e5a2c79c7ef3d13638b2c51a5aad199e4ef7edb02",
"appearance": "b3162edd92291a5ef402e8499cd52be05beb2fa7eda641163c3f6e98325de2bf",
"arc3": "fdca3d501fd557ac989376a12e9c4fa8231fde2b455c20724d1079a466153e9c",
"arc_center": "3a5fdeb1d64fc6741f944c4435870b05fccab62293e270da692c6681155cbc51",
@@ -89,7 +88,6 @@
"dimension": "a926b7c64bc8ca99d8d6dcbb81b95235bd3fc34fbed049aca8b01b4d1147c15d",
"disclosure_closed": "49a350c6f2efae78de0be96c524ebee1fd1291ea449a84224b88d37164a4e807",
"disclosure_open": "d13ce3f97e3e362a9904fa2bf3412f3f00e4136bc23538b92d41483961d280de",
"display": "58b45e5e2230eaf2c80f74b3be2e02a23714c90d6fb03b59ed093a1949993163",
"dots": "600f7ff209d93145fc938bc4f6a1e34473e6b9ee22a5b4718f133bc7da5ee58d",
"draft": "9c0fc361cacd0ad95ccd54794f4825dfc9901a83a3de752a588faec365d0ee09",
"draft_analysis": "eb39c0f90e9cf226da6307365a76015e900dd692dc3f0fdc8f02fe717226261f",
@@ -119,9 +117,7 @@
"folder": "c19f5230df45a1bda1e92e89de38bba21c91e6eca0517eb15847a7712012d1e5",
"gear": "25a1ca48a587850da1104a46005a330873ed196246e3d04e12cf61f26ad2b0d5",
"graph": "7156caedd2dbfa2ae53da18b7a2204b8db66c45126859d36caf049a3fd4335a4",
"grid": "084b5040d57e9d5183b3db1fad0d17a13ab5bae64676eef703cc06622f9c7438",
"ground": "23e2471a9d6872e83028e0e219e330cef1930a4d89456a0fefa1bda8a7e3eb81",
"ground_shadow": "a96809ed87f5047c2d7f1274908ed7b65b180613322ac9c10eb0f9d6296c1b66",
"help": "8ce4060577483dbdb510be147d2827de50ac20029354008fca55abc0c7edf05d",
"hem": "f9b1aafbedcf3dc90c26be308814638e0095fbe31b0647092507343fa60af011",
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
@@ -267,12 +263,6 @@
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
"select_bodies": "8e6e61e083e6097535da3b98d41b607b40fc81b72bbd8c7fb903618c5b35c568",
"select_components": "3e6cc847b334e92ce7ec5e5f35dfa929572e0974239790d76563ca4e20b26789",
"select_faces_edges": "7b99b5370b1647bf8cea57df7a1053caab641c2f4a94c4d797b9679e6f38d6f1",
"select_features": "b050bc19ce86d135cbe50998be681555110e92e179c4c7b101170c6b62c19943",
"select_parts": "569adc5595a64187411d8ffeea69eab3b76ddfa33aeead6fd728efdd993bcccc",
"select_sketch": "b0605b34a14d4173f7ed80d877109ec0b891a1b395bcab5743a603ed2eae240a",
"send": "f83b6cdab792f9567a81d46d7828ed20795513c57a2c7d00fbbe6bd14d2f5848",
"settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a",
"settings_keyboard": "4968c70788094365af9c6e2b1c15b2aac45cf21212a14b8a256daf1ffe17f41a",
@@ -319,11 +309,6 @@
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_hidden_edges": "a707dfc0bdcabe9144865f569abfcb478e493143cbd4331efc1404cf3082f6d2",
"style_shaded": "6e5f8f9a9030ab3717f8b2192bbfd3f6b515ad398acf6cb41bc24f1298bc73b4",
"style_shaded_edges": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_translucent": "c46b0cd485cfb9023514908357048b5cba9017a0c4976239da1e98f50e4ab5f9",
"style_wireframe": "af7e575d378b5b6850bee9d3ca384efab95c8d4c876c5e3524f3ea771c4380a3",
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
"surface_blend": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc",
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ambient occlusion</title>
<g opacity="0.22"><path d="M13 51L51 51A38 38 0 0 0 13 13Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.38"><path d="M13 51L40 51A27 27 0 0 0 13 24Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.62"><path d="M13 51L29 51A16 16 0 0 0 13 35Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M13 10L13 51L54 51" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 758 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Display</title>
<path d="M10 22L54 22M10 42L54 42" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="24" cy="22" r="10.5" fill="#ffffff"/>
<circle cx="42" cy="42" r="10.5" fill="#ffffff"/>
<circle cx="24" cy="22" r="6.4" fill="currentColor"/>
<circle cx="42" cy="42" r="6.4" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 448 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Grid</title>
<path d="M12 12L52 12L52 52L12 52Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25.3 12L25.3 52M38.7 12L38.7 52M12 25.3L52 25.3M12 38.7L52 38.7" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 407 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ground shadow</title>
<path d="M32 4.8 L48.1 13.9 L48.1 32.1 L32 41.2 L15.9 32.1 L15.9 13.9Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.34 31.35A25 11 0 1 0 54.66 31.35" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Bodies</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 254 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Components</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25ZM41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.46 18.35L22.91 26.75L22.91 43.55" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 621 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Faces and edges</title>
<path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L9 19M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 554 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Features</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 19L32 32M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M42 19C42 22.2 37.52 24.8 32 24.8C26.48 24.8 22 22.2 22 19C22 15.8 26.48 13.2 32 13.2C37.52 13.2 42 15.8 42 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 632 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Parts</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25Z" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 585 B

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sketch features</title>
<path d="M10 46C18 14 46 14 54 46" fill="none" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="10" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="54" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="32" cy="22.5" r="8.6" fill="#ffffff"/>
<circle cx="10" cy="46" r="5.8" fill="currentColor"/>
<circle cx="54" cy="46" r="5.8" fill="currentColor"/>
<circle cx="32" cy="22.5" r="5.8" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 563 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded with hidden edges</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L15.44 22.64M32 32L48.56 22.64M32 32L32 50.72" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 13.28M32 32L48.56 41.36M32 32L15.44 41.36" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="4.5 4"/>
</svg>

Before

Width:  |  Height:  |  Size: 619 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded</title>
<g opacity="0.5"><path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.8"><path d="M9 19 L32 32 L32 58 L9 45Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 32 L55 19 L55 45 L32 58Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 583 B

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Translucent</title>
<defs>
<clipPath id="tr-clip"><path d="M32 8.6 L52.7 20.3 L52.7 43.7 L32 55.4 L11.3 43.7 L11.3 20.3Z"/></clipPath>
</defs>
<g clip-path="url(#tr-clip)"><path d="M-71 72L9 -8M-60.5 72L19.5 -8M-50 72L30 -8M-39.5 72L40.5 -8M-29 72L51 -8M-18.5 72L61.5 -8M-8 72L72 -8M2.5 72L82.5 -8M13 72L93 -8M23.5 72L103.5 -8M34 72L114 -8M44.5 72L124.5 -8M55 72L135 -8" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.76 20.56M32 32L52.24 20.56M32 32L32 54.88" fill="none" stroke="#ffffff" stroke-width="6.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.3 20.3M32 32L52.7 20.3M32 32L32 55.4" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Wireframe</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 6M32 32L55 19M32 32L55 45M32 32L32 58M32 32L9 45M32 32L9 19" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

+1 -525
View File
File diff suppressed because it is too large Load Diff
+2 -881
View File
File diff suppressed because it is too large Load Diff
+1 -18
View File
@@ -1106,7 +1106,7 @@
"home.new_mm": "Nyt design",
"home.new_mm_hint": "Millimeter · åbner i Design",
"home.open": "Åbn…",
"home.open_hint": "Et .sc-design ({shortcut})",
"home.open_hint": "Et .solidcraft-design ({shortcut})",
"home.open_now": "Åbn nu",
"home.opened_days_ago": "For {count} dage siden",
"home.opened_hours_ago": "For {count} t siden",
@@ -1190,9 +1190,7 @@
"nav.free_orbit.help": "Musen og 3D-musen roterer gennem begge poler og bevarer rulning. Snap til akser med Shift er slået fra.",
"nav.free_orbit.label": "Fri rotation af visningen",
"nav.fusion.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, træk med højre roterer visningen",
"nav.hfss.help": "Alt+træk med venstre museknap panorerer, Shift+Alt+venstre zoomer, Ctrl+træk med venstre fra et tomt område roterer visningen",
"nav.inventor.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, Ctrl+midterste zoomer, træk med højre roterer visningen",
"nav.maya.help": "Alt+træk med venstre museknap roterer visningen, Alt+midterste panorerer, Alt+højre zoomer; træk med midterste museknap panorerer også",
"nav.openscad.help": "Træk med venstre museknap i et tomt område roterer visningen, Shift+venstre markerer med en ramme, træk i skitsegeometri redigerer den; træk med højre eller midterste museknap panorerer, Shift+højre eller Shift+midterste zoomer",
"nav.solidworks.help": "Træk med midterste museknap roterer visningen, Ctrl+midterste panorerer, Shift+midterste zoomer, træk med højre roterer visningen",
"nav.tinkercad.help": "Træk med højre museknap roterer visningen, træk med midterste eller Shift+højre panorerer",
@@ -1211,11 +1209,6 @@
"no picture": "intet billede",
"none": "ingen",
"parts.automatic_name": "automatisk",
"parts.dimension.d": "Nominel diameter",
"parts.dimension.height": "Højde",
"parts.dimension.pitch": "Stigning",
"parts.dimension.thickness": "Tykkelse",
"parts.dimension.width": "Bredde",
"pattern.alignment_2": "Justering 2",
"pattern.alignment_hint": "Antal omfatter originalen. Centrér fordeler kopierne ligeligt; Vend vælger siden med den ekstra kopi ved et lige samlet antal.",
"pattern.direction_1": "Retning 1",
@@ -1331,12 +1324,6 @@
"prefs.supersampling_brief": "Tegner visningen i større størrelse og nedskalerer den med filtrering: meget skarpt og mere krævende.",
"prefs.tab_accent_note": "Accentfarven følger den aktive fane",
"prefs.theme_file": "temafil",
"prefs.theme_follow": "Følg en temafil",
"prefs.theme_follow_error": "Ikke anvendt: {error}. Det seneste gyldige tema bevares.",
"prefs.theme_follow_launch": "Følges kun denne gang (--theme-file); genindlæses, når et andet værktøj overskriver filen.",
"prefs.theme_follow_note": "Genindlæses, når et andet værktøj overskriver filen. Valg af et tema stopper dette.",
"prefs.theme_follow_off": "Fra",
"prefs.theme_follow_stop": "Stop med at følge",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nu",
"prefs.timeline_quarter_hour": "−15",
@@ -2064,7 +2051,6 @@
"ui.heading_utilities": "VÆRKTØJER",
"ui.heading_utility": "VÆRKTØJ",
"ui.heading_weldments": "SVEJSNING",
"ui.height": "Højde",
"ui.help": "Hjælp",
"ui.hide": "Skjul",
"ui.hideAxes": "Skjul akser",
@@ -2103,7 +2089,6 @@
"ui.language": "Sprog",
"ui.layout_compact": "Kompakt",
"ui.layout_floating": "Flydende",
"ui.length": "Længde",
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
@@ -2191,7 +2176,6 @@
"ui.pinToShortcuts": "Fastgør til genveje",
"ui.pinToToolbar": "Fastgør til værktøjslinje",
"ui.pipe": "Rør",
"ui.pitch": "Stigning",
"ui.placement": "Placering",
"ui.plane": "Plan",
"ui.plane_at_angle": "Plan i vinkel",
@@ -2375,7 +2359,6 @@
"ui.visual_shaded_with_edges": "Skyggelagt med kanter",
"ui.visual_style": "Visuel stil",
"ui.visual_wireframe": "Trådmodel",
"ui.width": "Bredde",
"ui.workspace": "arbejdsområde",
"ui.workspace_animation_description": "Eksploderede visninger, bevægelsesstudier og storyboards",
"ui.workspace_available": "Tilgængelig",
+2 -723
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,5 +1,4 @@
{
"Base Center": "Base Centre",
"Center X": "Centre X",
"Center Y": "Centre Y",
"Center of mass": "Centre of mass",
@@ -17,12 +16,6 @@
"cmd.sketch_slot_center_point": "Centre Point Slot",
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"prefs.theme_follow": "Follow a theme file",
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
"prefs.theme_follow_off": "Off",
"prefs.theme_follow_stop": "Stop following",
"render.aging_add": "Add aging layer",
"render.aging_airborne": "Airborne dust / participating medium",
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
+2 -526
View File
File diff suppressed because it is too large Load Diff
+1 -205
View File
@@ -1,5 +1,4 @@
{
"(active rule)": "(regla activa)",
"0 turns it off": "0 lo desactiva",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Abre el borrador de GitHub de abajo y pega el informe copiado al portapapeles.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Abre el borrador de GitHub de abajo. El título y el informe se rellenarán automáticamente.",
@@ -36,16 +35,12 @@
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Aplique primero la apariencia. Exporta las apariencias asignadas y los materiales creados en este diseño.",
"Arc length": "Longitud del arco",
"Area": "Área",
"As Picked": "Según la selección",
"As-Built": "Tal como se construyó",
"Assign Rule": "Asignar regla",
"Attachment": "Vinculación",
"BLEND SURFACE": "SUPERFICIE DE TRANSICIÓN",
"BOUNDARY SURFACE": "SUPERFICIE DE CONTORNO",
"BROWSER": "EXPLORADOR",
"Ball": "Rótula",
"Base Height": "Altura de la base",
"Bend Line": "Línea de plegado",
"Bend Line Position": "Posición de la línea de plegado",
"Bend Position": "Posición del pliegue",
"Bend Radius": "Radio de plegado",
@@ -53,7 +48,6 @@
"Bends": "Pliegues",
"Bodies": "Cuerpos",
"Body": "Cuerpo",
"Both": "Ambos",
"Boundary": "Contorno",
"Bounding box": "Caja delimitadora",
"CHECK": "COMPROBAR",
@@ -76,7 +70,6 @@
"Checked": "Comprobado",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Elige un plano nuevo. La geometría y las cotas del boceto se mantienen en sus coordenadas bidimensionales.",
"Choose…": "Elegir…",
"Circle or sphere": "Círculo o esfera",
"Circular": "Circular",
"Circumference": "Circunferencia",
"Clockwise": "Horario",
@@ -88,7 +81,6 @@
"Config Table": "Tabla de config.",
"Configuration": "Configuración",
"Configuration rules": "Reglas de configuración",
"Constant radius": "Radio constante",
"Contact": "Contacto",
"Contact Sets": "Conjuntos de contacto",
"Contact sets": "Conjuntos de contacto",
@@ -113,8 +105,6 @@
"Curvature (G2)": "Curvatura (G2)",
"Curvature change (G3)": "Cambio de curvatura (G3)",
"Customize Toolbar": "Personalizar barra de herramientas",
"Cylindrical": "Cilíndrica",
"Deboss": "Grabado",
"Delete": "Eliminar",
"Delete all": "Eliminar todo",
"Delete rule": "Eliminar regla",
@@ -132,7 +122,6 @@
"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": "Doble clic con el botón central en la vista: Inicio (vista inicial que encuadra lo visible), Ajustar (encuadrar sin cambiar la orientación) o nada",
"Drive Joint": "Accionar unión",
"Duration": "Duración",
"Edge": "Arista",
"Edges": "Aristas",
"Edit": "Editar",
"Edit Profile Sketch": "Editar boceto del perfil",
@@ -140,7 +129,6 @@
"Enabled": "Activado",
"End bulge": "Abombamiento final",
"End continuity": "Continuidad final",
"Equal Distance": "Distancia igual",
"Explode": "Explosionar",
"Explode Text": "Descomponer texto",
"Export Animation…": "Exportar animación…",
@@ -160,8 +148,6 @@
"Face attachment lost": "Se ha perdido la vinculación de la cara",
"Faces": "Caras",
"Faces / edges / vertices": "Caras / aristas / vértices",
"Faces/Body": "Caras/Cuerpo",
"Factor must be a positive number": "El factor debe ser un número positivo",
"Fastener": "Sujeción",
"Favorites": "Favoritos",
"Favourite": "Favorito",
@@ -170,28 +156,20 @@
"Filter": "Filtrar",
"Find in Browser": "Buscar en el explorador",
"Finish": "Acabado",
"First edge": "Primera arista",
"First point": "Primer punto",
"Flange Gap": "Separación de pestaña",
"Flat Pattern": "Desarrollo plano",
"Flat size": "Tamaño del desarrollo",
"Follow camera (Look At sketches)": "Seguir cámara (orientar hacia los bocetos)",
"Follow the camera (Look At sketches)": "Seguir la cámara (orientar hacia los bocetos)",
"Font": "Fuente",
"Free Move": "Movimiento libre",
"From Bodies": "Desde cuerpos",
"From Snap": "Desde el enganche",
"From edges": "Aristas de origen",
"Full Length": "Longitud total",
"Gap": "Separación",
"Grid": "Cuadrícula",
"HISTORY": "HISTORIAL",
"Height": "Altura",
"Hem Gap": "Separación de reborde",
"Hidden edges": "Aristas ocultas",
"History layout": "Disposición del historial",
"Hole Depth": "Profundidad del agujero",
"Hole Diameter": "Diámetro del agujero",
"Holes": "Agujeros",
"Holes (edges on them)": "Agujeros (aristas que contienen)",
"Home view": "Vista de inicio",
@@ -207,14 +185,10 @@
"Isocurve": "Isocurva",
"Issue title": "Título de la incidencia",
"JOINTS": "UNIONES",
"Joint 1": "Unión 1",
"Joint 2": "Unión 2",
"Joint Origin1": "Origen de unión1",
"K Factor": "Factor K",
"Keep (points on it)": "Conservar (puntos sobre ella)",
"Keep Tools": "Conservar herramientas",
"Keep of the other surface": "Conservar de la otra superficie",
"Knife Edge": "Borde afilado",
"LIBRARY": "BIBLIOTECA",
"LINKS": "VÍNCULOS",
"Later": "Más tarde",
@@ -225,9 +199,7 @@
"Letter Spacing": "Espaciado entre letras",
"Library": "Biblioteca",
"Library file": "Archivo de biblioteca",
"Library response is missing": "Falta la respuesta de la biblioteca",
"Line Spacing": "Interlineado",
"Line on plane": "Línea en plano",
"Local library": "Biblioteca local",
"Loop": "Bucle",
"MANAGE": "GESTIONAR",
@@ -241,12 +213,9 @@
"Material": "Material",
"Material copied. Existing body assignments are unchanged.": "Material copiado. Se conservan las asignaciones actuales de los cuerpos.",
"Max": "Máx.",
"Max Thickness": "Espesor máx.",
"Mid Radius": "Radio intermedio",
"Middle double-click": "Doble clic central",
"Min": "Mín.",
"Min Draft": "Ángulo de salida mín.",
"Min Thickness": "Espesor mín.",
"Mitre": "Inglete",
"Model Parameters": "Parámetros del modelo",
"Modeled": "Modelado",
@@ -261,7 +230,6 @@
"Name": "Nombre",
"Name on import": "Nombre al importar",
"Nearest": "Más cercano",
"Neutral plane": "Plano neutro",
"New": "Nuevo",
"New Comp.": "Nuevo comp.",
"New Sketch": "Nuevo boceto",
@@ -272,7 +240,6 @@
"No matching appearances": "No hay apariencias coincidentes",
"No matching physical materials.": "No hay materiales físicos coincidentes.",
"No visible bodies or sketches to export.": "No hay cuerpos ni bocetos visibles para exportar.",
"Nominal Radius": "Radio nominal",
"None": "Ninguno",
"Not available yet": "Todavía no disponible",
"Notch": "Muesca",
@@ -288,25 +255,19 @@
"Overall length": "Longitud total",
"Param. Graph": "Gráfico de parám.",
"Parameter": "Parámetro",
"Part length must be greater than zero and at most 2000 mm": "La longitud de la pieza debe ser mayor que cero y como máximo 2000 mm",
"Parts": "Piezas",
"Pass through": "Pasante",
"Path": "Trayectoria",
"Pattern": "Patrón",
"Per Edge": "Por arista",
"Physical material definitions": "Definiciones de materiales físicos",
"Pick a part size": "Elige un tamaño de pieza",
"Pick the tool bodies": "Selecciona los cuerpos herramienta",
"Picked": "Seleccionado",
"Picked point": "Punto seleccionado",
"Pin-slot": "Pasador-ranura",
"Place at the origin instead": "Colocar en el origen",
"Planar": "Plana",
"Plastic Rules": "Reglas de plástico",
"Playback": "Reproducción",
"Plot": "Graficar",
"Point on line": "Punto en línea",
"Point on plane": "Punto en plano",
"Position": "Posición",
"Position (0 to 1)": "Posición (de 0 a 1)",
"Position (G0)": "Posición (G0)",
@@ -316,7 +277,6 @@
"Preview leaves the current design unchanged.": "La vista previa no modifica el diseño actual.",
"Profiles/Curves": "Perfiles/curvas",
"Quantity": "Cantidad",
"Quantity must be at least 2": "La cantidad debe ser al menos 2",
"RECENT": "RECIENTES",
"REFERENCE": "REFERENCIA",
"RULED SURFACE": "SUPERFICIE REGLADA",
@@ -328,8 +288,6 @@
"Recover unsaved design?": "¿Recuperar el diseño sin guardar?",
"Rectangular": "Rectangular",
"Reference face": "Cara de referencia",
"Relief Depth": "Profundidad de alivio",
"Relief Width": "Ancho de alivio",
"Remove Tools": "Eliminar herramientas",
"Remove from list": "Quitar de la lista",
"Remove override": "Quitar anulación",
@@ -346,7 +304,6 @@
"Restore the inherited component or physical material appearance": "Restaurar la apariencia heredada del componente o del material físico",
"Result": "Resultado",
"Retake screenshot": "Repetir captura de pantalla",
"Reveal Height": "Altura del escalón",
"Reverse": "Invertir",
"Reverse Direction": "Invertir dirección",
"Revert": "Revertir",
@@ -354,10 +311,7 @@
"Revert Pos.": "Revertir pos.",
"Revert Position": "Revertir posición",
"Review the report and screenshot before opening GitHub.": "Revisa el informe y la captura antes de abrir GitHub.",
"Revolute": "Revolución",
"Rigid": "Rígida",
"Rim Edge": "Arista del borde",
"Rim face": "Cara del borde",
"Roll History Marker Here": "Desplazar marcador de la línea de tiempo hasta aquí",
"Round": "Redondo",
"Rule": "Regla",
@@ -374,22 +328,11 @@
"Search": "Buscar",
"Search commands": "Buscar comandos",
"Search this library": "Buscar en esta biblioteca",
"Second edge": "Segunda arista",
"Second point": "Segundo punto",
"Section": "Sección",
"Section position": "Posición de la sección",
"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).": "Seleccionar otro: mantén pulsado el botón izquierdo sobre el modelo (o haz clic derecho › Seleccionar otro…) para ver todo lo que hay bajo el cursor; vuelve a pulsar en el mismo lugar para seleccionar el siguiente elemento. Haz doble clic con el botón central para volver a Inicio (Preferencias › Navegación).",
"Select bodies (a face picks its body)": "Selecciona cuerpos (al seleccionar una cara se selecciona su cuerpo)",
"Select bodies or faces instead": "Selecciona cuerpos o caras en su lugar",
"Select points of the sketch being edited": "Selecciona puntos del boceto que se está editando",
"Select the centre the pattern turns around first": "Selecciona primero el centro alrededor del cual gira el patrón",
"Select the curves to offset first": "Selecciona primero las curvas que se van a desfasar",
"Select the mirror line": "Selecciona la línea de simetría",
"Select the sketch curves or points to mirror first": "Selecciona primero las curvas o puntos del boceto que se van a reflejar",
"Select the sketch curves or points to move first": "Selecciona primero las curvas o puntos del boceto que se van a mover",
"Select the sketch curves or points to pattern first": "Selecciona primero las curvas o puntos del boceto para el patrón",
"Select the sketch curves or points to scale first": "Selecciona primero las curvas o puntos del boceto que se van a escalar",
"Select the sketch curves or points to transform first": "Selecciona primero las curvas o puntos del boceto que se van a transformar",
"Selection filter": "Filtro de selección",
"Semantic faces share their appearance across instances.": "Las caras semánticas comparten su apariencia entre instancias.",
"Send your report on GitHub": "Enviar tu informe a GitHub",
@@ -407,11 +350,8 @@
"Sketch geometry and profiles": "Geometría y perfiles del boceto",
"Sketch plane has no body-face attachment.": "El plano del boceto no está vinculado a ninguna cara del cuerpo.",
"Sketch points": "Puntos del boceto",
"Slider": "Deslizante",
"Slot width": "Anchura de la ranura",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se cerró antes de guardar estos diseños. Puedes abrir su último guardado automático.",
"Spacing 2 must be a non-zero number": "El espaciado 2 debe ser un número distinto de cero",
"Spacing must be a non-zero number": "El espaciado debe ser un número distinto de cero",
"Specialise": "Especializar",
"Speed": "Velocidad",
"Square": "Cuadrado",
@@ -421,7 +361,6 @@
"Start angle": "Ángulo inicial",
"Start bulge": "Abombamiento inicial",
"Start continuity": "Continuidad inicial",
"Stationary Side": "Lado fijo",
"Step": "Paso",
"Steps": "Pasos",
"Stitch the kept pieces": "Coser las piezas conservadas",
@@ -430,41 +369,29 @@
"Style": "Estilo",
"Suppressed": "Suprimido",
"Surface": "Superficie",
"Surfaces": "Superficies",
"Swap Ends": "Intercambiar extremos",
"TRIM": "RECORTAR",
"Tangent (G1)": "Tangencia (G1)",
"Tangent Chain": "Cadena tangente",
"Target Face (optional)": "Cara de destino (opcional)",
"Tear": "Desgarro",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "El boceto discontinuo sirve de guía del plano guardado. Selecciona una cara nueva para reparar la vinculación.",
"The design changed. Reopen Change Part Size for the component.": "El diseño ha cambiado. Vuelve a abrir Cambiar tamaño de pieza para el componente.",
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "La dirección debe ser un eje o una curva recta del boceto que esté en el plano del boceto",
"The mirror line cannot also be mirrored": "La línea de simetría no puede reflejarse también",
"The mirror line must be a straight curve of this sketch, not an origin axis": "La línea de simetría debe ser una curva recta de este boceto, no un eje del origen",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Falta la instancia de autor guardada. Selecciona una posición de autor válida antes de volver a vincular.",
"Then": "Entonces",
"These features will be deleted:": "Se eliminarán estas operaciones:",
"These features will fail:": "Estas operaciones fallarán:",
"Thickness Outside": "Espesor hacia fuera",
"Thickness Variation": "Variación de espesor",
"Thin": "Delgado",
"Thin Extrude": "Extrusión delgada",
"Third plane": "Tercer plano",
"Third point": "Tercer punto",
"This child is shared. Deleting it removes it from every copy of its parent.": "Este elemento secundario es compartido. Si lo eliminas, desaparecerá de todas las copias de su elemento principal.",
"This child is shared. Position changes apply to every copy of its parent.": "Este elemento secundario es compartido. Los cambios de posición se aplican a todas las copias de su elemento principal.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta cara no permite una vinculación persistente. Elige otra cara o utiliza expresamente un plano fijo.",
"Through point": "Por un punto",
"To Offset": "Hasta desfase",
"To Snap": "Hasta el enganche",
"To edges": "Aristas de destino",
"Tolerance": "Tolerancia",
"Too close": "Demasiado cerca",
"Trimming tool (plane, face or curves)": "Herramienta de recorte (plano, cara o curvas)",
"Turn off section analysis before exporting the view.": "Desactiva el análisis de sección antes de exportar la vista.",
"Type": "Tipo",
"Type a distance or an angle to move by": "Escribe una distancia o un ángulo para mover",
"UNTRIM": "ANULAR RECORTE",
"Unit": "Unidad",
"Updating the preview": "Actualizando vista previa",
@@ -493,17 +420,6 @@
"Z angle": "Ángulo Z",
"Z distance": "Distancia Z",
"Zebra": "Cebra",
"`joint` must be an id or a name": "`joint` debe ser un id o un nombre",
"`params` must be an object": "`params` debe ser un objeto",
"`values` must be a list": "`values` debe ser una lista",
"a chamfer needs a corner of exactly two lines": "un chaflán necesita una esquina de exactamente dos líneas",
"a file operation is in progress": "hay una operación de archivo en curso",
"a picked curve is no longer in a sketch": "una curva elegida ya no está en un boceto",
"a picked curve's sketch has not been solved": "el boceto de una curva elegida no se ha resuelto",
"a picked edge has no points": "una arista elegida no tiene puntos",
"a picked edge is gone": "una arista elegida ya no existe",
"a picked edge's body is gone": "el cuerpo de una arista elegida ya no existe",
"add a key for a joint first": "añade primero una clave para una unión",
"angle, height": "ángulo, altura",
"appearance.dielectric_ior": "Índice de refracción dieléctrico",
"appearance.metallic": "Metalicidad",
@@ -512,7 +428,6 @@
"appearance.pbr_opaque": "PBR opaco",
"appearance.roughness": "Rugosidad",
"appearance.surface_response": "Respuesta de la superficie",
"bom.group_children": "Subpiezas en la fila de su pieza",
"browser.body_name": "Cuerpo{number}",
"browser.calibrate_title": "Calibrar: {name}",
"browser.capture_many": "Se han movido {count} componentes, pero sus posiciones no se han capturado.",
@@ -523,8 +438,6 @@
"browser.move_title": "Mover: {name}",
"browser.parameters": "Parámetros ({count})",
"browser.precision": "Precisión: {precision}",
"browser.sketch_attachment_lost": "{name} (fijación perdida)",
"browser.sketch_editing": "{name} (en edición)",
"browser.sketch_name": "Boceto{number}",
"browser.units": "Unidades: {units}",
"cam.flat": "Cara plana",
@@ -534,11 +447,6 @@
"cam.roller": "Rodillo",
"cam.roller_pick": "Rodillo (opcional)",
"cam.tip_pick": "Punta (opcional)",
"choose a profile": "elige un perfil",
"choose the cam's and the follower's joints": "elige las uniones de la leva y del seguidor",
"choose the joints to couple": "elige las uniones que acoplar",
"click a plane or planar face": "hacer clic en un plano o una cara plana",
"click sketch curves": "hacer clic en curvas del boceto",
"cmd.appearance_assign": "Apariencia",
"cmd.appearance_library": "Biblioteca de apariencias",
"cmd.appearance_list": "Listar apariencias",
@@ -851,7 +759,6 @@
"cmd.sketch_spline_control_point_5": "Spline por puntos de control (grado 5)",
"cmd.sketch_spline_fit_point": "Spline por puntos de ajuste",
"cmd.sketch_spun_profile": "Perfil de revolución",
"cmd.sketch_toggle_diameter": "Alternar radio / diámetro",
"cmd.sketch_toggle_driven": "Alternar cota de referencia",
"cmd.sketch_trim_path": "Recortar a lo largo de la trayectoria",
"cmd.solid_align": "Alinear",
@@ -976,8 +883,6 @@
"dxf.progress.write": "Escribiendo DXF (5/5)",
"dxf.tolerance": "Tolerancia de curva",
"dxf.tolerance_help": "Solo cuerpos visibles y bocetos mostrados. Las curvas usan segmentos rectos. Una tolerancia menor da curvas más finas y consume más tiempo y memoria.",
"each rail's curves must be in one sketch": "las curvas de cada guía deben estar en un solo boceto",
"feature computation ended without a result": "el cálculo de la operación terminó sin resultado",
"file_load.built": "Construido hasta ahora: {built} de {total} operaciones.",
"file_load.converting": "Convirtiendo sólido {done} de {total}",
"file_load.inserting": "Insertando geometría",
@@ -1038,7 +943,7 @@
"home.new_mm": "Nuevo diseño",
"home.new_mm_hint": "Milímetros · se abre en Diseño",
"home.open": "Abrir…",
"home.open_hint": "Un diseño .sc ({shortcut})",
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
"home.open_now": "Abrir ahora",
"home.opened_days_ago": "Hace {count} días",
"home.opened_hours_ago": "Hace {count} h",
@@ -1172,63 +1077,18 @@
"no hole": "sin agujero",
"no interference": "sin interferencias",
"no joint that moves yet": "todavía no hay ninguna unión móvil",
"no motion study yet": "todavía no hay ningún estudio de movimiento",
"no picture": "sin imagen",
"no such joint": "no existe esa unión",
"none": "ninguno",
"not a move": "no es un movimiento",
"not a number": "no es un número",
"open a sketch first": "abre primero un boceto",
"parts.automatic_name": "automático",
"parts.dimension.across_flats": "Entrecaras",
"parts.dimension.bore": "Diámetro interior",
"parts.dimension.head_diameter": "Diámetro cabeza",
"parts.dimension.head_height": "Altura cabeza",
"parts.dimension.height": "Altura",
"parts.dimension.inner": "Diámetro interior",
"parts.dimension.outer": "Diámetro exterior",
"parts.dimension.socket": "Hexágono interior",
"parts.dimension.socket_depth": "Profundidad hexágono",
"parts.dimension.thickness": "Espesor",
"parts.dimension.width": "Anchura",
"parts.family.bearing": "Rodamiento rígido de bolas (envolvente)",
"parts.family.dowel_pin": "Pasador cilíndrico",
"parts.family.hex_bolt": "Tornillo de cabeza hexagonal",
"parts.family.hex_nut": "Tuerca hexagonal",
"parts.family.set_screw": "Prisionero con hexágono interior, punta plana",
"parts.family.socket_head_cap_screw": "Tornillo cilíndrico con hexágono interior",
"parts.family.washer": "Arandela plana",
"pattern.alignment_2": "Alineación 2",
"pattern.alignment_hint": "Los recuentos incluyen el original. Centrar equilibra las copias; Invertir elige el lado adicional cuando el total es par.",
"pattern.direction_1": "Dirección 1",
"pattern.direction_2": "Dirección 2",
"pattern.flip_direction": "Invertir dirección",
"pick a body or some of its faces": "elige un cuerpo o algunas de sus caras",
"pick a hole to seat it, or OK places it at the origin": "selecciona un agujero para colocarlo o pulsa Aceptar para situarlo en el origen",
"pick a planar face": "elige una cara plana",
"pick a plane or a face as the tool": "elige un plano o una cara como herramienta",
"pick a size": "elige un tamaño",
"pick edges of a body": "elige aristas de un cuerpo",
"pick edges of one body": "elige aristas de un solo cuerpo",
"pick edges of the holes to remove": "elige las aristas de los agujeros que quitar",
"pick faces of one body": "elige caras de un solo cuerpo",
"pick faces to offset": "elige las caras que desfasar",
"pick lines of one sketch": "elige líneas de un solo boceto",
"pick one or two vertices, edges or faces": "selecciona uno o dos vértices, aristas o caras",
"pick one plane or face as the tool, or sketch curves": "elige un plano o una cara como herramienta, o curvas de boceto",
"pick sections in two or more sketches": "elige secciones en dos o más bocetos",
"pick sketch curves or edges": "elige curvas de boceto o aristas",
"pick the face to untrim": "elige la cara a la que quitar el recorte",
"pick what to keep": "elige qué conservar",
"pick what to keep of the other surface": "elige qué conservar de la otra superficie",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Seguir un archivo de tema",
"prefs.theme_follow_error": "No aplicado: {error}. Se mantiene el último tema válido.",
"prefs.theme_follow_launch": "Solo en esta sesión (--theme-file); se recarga cada vez que otra herramienta reescribe el archivo.",
"prefs.theme_follow_note": "Se recarga cada vez que otra herramienta reescribe el archivo. Elegir un tema deja de seguirlo.",
"prefs.theme_follow_off": "Desactivado",
"prefs.theme_follow_stop": "Dejar de seguir",
"properties.title": "Propiedades: {name}",
"recovery.days_ago": "hace {count} días",
"recovery.detail": "{file} · {features} operaciones · autoguardado {when}",
@@ -1853,26 +1713,6 @@
"search.suggest": "¿Quisiste decir {label}?",
"search.try": "Escribe el nombre de un comando, una operación de este diseño o un parámetro. Para ejecutar un comando con valores, escribe su id y un objeto JSON: {example}",
"search.valid": "Válido",
"select a direction (an axis, a sketch line or a straight edge) first": "selecciona primero una dirección (un eje, una línea de boceto o una arista recta)",
"select a direction (an axis, a sketch line or a straight edge) for the profile to follow": "selecciona una dirección (un eje, una línea de boceto o una arista recta) que seguirá el perfil",
"select a point on a component": "selecciona un punto en un componente",
"select a profile, edges or sketch lines first": "selecciona primero un perfil, aristas o líneas de boceto",
"select an axis": "selecciona un eje",
"select an axis, a straight or circular edge, or a cylinder, cone or torus face first": "selecciona primero un eje, una arista recta o circular, o una cara cilíndrica, cónica o toroidal",
"select bodies first": "selecciona primero cuerpos",
"select corner points, or the two lines of one corner": "selecciona puntos de esquina, o las dos líneas de una esquina",
"select one corner point, or the two lines of one corner": "selecciona un punto de esquina, o las dos líneas de una esquina",
"select sheet edges first": "selecciona primero aristas de chapa",
"select the bend line (a sketch line) first": "selecciona primero la línea de plegado (una línea de boceto)",
"select the cam's profile (a sketch profile or the cam's edges)": "selecciona el perfil de la leva (un perfil de boceto o las aristas de la leva)",
"select the corner to cut first": "selecciona primero la esquina que cortar",
"select the corners to round first": "selecciona primero las esquinas que redondear",
"select the fixed face first (a plate's large face)": "selecciona primero la cara fija (la cara grande de una placa)",
"select the frame sketch's lines first": "selecciona primero las líneas del boceto del marco",
"select the path (sketch lines across the edge)": "selecciona la trayectoria (líneas de boceto a lo largo de la arista)",
"select the rim face first": "selecciona primero la cara del borde",
"select the sheet edge first": "selecciona primero la arista de la chapa",
"select two components": "selecciona dos componentes",
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista.",
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
@@ -1885,22 +1725,16 @@
"settings.reset_intro": "Estos ajustes no funcionan en este ordenador e impedirían iniciar SolidCraft, por lo que se han cambiado:",
"settings.reset_title": "Se han cambiado los ajustes",
"settings.supersampling_note": "Dibuja la vista a mayor resolución y después la reduce: bordes, líneas y cuadrícula más suaves en pantallas inferiores a 4K. Consume recursos de GPU: unos 2,25× píxeles a 1,5× y 4× a 2×.",
"sheet.flange_type_base": "Base",
"sheet.flange_type_contour": "Contorno",
"sheet.flange_type_edge": "Arista",
"shortcut.json_error": "los parámetros deben estar en formato JSON: {error}",
"shortcut.no_command": "no existe el comando `{id}`; el texto anterior al JSON debe ser un ID de comando",
"shortcut.pin": "Fijar en la parte superior del cuadro S",
"shortcut.unpin": "Desfijar",
"sketch is missing": "falta el boceto",
"sketch.degrees_of_freedom": "{count} grados de libertad",
"sketch.dof_left": "Quedan {count} grados de libertad",
"sketch.dof_left_one": "Queda {count} grado de libertad",
"sketch.draw_curves": "Dibujar como {linetype}",
"sketch.make_selection": "Convertir la selección a {linetype}",
"sketch.open_ends": "Abiertos {count}",
"sketch.status_conflict": "restricciones en conflicto",
"sketch.status_fully_constrained": "totalmente restringido",
"sketch.untied_ends": "Sin vincular {count}",
"sketch_panel.attached": "Vinculado a {body} / {path}",
"sketch_panel.author": "Ubicación de autoría: {path}",
@@ -1922,24 +1756,7 @@
"sketch_text.path_hint": "Curva del boceto que seguirá el texto: haz clic aquí y después en la curva",
"sketch_text.use_font_file": "Usar el archivo de fuente {path}",
"solid": "sólido",
"the angle must be a number": "el ángulo debe ser un número",
"the curves must be in one sketch": "las curvas deben estar en un solo boceto",
"the direction must be an axis, a sketch line or a straight edge": "la dirección debe ser un eje, una línea de boceto o una arista recta",
"the duration must be a number of seconds, 0.001 or more": "la duración debe ser un número de segundos, 0.001 o más",
"the line's sketch is gone": "el boceto de la línea ya no existe",
"the lines must be in one sketch": "las líneas deben estar en un solo boceto",
"the lines must be in one sketch, not the section's": "las líneas deben estar en un solo boceto, no en el de la sección",
"the parts library is empty": "la biblioteca de piezas está vacía",
"the parts library is not available": "la biblioteca de piezas no está disponible",
"the path curves must be in one other sketch": "las curvas de la trayectoria deben estar en un solo boceto distinto",
"the path's sketch is gone": "el boceto de la trayectoria ya no existe",
"the position must be a number": "la posición debe ser un número",
"the preview failed": "falló la vista previa",
"the profiles and curves must be in one sketch": "los perfiles y las curvas deben estar en un solo boceto",
"the reference face must be on the edges' body": "la cara de referencia debe estar en el cuerpo de las aristas",
"the scale must be above 0": "la escala debe ser mayor que 0",
"the section's profiles must be in one sketch": "los perfiles de la sección deben estar en un solo boceto",
"the two origins are on the same component": "los dos orígenes están en el mismo componente",
"thread.automatic_size": "Tamaño automático",
"thread.designation": "Designación",
"thread.iso_metric": "Métrica ISO",
@@ -1966,7 +1783,6 @@
"trace.points": "{count} puntos a lo largo de la trayectoria",
"trace.stopped_by_contact": "Termina en la muestra {sample}: {a} atravesaría {b}",
"trace.stopped_by_loop": "Termina en la muestra {sample}: el bucle que pasa por {joint} no puede cerrarse más",
"trim by sketch curves, or by one plane or face": "recorta con curvas de boceto, o con un plano o una cara",
"ui.1_degree_of_freedom": "1 grado de libertad",
"ui.3d_mouse_axes_reassigned": "Los ejes están reasignados en Preferencias, y eso sigue aplicándose sobre esta calibración.",
"ui.3d_mouse_button": "Botón {number}",
@@ -2393,8 +2209,6 @@
"ui.design_properties.revision": "Revisión",
"ui.diameter": "Diámetro",
"ui.dimension": "Cota",
"ui.direct_interaction": "Interacción directa",
"ui.direct_interaction_note": "Trabaja directamente sobre el modelo en la vista. Pulsa una cara plana o un perfil y tira: hacia arriba extruye, hacia abajo dentro de un cuerpo corta, y el movimiento lateral fija el paso",
"ui.direction": "Dirección",
"ui.display_quality": "Calidad de visualización",
"ui.display_quality_coarse": "Baja",
@@ -2418,7 +2232,6 @@
"ui.editJointLimits": "Editar límites de unión",
"ui.edit_section_analysis": "Editar análisis de sección",
"ui.ellipse": "Elipse",
"ui.emboss_wrap_note": "Los perfiles de líneas ortogonales tangentes a un cilindro compatible se envuelven automáticamente. Elegir una pared para definir el destino.",
"ui.end_radius": "Radio final",
"ui.end_scale": "Escala final",
"ui.entities": "Entidades",
@@ -2484,7 +2297,6 @@
"ui.heading_extrude": "EXTRUIR",
"ui.heading_fillet": "REDONDEO",
"ui.heading_finish_sketch": "FINALIZAR BOCETO",
"ui.heading_form": "FORMA",
"ui.heading_front": "FRONTAL",
"ui.heading_hole": "AGUJERO",
"ui.heading_home": "INICIO",
@@ -2533,7 +2345,6 @@
"ui.heading_utilities": "UTILIDADES",
"ui.heading_utility": "UTILIDAD",
"ui.heading_weldments": "SOLDADURA",
"ui.height": "Altura",
"ui.help": "Ayuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar ejes",
@@ -2561,14 +2372,11 @@
"ui.intersect": "Intersecar",
"ui.isolate": "Aislar",
"ui.join": "Unir",
"ui.joint_play": "▶ Reproducir",
"ui.joint_stop": "■ Detener",
"ui.joints": "Uniones",
"ui.keyboard_shortcuts": "Atajos de teclado",
"ui.language": "Idioma",
"ui.layout_compact": "Compacta",
"ui.layout_floating": "Flotante",
"ui.length": "Longitud",
"ui.length_decimals": "Decimales de longitud",
"ui.light": "Claro",
"ui.line": "Línea",
@@ -2576,7 +2384,6 @@
"ui.loft": "Solevación",
"ui.look_at": "Mirar hacia",
"ui.look_at_anim_desc": "Vista de eje, Mirar hacia, Inicio y Ajustar",
"ui.major_radius": "Radio mayor",
"ui.manipulator_free_alt": "Arrastre libre del manipulador también con Alt/Option",
"ui.manipulator_free_alt_tip": "Activado: Alt/Option o Ctrl/Cmd sobre las flechas de extrusión / manipulador desactiva el ajuste de valores. Desactivado: solo Ctrl/Cmd.",
"ui.manufacture": "Fabricación",
@@ -2606,10 +2413,8 @@
"ui.mesh_format_3mf": "3MF",
"ui.mesh_format_3mf_tagged": "3MF con etiquetas de cara",
"ui.mesh_format_stl": "STL",
"ui.minor_radius": "Radio menor",
"ui.mirror": "Simetría",
"ui.mirror_line": "Línea de simetría",
"ui.modifiers_either": "{first} o {second}",
"ui.move": "Mover",
"ui.multiple": "Múltiple",
"ui.named_views": "Vistas guardadas",
@@ -2708,7 +2513,6 @@
"ui.reverse_horizontal_pan_direction": "Invertir dirección del desplazamiento horizontal",
"ui.reverse_horizontal_pan_tip": "El desplazamiento horizontal con dos dedos y la rueda de pulgar desplazan en sentido contrario",
"ui.reverse_zoom_direction": "Invertir dirección del zoom",
"ui.revolutions": "Vueltas",
"ui.revolve": "Revolución",
"ui.ribbon_exit": "Salir",
"ui.ribbon_labels": "Mostrar texto bajo los iconos de la cinta",
@@ -2724,7 +2528,6 @@
"ui.scale": "Escala",
"ui.second_rail": "Segunda línea guía",
"ui.section_analysis": "Análisis de sección",
"ui.section_size": "Tamaño de la sección",
"ui.select": "Seleccionar",
"ui.selectOther": "Seleccionar otro…",
"ui.selectTangentChain": "Seleccionar cadena tangente",
@@ -2775,7 +2578,6 @@
"ui.start": "Iniciar",
"ui.start_over": "Volver a empezar",
"ui.start_page": "Página de inicio",
"ui.stitch_tolerance_hint": "estricta",
"ui.stretch": "Estirar",
"ui.supersampling": "Supermuestreo (SSAA)",
"ui.supersampling_adaptive": "Solo cuando la vista está inmóvil",
@@ -2806,8 +2608,6 @@
"ui.to_object": "Hasta objeto",
"ui.tool_direction": "Dirección de herramienta",
"ui.top": "Superior",
"ui.top_diameter": "Diámetro superior",
"ui.top_radius": "Radio superior",
"ui.torus": "Toro",
"ui.total_angle": "Ángulo total",
"ui.trackpad_pan_tip": "Los gestos del panel táctil desplazan la vista (desactivado: hacen zoom como la rueda); la rueda del ratón hace zoom, la rueda lateral desplaza horizontalmente y el gesto de pellizco siempre hace zoom",
@@ -2833,7 +2633,6 @@
"ui.visual_shaded_with_edges": "Sombreado con aristas",
"ui.visual_style": "Estilo visual",
"ui.visual_wireframe": "Estructura alámbrica",
"ui.width": "Anchura",
"ui.workspace": "espacio de trabajo",
"ui.workspace_animation_description": "Vistas explosionadas, estudios de movimiento y guiones gráficos",
"ui.workspace_available": "Disponible",
@@ -2889,8 +2688,5 @@
"update.up_to_date": "SolidCraft está actualizado.",
"update.update_now": "Actualizar ahora",
"update.you_have": "Tienes la versión {version}.",
"use a positive nominal diameter and a pitch smaller than it": "usa un diámetro nominal positivo y un paso menor que él",
"values must be finite numbers": "los valores deben ser números finitos",
"values must be numbers or expressions": "los valores deben ser números o expresiones",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ elegir · Intro / Tab completar · Esc cerrar"
}
+2 -1380
View File
File diff suppressed because it is too large Load Diff
+2 -902
View File
File diff suppressed because it is too large Load Diff
+1 -525
View File
File diff suppressed because it is too large Load Diff
+2 -960
View File
File diff suppressed because it is too large Load Diff
+2 -912
View File
File diff suppressed because it is too large Load Diff
+2 -929
View File
File diff suppressed because it is too large Load Diff
+3 -527
View File
File diff suppressed because it is too large Load Diff
+1 -487
View File
File diff suppressed because it is too large Load Diff
+2 -902
View File
File diff suppressed because it is too large Load Diff
+2 -712
View File
File diff suppressed because it is too large Load Diff
+2 -960
View File
File diff suppressed because it is too large Load Diff
+1 -16
View File
@@ -961,7 +961,7 @@
"home.new_mm": "Nowy projekt",
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
"home.open": "Otwórz…",
"home.open_hint": "Projekt .sc ({shortcut})",
"home.open_hint": "Projekt .solidcraft ({shortcut})",
"home.open_now": "Otwórz teraz",
"home.opened_days_ago": "{count} dni temu",
"home.opened_hours_ago": "{count} godz. temu",
@@ -1057,11 +1057,6 @@
"no picture": "brak obrazu",
"none": "brak",
"parts.automatic_name": "automatycznie",
"parts.dimension.d": "Średnica nominalna",
"parts.dimension.height": "Wysokość",
"parts.dimension.pitch": "Skok",
"parts.dimension.thickness": "Grubość",
"parts.dimension.width": "Szerokość",
"pattern.alignment_2": "Wyrównanie 2",
"pattern.alignment_hint": "Liczba obejmuje oryginał. Wyśrodkuj rozkłada kopie po obu stronach; Odwróć wybiera stronę z dodatkową kopią przy parzystej liczbie.",
"pattern.direction_1": "Kierunek 1",
@@ -1178,12 +1173,6 @@
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
"prefs.theme_file": "plik motywu",
"prefs.theme_follow": "Śledź plik motywu",
"prefs.theme_follow_error": "Nie zastosowano: {error}. Ostatni poprawny motyw pozostaje aktywny.",
"prefs.theme_follow_launch": "Śledzenie tylko w tej sesji (--theme-file); plik jest wczytywany ponownie po każdym nadpisaniu przez inne narzędzie.",
"prefs.theme_follow_note": "Plik jest wczytywany ponownie po każdym nadpisaniu przez inne narzędzie. Wybór motywu kończy śledzenie.",
"prefs.theme_follow_off": "Wyłączone",
"prefs.theme_follow_stop": "Zakończ śledzenie",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "teraz",
"prefs.timeline_quarter_hour": "−15",
@@ -1898,7 +1887,6 @@
"ui.heading_utilities": "NARZĘDZIA",
"ui.heading_utility": "NARZĘDZIE",
"ui.heading_weldments": "SPAWANIE",
"ui.height": "Wysokość",
"ui.help": "Pomoc",
"ui.hide": "Ukryj",
"ui.hideAxes": "Ukryj osie",
@@ -1937,7 +1925,6 @@
"ui.language": "Język",
"ui.layout_compact": "Kompaktowy",
"ui.layout_floating": "Pływający",
"ui.length": "Długość",
"ui.length_decimals": "Miejsca dziesiętne długości",
"ui.light": "Jasny",
"ui.line": "Linia",
@@ -2026,7 +2013,6 @@
"ui.pinToShortcuts": "Przypnij do skrótów",
"ui.pinToToolbar": "Przypnij do paska narzędzi",
"ui.pipe": "Rura",
"ui.pitch": "Skok",
"ui.placement": "Umiejscowienie",
"ui.placement_handles": "Uchwyty umieszczania",
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
@@ -2202,7 +2188,6 @@
"ui.visual_shaded_with_edges": "Cieniowany z krawędziami",
"ui.visual_style": "Styl wizualny",
"ui.visual_wireframe": "Model krawędziowy",
"ui.width": "Szerokość",
"ui.workspace": "środowisko pracy",
"ui.workspace_animation_description": "Widoki rozstrzelone, badania ruchu i scenorysy",
"ui.workspace_available": "Dostępne",
+1 -18
View File
@@ -1074,7 +1074,7 @@
"home.new_mm": "Novo projeto",
"home.new_mm_hint": "Milímetros · abre em Projeto",
"home.open": "Abrir…",
"home.open_hint": "Um projeto .sc ({shortcut})",
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Há {count} dias",
"home.opened_hours_ago": "Há {count} h",
@@ -1159,9 +1159,7 @@
"nav.free_orbit.label": "Órbita livre",
"nav.free_orbit.snap_help": "Mouse e mouse 3D giram através dos dois polos e preservam a rotação da imagem. Mantenha Shift pressionado durante uma órbita por arraste para alinhar a direção da vista e a rotação da imagem.",
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.hfss.help": "Alt+arrastar com o esquerdo desloca, Shift+Alt+esquerdo aplica zoom, Ctrl+arrastar com o esquerdo em área vazia orbita",
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.maya.help": "Alt+arrastar com o esquerdo orbita, Alt+meio desloca, Alt+direito aplica zoom; arrastar com o meio também desloca",
"nav.openscad.help": "Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
"nav.solidworks.help": "Arrastar com o meio orbita, Ctrl+meio desloca, Shift+meio aplica zoom, arrastar com o direito orbita",
"nav.tinkercad.help": "Arrastar com o direito orbita, arrastar com o meio ou Shift+direito desloca",
@@ -1180,11 +1178,6 @@
"no picture": "sem imagem",
"none": "nenhum",
"parts.automatic_name": "automático",
"parts.dimension.d": "Diâmetro nominal",
"parts.dimension.height": "Altura",
"parts.dimension.pitch": "Passo",
"parts.dimension.thickness": "Espessura",
"parts.dimension.width": "Largura",
"pattern.alignment_2": "Alinhamento 2",
"pattern.alignment_hint": "As contagens incluem o original. Centralizar equilibra as cópias; Inverter escolhe o lado extra para totais pares.",
"pattern.direction_1": "Direção 1",
@@ -1300,12 +1293,6 @@
"prefs.supersampling_brief": "Desenha a vista maior e a reduz com filtragem: muito nítida, e mais pesada.",
"prefs.tab_accent_note": "A cor de destaque acompanha a aba ativa",
"prefs.theme_file": "arquivo de tema",
"prefs.theme_follow": "Seguir um arquivo de tema",
"prefs.theme_follow_error": "Não aplicado: {error}. O último tema válido continua ativo.",
"prefs.theme_follow_launch": "Seguido apenas nesta sessão (--theme-file); recarregado sempre que outra ferramenta reescreve o arquivo.",
"prefs.theme_follow_note": "Recarregado sempre que outra ferramenta reescreve o arquivo. Escolher um tema interrompe o acompanhamento.",
"prefs.theme_follow_off": "Desativado",
"prefs.theme_follow_stop": "Parar de seguir",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "agora",
"prefs.timeline_quarter_hour": "−15",
@@ -2030,7 +2017,6 @@
"ui.heading_utilities": "UTILITÁRIOS",
"ui.heading_utility": "UTILITÁRIO",
"ui.heading_weldments": "SOLDAGEM",
"ui.height": "Altura",
"ui.help": "Ajuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar eixos",
@@ -2069,7 +2055,6 @@
"ui.language": "Idioma",
"ui.layout_compact": "Compacto",
"ui.layout_floating": "Flutuante",
"ui.length": "Comprimento",
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
@@ -2157,7 +2142,6 @@
"ui.pinToShortcuts": "Fixar nos atalhos",
"ui.pinToToolbar": "Fixar na barra de ferramentas",
"ui.pipe": "Tubo",
"ui.pitch": "Passo",
"ui.placement": "Posicionamento",
"ui.plane": "Plano",
"ui.plane_at_angle": "Plano em ângulo",
@@ -2332,7 +2316,6 @@
"ui.visual_shaded_with_edges": "Sombreado com arestas",
"ui.visual_style": "Estilo visual",
"ui.visual_wireframe": "Aramado",
"ui.width": "Largura",
"ui.workspace": "espaço de trabalho",
"ui.workspace_animation_description": "Vistas explodidas, estudos de movimento e storyboards",
"ui.workspace_available": "Disponível",
+1 -13
View File
@@ -899,7 +899,7 @@
"home.new_mm": "Proiect nou",
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
"home.open": "Deschide…",
"home.open_hint": "Un proiect .sc ({shortcut})",
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
"home.open_now": "Deschis acum",
"home.opened_days_ago": "Acum {count} zile",
"home.opened_hours_ago": "Acum {count} h",
@@ -993,9 +993,6 @@
"no picture": "fără imagine",
"none": "niciunul",
"parts.automatic_name": "automat",
"parts.dimension.height": "Înălțime",
"parts.dimension.thickness": "Grosime",
"parts.dimension.width": "Lățime",
"pattern.alignment_2": "Aliniere 2",
"pattern.alignment_hint": "Numărul include originalul. Centrat echilibrează copiile; Inversare alege partea suplimentară pentru totaluri pare.",
"pattern.direction_1": "Direcția 1",
@@ -1005,12 +1002,6 @@
"pick one or two vertices, edges or faces": "alegeți unul sau două vârfuri, muchii sau fețe",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Urmărește un fișier de temă",
"prefs.theme_follow_error": "Nu s-a aplicat: {error}. Ultima temă validă rămâne activă.",
"prefs.theme_follow_launch": "Urmărit doar în această sesiune (--theme-file); reîncărcat când alt instrument rescrie fișierul.",
"prefs.theme_follow_note": "Reîncărcat când alt instrument rescrie fișierul. Alegerea unei teme oprește urmărirea.",
"prefs.theme_follow_off": "Dezactivat",
"prefs.theme_follow_stop": "Oprește urmărirea",
"properties.title": "Proprietăți: {name}",
"recovery.days_ago": "acum {count} zile",
"recovery.detail": "{file} · {features} operații · salvat automat {when}",
@@ -1645,7 +1636,6 @@
"ui.heading_utilities": "UTILITARE",
"ui.heading_utility": "UTILITAR",
"ui.heading_weldments": "SUDURĂ",
"ui.height": "Înălțime",
"ui.help": "Ajutor",
"ui.hide": "Ascunde",
"ui.hideAxes": "Ascunde axele",
@@ -1677,7 +1667,6 @@
"ui.language": "Limbă",
"ui.layout_compact": "Compact",
"ui.layout_floating": "Plutitor",
"ui.length": "Lungime",
"ui.length_decimals": "Zecimale lungime",
"ui.light": "Luminos",
"ui.line": "Linie",
@@ -1934,7 +1923,6 @@
"ui.visual_shaded_with_edges": "Umbrit cu muchii",
"ui.visual_style": "Stil vizual",
"ui.visual_wireframe": "Cadru de sârmă",
"ui.width": "Lățime",
"ui.workspace": "spațiu de lucru",
"ui.workspace_animation_description": "Vederi explodate, studii de mișcare și storyboard-uri",
"ui.workspace_available": "Disponibil",
+2 -855
View File
File diff suppressed because it is too large Load Diff
+2 -960
View File
File diff suppressed because it is too large Load Diff
+2 -1499
View File
File diff suppressed because it is too large Load Diff
+2 -994
View File
File diff suppressed because it is too large Load Diff
+2 -526
View File
File diff suppressed because it is too large Load Diff
+2 -1102
View File
File diff suppressed because it is too large Load Diff
+2 -526
View File
File diff suppressed because it is too large Load Diff
@@ -5,12 +5,12 @@ GenericName=3D CAD
Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild
Exec=solidcraft %f
TryExec=solidcraft
Icon=io.tangencylabs.solidcraft
Icon=ai.storyteller.solidcraft
Terminal=false
StartupNotify=true
StartupWMClass=io.tangencylabs.solidcraft
StartupWMClass=ai.storyteller.solidcraft
Categories=Graphics;3DGraphics;Engineering;
Keywords=cad;3d;parametric;sketch;step;stl;modelling;modeling;
MimeType=application/x-solidcraft;model/step;model/iges;model/stl;model/3mf;
# Desktop ids of earlier releases; launchers keep pinned entries across the renames.
X-Flatpak-RenamedFrom=ai.storyteller.solidcraft.desktop;ai.storyteller.solvecraft.desktop;
# Releases before 0.8 used this desktop id; launchers keep pinned entries across the rename.
X-Flatpak-RenamedFrom=ai.storyteller.solvecraft.desktop;
@@ -2,7 +2,7 @@
<!-- AppStream metadata (software centres, Flathub). Template: package.sh substitutes @VERSION@
and @DATE@ from Cargo.toml and the build date. -->
<component type="desktop-application">
<id>io.tangencylabs.solidcraft</id>
<id>ai.storyteller.solidcraft</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>SolidCraft</name>
@@ -24,8 +24,8 @@
<li>Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server</li>
</ul>
</description>
<launchable type="desktop-id">io.tangencylabs.solidcraft.desktop</launchable>
<icon type="stock">io.tangencylabs.solidcraft</icon>
<launchable type="desktop-id">ai.storyteller.solidcraft.desktop</launchable>
<icon type="stock">ai.storyteller.solidcraft</icon>
<url type="homepage">https://docs.solidcraft.io/</url>
<url type="bugtracker">https://github.com/bherbruck/solidcraft/issues</url>
<url type="help">https://docs.solidcraft.io/</url>
@@ -56,15 +56,11 @@
<binary>solidcraft</binary>
<binary>solidcraft-cli</binary>
<mediatype>application/x-solidcraft</mediatype>
<!-- The app and desktop ids of earlier releases (docs/compatibility.md). -->
<id>ai.storyteller.solidcraft</id>
<id>ai.storyteller.solidcraft.desktop</id>
<!-- The desktop id of releases before 0.8 (docs/compatibility.md). -->
<id>ai.storyteller.solvecraft.desktop</id>
</provides>
<!-- Earlier app ids (docs/compatibility.md): ai.storyteller.solidcraft from 0.8 until the move
to Tangency Labs, ai.storyteller.solvecraft before 0.8. -->
<!-- Releases before 0.8 used the app id ai.storyteller.solvecraft (docs/compatibility.md). -->
<replaces>
<id>ai.storyteller.solidcraft</id>
<id>ai.storyteller.solvecraft</id>
</replaces>
<content_rating type="oars-1.1"/>
@@ -4,13 +4,10 @@
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-solidcraft">
<comment>SolidCraft design</comment>
<icon name="io.tangencylabs.solidcraft"/>
<icon name="ai.storyteller.solidcraft"/>
<sub-class-of type="application/json"/>
<!-- shared-mime-info also gives *.sc to Scala scripts (text/x-scala, weight 50); with
SolidCraft installed its designs win. -->
<glob pattern="*.sc" weight="60"/>
<!-- Compatibility shim (docs/compatibility.md): designs saved before .sc, and before the rename. -->
<glob pattern="*.solidcraft"/>
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename. -->
<glob pattern="*.solvecraft"/>
<alias type="application/x-solvecraft"/>
</mime-type>
+3 -25
View File
@@ -13,7 +13,7 @@
<key>CFBundleExecutable</key>
<string>SolidCraft</string>
<key>CFBundleIdentifier</key>
<string>io.tangencylabs.solidcraft</string>
<string>ai.storyteller.solidcraft</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
@@ -45,11 +45,8 @@
<key>CFBundleTypeName</key>
<string>SolidCraft design</string>
<key>CFBundleTypeExtensions</key>
<!-- solidcraft: designs saved before .sc; solvecraft: before the rename (compatibility
shims, docs/compatibility.md). -->
<array><string>sc</string><string>solidcraft</string><string>solvecraft</string></array>
<key>LSItemContentTypes</key>
<array><string>io.tangencylabs.solidcraft.design</string></array>
<!-- solvecraft: designs saved before the rename (compatibility shim, docs/compatibility.md). -->
<array><string>solidcraft</string><string>solvecraft</string></array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
@@ -66,24 +63,5 @@
<string>Alternate</string>
</dict>
</array>
<!-- The design type: .sc, and the older .solidcraft and .solvecraft (docs/compatibility.md). -->
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>io.tangencylabs.solidcraft.design</string>
<key>UTTypeDescription</key>
<string>SolidCraft design</string>
<key>UTTypeConformsTo</key>
<array><string>public.data</string><string>public.content</string></array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array><string>sc</string><string>solidcraft</string><string>solvecraft</string></array>
<key>public.mime-type</key>
<array><string>application/x-solidcraft</string></array>
</dict>
</dict>
</array>
</dict>
</plist>
+3 -3
View File
@@ -11,6 +11,6 @@ Some earlier releases were installed as SolveCraft.app. Move that one to the Tra
once SolidCraft.app is in Applications, and replace any Dock shortcut with the
old name (open SolidCraft.app, then choose Options > Keep in Dock).
Designs saved as .solidcraft or .solvecraft still open, and SolidCraft copies
your settings and recovery files from the old SolveCraft folder on its first
launch. New designs are saved as .sc.
Designs saved as .solvecraft still open, and SolidCraft copies your settings
and recovery files from the old SolveCraft folder on its first launch.
New designs are saved as .solidcraft.
+6 -9
View File
@@ -9,7 +9,7 @@
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolidCraft by
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli (and adds its folder to the system PATH), the licence files (including
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.sc` (and of the older `.solidcraft` and pre-rename `.solvecraft`), and offers it under
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.solidcraft` (and of the pre-rename `.solvecraft`), and offers it under
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
-->
@@ -84,17 +84,15 @@
Icon="SolidcraftIcon.exe" IconIndex="0" />
</File>
<!-- .sc: SolidCraft's own format; SolidCraft is its default app. -->
<!-- .solidcraft: SolidCraft's own format; SolidCraft is its default app. -->
<ProgId Id="Solidcraft.Design" Description="SolidCraft Design" Icon="SolidcraftExe" IconIndex="0">
<Extension Id="sc" ContentType="application/x-solidcraft">
<Extension Id="solidcraft" ContentType="application/x-solidcraft">
<Verb Id="open" Command="Open" TargetFile="SolidcraftExe" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
<!-- Compatibility shim (docs/compatibility.md): designs saved as .solidcraft (before .sc)
and as .solvecraft (before the rename) open as the same ProgId. Registered by hand: a
second Extension under one ProgId would write its open verb twice (WIX0091). -->
<RegistryValue Root="HKLM" Key="Software\Classes\.solidcraft" Value="Solidcraft.Design" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.solidcraft" Name="Content Type" Value="application/x-solidcraft" Type="string" />
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename open as
the same ProgId. Registered by hand: a second Extension under one ProgId would write
its open verb twice (WIX0091). -->
<RegistryValue Root="HKLM" Key="Software\Classes\.solvecraft" Value="Solidcraft.Design" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.solvecraft" Name="Content Type" Value="application/x-solvecraft" Type="string" />
<!-- STEP, IGES and meshes: offered under "Open with", never forced as the default. -->
@@ -110,7 +108,6 @@
<!-- "Open with" app list and the Run dialog (Win+R "solidcraft"). -->
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe" Name="FriendlyAppName" Value="SolidCraft" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\shell\open\command" Value="&quot;[#SolidcraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".sc" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".solidcraft" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".solvecraft" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".step" Value="" Type="string" />
-19
View File
@@ -8,15 +8,6 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
impl Tokens {
pub const fn light() -> Tokens {
Tokens {
nav_hover: Color32::from_rgb(233, 237, 242),
nav_press: Color32::from_rgb(220, 226, 234),
nav_dim: Color32::from_rgb(136, 149, 167),
nav_viewport: Color32::from_rgb(238, 241, 245),
nav_tip: Color32::from_rgb(27, 36, 48),
nav_tip_ink: Color32::from_rgb(255, 255, 255),
nav_tip_key_border: Color32::from_rgba_unmultiplied_const(128, 140, 160, 153),
nav_field: Color32::from_rgb(255, 255, 255),
borders: crate::theme::Borders::LEGACY,
icon_palette: None,
file_dim: Color32::from_rgb(58, 70, 86),
@@ -82,7 +73,6 @@ impl Tokens {
form_ink: Color32::from_rgb(27, 36, 48),
form_dim: Color32::from_rgb(74, 88, 104),
form_primary: Color32::from_rgb(35, 110, 173),
form_primary_fill: Color32::from_rgb(35, 110, 173),
form_error: Color32::from_rgb(161, 52, 44),
form_error_fill: Color32::from_rgb(252, 239, 237),
form_success: Color32::from_rgb(39, 107, 44),
@@ -323,14 +313,6 @@ impl Tokens {
t.render_ink = Color32::from_rgb(224, 116, 174);
t.render_soft = Color32::from_rgb(64, 35, 54);
t.tab_on_fill_dark = Color32::BLACK;
t.nav_hover = Color32::from_rgb(58, 65, 80);
t.nav_press = Color32::from_rgb(70, 78, 96);
t.nav_dim = Color32::from_rgb(124, 135, 154);
t.nav_viewport = Color32::from_rgb(38, 43, 53);
t.nav_tip = Color32::from_rgb(238, 241, 245);
t.nav_tip_ink = Color32::from_rgb(27, 36, 48);
t.nav_tip_key_border = Color32::from_rgba_unmultiplied_const(128, 140, 160, 153);
t.nav_field = Color32::from_rgb(47, 52, 62);
t.dialog_body = Color32::from_rgb(39, 43, 52);
t.dialog_footer = Color32::from_rgb(47, 52, 62);
t.dialog_rule = Color32::from_rgb(53, 59, 71);
@@ -381,7 +363,6 @@ impl Tokens {
t.form_ink = t.text;
t.form_dim = Color32::from_rgb(180, 187, 200);
t.form_primary = t.accent;
t.form_primary_fill = t.form_primary;
t.form_error = t.error;
t.form_error_fill = t.timeline_item_error;
t.form_success = Color32::from_rgb(143, 219, 154);
-255
View File
@@ -1,255 +0,0 @@
{
"base": "dark",
"colors": {
"accent": "#ffffff",
"accent_soft": "#000000",
"agent_busy": "#ffffff",
"agent_ok": "#ffffff",
"app_bar": "#000000",
"app_bar_hover": "#000000",
"app_bar_hover_button": "#000000",
"app_bar_hover_strong": "#000000",
"app_bar_hover_subtle": "#000000",
"app_bar_separator": "#ffffff",
"app_bar_tab_active": "#000000",
"app_bar_text": "#ffffff",
"axis_x": "#ffffff",
"axis_y": "#ffffff",
"axis_z": "#ffffff",
"bend_down": "#ffffff",
"bend_up": "#ffffff",
"body": "#000000",
"body_edge": "#ffffff",
"body_selected": "#ffffff",
"border": "#ffffff",
"box_crossing": "#ffffff",
"box_window": "#ffffff",
"chip": "#ffffff",
"construction_geom": "#ffffff",
"construction_plane": "#000000",
"continuity": [
"#ffffff",
"#ffffff",
"#ffffff",
"#ffffff"
],
"control_border": "#ffffff",
"control_fill": "#000000",
"cube_border": "#ffffff",
"cube_face": "#000000",
"cube_hover": "#000000",
"cube_text": "#ffffff",
"dialog_bg": "#000000",
"dialog_body": "#000000",
"dialog_border": "#ffffff",
"dialog_footer": "#000000",
"dialog_label": "#ffffff",
"dialog_rule": "#ffffff",
"dimension": "#ffffff",
"dimension_driven": "#ffffff",
"error": "#ffffff",
"field": "#000000",
"file_background": "#000000",
"file_border": "#ffffff",
"file_dim": "#ffffff",
"file_image_tint": "#ffffff",
"file_primary": "#000000",
"file_primary_border": "#ffffff",
"file_primary_ink": "#ffffff",
"file_rail": "#000000",
"file_rail_hover": "#000000",
"file_rail_ink": "#ffffff",
"file_recovery": "#000000",
"file_recovery_border": "#ffffff",
"file_recovery_ink": "#ffffff",
"file_recovery_well": "#000000",
"file_surface": "#000000",
"form_accent": "#ffffff",
"form_dim": "#ffffff",
"form_error": "#ffffff",
"form_error_fill": "#000000",
"form_fill": "#ffffff",
"form_ink": "#ffffff",
"form_primary": "#000000",
"form_primary_fill": "#ffffff",
"form_soft": "#000000",
"form_success": "#ffffff",
"form_success_fill": "#000000",
"form_tab_ink": "#ffffff",
"glyph_bg": "#000000",
"glyph_edge": "#ffffff",
"glyph_text": "#ffffff",
"grid": "#000000",
"grid_major": "#ffffff",
"hover": "#000000",
"hover_edge": "#ffffff",
"hover_edge_halo": "#000000",
"hover_profile": "#ffffff",
"hover_profile_edge": "#000000",
"icon": "#ffffff",
"icons.agent": "#ffffff",
"icons.highlight": "#000000",
"icons.marker_halo": "#000000",
"interference": "#ffffff",
"logo": [
"#ffffff",
"#ffffff",
"#ffffff"
],
"manipulator": "#ffffff",
"mesh_accent": "#ffffff",
"mesh_fill": "#ffffff",
"mesh_ink": "#ffffff",
"mesh_soft": "#000000",
"nav_dim": "#ffffff",
"nav_field": "#000000",
"nav_hover": "#000000",
"nav_press": "#ffffff",
"nav_tip": "#ffffff",
"nav_tip_ink": "#000000",
"nav_tip_key_border": "#000000",
"nav_viewport": "#000000",
"on_accent": "#000000",
"origin_plane": "#000000",
"origin_plane_edge": "#ffffff",
"origin_plane_hover": "#ffffff",
"origin_point": "#ffffff",
"overlay": "#000000",
"panel": "#000000",
"panel_header": "#000000",
"preview_add": "#ffffff",
"preview_cut": "#ffffff",
"preview_new": "#ffffff",
"preview_sketch": "#ffffff",
"profile_fill": "#000000",
"render_accent": "#ffffff",
"render_fill": "#ffffff",
"render_ink": "#ffffff",
"render_soft": "#000000",
"rubber_band": "#ffffff",
"section_cap": "#ffffff",
"sel_edge": "#000000",
"sel_edge_rim": "#000000",
"sel_face": "#ffffff",
"sel_profile": "#ffffff",
"sel_profile_edge": "#000000",
"sel_vertex": "#ffffff",
"selected_bg": "#ffffff",
"selected_ink": "#000000",
"sheet_fill": "#ffffff",
"sheet_ink": "#ffffff",
"sheet_soft": "#000000",
"sketch": "#ffffff",
"sketch_accent": "#ffffff",
"sketch_centerline": "#ffffff",
"sketch_construction": "#ffffff",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#ffffff",
"sketch_locked": "#ffffff",
"sketch_lost": "#ffffff",
"sketch_point": "#ffffff",
"sketch_point_free": "#ffffff",
"sketch_projected": "#ffffff",
"sketch_soft": "#000000",
"surface_accent": "#ffffff",
"surface_fill": "#ffffff",
"surface_ink": "#ffffff",
"surface_soft": "#000000",
"tab_on_fill_dark": "#ffffff",
"text": "#ffffff",
"text_dim": "#ffffff",
"timeline": "#000000",
"timeline_item": "#000000",
"timeline_item_error": "#000000",
"timeline_marker": "#ffffff",
"toolbar": "#000000",
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffffff"
},
"format": "solidcraft-theme/1",
"name": "1-bit Dark",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#ffffff",
"line": "#ffffff",
"passive-1": "#000000",
"passive-2": "#000000",
"passive-3": "#000000",
"guide": "#ffffff",
"solid": [
"#000000",
"#000000",
"#ffffff"
],
"surface": [
"#000000",
"#000000",
"#ffffff"
],
"mesh": [
"#000000",
"#000000",
"#ffffff"
],
"form": [
"#000000",
"#000000",
"#ffffff"
],
"sketch": [
"#000000",
"#000000",
"#ffffff"
],
"constraint": [
"#000000",
"#000000",
"#ffffff"
],
"construction": [
"#000000",
"#000000",
"#ffffff"
],
"active-1": "#000000",
"active-2": "#000000",
"active-3": "#ffffff",
"component-orange-1": "#000000",
"component-orange-2": "#000000",
"component-orange-3": "#ffffff",
"component-yellow-1": "#000000",
"component-yellow-2": "#000000",
"component-yellow-3": "#ffffff"
},
"borders": {
"primary": {
"width": 1,
"color": "#ffffff"
},
"secondary": {
"width": 1,
"color": "#ffffff"
},
"tertiary": {
"width": 1,
"color": "#ffffff"
},
"container": {
"width": 1,
"color": "#ffffff"
},
"control": {
"width": 1,
"color": "#ffffff"
}
}
}
-255
View File
@@ -1,255 +0,0 @@
{
"base": "light",
"colors": {
"accent": "#000000",
"accent_soft": "#ffffff",
"agent_busy": "#000000",
"agent_ok": "#000000",
"app_bar": "#ffffff",
"app_bar_hover": "#ffffff",
"app_bar_hover_button": "#ffffff",
"app_bar_hover_strong": "#ffffff",
"app_bar_hover_subtle": "#ffffff",
"app_bar_separator": "#000000",
"app_bar_tab_active": "#ffffff",
"app_bar_text": "#000000",
"axis_x": "#000000",
"axis_y": "#000000",
"axis_z": "#000000",
"bend_down": "#000000",
"bend_up": "#000000",
"body": "#ffffff",
"body_edge": "#000000",
"body_selected": "#000000",
"border": "#000000",
"box_crossing": "#000000",
"box_window": "#000000",
"chip": "#000000",
"construction_geom": "#000000",
"construction_plane": "#ffffff",
"continuity": [
"#000000",
"#000000",
"#000000",
"#000000"
],
"control_border": "#000000",
"control_fill": "#ffffff",
"cube_border": "#000000",
"cube_face": "#ffffff",
"cube_hover": "#ffffff",
"cube_text": "#000000",
"dialog_bg": "#ffffff",
"dialog_body": "#ffffff",
"dialog_border": "#000000",
"dialog_footer": "#ffffff",
"dialog_label": "#000000",
"dialog_rule": "#000000",
"dimension": "#000000",
"dimension_driven": "#000000",
"error": "#000000",
"field": "#ffffff",
"file_background": "#ffffff",
"file_border": "#000000",
"file_dim": "#000000",
"file_image_tint": "#ffffff",
"file_primary": "#ffffff",
"file_primary_border": "#000000",
"file_primary_ink": "#000000",
"file_rail": "#ffffff",
"file_rail_hover": "#ffffff",
"file_rail_ink": "#000000",
"file_recovery": "#ffffff",
"file_recovery_border": "#000000",
"file_recovery_ink": "#000000",
"file_recovery_well": "#ffffff",
"file_surface": "#ffffff",
"form_accent": "#000000",
"form_dim": "#000000",
"form_error": "#000000",
"form_error_fill": "#ffffff",
"form_fill": "#000000",
"form_ink": "#000000",
"form_primary": "#ffffff",
"form_primary_fill": "#000000",
"form_soft": "#ffffff",
"form_success": "#000000",
"form_success_fill": "#ffffff",
"form_tab_ink": "#000000",
"glyph_bg": "#ffffff",
"glyph_edge": "#000000",
"glyph_text": "#000000",
"grid": "#ffffff",
"grid_major": "#000000",
"hover": "#ffffff",
"hover_edge": "#000000",
"hover_edge_halo": "#ffffff",
"hover_profile": "#000000",
"hover_profile_edge": "#ffffff",
"icon": "#000000",
"icons.agent": "#000000",
"icons.highlight": "#ffffff",
"icons.marker_halo": "#ffffff",
"interference": "#000000",
"logo": [
"#000000",
"#000000",
"#000000"
],
"manipulator": "#000000",
"mesh_accent": "#000000",
"mesh_fill": "#000000",
"mesh_ink": "#000000",
"mesh_soft": "#ffffff",
"nav_dim": "#000000",
"nav_field": "#ffffff",
"nav_hover": "#ffffff",
"nav_press": "#000000",
"nav_tip": "#000000",
"nav_tip_ink": "#ffffff",
"nav_tip_key_border": "#ffffff",
"nav_viewport": "#ffffff",
"on_accent": "#ffffff",
"origin_plane": "#ffffff",
"origin_plane_edge": "#000000",
"origin_plane_hover": "#000000",
"origin_point": "#000000",
"overlay": "#ffffff",
"panel": "#ffffff",
"panel_header": "#ffffff",
"preview_add": "#000000",
"preview_cut": "#000000",
"preview_new": "#000000",
"preview_sketch": "#000000",
"profile_fill": "#ffffff",
"render_accent": "#000000",
"render_fill": "#000000",
"render_ink": "#000000",
"render_soft": "#ffffff",
"rubber_band": "#000000",
"section_cap": "#000000",
"sel_edge": "#ffffff",
"sel_edge_rim": "#ffffff",
"sel_face": "#000000",
"sel_profile": "#000000",
"sel_profile_edge": "#ffffff",
"sel_vertex": "#000000",
"selected_bg": "#000000",
"selected_ink": "#ffffff",
"sheet_fill": "#000000",
"sheet_ink": "#000000",
"sheet_soft": "#ffffff",
"sketch": "#000000",
"sketch_accent": "#000000",
"sketch_centerline": "#000000",
"sketch_construction": "#000000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
"sketch_ink": "#000000",
"sketch_locked": "#000000",
"sketch_lost": "#000000",
"sketch_point": "#000000",
"sketch_point_free": "#000000",
"sketch_projected": "#000000",
"sketch_soft": "#ffffff",
"surface_accent": "#000000",
"surface_fill": "#000000",
"surface_ink": "#000000",
"surface_soft": "#ffffff",
"tab_on_fill_dark": "#000000",
"text": "#000000",
"text_dim": "#000000",
"timeline": "#ffffff",
"timeline_item": "#ffffff",
"timeline_item_error": "#ffffff",
"timeline_marker": "#000000",
"toolbar": "#ffffff",
"viewport_bottom": "#ffffff",
"viewport_top": "#ffffff",
"warning": "#000000"
},
"format": "solidcraft-theme/1",
"name": "1-bit",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#000000",
"line": "#000000",
"passive-1": "#ffffff",
"passive-2": "#ffffff",
"passive-3": "#ffffff",
"guide": "#000000",
"solid": [
"#ffffff",
"#ffffff",
"#000000"
],
"surface": [
"#ffffff",
"#ffffff",
"#000000"
],
"mesh": [
"#ffffff",
"#ffffff",
"#000000"
],
"form": [
"#ffffff",
"#ffffff",
"#000000"
],
"sketch": [
"#ffffff",
"#ffffff",
"#000000"
],
"constraint": [
"#ffffff",
"#ffffff",
"#000000"
],
"construction": [
"#ffffff",
"#ffffff",
"#000000"
],
"active-1": "#ffffff",
"active-2": "#ffffff",
"active-3": "#000000",
"component-orange-1": "#ffffff",
"component-orange-2": "#ffffff",
"component-orange-3": "#000000",
"component-yellow-1": "#ffffff",
"component-yellow-2": "#ffffff",
"component-yellow-3": "#000000"
},
"borders": {
"primary": {
"width": 1,
"color": "#000000"
},
"secondary": {
"width": 1,
"color": "#000000"
},
"tertiary": {
"width": 1,
"color": "#000000"
},
"container": {
"width": 1,
"color": "#000000"
},
"control": {
"width": 1,
"color": "#000000"
}
}
}
-3
View File
@@ -54,9 +54,6 @@
"dimension_driven": "#6c6863",
"error": "#c71529",
"field": "#fffdfa",
"file_rail": "#e1ddd8",
"file_rail_hover": "#ebe7e2",
"file_rail_ink": "#13110e",
"form_accent": "#5844bc",
"form_dim": "#6c6863",
"form_error": "#c71529",
-224
View File
@@ -1,224 +0,0 @@
{
"base": "dark",
"colors": {
"accent": "#55ffff",
"accent_soft": "#ff55ff",
"agent_busy": "#55ffff",
"agent_ok": "#55ff55",
"app_bar": "#000000",
"app_bar_hover": "#ffffff18",
"app_bar_hover_button": "#ffffff22",
"app_bar_hover_strong": "#ffffff30",
"app_bar_hover_subtle": "#ffffff10",
"app_bar_separator": "#00aaaa",
"app_bar_tab_active": "#aa00aa",
"app_bar_text": "#ffffff",
"axis_x": "#ff5555",
"axis_y": "#55ff55",
"axis_z": "#5555ff",
"bend_down": "#ffff55",
"bend_up": "#55ff55",
"body": "#aa00aa",
"body_edge": "#55ffff",
"body_selected": "#ffff55",
"border": "#55ffff",
"box_crossing": "#ff55ff",
"box_window": "#55ffff",
"chip": "#aa00aa",
"construction_geom": "#aaaaaa",
"construction_plane": "#ffff5522",
"continuity": [
"#55ff55",
"#5555ff",
"#ffff55",
"#ff55ff"
],
"control_border": "#55ffff",
"control_fill": "#000000",
"cube_border": "#55ffff",
"cube_face": "#000000",
"cube_hover": "#aa00aa",
"cube_text": "#ffffff",
"dialog_bg": "#000000",
"dialog_body": "#000000",
"dialog_border": "#55ffff",
"dialog_footer": "#000000",
"dialog_label": "#55ffff",
"dialog_rule": "#00aaaa",
"dimension": "#ffffff",
"dimension_driven": "#aaaaaa",
"error": "#ff5555",
"field": "#000000",
"file_background": "#000000",
"file_border": "#55ffff",
"file_dim": "#55ffff",
"file_image_tint": "#ffffff",
"file_primary": "#000000",
"file_primary_border": "#55ffff",
"file_primary_ink": "#55ffff",
"file_rail": "#000000",
"file_rail_hover": "#aa00aa",
"file_rail_ink": "#ffffff",
"file_recovery": "#000000",
"file_recovery_border": "#ff55ff",
"file_recovery_ink": "#ff55ff",
"file_recovery_well": "#000000",
"file_surface": "#000000",
"form_accent": "#aa0000",
"form_dim": "#55ffff",
"form_error": "#ff5555",
"form_error_fill": "#000000",
"form_fill": "#ff5555",
"form_ink": "#ffffff",
"form_primary": "#ff55ff",
"form_primary_fill": "#ff55ff",
"form_soft": "#000000",
"form_success": "#55ff55",
"form_success_fill": "#000000",
"form_tab_ink": "#ff5555",
"glyph_bg": "#000000",
"glyph_edge": "#ff55ff",
"glyph_text": "#ff55ff",
"grid": "#555555",
"grid_major": "#00aaaa",
"hover": "#aa00aa",
"hover_edge": "#ffffff",
"hover_edge_halo": "#000000",
"hover_profile": "#ffffff40",
"hover_profile_edge": "#ffffff",
"icon": "#ffffff",
"icons.agent": "#ff55ff",
"icons.highlight": "#ffffff",
"icons.marker_halo": "#000000cc",
"interference": "#ff5555",
"logo": [
"#ffffff",
"#55ffff",
"#ff55ff"
],
"manipulator": "#55ffff",
"mesh_accent": "#00aa00",
"mesh_fill": "#55ff55",
"mesh_ink": "#55ff55",
"mesh_soft": "#000000",
"nav_dim": "#55ffff",
"nav_field": "#000000",
"nav_hover": "#aa00aa",
"nav_press": "#ff55ff",
"nav_tip": "#ffffff",
"nav_tip_ink": "#000000",
"nav_tip_key_border": "#55ffff",
"nav_viewport": "#000000",
"on_accent": "#000000",
"origin_plane": "#55ffff20",
"origin_plane_edge": "#55ffff80",
"origin_plane_hover": "#ff55ff40",
"origin_point": "#ffffff",
"overlay": "#000000",
"panel": "#000000",
"panel_header": "#000000",
"preview_add": "#55ffff",
"preview_cut": "#ff555566",
"preview_new": "#ffff55",
"preview_sketch": "#55ff55",
"profile_fill": "#55ffff22",
"render_accent": "#aaaaaa",
"render_fill": "#ffffff",
"render_ink": "#ffffff",
"render_soft": "#000000",
"rubber_band": "#55ffff",
"section_cap": "#ffff55",
"sel_edge": "#ffff55",
"sel_edge_rim": "#000000",
"sel_face": "#ffff55",
"sel_profile": "#ffff55",
"sel_profile_edge": "#ffff55",
"sel_vertex": "#ffff55",
"selected_bg": "#ff55ff",
"selected_ink": "#000000",
"sheet_fill": "#aaaaaa",
"sheet_ink": "#aaaaaa",
"sheet_soft": "#000000",
"sketch": "#55ffff",
"sketch_accent": "#ff55ff",
"sketch_centerline": "#ffff55",
"sketch_construction": "#aaaaaa",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#ff55ff",
"sketch_locked": "#55ff55",
"sketch_lost": "#ff5555",
"sketch_point": "#ffffff",
"sketch_point_free": "#55ffff",
"sketch_projected": "#ff55ff",
"sketch_soft": "#000000",
"surface_accent": "#aa5500",
"surface_fill": "#ffff55",
"surface_ink": "#ffff55",
"surface_soft": "#000000",
"tab_on_fill_dark": "#000000",
"text": "#ffffff",
"text_dim": "#55ffff",
"timeline": "#000000",
"timeline_item": "#000000",
"timeline_item_error": "#aa0000",
"timeline_marker": "#55ffff",
"toolbar": "#000000",
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffff55"
},
"format": "solidcraft-theme/1",
"name": "CGA",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#000000",
"line": "#ffffff",
"passive-1": "#ffffff",
"passive-2": "#aaaaaa",
"passive-3": "#555555",
"guide": "#55ffff",
"solid": [
"#ffffff",
"#55ffff",
"#00aaaa"
],
"surface": [
"#ffffff",
"#ffff55",
"#aa5500"
],
"mesh": [
"#ffffff",
"#55ff55",
"#00aa00"
],
"form": [
"#ffffff",
"#ff5555",
"#aa0000"
],
"sketch": [
"#ffffff",
"#ff55ff",
"#aa00aa"
],
"constraint": [
"#ffffff",
"#ff55ff",
"#aa00aa"
],
"construction": [
"#ffffff",
"#ffff55",
"#aa5500"
]
}
}
-3
View File
@@ -54,9 +54,6 @@
"dimension_driven": "#8f9296",
"error": "#f85052",
"field": "#2c2e31",
"file_rail": "#0f1113",
"file_rail_hover": "#222527",
"file_rail_ink": "#e1e3e5",
"form_accent": "#a6a2fc",
"form_dim": "#8f9296",
"form_error": "#ff6060",
+32 -421
View File
@@ -3,7 +3,7 @@
//! a component's over the physical material's look; a body with none of these keeps the colour
//! it was imported with.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solidcraft_geom::Vec3;
@@ -129,95 +129,6 @@ impl Appearances {
pub fn is_empty(&self) -> bool {
self.bodies.is_empty() && self.faces.is_empty() && self.components.is_empty()
}
/// Bound bulk appearance operations, including designs read from untrusted files.
pub fn check_budget(&self) -> crate::Result<()> {
if self.bodies.len().saturating_add(self.faces.len()).saturating_add(self.components.len()) > 100_000 {
return Err(crate::DocError::Invalid("at most 100000 appearance overrides can be processed".into()));
}
Ok(())
}
/// Build a replacement without changing the source. Equality includes the label and
/// every optical value. Physical-material defaults are deliberately outside this scope.
/// `None` means no change, allowing callers to preserve history and revision.
pub fn replacing(&self, from: &Look, to: &Look, scope: Option<&AppearanceScope>) -> crate::Result<Option<(Self, usize)>> {
self.check_budget()?;
for look in [from, to] {
if !look.opacity.is_finite() || !(0.0..=1.0).contains(&look.opacity) {
return Err(crate::DocError::Invalid("opacity must be finite and in 0..1".into()));
}
look.opaque_surface()?;
}
if from == to {
return Ok(None);
}
let body_in_scope = |body: &str| scope.is_none_or(|s| s.bodies.contains(body));
let component_in_scope = |id: &u64| scope.is_none_or(|s| s.components.contains(id));
let count = self.bodies.iter().filter(|(body, look)| body_in_scope(body) && *look == from).count()
+ self.faces.iter().filter(|face| body_in_scope(&face.body) && &face.look == from).count()
+ self.components.iter().filter(|(id, look)| component_in_scope(id) && *look == from).count();
if count == 0 {
return Ok(None);
}
let mut replaced = self.clone();
for (body, look) in &mut replaced.bodies {
if body_in_scope(body) && look == from {
*look = to.clone();
}
}
for face in &mut replaced.faces {
if body_in_scope(&face.body) && &face.look == from {
face.look = to.clone();
}
}
for (id, look) in &mut replaced.components {
if component_in_scope(id) && look == from {
*look = to.clone();
}
}
Ok(Some((replaced, count)))
}
}
/// Exact override containers for replacement. A body includes its face overrides;
/// a component selects its own override, independently of the bodies it owns.
#[derive(Clone, Debug, Default)]
pub struct AppearanceScope {
pub bodies: BTreeSet<String>,
pub components: BTreeSet<u64>,
}
/// A target in an evaluated model. Face indices are those of `model.faces`.
#[derive(Clone, Copy, Debug)]
pub enum AppearanceTarget<'a> {
Body(&'a str),
Face { body: &'a str, face: usize },
Component(u64),
}
/// The four override steps, in descending precedence. Imported paint is a display
/// fallback, not an override or physical material, and is not a fifth step.
#[derive(Clone, Debug, PartialEq)]
pub struct AppearanceChain {
pub face: Option<Look>,
pub body: Option<Look>,
pub component: Option<Look>,
pub material: Option<Look>,
}
impl AppearanceChain {
pub fn steps(&self) -> [(&'static str, Option<&Look>); 4] {
[("face", self.face.as_ref()), ("body", self.body.as_ref()), ("component", self.component.as_ref()), ("material", self.material.as_ref())]
}
pub fn active(&self) -> Option<&'static str> {
self.steps().into_iter().find_map(|(step, look)| look.map(|_| step))
}
pub fn resolved(&self) -> Option<&Look> {
self.steps().into_iter().find_map(|(_, look)| look)
}
}
/// The built-in appearance library: (name, colour, opacity).
@@ -250,96 +161,15 @@ pub fn material_look(material: &str) -> Option<Look> {
}
impl Document {
/// Read the same precedence used by body and face display resolution. Components
/// have no material assignment of their own: the material step is present only
/// when their directly owned bodies all have the same material look. Empty or
/// mixed components have no material step. Root is not an assignable component.
pub fn appearance_chain(&self, state: &crate::ModelState, target: AppearanceTarget<'_>) -> crate::Result<AppearanceChain> {
self.appearances.check_budget()?;
match target {
AppearanceTarget::Body(name) | AppearanceTarget::Face { body: name, .. } => {
let body = state.body(name).ok_or_else(|| crate::DocError::Invalid(format!("no body `{name}`")))?;
let mut chain = self.body_appearance_chain(body.definition_name(), self.model_body_component(body));
if let AppearanceTarget::Face { face, .. } = target {
if face >= body.body.face_count() {
return Err(crate::DocError::Invalid("unknown face index".into()));
}
let mut overrides = Vec::new();
self.apply_face_overrides(body, &mut overrides);
chain.face = overrides.into_iter().find_map(|(index, look)| (index == face).then_some(look));
}
Ok(chain)
}
AppearanceTarget::Component(id) => {
if id == 0 || !self.components.iter().any(|c| c.id == id) {
return Err(crate::DocError::Invalid("unknown or root component".into()));
}
if state.bodies.len() > 100_000 {
return Err(crate::DocError::Invalid("too many bodies to read a component appearance chain".into()));
}
// Legacy ownership can refer to a feature. Index it once, rather
// than scanning the timeline for each body of a large component.
let features: BTreeMap<_, _> = self.features.iter().rev().map(|f| (f.id, f.component)).collect();
let owner = |b: &&crate::ModelBody| {
if b.transferred || b.source_name.is_some() {
b.component
} else {
self.body_components.get(&b.name).copied().or_else(|| features.get(&b.feature).copied()).unwrap_or(0)
}
};
let mut materials = state
.bodies
.iter()
.filter(|b| owner(b) == id)
.map(|b| self.materials.get(b.definition_name()).and_then(|m| self.material_look(m)));
let material = materials.next().flatten().filter(|first| materials.all(|look| look.as_ref() == Some(first)));
Ok(AppearanceChain { face: None, body: None, component: self.appearances.components.get(&id).cloned(), material })
}
}
}
fn body_appearance_chain(&self, name: &str, component: u64) -> AppearanceChain {
AppearanceChain {
face: None,
body: self.appearances.bodies.get(name).cloned(),
component: (component != 0).then(|| self.appearances.components.get(&component).cloned()).flatten(),
material: self.materials.get(name).and_then(|material| self.material_look(material)),
}
}
/// Resolve a picked face through the same appearance chain used by the viewport.
pub fn model_face_look(&self, body: &crate::ModelBody, face: usize) -> Option<Look> {
self.face_colors(body).into_iter().find(|(index, _)| *index == face).map(|(_, look)| look).or_else(|| self.model_display_look(body))
}
/// Body appearance including the imported fallback used by `body_color`.
pub fn model_display_look(&self, body: &crate::ModelBody) -> Option<Look> {
self.model_body_look(body).or_else(|| {
let color = body.body.color().or_else(|| body.imported.as_ref().and_then(|p| p.color))?;
Some(Look::custom(color.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8), self.model_body_opacity(body)))
})
}
/// Distinct appearances used by an object, including its face overrides. `None` is
/// the unassigned/imported body appearance. Repeated faces count as one object user.
pub fn model_used_looks(&self, body: &crate::ModelBody) -> Vec<Option<Look>> {
let faces = self.face_colors(body);
let mut looks = Vec::new();
if faces.len() < body.body.face_count() || faces.is_empty() {
looks.push(self.model_display_look(body));
}
for (_, look) in faces {
let look = Some(look);
if !looks.contains(&look) {
looks.push(look);
}
}
looks
}
/// Shared definition appearance for a computed occurrence body.
pub fn model_body_look(&self, body: &crate::ModelBody) -> Option<Look> {
self.body_appearance_chain(body.definition_name(), self.model_body_component(body)).resolved().cloned()
let name = body.definition_name();
self.appearances
.bodies
.get(name)
.cloned()
.or_else(|| self.appearances.components.get(&self.model_body_component(body)).cloned())
.or_else(|| self.materials.get(name).and_then(|material| self.material_look(material)))
}
/// Effective body opacity, including paint carried from an imported body.
pub fn model_body_opacity(&self, body: &crate::ModelBody) -> f64 {
@@ -352,7 +182,15 @@ impl Document {
/// A body's look (without face appearances): its own, else its component's, else its
/// material's.
pub fn body_look(&self, name: &str, feature: u64) -> Option<Look> {
self.body_appearance_chain(name, self.body_component(name, feature)).resolved().cloned()
self.appearances
.bodies
.get(name)
.cloned()
.or_else(|| {
let c = self.body_component(name, feature);
(c != 0).then(|| self.appearances.components.get(&c).cloned()).flatten()
})
.or_else(|| self.materials.get(name).and_then(|m| self.material_look(m)))
}
/// Face appearances on a body: (point on the face, look).
@@ -363,46 +201,21 @@ impl Document {
/// The faces of a body with appearances of their own: (B-rep face index, look). A later
/// assignment to the same face wins.
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
let mut out = self.imported_face_colors(b);
for (index, look) in self.authored_face_colors(b) {
out.retain(|(old, _)| *old != index);
out.push((index, look));
}
out
}
/// Imported paint alone, after applying body-appearance precedence.
pub(crate) fn imported_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
// An assigned body appearance replaces imported paint; design face appearances still win.
// Face colours the body was imported with, under the design's own.
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
if self.appearances.bodies.contains_key(b.definition_name()) {
Vec::new()
} else {
match b.body.paint() {
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
// Carried from an earlier version of the body: by face name (worked out only here:
// the viewport asks every body for its face colours on every change).
None => match &b.imported {
Some(imp) => crate::naming::face_names(b)
.iter()
.enumerate()
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
.collect(),
None => Vec::new(),
},
}
}
}
/// Design-authored face overrides, without imported STEP paint. Linked designs carry
/// these independently so an inherited body look does not hide their face overrides.
pub fn authored_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
let mut out = Vec::new();
self.apply_face_overrides(b, &mut out);
out
}
fn apply_face_overrides(&self, b: &crate::ModelBody, out: &mut Vec<(usize, Look)>) {
let mut out: Vec<(usize, Look)> = match b.body.paint() {
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
// Carried from an earlier version of the body: by face name (worked out only here:
// the viewport asks every body for its face colours on every change).
None => match &b.imported {
Some(imp) => crate::naming::face_names(b)
.iter()
.enumerate()
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
.collect(),
None => Vec::new(),
},
};
for f in self.appearances.faces.iter().filter(|f| f.body == b.definition_name()) {
let (p, look) = (crate::apply_point(&b.source_transform, f.point), f.look.clone());
if f.strict_name {
@@ -442,6 +255,7 @@ impl Document {
out.push((i, look));
}
}
out
}
}
@@ -525,209 +339,6 @@ mod tests {
assert!(!mixed.differs_only_in_appearance(&before), "a material change is not appearance-only");
}
fn imported_box() -> crate::ModelBody {
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
let faces = body
.faces(0.01)
.unwrap()
.iter()
.map(|face| solidcraft_kernel::FacePaint { face: face.index, color: [1., 0., 0.], opacity: 0.5 })
.collect();
crate::ModelBody::new("Imported".into(), body.with_paint(Some(solidcraft_kernel::Paint { opacity: 1., faces })), 1)
}
fn check_imported_appearance_precedence(body: &crate::ModelBody) {
let mut doc = Document::default();
let face_count = body.body.faces(0.01).unwrap().len();
let imported = doc.face_colors(body);
assert_eq!(imported.len(), face_count);
assert!(imported.iter().all(|(_, look)| *look == Look::custom([255, 0, 0], 0.5)));
let body_look = Look::custom([0, 0, 255], 0.8);
doc.appearances.bodies.insert(body.definition_name().into(), body_look.clone());
assert_eq!(doc.model_body_look(body), Some(body_look.clone()));
assert_eq!(doc.model_body_opacity(body), body_look.opacity);
assert!(doc.face_colors(body).is_empty(), "all faces must inherit the assigned body appearance");
let face_look = Look::custom([0, 255, 0], 1.);
let name = crate::naming::face_names(body)[0].clone();
doc.appearances.faces.push(FaceLook {
body: body.definition_name().into(),
point: Vec3::ZERO,
look: face_look.clone(),
name: Some(name),
strict_name: false,
certificate: None,
});
assert_eq!(doc.face_colors(body), vec![(0, face_look.clone())]);
doc.appearances.bodies.clear();
let restored = doc.face_colors(body);
assert_eq!(restored.len(), face_count);
assert!(restored.iter().all(|(face, look)| *look == if *face == 0 { face_look.clone() } else { Look::custom([255, 0, 0], 0.5) }));
}
#[test]
fn linked_paint_retains_imported_faces_without_baking_authored_overrides() {
let mut state = crate::ModelState::default();
state.bodies.push(imported_box());
let body = &state.bodies[0];
let mut doc = Document::default();
doc.appearances.faces.push(FaceLook {
body: body.name.clone(),
point: Vec3::ZERO,
look: Look::custom([0, 255, 0], 1.0),
name: Some(crate::naming::face_names(body)[0].clone()),
strict_name: false,
certificate: None,
});
let exported = doc.painted(&state);
assert!(exported.bodies[0].body.paint().unwrap().faces.iter().any(|face| face.color == [0., 1., 0.]));
let linked = doc.painted_for_link(&state);
let faces = &linked.bodies[0].body.paint().unwrap().faces;
assert_eq!(faces.len(), body.body.faces(0.01).unwrap().len());
assert!(faces.iter().all(|face| face.color == [1., 0., 0.] && face.opacity == 0.5));
}
#[test]
fn body_appearance_overrides_imported_face_paint() {
check_imported_appearance_precedence(&imported_box());
}
#[test]
fn body_appearance_overrides_carried_imported_face_paint() {
let mut body = imported_box();
body.imported = ImportedLook::of(&body);
body.body = body.body.clone().with_paint(None);
body.source_name = Some(body.name.clone());
body.name = "Occurrence".into();
check_imported_appearance_precedence(&body);
}
fn chain_fixture() -> (Document, crate::ModelState) {
let mut doc = Document::new("Appearance chain");
doc.components.push(crate::Component { id: 1, name: "Part".into(), ..Default::default() });
doc.body_components.insert("B".into(), 1);
let mut state = crate::ModelState::default();
state.bodies.push(crate::ModelBody::new("B".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap(), 1));
(doc, state)
}
#[test]
fn appearance_chain_agrees_with_display_and_definition_identity() {
let (mut doc, mut state) = chain_fixture();
let body_look = library("ABS - Red").unwrap();
let face_look = library("Steel - Satin PBR").unwrap();
doc.appearances.bodies.insert("B".into(), body_look.clone());
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
doc.materials.insert("B".into(), "Brass".into());
let body = &state.bodies[0];
let spot = crate::naming::face_spots(body)[0].unwrap().point;
let face_name = crate::naming::face_names(body)[0].clone();
doc.appearances.faces.push(FaceLook {
body: "B".into(),
point: spot,
look: face_look.clone(),
name: Some(face_name),
strict_name: false,
certificate: None,
});
let chain = doc.appearance_chain(&state, AppearanceTarget::Face { body: "B", face: 0 }).unwrap();
assert_eq!(chain.active(), Some("face"));
assert_eq!(chain.resolved(), Some(&face_look));
assert_eq!(doc.model_face_look(body, 0).as_ref(), chain.resolved());
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
assert_eq!(chain.active(), Some("body"));
assert_eq!(doc.model_body_look(body).as_ref(), chain.resolved());
assert_eq!(doc.body_look("B", 1).as_ref(), chain.resolved());
assert_eq!(chain.component.as_ref().unwrap().name, "ABS - Blue");
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
// A computed placement resolves the definition's assignments, not its label.
state.bodies[0].name = "Occurrence/B".into();
state.bodies[0].source_name = Some("B".into());
state.bodies[0].component = 1;
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("Occurrence/B")).unwrap();
assert_eq!(chain.body, Some(body_look));
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
assert!(doc.appearance_chain(&state, AppearanceTarget::Face { body: "Occurrence/B", face: usize::MAX }).is_err());
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("missing")).is_err());
}
#[test]
fn appearance_chain_component_material_is_common_or_absent_and_imports_stay_fallbacks() {
let (mut doc, mut state) = chain_fixture();
state.bodies[0].imported = Some(std::sync::Arc::new(ImportedLook { color: Some([0.2, 0.4, 0.6]), ..Default::default() }));
let body = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
assert_eq!(body.active(), None);
assert!(doc.model_display_look(&state.bodies[0]).is_some());
doc.materials.insert("B".into(), "Brass".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
let mut second = state.bodies[0].clone();
second.name = "C".into();
doc.body_components.insert("C".into(), 1);
state.bodies.push(second);
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), None);
doc.materials.insert("C".into(), "Brass".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
doc.materials.insert("C".into(), "Steel".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("component"));
state.bodies.clear();
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(0)).is_err());
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(99)).is_err());
}
#[test]
fn appearance_replacement_validates_values_and_design_budget_without_mutation() {
let from = library("ABS - Red").unwrap();
let to = library("ABS - Blue").unwrap();
let mut appearances = Appearances::default();
appearances.bodies.insert("B".into(), from.clone());
for opacity in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1., 2.] {
let mut invalid = to.clone();
invalid.opacity = opacity;
assert!(appearances.replacing(&from, &invalid, None).is_err());
assert!(appearances.replacing(&invalid, &to, None).is_err());
assert_eq!(appearances.bodies["B"], from);
}
let mut invalid = library("Steel - Satin PBR").unwrap();
invalid.opacity = 0.5;
assert!(appearances.replacing(&from, &invalid, None).is_err());
let (changed, count) = appearances.replacing(&from, &to, None).unwrap().unwrap();
assert_eq!(count, 1);
assert_eq!(changed.bodies["B"], to);
assert_eq!(appearances.bodies["B"], from);
assert!(appearances.replacing(&from, &from, None).unwrap().is_none());
for i in 0..100_000 {
appearances.components.insert(i, from.clone());
}
assert!(appearances.replacing(&from, &to, None).is_err());
let (mut doc, state) = chain_fixture();
doc.appearances = appearances;
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("B")).is_err());
}
#[test]
fn appearance_chain_of_ten_thousand_body_component_has_four_steps() {
let (mut doc, mut state) = chain_fixture();
let mut body = state.bodies[0].clone();
body.source_name = Some("B".into());
body.component = 1;
state.bodies = (0..10_000)
.map(|i| {
let mut b = body.clone();
b.name = format!("Placed {i}");
b
})
.collect();
doc.materials.insert("B".into(), "Brass".into());
let chain = doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap();
assert_eq!(chain.steps().len(), 4);
assert_eq!(chain.active(), Some("material"));
}
#[test]
fn library_and_materials() {
assert_eq!(library("abs - red").map(|l| l.color), Some([196, 40, 40]));
+9 -17
View File
@@ -58,22 +58,6 @@ impl Document {
let occurrences: BTreeSet<u64> =
self.occurrences.iter().filter(|o| inside.contains(&o.component) || inside.contains(&o.parent)).map(|o| o.id).collect();
let joints = self.remove_occurrences(&occurrences);
let bodies: BTreeSet<String> = self.body_components.iter().filter(|(_, c)| inside.contains(*c)).map(|(name, _)| name.clone()).collect();
self.body_components.retain(|_, c| !inside.contains(c));
self.body_offsets.retain(|name, _| !bodies.contains(name));
self.components.retain(|c| !inside.contains(&c.id));
self.parts.retain(|c, _| !inside.contains(c));
self.links.retain(|c, _| !inside.contains(c));
self.remove_configuration_components(&components);
Ok(RemovedTree { components, features, deleted, occurrences: occurrences.into_iter().collect(), joints: joints.into_iter().collect() })
}
/// Remove these occurrences and every assembly record that refers to them: the joints with
/// either side on one (with those joints' motion links and motion keys), the joint origins
/// on them, their places in contact sets and their exploded moves. Returns the joints removed.
pub fn remove_occurrences(&mut self, occurrences: &BTreeSet<u64>) -> BTreeSet<u64> {
let joints: BTreeSet<u64> = self
.assembly
.joints
@@ -95,6 +79,14 @@ impl Document {
view.moves.retain(|(o, _)| !occurrences.contains(o));
}
self.occurrences.retain(|o| !occurrences.contains(&o.id));
joints
let bodies: BTreeSet<String> = self.body_components.iter().filter(|(_, c)| inside.contains(*c)).map(|(name, _)| name.clone()).collect();
self.body_components.retain(|_, c| !inside.contains(c));
self.body_offsets.retain(|name, _| !bodies.contains(name));
self.components.retain(|c| !inside.contains(&c.id));
self.parts.retain(|c, _| !inside.contains(c));
self.links.retain(|c, _| !inside.contains(c));
self.remove_configuration_components(&components);
Ok(RemovedTree { components, features, deleted, occurrences: occurrences.into_iter().collect(), joints: joints.into_iter().collect() })
}
}
+10 -55
View File
@@ -693,11 +693,6 @@ pub enum FeatureKind {
height: Expr,
#[serde(default)]
operation: Operation,
/// Up to two straight edges of the face it stands on (#6380), as a Single hole's
/// references: the base centre sits at each one's distance from its edge, in the face's
/// plane, and follows the edges when the part changes. `base` keeps where it was placed.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
references: Vec<HoleReference>,
},
Sphere {
#[serde(with = "point_expr")]
@@ -2617,42 +2612,13 @@ pub struct StandardPart {
/// A component whose contents come from another design file (Insert Design): where that file
/// is, and the Import feature in the component that holds its bodies (Update Links replaces
/// them; Break Link keeps them and forgets the file).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DesignLink {
/// The design file: relative to this design's folder once this design is saved, else as
/// given (`/` separates folders).
pub path: String,
/// The Import feature holding the linked design's bodies.
pub feature: u64,
/// Last inherited values, by Import body slot. Differing assembly assignments are
/// local overrides and survive a source update. Older links start with no baseline.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inherited: Vec<LinkedBodyProperties>,
}
/// The values last inherited by a linked body, not another editable material library.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LinkedBodyProperties {
pub material: String,
/// SHA-256 of the inherited custom definition, for equality only. The complete
/// engineering card stays in user_materials, not duplicated once per linked body.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub material_fingerprint: Option<String>,
/// Explicit body/component look only: material fallback must remain a fallback.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub appearance: Option<crate::appearance::Look>,
/// Last inherited authored face overrides. None marks links saved before face
/// inheritance; Some(empty) records that the source has no authored face overrides.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub faces: Option<Vec<crate::appearance::FaceLook>>,
}
impl LinkedBodyProperties {
pub fn fingerprint(material: &UserMaterial) -> Result<String> {
use sha2::{Digest, Sha256};
let bytes = serde_json::to_vec(material).map_err(|e| DocError::Invalid(e.to_string()))?;
Ok(format!("{:x}", Sha256::digest(bytes)))
}
}
/// A name the design's counter hands out (`Body1`).
@@ -3160,22 +3126,14 @@ impl Document {
/// The model with each body carrying the colour, opacity and face colours it is shown with
/// (for export).
pub fn painted(&self, state: &crate::ModelState) -> crate::ModelState {
self.painted_with_faces(state, true)
}
/// Linked designs carry authored face overrides as editable properties. Their STEP
/// geometry keeps imported paint, without baking a second copy of those overrides.
pub fn painted_for_link(&self, state: &crate::ModelState) -> crate::ModelState {
self.painted_with_faces(state, false)
}
fn painted_with_faces(&self, state: &crate::ModelState, authored_faces: bool) -> crate::ModelState {
let mut st = state.clone();
for b in &mut st.bodies {
let c = self.body_color(b);
let face_colors = if authored_faces { self.face_colors(b) } else { self.imported_face_colors(b) };
let faces: Vec<solidcraft_kernel::FacePaint> =
face_colors.into_iter().map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 }).collect();
let faces: Vec<solidcraft_kernel::FacePaint> = self
.face_colors(b)
.into_iter()
.map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
.collect();
let opacity = self.model_body_opacity(b) as f32;
let paint = solidcraft_kernel::Paint { opacity, faces };
if c != b.body.color() || b.body.paint() != Some(&paint) {
@@ -3547,14 +3505,11 @@ impl Document {
let id = f.id;
let insertion_group = self.marker.and_then(|marker| self.timeline_group_at_insertion(marker));
match self.marker {
// A marker right after the last feature moves on past a feature added there, or the
// new feature would be added rolled back (#7741).
Some(marker) => {
let position = marker.min(self.features.len());
self.features.insert(position, f);
self.marker = Some(position + 1);
Some(m) if m < self.features.len() => {
self.features.insert(m, f);
self.marker = Some(m + 1);
}
None => self.features.push(f),
_ => self.features.push(f),
}
if let Some(group_id) = insertion_group {
let positions: std::collections::BTreeMap<u64, usize> =

Some files were not shown because too many files have changed in this diff Show More