Compare commits

..

55 Commits

Author SHA1 Message Date
bherbruck 8e2b0cd60d Release 0.30.1 2026-11-02 11:11:46 -05:00
bherbruck afd81380ab Release 0.30.0 2026-11-01 21:55:18 -05:00
bherbruck 3a5195b0a0 Release 0.29.0 2026-10-31 19:20:30 -04:00
bherbruck ee6b85605a Release 0.28.0 2026-10-30 19:50:14 -04:00
bherbruck aa1fe28ddf Release 0.27.1 2026-10-29 23:34:31 -04:00
bherbruck 5b0eb08473 Release 0.27.0 2026-10-29 19:53:24 -04:00
bherbruck 9df8837d43 Release 0.26.1 2026-10-29 06:33:09 -04:00
bherbruck 409ab1ed45 Release 0.26.0 (zip 9.1.3) 2026-10-28 19:41:38 -04:00
bherbruck 5e03fe2f4b Release 0.26.0 2026-10-28 19:32:17 -04:00
bherbruck 37a586dd61 Release 0.25.1 2026-10-27 20:58:30 -04:00
bherbruck a79f40a711 Release 0.25.0 2026-10-27 18:05:39 -04:00
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
4341 changed files with 111722 additions and 389742 deletions
+8 -3
View File
@@ -1,9 +1,14 @@
# cargo-mutants (docs/mutation-testing.md). Mutants are judged by the black-box suites that will
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test).
# The kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test)
# and the pull-request sample of the robustness goldens (every 8th seed, as ci.yml). The
# kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
# backend-neutral suites would miss when the kernel is swapped.
profile = "mutants"
additional_cargo_test_args = [
"--test", "oracle_cases", "--", "--include-ignored",
"--test", "oracle_cases", "--lib", "--", "--include-ignored",
"oracle_suite",
"robustness_goldens::seed_00", "robustness_goldens::seed_08", "robustness_goldens::seed_16",
"robustness_goldens::seed_24", "robustness_goldens::seed_32", "robustness_goldens::seed_40",
"robustness_goldens::seed_48", "robustness_goldens::seed_56", "robustness_goldens::seed_64",
"robustness_goldens::seed_72", "robustness_goldens::seed_80", "robustness_goldens::seed_88",
]
-20
View File
@@ -1,20 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
auto_review:
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
ignore_usernames:
- "dependabot[bot]"
path_instructions:
- path: "**"
instructions: |
Project rule: don't change anyone's existing defaults unless the change is purely additive and belongs in the application.
Flag any change to something a user already relies on: a default setting or preference value, what a command or tool does when used the usual way, the UI's default layout or behaviour, a keyboard or mouse shortcut, or the result an existing saved design rebuilds to.
Don't flag purely additive changes: a new command, a new setting whose default keeps today's behaviour, or a new option that appears only when someone chooses it. Don't flag bug fixes that only turn a crash or an error into the result the feature always promised.
When you flag one, start the comment with "Changes a default:", say what a user sees before and after, whether a setting keeps the old behaviour, and whether the PR description's `Changes a default:` line says so.
pre_merge_checks:
custom_checks:
- name: "Existing defaults unchanged"
mode: "warning"
instructions: |
Pass if the pull request changes no default a user already relies on (default setting or preference values, what a command or tool does when used the usual way, the UI's default layout or behaviour, shortcuts, or the result an existing saved design rebuilds to), or if its changes are purely additive.
Fail otherwise, naming each changed default and what a user sees before and after. Also fail if the description says `Changes a default: no` while the code changes one.
+8 -5
View File
@@ -12,6 +12,7 @@ not (
(package(solidcraft-engine) & (
test(=tests::hostile_params_never_panic)
| test(/^tests::robustness::/)
| test(/^tests::robustness_goldens::seed_/)
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
@@ -43,12 +44,13 @@ threads-required = 12
priority = 100
# The slowest tests start first, so a shard does not end with one of them running alone on a
# core while the others sit idle. Medians from the CI test timings (the junit artifacts), slowest
# first; a test over ~100 s belongs here.
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
[[profile.ci.overrides]]
filter = """
(package(solidcraft-engine) & (
test(/^tests::robustness::/)
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
| test(/^tests::robustness::/)
| test(=tests::hostile_params_never_panic)
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
@@ -90,13 +92,14 @@ threads-required = "num-cpus"
priority = 100
[[profile.local-ci.overrides]]
filter = "package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic)"
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
priority = 99
[[profile.local-ci.overrides]]
filter = """
(package(solidcraft-engine) & (
test(/^tests::robustness::/)
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
| test(/^tests::robustness::/)
| test(=tests::hostile_params_never_panic)
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
-2
View File
@@ -1,5 +1,3 @@
<!-- Title: which part of the app changes and what a user now sees or can do, clear to someone who hasn't seen your chat, the issue or the code. -->
## Change
Start with what this is, in plain words someone new to the project would understand: what it does and who it's for. Don't assume anyone knows the name of your tool, project or feature. Then describe the problem and resulting behavior. Link the issue when applicable.
+53 -50
View File
@@ -33,8 +33,6 @@
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
# The release gate (release-gate.yml) calls this workflow (workflow_call) as a Run workflow with
# cross-compilation on.
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
@@ -52,14 +50,10 @@ on:
# only (other labels are skipped, see the top of this file).
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled]
# The kernel-smlib branch (ADR 0002) runs no CI for now (owner, 2026-10-21). Drop this
# line before the branch merges back into main.
branches-ignore: [kernel-smlib]
# The merge queue: each queued group runs the full suite before it lands on main.
merge_group:
push:
# develop: the SMLib kernel's trunk until it merges into main (its PRs land there).
branches: [main, develop]
branches: [main]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
@@ -68,14 +62,6 @@ on:
description: "Also check WebAssembly and RISC-V (before a release)"
type: boolean
default: false
# The release gate (release-gate.yml): the event stays the caller's (workflow_dispatch), so a
# call runs exactly what Run workflow runs.
workflow_call:
inputs:
cross_compile:
description: "Also check WebAssembly and RISC-V (before a release)"
type: boolean
default: false
permissions:
contents: read
@@ -131,6 +117,7 @@ jobs:
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.
gpu: ${{ steps.paths.outputs.gpu }}
@@ -169,6 +156,7 @@ jobs:
run: |
native=false
kernel=false
goldens=false
windows=false
gpu_paths=false
deps=false
@@ -250,12 +238,12 @@ jobs:
if [ "${tested:-0}" -gt 0 ]; then
echo "The merge queue already tested $PUSH_SHA: repository checks only."
elif tested=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=$GITHUB_REF_NAME&status=success&per_page=1" \
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=1" \
| grep -o '"head_sha": *"[0-9a-f]*"' | head -n 1 | grep -o '[0-9a-f]\{40\}') \
&& [ -n "$tested" ] && git merge-base --is-ancestor "$tested" "$PUSH_SHA" \
&& git diff --no-renames --name-only -z "$tested" "$PUSH_SHA" > "$RUNNER_TEMP/ci-paths" \
&& select_packages "$RUNNER_TEMP/ci-paths" && [ "$full" != true ] && [ -z "$packages" ]; then
# Nothing a build reads changed since the last push to this branch whose run passed (the
# Nothing a build reads changed since the last push to main whose run passed (the
# push before may never have run: a newer push cancels a waiting one).
echo "$tested..$PUSH_SHA changes no code: repository checks only."
else
@@ -294,6 +282,13 @@ jobs:
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
while IFS= read -r -d '' path; do
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
# on any other change they would only re-check unchanged results; the merge queue,
# main and the nightly run every seed before anything lands.
case "$path" in
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
esac
done < "$RUNNER_TEMP/ci-paths"
# Computed as for a merge group, then turned off below (the fast lane).
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
@@ -305,6 +300,10 @@ jobs:
# selected means no code changed.
select_packages "$RUNNER_TEMP/ci-paths"
if [ "$full" = true ] || [ -n "$packages" ]; then native=true; fi
# The goldens are solidcraft-engine tests: its tests must be in the archive.
if [ "$goldens" = true ] && [ "$full" != true ]; then
case " $packages " in *" solidcraft-engine "*) ;; *) packages="$packages solidcraft-engine" ;; esac
fi
fi
# The reference tests replay recipes through solidcraft-cli: needed when it, or anything
# it depends on, is selected.
@@ -319,12 +318,13 @@ jobs:
doctests=$native
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; doctests=false; fi
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
# Clippy, the tests and the doc tests.
# Clippy, the tests, the doc tests and the goldens.
quick=false
if [ "$EVENT_NAME" = pull_request ] && [ "$APPROVED" != true ]; then
quick=$native
native=false
oracle=false
goldens=false
fi
# A push that only merges main into the change (Merge branch 'main', Update branch) when
# the change itself already passed: nothing new to test here, and main tests the merged
@@ -337,6 +337,7 @@ jobs:
if [ -n "$carried" ]; then
echo "Only a merge of main into a change that passed in $carried: repository checks only."
native=false
goldens=false
fi
fi
# The GPU path tests (their own job, about 56 runner-minutes) are for render changes only:
@@ -362,6 +363,7 @@ jobs:
echo "native=$native"
echo "gpu=$gpu"
echo "kernel=$kernel"
echo "goldens=$goldens"
echo "windows=$windows"
echo "oracle=$oracle"
echo "doctests=$doctests"
@@ -387,7 +389,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: repo-checks
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Format
run: cargo fmt --all -- --check
- name: Asset attribution
@@ -407,10 +409,6 @@ jobs:
# New UI text must reach every complete catalog; report it here, before review (#8542).
- name: Translations in the complete catalogs
run: cargo xtask i18n --check
- name: Model decisions use exact geometry, not meshes
run: |
cargo xtask mesh-decisions
cargo test -p xtask mesh_decisions --locked
- name: Agent router and skills cite real files
run: |
cargo xtask skills
@@ -441,7 +439,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: quick-compile
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Cargo check
run: cargo check --workspace --all-targets --locked
@@ -460,7 +458,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-clippy
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Clippy
env:
FULL: ${{ needs.changes.outputs.full }}
@@ -503,7 +501,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -574,7 +572,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -585,8 +583,13 @@ jobs:
- name: Run tests
env:
EVENT_NAME: ${{ github.event_name }}
GOLDENS: ${{ needs.changes.outputs.goldens }}
FULL: ${{ needs.changes.outputs.full }}
PACKAGES: ${{ needs.changes.outputs.packages }}
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
# queue and the nightly run every seed.
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
run: |
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml); `default()`
# below is that filter, so a pull request still runs only what it ran before.
@@ -600,23 +603,29 @@ jobs:
# The boolean property tests draw new random inputs every run; they run only in the
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
base='not (package(solidcraft-kernel) & binary(boolean_props))'
elif [ "$GOLDENS" = true ]; then
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
# also checks every seed but the slow ones, in the same run so they share its cores.
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
fi
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
rm -f "$junit"
# Pull requests test the affected packages (scripts/ci/affected.py).
scope=(--workspace)
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
status=0
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
# took about 36 of this job's 45 minutes.
# A failed test does not fail the step by itself: the tests known to fail today
# (tests/KNOWN_FAILING_TESTS) may fail, any other fails it, and so does a listed test
# that passes. nextest's exit code 100 is "tests failed"; any other is its own error
# (a build error, a bad filter) and fails the step at once.
status=0
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
cargo xtask known-failing --junit "$junit"
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=1
if [ "$status" != 0 ]; then
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
fi
exit 1
fi
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
@@ -690,7 +699,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-gpu
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -705,14 +714,8 @@ jobs:
# None of these is in the default filter (the heavy tests), so pull requests, the merge
# queue, main and nightly all run every one of them, as before. `count` deals the tests
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
# Graded against tests/KNOWN_FAILING_TESTS, as in Native Linux tests.
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
rm -f "$junit"
status=0
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4" || status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
cargo xtask known-failing --junit "$junit"
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4"
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
@@ -846,7 +849,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: doc-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -904,7 +907,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: oracle
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -943,7 +946,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-fxc
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
# One test at a time: WARP's shader JIT crashes now and then (a null read in
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
@@ -967,7 +970,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-startup
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Backend order and probing on Windows
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
- name: Build the app
@@ -1104,7 +1107,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: wasm
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo xtask wasm
riscv64-gates:
@@ -1126,7 +1129,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: riscv64
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cross-compilation dependencies
run: |
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
@@ -1142,7 +1145,7 @@ jobs:
EOF
sudo dpkg --add-architecture riscv64
sudo apt-get update
sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
sudo apt-get install -y gcc-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 libxi-dev:riscv64 libgl1-mesa-dev:riscv64
- name: Cargo check on RISC-V
env:
-104
View File
@@ -1,104 +0,0 @@
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
# runs every case in tests/kernel-cases/fillets/ through one release build of
# solidcraft-cli, and fails on a regression (a case that fails and
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
#
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
# crates/engine/src/cmd/features.rs select it whenever those files change. Pushes to main and
# develop that touch the same paths run it too and save the compiler cache (a Blacksmith sticky
# disk) that pull requests then mount read-only.
name: Fillet rig
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "crates/kernel/src/blend*.rs"
- "crates/kernel/src/fillet*.rs"
- "crates/kernel/src/*corner*.rs"
- "crates/kernel/src/continuity*.rs"
- "crates/smlib/**"
- "crates/doc/src/eval*.rs"
- "crates/engine/src/cmd/features.rs"
- "tests/kernel-cases/fillets/**"
# The rig itself: the grading, the runner and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- ".github/workflows/fillet-rig.yml"
push:
branches: [main, develop]
# The same paths as pull_request.
paths:
- "crates/kernel/src/blend*.rs"
- "crates/kernel/src/fillet*.rs"
- "crates/kernel/src/*corner*.rs"
- "crates/kernel/src/continuity*.rs"
- "crates/smlib/**"
- "crates/doc/src/eval*.rs"
- "crates/engine/src/cmd/features.rs"
- "tests/kernel-cases/fillets/**"
# The rig itself: the grading, the runner and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- ".github/workflows/fillet-rig.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: fillet-rig-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
fillets:
name: Fillet rig
# Draft pull requests run no CI, as in ci.yml.
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/runner/.cache/sccache
SCCACHE_CACHE_SIZE: 10G
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Size parallel compiles to the job's memory
run: scripts/ci/build-jobs.sh
- uses: dtolnay/rust-toolchain@stable
# Only main and develop commit the cache; pull requests and forks mount a clone and never
# write back.
- name: Mount the compiler cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-sccache-fillet-rig
path: /home/runner/.cache/sccache
commit: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: fillet-rig
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
- name: Build the CLI (release)
run: cargo build --release --locked -p solidcraft-cli
- name: Run the fillet cases against the known-failing list
run: >-
cargo run -q --locked -p xtask -- kernel-cases --area fillets
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
- name: Keep the results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fillet-rig-results
path: ${{ runner.temp }}/fillet-rig/results.json
if-no-files-found: ignore
-2
View File
@@ -21,8 +21,6 @@ concurrency:
jobs:
publish:
name: Request the combined docs, web, and Flatpak deployment
# A pre-release stays off the Flatpak feed: the feed's one branch is what `flatpak update` offers.
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
runs-on: ubuntu-24.04
env:
GH_TOKEN: ${{ github.token }}
-238
View File
@@ -1,238 +0,0 @@
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes, and when the solidcore pin in
# the root Cargo.toml and Cargo.lock moves (the plan reads their diff); the ladder runs whenever any
# area does. One job then builds solidcraft-cli once (release), checks that every
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
#
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
# request, so the list only shrinks).
# - The ladder fails on any case whose status or counts differ from
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
#
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
# other run mounts read-only.
name: Kernel rig
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "crates/kernel/**"
- "crates/geom/**"
- "crates/smlib/**"
- "crates/step/**"
- "crates/io/src/**"
- "crates/doc/src/**"
- "crates/engine/src/**"
- "tests/kernel-cases/**"
# The solidcore pin (the plan runs every area only when its lines change).
- "Cargo.toml"
- "Cargo.lock"
# The rig itself: the grading, the runner, the selection and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "xtask/src/kernel_ladder.rs"
- "scripts/ci/kernel_rig_plan.py"
- "scripts/ci/test_kernel_rig_plan.py"
- ".github/workflows/kernel-rig.yml"
push:
branches: [main, develop]
# The same paths as pull_request.
paths:
- "crates/kernel/**"
- "crates/geom/**"
- "crates/smlib/**"
- "crates/step/**"
- "crates/io/src/**"
- "crates/doc/src/**"
- "crates/engine/src/**"
- "tests/kernel-cases/**"
# The solidcore pin (the plan runs every area only when its lines change).
- "Cargo.toml"
- "Cargo.lock"
# The rig itself: the grading, the runner, the selection and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "xtask/src/kernel_ladder.rs"
- "scripts/ci/kernel_rig_plan.py"
- "scripts/ci/test_kernel_rig_plan.py"
- ".github/workflows/kernel-rig.yml"
merge_group:
workflow_call:
workflow_dispatch:
permissions:
contents: read
concurrency:
# A new push to a pull request replaces its old run.
group: kernel-rig-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
plan:
name: Select the rig areas
# Draft pull requests run no CI, as in ci.yml.
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
shards: ${{ steps.plan.outputs.shards }}
# Whether the build job, and with it the coverage check, runs.
build: ${{ steps.plan.outputs.build }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# A pull request's diff needs its base's history.
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
- name: Selection self-test
run: python3 -I scripts/ci/test_kernel_rig_plan.py
- id: plan
name: Map the changed files to areas
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
# gate and a run by hand run every area.
if [ "$EVENT_NAME" = pull_request ]; then
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
git diff --no-renames "$BASE_SHA...$HEAD_SHA" -- Cargo.toml Cargo.lock > "$RUNNER_TEMP/rig-pin.diff"
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --pin-diff "$RUNNER_TEMP/rig-pin.diff" \
--github-output "$GITHUB_OUTPUT"
else
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
fi
build:
name: Build the CLI
needs: plan
if: needs.plan.outputs.build == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/runner/.cache/sccache
SCCACHE_CACHE_SIZE: 10G
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Size parallel compiles to the job's memory
run: scripts/ci/build-jobs.sh
- uses: dtolnay/rust-toolchain@stable
# Only pushes to main and develop commit the cache; pull requests, merge groups and forks
# mount a clone and never write back.
- name: Mount the compiler cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-sccache-kernel-rig
path: /home/runner/.cache/sccache
commit: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: kernel-rig
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
- name: Build the CLI (release) and the rig's runner
run: |
cargo build --release --locked -p solidcraft-cli
cargo build --locked -p xtask
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
# job of this workflow.
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
- name: Every command has a case
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
- name: Pack the binaries
if: needs.plan.outputs.shards != '[]'
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: needs.plan.outputs.shards != '[]'
with:
name: kernel-rig-bin
path: ${{ runner.temp }}/kernel-rig-bin.tar
retention-days: 1
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
shard:
name: Kernel rig (${{ matrix.shard }})
needs: [plan, build]
if: needs.plan.outputs.shards != '[]'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
# needs splitting.
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: kernel-rig-bin
path: ${{ runner.temp }}
- name: Unpack the binaries
run: mkdir -p target && tar -xf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target
- name: Run the cases against the known-failing list (the ladder against its golden)
env:
SHARD: ${{ matrix.shard }}
run: |
if [ "$SHARD" = ladder ]; then
set -- --ladder --gate
else
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
fi
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
- name: Keep the results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: kernel-rig-results-${{ matrix.shard }}
path: ${{ runner.temp }}/kernel-rig/**/results.json
if-no-files-found: ignore
# One check for the whole gate, for branch protection and the merge queue.
kernel-rig:
name: kernel-rig
needs: [plan, build, shard]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
# A skipped build or shard counts as passed only when the plan selected none.
- name: Every selected job passed
env:
PLAN: ${{ needs.plan.result }}
WANT_BUILD: ${{ needs.plan.outputs.build }}
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
BUILD: ${{ needs.build.result }}
SHARDS: ${{ needs.shard.result }}
run: |
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
test "$PLAN" = success
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
+2 -6
View File
@@ -1,6 +1,5 @@
# The documentation site on GitHub Pages: the book (docs/book) at https://docs.solidcraft.io/
# and the web app at /app/ (the latest release), /app-rc/ (the newest release candidate) and
# /app-edge/ (this commit). Needs Settings → Pages → Source = GitHub Actions; skipped while the
# and the bundled web app at /app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the
# repository is private.
name: Pages
@@ -67,10 +66,7 @@ jobs:
mkdir -p site
cp -r target/book/. site/
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
- name: Add the web app channels (stable and release candidate from their releases, edge from this build)
env:
GH_TOKEN: ${{ github.token }}
run: bash packaging/web-channels.sh site dist/web
cp -r dist/web site/app
- uses: actions/configure-pages@v5
id: pages
- name: Install Flatpak repository tools
-74
View File
@@ -1,74 +0,0 @@
# The release gate: everything, in full and in parallel, on one branch's head, before that branch
# replaces main as a release (develop, rebased onto main). Run it by hand:
# gh workflow run release-gate.yml --ref develop
# It calls the workflows that own each check instead of copying their jobs:
# - ci.yml as Run workflow with cross-compilation: every native suite in full (not scoped), the
# reference tests, the doc tests, the GPU path tests, the action matrix (all 16 shards) and its
# determinism check, the property tests, the repository checks (web-offline among them),
# WebAssembly and RISC-V, Windows D3D12 FXC and Windows app startup;
# - kernel-rig.yml: every rig area against its KNOWN_FAILING and the ladder against its golden;
# - release.yml as a dry run (draft false, packages_only): the Linux, Flatpak, Snap, FreeBSD,
# macOS, Windows and web packages with their smoke tests and package checks, unsigned (no
# secrets are passed, so no signing secret or SignPath quota is used) and without the release
# job (checksums, update signing, publishing).
# The gate passes only when all three pass. Before main is replaced, tag the old main
# (main-before-develop-<date>) and push the tag.
name: Release gate
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-gate-${{ github.ref }}
cancel-in-progress: false
jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
permissions:
contents: read
# Select checks reads this commit's other runs.
actions: read
with:
cross_compile: true
kernel-rig:
name: Kernel rig
uses: ./.github/workflows/kernel-rig.yml
permissions:
contents: read
packages:
name: Packages
uses: ./.github/workflows/release.yml
# No secrets: the signing steps skip without them. GitHub checks a called workflow's job
# permissions before the run, skipped jobs included, so the release job's contents: write must
# be granted here; packages_only skips that job, and no step that runs writes to the
# repository. actions: read is the Windows job's.
permissions:
contents: write
actions: read
with:
draft: false
packages_only: true
release-gate:
name: release-gate
needs: [ci, kernel-rig, packages]
if: always()
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Every part passed
env:
CI: ${{ needs.ci.result }}
KERNEL_RIG: ${{ needs.kernel-rig.result }}
PACKAGES: ${{ needs.packages.result }}
run: |
echo "ci=$CI kernel-rig=$KERNEL_RIG packages=$PACKAGES"
test "$CI" = success
test "$KERNEL_RIG" = success
test "$PACKAGES" = success
+2 -45
View File
@@ -21,8 +21,6 @@
# releases/latest/download/<name> links in the README keep working across releases.
#
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
# The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and
# without the release job (packages_only).
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
name: Release
@@ -43,25 +41,6 @@ on:
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
type: string
default: ""
# The release gate (release-gate.yml): a dry run (draft false) of every package and its checks.
# The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does.
# The gate passes no secrets, so nothing is signed (the signing steps skip without their
# secrets, as on a fork), and packages_only skips the release job (checksums, update signing,
# publishing).
workflow_call:
inputs:
packages_only:
description: "Build and check every package only: no release job"
type: boolean
default: false
draft:
description: "Create/update a draft (release branch only; off: build artifacts only)"
type: boolean
default: false
signpath_signing_policy:
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
type: string
default: ""
permissions:
contents: read
@@ -234,8 +213,6 @@ jobs:
if: needs.version.outputs.build == 'true'
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
runs-on: ubuntu-24.04
# A pre-release or nightly without an Arch package is still worth having; a stable release isn't.
continue-on-error: ${{ contains(needs.version.outputs.version, '-') }}
strategy:
fail-fast: false
matrix:
@@ -252,29 +229,10 @@ jobs:
# makepkg in an archlinux:base-devel container. The package is named after the tarball's
# .desktop file (io.tangencylabs.solidcraft) and written as
# dist/release/solidcraft-<version>-linux-<arch>.pkg.tar.zst.
# An Arch version can't hold a hyphen, and the action reads the version from the tarball's name
# (a hyphenated one comes out as 0.0.0): 0.40.0-rc.1 goes in as 0.40.0rc.1, which pacman sorts
# before 0.40.0, and the package keeps the release's name.
- id: pkgver
name: Arch version
run: |
pkgver="${VERSION//-/}"
echo "pkgver=$pkgver" >>"$GITHUB_OUTPUT"
if [ "$pkgver" != "$VERSION" ]; then
cp "dist/linux/solidcraft-$VERSION-linux-$ARCH.tar.gz" "dist/linux/solidcraft-$pkgver-linux-$ARCH.tar.gz"
fi
env:
ARCH: ${{ matrix.arch }}
- uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main
with:
tarball: dist/linux/solidcraft-${{ steps.pkgver.outputs.pkgver }}-linux-${{ matrix.arch }}.tar.gz
tarball: dist/linux/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.tar.gz
arch: ${{ matrix.arch }}
- name: The package under the release's version
if: steps.pkgver.outputs.pkgver != env.VERSION
env:
ARCH: ${{ matrix.arch }}
PKGVER: ${{ steps.pkgver.outputs.pkgver }}
run: sudo mv "dist/release/solidcraft-$PKGVER-linux-$ARCH.pkg.tar.zst" "dist/release/solidcraft-$VERSION-linux-$ARCH.pkg.tar.zst"
- name: Check the package
env:
PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst
@@ -654,8 +612,7 @@ jobs:
release:
name: GitHub Release or dry-run artifacts
needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web]
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
if: needs.version.outputs.build == 'true' && !inputs.packages_only
if: needs.version.outputs.build == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
-3
View File
@@ -10,8 +10,6 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
- New kernel cases go to solidcore: `cargo xtask lists`.
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
@@ -24,7 +22,6 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
- Show observable changes with real screenshots or a recording, captured yourself.
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
+5 -42
View File
@@ -103,22 +103,6 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/kernel-cases/import/1721-closed-bspline-tube-face/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#1721 | CC0-1.0 |
| tests/kernel-cases/import/2043-outer-bound-opposing-surface-sense/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#2043 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/258-cone-point-vertex/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-cone-to-point/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-tiny-pin/input.step | step-conformance contributors | copy of tests/step-conformance/tiny_pin.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/3098-bspline-edges-over-tiny-knot-span/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3098 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3208-extruded-ellipse-face-over-parameter-seam/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3208 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3352 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3730-void-oriented-shell-false/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3730 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3880-cylinder-seam-edge/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_seam.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3880-nurbs-hemisphere-dome/input.step | step-conformance contributors | copy of tests/step-conformance/dome_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3963-closed-spline-edge-vertex-past-start/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3963 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4005-cylinder-written-as-near-zero-cone/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4005 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4006-plane-written-as-cone-of-nearly-right-angle/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4006 | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/*.json.gz, tests/ssi-corpus/LICENSE-MIT, tests/ssi-corpus/LICENSE-APACHE | ssi-corpus contributors (jyviko) | 119 surface/surface intersection cases with exact ground truth from https://github.com/jyviko/ssi-corpus (cases/data, b04c8a6), gzip-compressed, unchanged; SSI scorer fixtures (tests/ssi-corpus/README.md) | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/y23_*.json.gz | Yang, Jia and Yan (paper authors: published surfaces, figures and topology); ssi-corpus contributors (reconstructions and truth) | 25 cases built from Yang, Jia & Yan 2023, "Topology Guaranteed B-Spline Surface/Surface Intersection", ACM Transactions on Graphics 42(6), Article 211; each cites the paper in its `source` field | CC-BY 4.0 (paper); MIT OR Apache-2.0 (case files) |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -245,18 +229,6 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/plus.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/pan.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/perspective.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_appearance.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_assistant.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_design.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_general.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_graphics.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_layout.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_mouse3d.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_navigation.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_plugins.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_shortcuts.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_sketch.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/prefs_tools.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -783,10 +755,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| docs/design/preferences/figures/view-shadow.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
| assets/locales/pending/theme-blur-ui.json | SolidCraft contributors | Original blur controls, notices and translations | MIT OR Apache-2.0 |
| crates/ui-egui/tests/fixtures/theme-blur.json | SolidCraft contributors | Original theme for blur option scenarios and manager review | MIT OR Apache-2.0 |
| assets/locales/ka.json | David Machakhelidze (devius) | Original Georgian translation of SolidCraft's English catalog, written with Claude Code from the meaning of each English string; terminology and doubtful strings decided by a native Georgian speaker; no commercial CAD text used | MIT OR Apache-2.0 |
## Code adapted from sibling projects
@@ -826,15 +794,6 @@ in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
The glTF test files are built by the tests themselves (crates/io/src/gltf_in_tests.rs,
crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
## Geometry kernel crates
| Crate | Used for | Licence |
|---|---|---|
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the B-rep kernel behind crates/smlib | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
Linked as a crate; no code from it is copied into SolidCraft. Its licence text and NOTICE go
into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
## FreeCAD and Rhino import library
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
@@ -850,13 +809,17 @@ copied into SolidCraft.
| SolidCraft file | Original contribution | Licence |
|---|---|---|
| crates/ui-egui/src/viewport.rs (`nearest_quarter_turn`, `axis_view_orbit`) | N3MI-DG's [PR #101](https://github.com/bherbruck/solidcraft/pull/101): [minimal TOP/BOTTOM turns](https://github.com/bherbruck/solidcraft/commit/c28e780f52490784ef5886f094b9d3f6d10c3e3f) and [drag-to-orbit](https://github.com/bherbruck/solidcraft/commit/f30b30be6ecc4e2e6544a851243137c01a354934), adapted to the axis gizmo | MIT OR Apache-2.0 |
| crates/smlib/src/core/offset.rs (`draft`: the tilted planes and `SmBrep::LocalOperation`, the tail of the capi's `draft_faces`) | ablasik's `draft` in `crates/smlib/src/ffi/rs_ops2.rs` on [ablasik/solidcraft `app/solidcore-backend-on-develop`](https://github.com/ablasik/solidcraft/blob/9c7e8e31565e1823f0591b58e2967351c32af13f/crates/smlib/src/ffi/rs_ops2.rs), adapted to `core::offset`'s errors and checks | MIT OR Apache-2.0 |
## Vendored third-party code
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
## Vendored renderer compatibility fix
+6 -29
View File
@@ -161,23 +161,9 @@ the most complete description of the house style.
| UI scenarios | `crates/ui-egui/tests/scenarios/*.json` | part of `cargo test` |
| Reference tests | `cargo xtask oracle` | `tests/oracle/` (see below) |
The reference for what the kernel builds is the kernel-cases ladder (`tests/kernel-cases/ladder/`,
`cargo xtask kernel-cases --ladder`, [docs/kernel-ladder.md](docs/kernel-ladder.md)).
**Kernel cases go to solidcore.** A case that checks a kernel operation (a boolean, a blend, a
shell, a sweep, an import's geometry) is a new entry in solidcore's corpus,
`crates/solidcore/tests/kernel_cases/<area>/` in
[tangency-labs/solidcore](https://github.com/tangency-labs/solidcore), with an answer in closed
form or exact. Draft it as a rig case here, export it with
`cargo xtask kernel-cases --area <area> --export DIR`, add `DIR/<area>/<case>.json` and its
`.bodies.json.gz` there, and delete the draft; or write the entry by hand as that folder's README
describes. `tests/kernel-cases/` takes no new cases: its `CASES` list only shrinks
(`cargo xtask lists`). App-level cases (what a command does, the UI, product rules) stay in
SolidCraft, as tests in their area's existing test file or UI scenarios.
The naming corpus checks persistent names: every name a corpus design stores still finds a face or
an edge, and two builds name its faces and edges alike
(`cargo test --profile ci -p solidcraft-cli --test naming_corpus`).
A geometry change that changes the robustness goldens re-records them in the same pull request:
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
black-box suites miss.
@@ -236,7 +222,8 @@ Pull requests skip a few heavy tests (one to twelve minutes each), which run in
Check that validation after merging. They are listed in
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
@@ -256,8 +243,7 @@ the surface itself). Expected values are either worked out in closed form
(`tools/oracle-analytic/`) or computed by building the same recipe in FreeCAD, run as a separate
program (`tools/freecad-oracle/`); each file says which. The generated
[reference test report](docs/oracle.md) has the current case count and results. The reference-test
CI job runs `cargo xtask oracle` during full validation, and on pull requests that reach
`solidcraft-cli`. A case that
CI job runs `cargo xtask oracle` during full validation; pull-request runs skip it. A case that
fails because of a known bug names its issue in `expected.json` (`"known_failure": "#123"`);
never change an expected value to match SolidCraft's output.
@@ -353,15 +339,6 @@ and a fresh CI run:
PRs in groups by area, so PRs that touch the same code meet a conflict at most once.
4. Reference the issue (`Fixes #12`). In the PR, say what you tested and how. For geometry work, include the measured numbers (and
the expected ones).
5. Write the PR title and first sentence for someone who hasn't seen your chat, the issue or the
code. Maintainers pick what to review from the title, and it becomes the commit on `main`. Say
which part of the app changes and what a user now sees or can do, and name things rather than
leaning on "there", "it" or "this".
- Unclear: `Send to Slicer: pick one print face per body there, and show print faces only while it is open`
- Clear: `3D printing: choose the face each body rests on in the Send to Slicer window, not the face menu`
Use plain, literal English in PR text: one idea per sentence, active voice, and one word for one
thing. This helps everyone, including readers whose first language isn't English.
Commenting `/claim` assigns you, adds the `in progress` label, and replies with its automatic cleanup
deadline. It frees a claim with no linked PR after 72 hours, or a claim with a PR after 14 days
Generated
+327 -91
View File
@@ -186,6 +186,15 @@ dependencies = [
"libc",
]
[[package]]
name = "approx"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
dependencies = [
"num-traits",
]
[[package]]
name = "arboard"
version = "3.6.1"
@@ -206,6 +215,12 @@ dependencies = [
"x11rb",
]
[[package]]
name = "array-macro"
version = "2.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -518,6 +533,15 @@ dependencies = [
"piper",
]
[[package]]
name = "branches"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0"
dependencies = [
"rustc_version",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -765,6 +789,17 @@ dependencies = [
"libc",
]
[[package]]
name = "cgmath"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
dependencies = [
"approx",
"num-traits",
"serde",
]
[[package]]
name = "chacha20"
version = "0.10.2"
@@ -787,6 +822,19 @@ dependencies = [
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -857,6 +905,12 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1034,6 +1088,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "derive_more"
version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.119",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -1180,7 +1247,7 @@ dependencies = [
"bitflags 2.13.2",
"emath",
"epaint",
"itertools",
"itertools 0.15.0",
"log",
"nohash-hasher",
"profiling",
@@ -1598,8 +1665,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -1914,6 +1983,30 @@ dependencies = [
"typenum",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -2073,6 +2166,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.15.0"
@@ -2167,6 +2269,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "katexit"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
@@ -2317,6 +2430,15 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "matext4cgmath"
version = "0.1.0"
dependencies = [
"cgmath",
"katexit",
"num-complex",
]
[[package]]
name = "memchr"
version = "2.8.3"
@@ -3192,6 +3314,30 @@ dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -3364,6 +3510,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rclite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2"
dependencies = [
"branches",
]
[[package]]
name = "read-fonts"
version = "0.41.0"
@@ -4051,27 +4206,9 @@ dependencies = [
"serde",
]
[[package]]
name = "solidcore"
version = "1.0.1"
source = "git+https://github.com/tangency-labs/solidcore?rev=8ddce901671f1cebd9214a31d21895d725e886c8#8ddce901671f1cebd9214a31d21895d725e886c8"
dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "solidcore-record"
version = "0.1.0"
source = "git+https://github.com/tangency-labs/solidcore?rev=f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f#f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4102,32 +4239,29 @@ dependencies = [
"ureq",
"windows-sys 0.61.2",
"winit",
"winreg",
"x11rb",
"zbus 5.19.0",
]
[[package]]
name = "solidcraft-3dx-client"
version = "0.40.0-rc.2"
version = "0.30.1"
[[package]]
name = "solidcraft-cli"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"image",
"log",
"serde_json",
"solidcore-record",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-kernel",
"solidcraft-mcp",
]
[[package]]
name = "solidcraft-derive"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4136,17 +4270,15 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"log",
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-derive",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
@@ -4155,7 +4287,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"image",
"log",
@@ -4163,12 +4295,10 @@ dependencies = [
"serde",
"serde_json",
"serde_path_to_error",
"solidcore-record",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-io",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
@@ -4178,17 +4308,16 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"serde",
"serde_json",
"solidcore-record",
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft-gpu"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"image",
"log",
@@ -4197,7 +4326,6 @@ dependencies = [
"robust",
"serde",
"serde_json",
"solidcore-record",
"solidcraft-engine",
"solidcraft-geom",
"solidcraft-io",
@@ -4209,7 +4337,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4219,46 +4347,42 @@ dependencies = [
"gethostname",
"gltf",
"log",
"png",
"quick-xml",
"rustix 1.1.5",
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-sketch",
"solidcraft-testkit",
"thiserror 2.0.21",
"tiny-skia",
"zip",
]
[[package]]
name = "solidcraft-kernel"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"log",
"proptest",
"rayon",
"serde",
"serde_json",
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
"solidcraft-smlib",
"solidcraft-step",
"spade",
"thiserror 2.0.21",
"truck-meshalgo",
"truck-modeling",
"truck-shapeops",
"truck-stepio",
"truck-topology",
"web-time",
]
[[package]]
name = "solidcraft-mcp"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"image",
"serde_json",
@@ -4266,17 +4390,9 @@ dependencies = [
"solidcraft-gpu",
]
[[package]]
name = "solidcraft-profile"
version = "0.40.0-rc.2"
dependencies = [
"serde_json",
"web-time",
]
[[package]]
name = "solidcraft-render"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"image",
"moxcms",
@@ -4284,7 +4400,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-geom",
"web-time",
"weezl",
@@ -4292,55 +4407,31 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"harfrust",
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-geom",
"thiserror 2.0.21",
"ttf-parser",
"unicode-bidi",
]
[[package]]
name = "solidcraft-smlib"
version = "0.40.0-rc.2"
dependencies = [
"flate2",
"log",
"serde_json",
"solidcore",
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
]
[[package]]
name = "solidcraft-space-mouse"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-step"
version = "0.40.0-rc.2"
dependencies = [
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
]
[[package]]
name = "solidcraft-testkit"
version = "0.40.0-rc.2"
version = "0.30.1"
[[package]]
name = "solidcraft-ui-egui"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"arboard",
"egui",
@@ -4355,7 +4446,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
@@ -4372,7 +4462,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"eframe",
"egui",
@@ -4433,6 +4523,16 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.119"
@@ -4682,6 +4782,144 @@ dependencies = [
"once_cell",
]
[[package]]
name = "truck-base"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c279de9e92e5dc20a188deb0bb9a4bd421a6a185e57e03e19025e84a36c5a05"
dependencies = [
"cgmath",
"matext4cgmath",
"rustc-hash 2.1.3",
"serde",
]
[[package]]
name = "truck-derivers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421ff1c5a303aed64e295d9b30e8424ee7a2fa251617a59ae4acfd078496009f"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "truck-geometry"
version = "0.5.0"
dependencies = [
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "truck-geotrait"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a31fe98d5a0589ad20576260e214af23dea21a8d125da81ad3bbcb666f411f"
dependencies = [
"getrandom 0.2.17",
"rand 0.8.8",
"thiserror 1.0.69",
"truck-base",
"truck-derivers",
]
[[package]]
name = "truck-meshalgo"
version = "0.4.0"
dependencies = [
"array-macro",
"derive_more",
"itertools 0.13.0",
"rayon",
"rustc-hash 2.1.3",
"spade",
"truck-base",
"truck-geometry",
"truck-modeling",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-modeling"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8082c2dfac8c2732a014d81807fb5d24dbac0d72ee4de2a705b7fbc05fa05b18"
dependencies = [
"derive_more",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geometry",
"truck-geotrait",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-polymesh"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f22def310af4a89c37beb4839bbf702b302d2c112ea29553759f0e16622fbf"
dependencies = [
"array-macro",
"bytemuck",
"itertools 0.13.0",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "truck-shapeops"
version = "0.4.0"
dependencies = [
"derive_more",
"itertools 0.13.0",
"rustc-hash 2.1.3",
"truck-base",
"truck-geometry",
"truck-geotrait",
"truck-meshalgo",
"truck-topology",
]
[[package]]
name = "truck-stepio"
version = "0.3.0"
dependencies = [
"chrono",
"truck-geometry",
"truck-modeling",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-topology"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf62a1610405c5e39ab5eb6948f326fc1641689734a6b8207a80792133fe0e78"
dependencies = [
"parking_lot",
"rayon",
"rclite",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -5779,11 +6017,9 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.40.0-rc.2"
version = "0.30.1"
dependencies = [
"flate2",
"proc-macro2",
"quick-xml",
"serde",
"serde_json",
"sha2 0.10.9",
+19 -16
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.40.0-rc.2"
version = "0.30.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -16,11 +16,8 @@ repository = "https://github.com/bherbruck/solidcraft"
robust = "1.2"
# internal
solidcraft-geom = { path = "crates/geom" }
solidcraft-profile = { path = "crates/profile" }
solidcraft-sketch = { path = "crates/sketch" }
solidcraft-kernel = { path = "crates/kernel" }
solidcraft-smlib = { path = "crates/smlib" }
solidcraft-step = { path = "crates/step" }
solidcraft-render = { path = "crates/render" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
@@ -31,14 +28,6 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
solidcraft-mcp = { path = "crates/mcp" }
solidcraft-derive = { path = "crates/derive" }
solidcraft-3dx-client = { path = "crates/threedx-client" }
# The Rust port of NVIDIA's SMLib (Apache-2.0), the B-rep kernel behind crates/smlib. Pinned;
# move the rev on its own commit. For a
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "8ddce901671f1cebd9214a31d21895d725e886c8" }
# Geometry as data (solidcore's layers §1, "Record"): the B-rep record, its structural validation
# and the native body file. Pinned like solidcore; for a local checkout, patch it as above.
solidcore-record = { git = "https://github.com/tangency-labs/solidcore", rev = "f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f" }
# test tooling: a dev-dependency only (xtask/src/layers.rs)
solidcraft-testkit = { path = "crates/testkit" }
@@ -58,7 +47,14 @@ sha2 = "0.10"
moxcms = { version = "0.8.1", default-features = false, features = ["extended_range"] }
semver = "1"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0).
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
truck-modeling = "0.6"
truck-shapeops = { version = "0.4", default-features = false }
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
truck-stepio = { version = "0.3", default-features = false }
truck-topology = "0.6"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
# tree through truck-meshalgo.
spade = { version = "2.15", default-features = false, features = ["std"] }
rayon = "1.12"
@@ -131,7 +127,9 @@ opt-level = 2
opt-level = 1
# Tests and lints (`cargo xtask ci`, GitHub CI): quick to build, quick enough to run. Light
# optimisation, line tables only (file:line in backtraces), no LTO, no debug assertions.
# optimisation, line tables only (file:line in backtraces), no LTO. Debug assertions are off because
# truck's debug-only checks reject valid geometry (seam edges, closed edges); that was the only
# reason the tests used to build with --release, which cost most of CI's time.
[profile.ci]
inherits = "dev"
opt-level = 1
@@ -154,9 +152,14 @@ codegen-units = 16
# binary). See docs/crash-reports.md.
debug = "line-tables-only"
# Vendored crates with SolidCraft fixes (see vendor/README.md and ATTRIBUTION.md).
# truck-shapeops with SolidCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md).
[patch.crates-io]
truck-geometry = { path = "vendor/truck-geometry" }
truck-shapeops = { path = "vendor/truck-shapeops" }
truck-stepio = { path = "vendor/truck-stepio" }
truck-meshalgo = { path = "vendor/truck-meshalgo" }
matext4cgmath = { path = "vendor/matext4cgmath" }
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
egui-wgpu = { path = "vendor/egui-wgpu" }
-6
View File
@@ -173,12 +173,6 @@ and the [foundation ledger](docs/drawing-foundations.md) track the bounded imple
packages. The first geometry increment defines validated section records in a plane-local
frame; the ledger separates that contract from section producers and the Drawing workspace.
Drawing UI scaffold: the native workspace now supplies the shared four-tab ribbon,
source Browser, compact dockable sheet strip and resizable Sheet Settings/options
panel. Planned tools remain unavailable; only existing source services execute.
Drawing files and paper rendering are still deferred. See the
[UI implementation roadmap](docs/drawing-workspace-ui.md).
Desktop view DXF export runs in the file-operation worker with five-stage progress and
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
+1167 -388
View File
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -20,15 +20,11 @@ name = "solvecraft-cli"
path = "src/alias.rs"
[features]
default = ["raytrace-cpu", "profile"]
default = ["raytrace-cpu"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# The profiler, compiled in so any build can say where its time goes (`--profile`,
# SOLIDCRAFT_PROFILE=1); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
[dependencies]
solidcore-record = { workspace = true }
solidcraft-engine = { workspace = true }
solidcraft-gpu = { workspace = true }
solidcraft-mcp = { workspace = true }
@@ -38,5 +34,3 @@ serde_json = { workspace = true }
[dev-dependencies]
# Snapshot tests decode the pictures to compare them with a tolerance.
image = { workspace = true }
# Tests measure meshes at their own tolerances.
solidcraft-kernel = { workspace = true }
+3 -3
View File
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
run.push(t.elapsed().as_secs_f64() * 1000.0);
let t = std::time::Instant::now();
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
s.world_state().warm_meshes(|| true);
shown.push(t.elapsed().as_secs_f64() * 1000.0);
if run.len() == 100 || i + 1 == commands.len() {
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
@@ -198,7 +198,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
shown.clear();
}
}
let world = s.world_state().map_err(|e| e.to_string())?;
let world = s.world_state();
println!(
"{}",
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
@@ -236,7 +236,7 @@ mod tests {
}
}
assert!(s.model.results.iter().all(|r| r.error.is_none()));
let world = s.world_state().unwrap();
let world = s.world_state();
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
}
-4
View File
@@ -63,10 +63,6 @@ fn replay(report: &Value) -> Result<(), String> {
Some(d) => super::load(d)?,
None => solidcraft_engine::Session::default(),
};
// The ids the session drew from there, so the commands give the same objects the same ids.
if let Some(seed) = script["ids"].as_u64() {
s.seed_ids(seed);
}
std::panic::set_hook(Box::new(|info| {
let loc = info.location().map_or_else(|| "(unknown)".to_string(), |l| format!("{}:{}", l.file(), l.line()));
let msg = info.payload_as_str().unwrap_or("(not a text message)").to_string();
+26 -3
View File
@@ -40,6 +40,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
"unsupported": run.unsupported,
"rejected": run.rejected,
"boolean_fallbacks": run.boolean_fallbacks,
"boolean_reports_truncated": run.boolean_reports_truncated,
"panics": PANICS.swap(0, std::sync::atomic::Ordering::SeqCst),
});
if run.outcome.is_bug() {
@@ -189,9 +191,23 @@ fn summarise(results: &[Value], took: Duration) -> String {
let mut boolean: BTreeMap<String, usize> = BTreeMap::new();
let mut at_step: BTreeMap<String, usize> = BTreeMap::new();
let mut unsupported: BTreeMap<String, usize> = BTreeMap::new();
let mut fallbacks: BTreeMap<String, [u64; 2]> = BTreeMap::new();
let mut truncated = [0usize; 2];
let mut steps = 0;
let (mut dp, mut bp) = (0, 0);
for v in results {
for (i, group) in ["design", "boolean"].into_iter().enumerate() {
if v[group]["boolean_reports_truncated"].as_bool().unwrap_or(false) {
truncated[i] += 1;
}
if let Some(counts) = v[group]["boolean_fallbacks"].as_object() {
for (strategy, count) in counts {
if let Some(total) = fallbacks.entry(strategy.clone()).or_default().get_mut(i) {
*total = total.saturating_add(count.as_u64().unwrap_or(0));
}
}
}
}
dp += usize::from(v["design"]["panics"].as_u64().unwrap_or(0) > 0);
bp += usize::from(v["boolean_panics"].as_u64().unwrap_or(0) > 0);
let k = if v.get("timeout").is_some() {
@@ -230,6 +246,11 @@ fn summarise(results: &[Value], took: Duration) -> String {
for (k, c) in &boolean {
s += &format!("- {k}: {}\n", pct(*c));
}
s += "\nSuccessful Boolean retries (events, including nested operations):\n";
for (strategy, [designs, checks]) in &fallbacks {
s += &format!("- {strategy}: designs {designs}, boolean checks {checks}\n");
}
s += &format!("- Capped reports: designs {}, boolean checks {}\n", truncated[0], truncated[1]);
s
}
@@ -238,13 +259,15 @@ mod tests {
use super::*;
#[test]
fn report_counts_outcomes_and_timeouts() {
fn report_counts_retry_events_separately_and_marks_incomplete_logs() {
let results = [
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":2}},"boolean":{"outcome":"ok","boolean_fallbacks":{"shifted_operands":3},"boolean_reports_truncated":true}}),
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":1}},"boolean":{"outcome":"ok"}}),
json!({"timeout":90}),
];
let text = summarise(&results, Duration::ZERO);
assert!(text.contains("shifted_operands: designs 3, boolean checks 3"), "{text}");
assert!(text.contains("Capped reports: designs 0, boolean checks 1"), "{text}");
assert!(text.contains("timeout: 1 (33.3 %)"), "{text}");
}
}
File diff suppressed because it is too large Load Diff
@@ -1,299 +0,0 @@
//! `kernel-cases --export=DIR`: one kernel rig case as a corpus entry for solidcore's rig. The
//! case's commands run as the rig runs them while the kernel's operations are traced
//! (`smlib_trace`); the entry holds the operation (or the chain of operations) that made the
//! bodies the case ends with, each operand that came in as a record given as that record (the
//! B-rep record, solidcore-record's JSON), the fix set, the case's expected values and the rig's
//! own result. A case whose result no kernel operation alone accounts for is written too, with
//! the reasons, so nothing is dropped silently.
//!
//! An entry's operation is expressible (`operation_expressible`) when the run failed in a kernel
//! operation (the last one traced failed), or when every body the case ends with is the record a
//! traced operation's output was written as, or an operand some traced operation took, as its
//! record. The entry is wholly expressible (`expressible`) when, besides, every value it grades is
//! the kernel's: none of `continuity`, `watertight`, `files`, `results`, `construction` or
//! `flat_pattern` (SolidCraft computes them), no face, edge or vertex count that the rig's merging
//! across analytic surfaces (`merged_topology`) changes, and no refusal graded on SolidCraft's
//! wording of the kernel's error. Those are listed in `ungraded`.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcore_record::brep::Brep;
use solidcraft_engine::kernel::smlib_trace::{self, Event};
use crate::read_json;
/// What a traced case leaves for its entry: the trace of its commands, and the bodies it ends
/// with by name or the engine's error.
#[derive(Default)]
pub struct Capture {
pub events: Vec<Event>,
pub ran: Option<Result<Vec<(String, Brep)>, String>>,
}
/// The expected values SolidCraft computes, never the kernel.
const GRADER_ONLY: [&str; 6] = ["continuity", "watertight", "files", "results", "construction", "flat_pattern"];
/// The case directory's area (its path under `tests/kernel-cases/`, a ladder case's tier under
/// `ladder/`) and its id.
fn area_and_id(dir: &str) -> Result<(String, String), String> {
let p = Path::new(dir);
let parts: Vec<String> = p.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect();
let at = parts.iter().rposition(|c| c == "kernel-cases").ok_or_else(|| format!("{dir}: not under tests/kernel-cases"))?;
let rest = parts.get(at + 1..).unwrap_or_default();
match rest.split_last() {
Some((id, area)) if !area.is_empty() => Ok((area.join("/"), id.clone())),
_ => Err(format!("{dir}: no area")),
}
}
/// The case's line in its area's `KNOWN_FAILING`: why it fails today; `None` when not listed.
fn known_failing(dir: &str, id: &str) -> Option<String> {
let list = Path::new(dir).parent()?.join("KNOWN_FAILING");
let text = std::fs::read_to_string(list).ok()?;
text.lines().filter(|l| !l.trim_start().starts_with('#')).find_map(|l| {
let (case, why) = l.split_once(char::is_whitespace).unwrap_or((l, ""));
(case == id).then(|| why.trim().to_string())
})
}
/// The trace read as operations and the records that went in and came out.
struct Trace<'a> {
/// Each operation: its name, operands, parameters and outputs or error.
calls: Vec<(&'static str, &'a [u64], &'a Value, &'a Result<Vec<u64>, String>)>,
/// The record each body made from a record was made from.
imports: HashMap<u64, (&'a Brep, bool, bool)>,
/// The records bodies were written as, in order.
exports: Vec<(u64, &'a Brep)>,
/// The operation that made each body.
producer: HashMap<u64, usize>,
}
impl<'a> Trace<'a> {
fn of(events: &'a [Event]) -> Trace<'a> {
let mut t = Trace { calls: Vec::new(), imports: HashMap::new(), exports: Vec::new(), producer: HashMap::new() };
for e in events {
match e {
Event::Import { id, record, trusted, keeping_splines } => {
t.imports.insert(*id, (record, *trusted, *keeping_splines));
}
Event::Export { id, record } => t.exports.push((*id, record)),
Event::Call { op, inputs, params, outputs } => {
if let Ok(made) = outputs {
for id in made {
t.producer.insert(*id, t.calls.len());
}
}
t.calls.push((*op, inputs, params, outputs));
}
}
}
t
}
/// Bodies some operation took as an operand.
fn operands(&self) -> BTreeSet<u64> {
self.calls.iter().flat_map(|c| c.1.iter().copied()).collect()
}
}
/// The operations `ends` need, in the order they ran: each body's maker, and its operands' makers
/// in turn, back to bodies made from records. Operands made by nothing traced are listed in
/// `unknown`.
fn chain(t: &Trace<'_>, ends: &[u64], unknown: &mut BTreeSet<u64>) -> Vec<usize> {
let mut need = BTreeSet::new();
let mut todo: Vec<u64> = ends.to_vec();
while let Some(id) = todo.pop() {
if t.imports.contains_key(&id) {
continue;
}
match t.producer.get(&id) {
Some(&i) => {
if need.insert(i)
&& let Some(c) = t.calls.get(i)
{
todo.extend(c.1.iter().copied());
}
}
None => {
unknown.insert(id);
}
}
}
need.into_iter().collect()
}
/// The entry of case `dir`, run with result `out`, into `root/<area>/<id>.json`, and the fix set
/// into `root/fixes.json`.
pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<(), String> {
let (area, id) = area_and_id(dir)?;
let want = read_json(&format!("{dir}/expected.json"))?;
let t = Trace::of(&capture.events);
// Why the operation is not the kernel's alone, and what the entry cannot grade without
// SolidCraft.
let mut why_not: Vec<String> = Vec::new();
let mut ungraded: Vec<String> = Vec::new();
let has = |k: &str| want.get(k).is_some_and(|v| !v.is_null());
if let Some(missing) = want["missing"].as_str() {
why_not.push(format!("SolidCraft has no command for it yet: {missing}"));
}
for k in GRADER_ONLY.iter().filter(|k| has(k)) {
ungraded.push(format!("{k}: SolidCraft computes it"));
}
let mut result: Vec<Value> = Vec::new();
let mut ends: Vec<u64> = Vec::new();
let mut failed_call: Option<usize> = None;
let mut outcome = json!({"ran": true});
match (&capture.ran, has("missing")) {
(_, true) => {}
(None, false) => why_not.push("the case did not run".into()),
(Some(Err(e)), false) => {
// The run stopped. It stopped in the kernel when the last operation that failed is
// followed by no operation but copies (a copy or a tagged copy, which build nothing):
// nothing was built after it, so the engine's error reports that failure in its own
// words. An operation that ran after it means the engine went on and failed itself.
outcome = json!({"ran": false, "engine_error": e});
let last_failed = t.calls.iter().rposition(|c| c.3.is_err());
let built_after = |i: usize| t.calls.iter().skip(i + 1).any(|c| !matches!(c.0, "copy" | "tagged"));
match last_failed.filter(|&i| !built_after(i)).and_then(|i| t.calls.get(i).map(|c| (i, c))) {
Some((i, c)) => {
failed_call = Some(i);
ends.extend(c.1.iter().copied());
if let Err(k) = c.3 {
outcome["kernel_error"] = json!(k);
if let Some(why) = want["refusal"].as_str().filter(|w| !k.contains(*w)) {
ungraded.push(format!("refusal {why:?}: SolidCraft's wording of the kernel's error"));
}
}
}
None if t.calls.is_empty() => why_not.push(format!("no kernel operation ran before the failure: {e}")),
None => why_not.push(format!("the failure is SolidCraft's, not a kernel operation's: {e}")),
}
}
(Some(Ok(world)), false) => {
let operands = t.operands();
let mut made = 0;
for (name, record) in world {
let from_op = t.exports.iter().rev().find(|(id, r)| *r == record && t.producer.contains_key(id)).map(|(id, _)| *id);
let operand = t.imports.iter().filter(|(i, (r, _, _))| *r == record && operands.contains(i)).map(|(i, _)| *i).min();
match (from_op, operand) {
(Some(id), _) => {
made += 1;
ends.push(id);
result.push(json!({"body": name, "made": format!("b{id}")}));
}
(None, Some(id)) => {
ends.push(id);
result.push(json!({"body": name, "operand": format!("b{id}")}));
}
(None, None) => why_not.push(format!(
"body {name:?} is no kernel operation's output nor an operand: SolidCraft built it or changed the kernel's record"
)),
}
}
if world.is_empty() {
why_not.push("the case ends with no body".into());
} else if made == 0 && why_not.is_empty() {
why_not.push("no kernel operation made a body the case ends with".into());
}
// Counts graded as SolidCraft merges them, where that differs from the records'.
let raw = |k: &str| -> u64 {
world
.iter()
.map(|(_, r)| match k {
"faces" => r.faces.len(),
"edges" => r.edges.len(),
_ => r.vertices.len(),
} as u64)
.sum()
};
for k in ["faces", "edges", "vertices"] {
if let Some(merged) = out["measured"][k].as_u64().filter(|&m| has(k) && m != raw(k)) {
ungraded.push(format!("{k}: graded merged across analytic surfaces ({merged}), the records have {}", raw(k)));
}
}
outcome["raw_counts"] = json!({"faces": raw("faces"), "edges": raw("edges"), "vertices": raw("vertices")});
}
}
let mut unknown = BTreeSet::new();
let mut order = chain(&t, &ends, &mut unknown);
if !unknown.is_empty() {
why_not.push(format!("operands made by nothing traced: {unknown:?}"));
}
if let Some(i) = failed_call.filter(|i| !order.contains(i)) {
order.push(i);
}
// Operand records: every body the chain takes, or the case ends with, that came in as a record.
let mut bodies = BTreeMap::new();
let given = |id: u64, bodies: &mut BTreeMap<String, Value>| {
if let Some((record, trusted, splines)) = t.imports.get(&id) {
bodies.insert(format!("b{id}"), json!({"trusted": trusted, "keeping_splines": splines, "record": record}));
}
};
let mut calls: Vec<Value> = Vec::new();
for (op, inputs, params, outputs) in order.iter().filter_map(|&i| t.calls.get(i)) {
for &id in inputs.iter() {
given(id, &mut bodies);
}
let mut c = json!({"op": op, "inputs": inputs.iter().map(|i| format!("b{i}")).collect::<Vec<_>>(), "params": params});
match outputs {
Ok(made) => c["outputs"] = json!(made.iter().map(|i| format!("b{i}")).collect::<Vec<_>>()),
Err(e) => c["error"] = json!(e),
}
calls.push(c);
}
for r in &result {
if let Some(id) = r["operand"].as_str().and_then(|b| b.strip_prefix('b')).and_then(|n| n.parse().ok()) {
given(id, &mut bodies);
}
}
let rig = json!({
"status": out["status"], "error": out["error"], "refused_with": out["refused_with"], "measured": out["measured"],
"known_failing": known_failing(dir, &id),
});
let operation_expressible = why_not.is_empty();
let entry = json!({
"case": id,
"area": area,
"sources": want["sources"],
"what": want["what"],
"expressible": operation_expressible && ungraded.is_empty(),
"operation_expressible": operation_expressible,
"why_not": why_not,
"ungraded": ungraded,
"expected": want,
"rig": rig,
"outcome": outcome,
"calls": calls,
"bodies": bodies,
"result": result,
"traced_ops": t.calls.iter().map(|c| c.0).collect::<Vec<_>>(),
});
let folder: PathBuf = root.join(&area);
std::fs::create_dir_all(&folder).map_err(|e| format!("{}: {e}", folder.display()))?;
let path = folder.join(format!("{id}.json"));
let text = serde_json::to_string(&entry).map_err(|e| e.to_string())?;
std::fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display()))?;
// Cases run in parallel processes: each writes its own file and renames it over the shared
// one, so no reader sees a torn file (every process writes the same fix set).
let fixes = serde_json::to_string_pretty(&smlib_trace::fixes()).map_err(|e| e.to_string())?;
let own = root.join(format!("fixes.json.{}", std::process::id()));
std::fs::write(&own, fixes).map_err(|e| format!("{}: {e}", own.display()))?;
std::fs::rename(&own, root.join("fixes.json")).map_err(|e| format!("{}: {e}", root.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_area_is_the_path_under_kernel_cases() {
assert_eq!(area_and_id("tests/kernel-cases/booleans/1894-x").unwrap(), ("booleans".into(), "1894-x".into()));
assert_eq!(area_and_id("/r/tests/kernel-cases/ladder/07-fillets/003-y").unwrap(), ("ladder/07-fillets".into(), "003-y".into()));
assert!(area_and_id("/r/other/x").is_err());
}
}
+22 -105
View File
@@ -10,13 +10,12 @@
//! 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]
//! the standard Assistant model test (#4851) on a running app
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes] [--quality LEVEL]
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
@@ -34,12 +33,9 @@ mod bench_agent;
mod bench_assistant;
mod crashes;
mod fuzz;
mod kernel_cases;
mod kernel_cases_export;
mod logger;
mod oracle;
mod preview;
mod profiling;
mod recipe;
mod render;
mod spatial;
@@ -62,19 +58,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] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n [--param-check off|warn|strict]\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 (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\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 (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
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 [--param-check off|warn|strict]\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 (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\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] [--profile] [--profile-out trace.json] (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 (--profile and --profile-out as for run)\n";
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";
/// `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";
/// `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] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\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.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";
/// `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";
/// `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] [--quality coarse|normal|fine|ultra]\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 --quality: the display quality the bodies are meshed at, as the app's setting (default normal))\n";
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";
/// `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.
@@ -85,8 +81,6 @@ const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
/// `oracle`'s lines in the usage.
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
/// `kernel-cases`'s lines in the usage.
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--export=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --export: each case's\n kernel operations, input records and expected values for solidcore's rig, in DIR/<area>/<case>.json; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\n";
/// `fuzz`'s lines in the usage.
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
@@ -124,7 +118,6 @@ const COMMANDS: &[Command] = &[
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
Command { name: "kernel-cases", usage: USAGE_KERNEL_CASES, listed: true },
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
Command { name: "mcp", usage: USAGE_MCP, listed: true },
@@ -140,19 +133,6 @@ const COMMANDS: &[Command] = &[
fn main() -> ExitCode {
let mut args: Vec<String> = std::env::args().skip(1).collect();
logger::init(&mut args);
// `--profile` / `--profile-out` (run, eval, bench-open) or SOLIDCRAFT_PROFILE (any command):
// record from here, and print the table when the command ends.
let profiling = match args.first().map(String::as_str) {
Some("run" | "eval" | "inspect" | "bench-open") => profiling::start(&args[1..]),
_ => profiling::start(&[]),
};
let profiling = match profiling {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
if let Err(e) = max_file_size(&mut args) {
eprintln!("error: {e}");
return ExitCode::FAILURE;
@@ -190,7 +170,6 @@ fn main() -> ExitCode {
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
}),
Some("oracle") => oracle::run(&args[1..]),
Some("kernel-cases") => kernel_cases::run(&args[1..]),
Some("fuzz") => fuzz::run(&args[1..]),
Some("fuzz-one") => fuzz::one(&args[1..]),
Some("mcp") => {
@@ -227,11 +206,6 @@ fn main() -> ExitCode {
}
_ => Err(usage()),
};
// The table also when the command failed: where it spent its time before failing.
let r = match (r, profiling.map(profiling::Profiling::finish)) {
(Ok(()), Some(Err(e))) => Err(e),
(r, _) => r,
};
match r {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
@@ -312,15 +286,8 @@ pub fn load(path: &str) -> Result<Session, String> {
/// `load`, with scripts and recipes run at the parameter-check level `check`, which the
/// session keeps.
fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
load_with(path, check, solidcraft_engine::kernel::Validation::default())
}
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
/// the session keeps.
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
let mut s = Session::default();
s.param_check = check;
s.set_validation(validation);
if solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path) || solidcraft_engine::io::is_point_cloud_path(path) {
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
return Ok(s);
@@ -328,7 +295,7 @@ fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validatio
let bytes = solidcraft_engine::io::limits::read(path, solidcraft_engine::io::limits::Kind::Design).map_err(|e| format!("{path}: {e}"))?;
if solidcraft_engine::io::is_design_container(&bytes) {
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
let mut session = Session::opened_at_validating(doc, None, Some(path), validation);
let mut session = Session::opened_at(doc, None, Some(path));
session.param_check = check;
return Ok(session);
}
@@ -392,16 +359,6 @@ fn positional(args: &[String]) -> Option<&String> {
None
}
/// Is `path` a command script (or a recipe), not a design: JSON that is no saved design.
fn is_script(path: &str) -> bool {
let Ok(bytes) = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES) else { return false };
if solidcraft_engine::io::is_design_container(&bytes) {
return false;
}
serde_json::from_slice::<Value>(&bytes)
.is_ok_and(|v| !v.get("format").and_then(Value::as_str).is_some_and(|f| solidcraft_engine::doc::format::version_of(f).is_some()))
}
/// `--json-out FILE`: where the JSON a command prints goes instead of stdout.
fn json_out(args: &[String]) -> Result<Option<&str>, String> {
if !args.iter().any(|a| a == "--json-out") {
@@ -438,31 +395,24 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
known_args(
"run",
args,
&["--in", "--out", "--save", "--json-out", "--param-check", "--profile-out", "--validation"],
&["--in", "--out", "--save", "--json-out", "--param-check"],
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
true,
)?;
let validation = validation_flag("run", args)?;
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
return Err(usage());
}
let want_results = args.iter().any(|a| a == "--results");
// `--profile` records the profiler's spans (the table goes to stderr, `profiling.rs`); for a
// script it also times each command and splits out its kernel time, implying `--timings`.
let script = path.is_some_and(|p| is_script(p));
let profile = script && args.iter().any(|a| a == "--profile" || a == "--profile-out");
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
let profile = args.iter().any(|a| a == "--profile");
let timings = profile || args.iter().any(|a| a == "--timings");
let (mut s, results) = if want_results || timings {
// Collect every command's result or time (scripts only), then process requested files too.
let mut s = match base {
Some(b) => load_with(b, check, validation)?,
None => {
let mut s = Session::default();
s.set_validation(validation);
s
}
Some(b) => load_checked(b, check)?,
None => Session::default(),
};
s.param_check = check;
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
@@ -479,7 +429,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
};
(s, Some(out))
} else {
(open_validating(base, path, check, validation)?, None)
(open(base, path, check)?, None)
};
// A feature that fails leaves the bodies as they were before it (a parameter change can
// break a feature without failing the command that made it): writing them would hand out
@@ -506,38 +456,18 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
/// The session `run`, `eval` and `snapshot` start from: `--in` (a design or STEP file) with the
/// positional script run on top of it, or either one alone, with scripts run at `check`.
fn open(base: Option<&str>, path: Option<&String>, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
open_validating(base, path, check, solidcraft_engine::kernel::Validation::default())
}
/// [`open`], the features computed under `validation`.
fn open_validating(
base: Option<&str>,
path: Option<&String>,
check: solidcraft_engine::schema::ParamCheck,
validation: solidcraft_engine::kernel::Validation,
) -> Result<Session, String> {
let load = |p: &str| load_with(p, check, validation);
match (base, path) {
(Some(b), Some(p)) => {
let mut s = load(b)?;
let mut s = load_checked(b, check)?;
run_script_warning(&mut s, p, &read_json(p)?)?;
Ok(s)
}
(Some(b), None) => load(b),
(None, Some(p)) => load(p),
(Some(b), None) => load_checked(b, check),
(None, Some(p)) => load_checked(p, check),
(None, None) => Err(usage()),
}
}
/// `--validation MODE` (`diagnostics.validation`): the default, each_feature, when not given.
fn validation_flag(cmd: &str, args: &[String]) -> Result<solidcraft_engine::kernel::Validation, String> {
match flag(args, "--validation") {
Some(m) => solidcraft_engine::kernel::Validation::parse(m)
.ok_or_else(|| format!("{cmd}: --validation must be each_step, each_feature or off, not `{m}`")),
None => Ok(solidcraft_engine::kernel::Validation::default()),
}
}
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
@@ -575,7 +505,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
known_args("eval", args, &["--in", "--json-out", "--profile-out"], &["--profile"], true)?;
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
@@ -657,7 +587,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
known_args(
"snapshot",
args,
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section", "--quality"],
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
&["--no-grid", "--no-axes"],
true,
)?;
@@ -670,16 +600,6 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
Some(x) => x.parse().ok().filter(|n| (16..=8192).contains(n)).ok_or_else(|| format!("{name} must be 16 to 8192 pixels, not `{x}`")),
};
let (w, h) = (size("--width", 1280)?, size("--height", 800)?);
// The display quality the bodies are meshed at, as the app's Display Quality setting: set
// before the design opens, so every display plan is made at it.
if let Some(q) = flag(args, "--quality") {
use solidcraft_engine::doc::display_quality::{DisplayQuality, set_current};
let level = DisplayQuality::parse(q).ok_or_else(|| {
let ids: Vec<&str> = DisplayQuality::ALL.iter().map(|q| q.id()).collect();
format!("--quality must be one of {}, not `{q}`", ids.join(", "))
})?;
set_current(level);
}
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
// How the bodies are drawn and where the model is cut, through the commands the app's
// Appearance dialog and Section Analysis run (`preview`).
@@ -699,12 +619,9 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
}
(None, None) => {}
}
let cam = match s.camera {
Some(cam) => cam,
None => solidcraft_engine::view::home_camera(&s).map_err(|e| e.to_string())?,
};
let cam = s.camera.unwrap_or_else(|| solidcraft_engine::view::home_camera(&s));
let guides = Guides { grid: !args.iter().any(|a| a == "--no-grid"), axes: !args.iter().any(|a| a == "--no-axes") };
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides).map_err(|e| e.to_string())?;
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides);
let png = solidcraft_engine::capture::png(&scene, &cam, w, h)?;
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
@@ -933,7 +850,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
let r = s.execute(id, p).map_err(|e| format!("{id}: {e}"))?;
let ms = t.elapsed().as_secs_f64() * 1000.0;
let t = std::time::Instant::now();
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
s.world_state().warm_meshes(|| true);
let view_ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", json!({"step": "op", "command": id, "ms": ms, "view_ms": view_ms}));
Ok(r)
@@ -989,7 +906,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
let t = std::time::Instant::now();
let st = s.world_state().map_err(|e| e.to_string())?;
let st = s.world_state();
let place_ms = t.elapsed().as_secs_f64() * 1000.0;
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
@@ -1059,7 +976,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let t = std::time::Instant::now();
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
let st = s.world_state().map_err(|e| e.to_string())?;
let st = s.world_state();
let t = std::time::Instant::now();
st.warm_meshes(|| true);
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
+19 -19
View File
@@ -5,7 +5,7 @@
#![allow(clippy::needless_range_loop)]
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
use crate::spatial::{self, Tri};
use crate::{read_json, recipe};
@@ -13,28 +13,28 @@ use crate::{read_json, recipe};
/// Default relative tolerance for volume and area (our curved-face tessellation is within ~2e-4).
pub const REL_TOL: f64 = 1e-3;
/// Default tolerance for centre of mass and bounding box, as a fraction of the bbox diagonal.
pub(crate) const ABS_FRAC: f64 = 1e-3;
const ABS_FRAC: f64 = 1e-3;
/// Chord tolerance used to triangulate our part for the surface-distance check (mm).
const MESH_TOL: f64 = 0.01;
/// Relative tolerance on the principal second moments of the two triangulations.
const INERTIA_REL: f64 = 2e-3;
/// What SolidCraft built: totals over every body.
pub(crate) struct Got {
pub(crate) body_count: usize,
pub(crate) volume: f64,
pub(crate) area: f64,
pub(crate) com: [f64; 3],
pub(crate) bbox: ([f64; 3], [f64; 3]),
pub(crate) faces: u64,
struct Got {
body_count: usize,
volume: f64,
area: f64,
com: [f64; 3],
bbox: ([f64; 3], [f64; 3]),
faces: u64,
}
pub(crate) fn v3(v: &Value) -> Option<[f64; 3]> {
fn v3(v: &Value) -> Option<[f64; 3]> {
let a = v.as_array()?;
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?])
}
pub(crate) fn totals(m: &Value) -> Got {
fn totals(m: &Value) -> Got {
let bodies = m["bodies"].as_array().cloned().unwrap_or_default();
let (mut volume, mut area, mut faces) = (0.0, 0.0, 0);
let mut moment = [0.0; 3];
@@ -57,21 +57,21 @@ pub(crate) fn totals(m: &Value) -> Got {
Got { body_count: bodies.len(), volume, area, com, bbox: (lo, hi), faces }
}
pub(crate) struct Checks {
pub(crate) list: Vec<Value>,
pub(crate) pass: bool,
struct Checks {
list: Vec<Value>,
pass: bool,
}
impl Checks {
pub(crate) fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
self.pass &= ok;
self.list.push(json!({"check": what, "got": got, "want": want, "ok": ok}));
}
pub(crate) fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
let ok = w.is_some_and(|w| g.is_finite() && (g - w).abs() <= rel * w.abs().max(1.0));
self.push(what, json!(g), json!(w), ok);
}
pub(crate) fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
let ok = w.is_some_and(|w| g.iter().zip(w).all(|(g, w)| g.is_finite() && (g - w).abs() <= tol));
self.push(what, json!(g), json!(w), ok);
}
@@ -186,8 +186,8 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
/// Our part as a triangle soup (curved faces refined onto the surface).
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
let mut ours: Vec<Tri> = Vec::new();
for b in &s.world_state().map_err(|e| e.to_string())?.bodies {
let m = b.body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
for b in &s.world_state().bodies {
let m = b.body.tessellate_with(MESH_TOL, true).map_err(|e| e.to_string())?;
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
for t in &m.triangles {
if let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) {
-50
View File
@@ -1,50 +0,0 @@
//! `--profile` and `--profile-out FILE` for `run`, `eval` and `bench-open` (and
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, the kernel's calls to the
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
use solidcraft_engine::profile;
/// Rows the table prints.
const TOP: usize = 30;
/// A recording that prints its table when finished.
pub struct Profiling {
trace: Option<String>,
}
/// Start recording when `args` (after the command) ask for it, or `SOLIDCRAFT_PROFILE` does.
pub fn start(args: &[String]) -> Result<Option<Profiling>, String> {
let trace = match args.iter().position(|a| a == "--profile-out") {
Some(i) => match args.get(i + 1) {
Some(f) if !f.starts_with("--") => Some(f.clone()),
_ => return Err("--profile-out needs a FILE".into()),
},
None => None,
};
let asked = trace.is_some() || args.iter().any(|a| a == "--profile");
if !asked && !solidcraft_engine::timing::profile_from_env() {
return Ok(None);
}
if !profile::COMPILED {
return Err("this solidcraft-cli was built without the profiler (its `profile` feature)".into());
}
profile::reset();
profile::set_enabled(true);
Ok(Some(Profiling { trace }))
}
impl Profiling {
/// Stop recording, print the table on stderr and write the trace.
pub fn finish(self) -> Result<(), String> {
profile::set_enabled(false);
eprint!("{}", profile::report().table(TOP));
if let Some(path) = self.trace {
let mut bytes = Vec::new();
profile::write_chrome_trace(&mut bytes).map_err(|e| format!("{path}: {e}"))?;
std::fs::write(&path, bytes).map_err(|e| format!("{path}: {e}"))?;
eprintln!("profile: trace written to {path} (open it in chrome://tracing or ui.perfetto.dev)");
}
Ok(())
}
}
+8 -17
View File
@@ -21,7 +21,6 @@
//! Each design runs in a child process (this test binary, [`action_matrix_worker`]) so an action
//! that hangs or aborts is recorded as such and the rest of the design still runs.
use solidcraft_engine::geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
@@ -29,10 +28,10 @@ use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::doc::{FaceAttachmentStatus, ModelBody, naming};
use solidcraft_engine::geom::Vec3;
/// One in this many actions runs on pull requests (by a stable hash of its key).
const PR_SAMPLE: u64 = 40;
@@ -197,7 +196,7 @@ fn round3(x: f64) -> f64 {
/// Every action on every entity of the design's final bodies, in a fixed order.
fn actions(s: &Session) -> Vec<Action> {
let st = s.world_state().unwrap();
let st = s.world_state();
let placed = s.placed_document();
let paths: BTreeMap<String, Vec<u64>> =
placed.body_frames_with_source(&s.model.state()).into_iter().map(|(world, _, _, path, _)| (world, path)).collect();
@@ -1339,9 +1338,7 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
fn a_round_on_a_knuckle_cap_circle() {
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
@@ -1361,9 +1358,7 @@ fn a_round_on_a_knuckle_cap_circle() {
#[test]
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1380,9 +1375,7 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
#[test]
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1397,9 +1390,7 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
#[test]
fn a_hole_square_to_a_45_degree_flange() {
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1657,7 +1648,7 @@ impl TipConstruction {
other => panic!("expected an axis, got {other:?}"),
}
}
fn plane(&self) -> solidcore_record::Plane {
fn plane(&self) -> solidcraft_engine::geom::Plane {
match &self.0 {
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
other => panic!("expected a plane, got {other:?}"),
@@ -1737,7 +1728,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
st.bodies
.iter()
.map(|b| {
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
let mesh = b.body.tessellate(0.1).unwrap();
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
});
+3 -3
View File
@@ -4,7 +4,7 @@
use std::f64::consts::PI;
use serde_json::Value;
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
use solidcraft_engine::{Session, io, kernel};
fn assert_two_through_holes(session: &Session, stage: &str) {
@@ -26,8 +26,8 @@ fn assert_two_through_holes(session: &Session, stage: &str) {
let expected = 2.0 * PI * 4.0_f64.powi(2) * 6.0;
assert!((removed - expected).abs() < expected * 1e-3, "{stage}: the two holes remove {removed} mm³, expected {expected} mm³");
let before_mesh = uncut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let after_mesh = bracket.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let before_mesh = uncut.tessellate(0.01).unwrap();
let after_mesh = bracket.tessellate(0.01).unwrap();
let down = Vec3::new(0.0, 0.0, -1.0);
for y in [-15.0, -45.0] {
let center = Vec3::new(25.0, y, 1.0);
+4 -6
View File
@@ -1,12 +1,10 @@
# Design file corpus
Designs saved by older SolidCraft builds. `tests/design_corpus.rs` opens each one with the
current build and checks that it is upgraded to the current file format, that it evaluates
(a feature may fail only by asking for a reference it lost to be picked again; what it rebuilds
to is today's), and that damaged and hostile variants of it never panic. The `.expected.json`
next to each is `solidcraft-cli eval` of that file by the build that saved it (bodies, volumes,
areas, world bounding boxes), kept as a record of what that build made; no test compares with
it.
current build and checks that it evaluates as it did when saved (the `.expected.json` next to
it is `solidcraft-cli eval` of that file by the build that saved it: bodies, volumes, areas,
world bounding boxes), that it is upgraded to the current file format, and that damaged and
hostile variants of it never panic.
| File | Saved by | Era |
|---|---|---|
@@ -90,6 +90,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.131943,
@@ -100,6 +104,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 1.0812709999999999,
@@ -110,6 +118,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.450002,
@@ -120,6 +132,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 5.191514000000001,
@@ -54,6 +54,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 1.178957,
@@ -64,6 +68,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 83.24342399999999,
@@ -74,6 +82,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.150889,
@@ -84,6 +96,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 94.465481,
@@ -94,6 +110,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 50.426736,
@@ -104,6 +124,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 40.383005,
@@ -114,6 +138,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 7,
"ms": 59.041461000000005,
@@ -124,6 +152,18 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [
{
"face_shift_mm": 0.0,
"rotation_rad": 0.0,
"strategy": "planar_fallback",
"tolerance_mm": 0.0,
"translation_mm": 0.0
}
],
"truncated": false
},
"error": null,
"id": 8,
"ms": 562.5477970000001,
@@ -131,9 +171,13 @@
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
"warning": "Boolean retry: exact planar fallback (no displacement)"
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 9,
"ms": 89.56076,
@@ -144,6 +188,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 10,
"ms": 65.330634,
@@ -154,6 +202,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 11,
"ms": 97.229403,
@@ -99,6 +99,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.204585,
@@ -109,6 +113,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 2.318662,
@@ -119,6 +127,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.109196,
@@ -129,6 +141,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 0.338859,
@@ -139,6 +155,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 0.312259,
@@ -149,6 +169,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 0.23144699999999999,
+71 -40
View File
@@ -1,8 +1,6 @@
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open, are upgraded to
//! the current format and evaluate (what they rebuild to today is today's, not what the build
//! that saved them made); damaged and hostile design files never panic.
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open and evaluate as
//! they did when they were saved; damaged and hostile design files never panic.
use solidcraft_engine::geom::prelude::*;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
@@ -23,24 +21,67 @@ fn open(path: &Path) -> Session {
s
}
/// Every corpus design opens, is upgraded to the current format, evaluates, and saves and reads
/// back the same. A feature may fail only by asking for a reference it lost to be picked again
/// (a name saved before format 14 finds nothing today).
/// Differences that are deliberate behaviour changes made before the file format had versions
/// (no migration can tell those files apart): (file, check, why).
const KNOWN: [(&str, &str, &str); 2] = [
(
"m0-revolve-primitives",
"Body1 bbox",
"partial revolves turn toward the sketch normal since ac43365 (as most parametric CAD tools do); M0 turned them the other way",
),
("components-arm", "body_count", "Measure counted only the root component's bodies then; it measures every placed body now"),
];
fn known(file: &str, check: &str) -> bool {
KNOWN.iter().any(|(f, c, _)| *f == file && *c == check)
}
/// Volumes and areas then came from coarser tessellation (and curved booleans and fillets have
/// become exact since): the same part within this relative tolerance (the oracle's).
const REL: f64 = 1e-3;
fn close(a: f64, b: f64, rel: f64) -> bool {
(a - b).abs() <= rel * b.abs().max(1.0)
}
#[test]
fn old_designs_open_upgrade_and_evaluate() {
fn old_designs_open_and_evaluate_as_saved() {
let mut problems = Vec::new();
for p in corpus() {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let want: Value = serde_json::from_slice(&std::fs::read(p.with_extension("expected.json")).unwrap()).unwrap();
let mut s = open(&p);
for r in &s.model.results {
if let Some(e) = r.error.as_deref().filter(|e| solidcraft_engine::doc::reselect_parts(e).is_none()) {
if let Some(e) = &r.error {
problems.push(format!("{name}: feature {}: {e}", r.id));
}
}
assert_eq!(s.doc.format, solidcraft_engine::doc::format::format_string(solidcraft_engine::doc::format::FORMAT), "{name}: upgraded");
let got = s.execute("inspect.measure", &json!({})).unwrap();
if got["body_count"].as_u64().unwrap_or(0) == 0 {
problems.push(format!("{name}: no bodies"));
if got["body_count"] != want["body_count"] && !known(&name, "body_count") {
problems.push(format!("{name}: {} bodies, saved with {}", got["body_count"], want["body_count"]));
}
for w in want["bodies"].as_array().into_iter().flatten() {
let bname = w["name"].as_str().unwrap_or_default();
let Some(g) = got["bodies"].as_array().into_iter().flatten().find(|g| g["name"] == w["name"]) else {
problems.push(format!("{name}: body {bname} missing"));
continue;
};
for k in ["volume_mm3", "area_mm2"] {
let (a, b) = (g[k].as_f64().unwrap_or(f64::NAN), w[k].as_f64().unwrap_or(f64::NAN));
if !close(a, b, REL) {
problems.push(format!("{name}: {bname} {k} {a} (saved {b})"));
}
}
if known(&name, &format!("{bname} bbox")) {
continue;
}
for (end, i) in [("min", 0), ("min", 1), ("min", 2), ("max", 0), ("max", 1), ("max", 2)] {
let (a, b) = (g["bbox"][end][i].as_f64().unwrap_or(f64::NAN), w["bbox"][end][i].as_f64().unwrap_or(f64::NAN));
if (a - b).abs() > 1e-3 * b.abs().max(1.0) {
problems.push(format!("{name}: {bname} bbox {end}[{i}] {a} (saved {b})"));
}
}
}
// Saved again, it reads back the same.
let again = solidcraft_engine::doc::Document::from_json(&s.doc.to_json()).unwrap();
@@ -53,48 +94,40 @@ fn old_designs_open_upgrade_and_evaluate() {
}
/// Names saved before format 14 were worked out from geometry, pieces of a split face numbered by
/// where their centres lay (`F2:top#2`). They open marked as such and find no face or edge today:
/// each feature holding one fails, asking for its reference to be picked again, and the rest of
/// the design builds.
/// where their centres lay (`F2:top#2`). They keep naming the faces they named: marked as such,
/// they are found as the build that saved them found them, without new notes.
#[test]
fn names_saved_before_exact_history_ask_for_their_faces_again() {
fn names_saved_before_exact_history_find_the_faces_they_named() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/naming-split-pieces.solidcraft");
let s = open(&path);
let feature = |name: &str| s.doc.features.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name}"));
assert_eq!(feature("Round").edge_names, vec!["?F2:side:l2|F2:top#2".to_string()]);
assert_eq!(feature("Bevel").edge_names, vec!["?F2:side:l4|F2:top#1".to_string()]);
assert_eq!(feature("Raise").face_names, vec!["?F2:top#1".to_string()]);
let asked = |name: &str| {
let r = s.model.result(feature(name).id).unwrap_or_else(|| panic!("{name}"));
r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts).map(|(f, reference)| (f.to_string(), reference.to_string()))
};
assert_eq!(asked("Round"), Some(("Round".into(), "?F2:side:l2|F2:top#2".into())));
assert_eq!(asked("Bevel"), Some(("Bevel".into(), "?F2:side:l4|F2:top#1".into())));
assert_eq!(asked("Raise"), Some(("Raise".into(), "?F2:top#1".into())));
for r in s.model.results.iter().filter(|r| !["Round", "Bevel", "Raise"].contains(&r.name.as_str())) {
assert!(r.error.is_none(), "{}: {:?}", r.name, r.error);
for r in &s.model.results {
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
}
assert!(!s.model.state().bodies.is_empty());
// The raised piece is the left one, as saved (the stored point is on it).
let state = s.model.state();
let solid = |p: [f64; 3]| state.bodies.iter().any(|b| b.body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2])));
assert!(solid([7.0, 15.0, 11.0]) && !solid([32.0, 15.0, 11.0]));
}
/// Body names saved before format 14 named the pieces of a split body in the kernel's shell
/// order: such a design opens with every feature marked `shell_piece_order` and evaluates, each
/// body name it stores naming a piece. (Which piece takes which name is not promised to be as it
/// was saved.) Its fillet and chamfer hold edge names saved then, which find nothing today: they
/// ask for their edges to be picked again.
/// Body names saved before format 14 name the pieces of a split body in the kernel's shell order.
/// The features after the split that refer to a piece by its body name still find that piece:
/// the far one stays `Body1` (chamfered), the near one `Body1 (1)` (filleted, coloured).
#[test]
fn body_names_saved_before_exact_history_open_marked_and_evaluate() {
fn body_names_saved_before_exact_history_keep_their_pieces() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/split-body-pieces.solidcraft");
let s = open(&path);
assert!(s.doc.features.iter().all(|f| f.shell_piece_order), "every saved feature keeps the shell order");
for r in &s.model.results {
let asked = r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts);
assert!(r.error.is_none() || asked.is_some_and(|(_, reference)| reference.starts_with('?')), "{}: {:?}", r.name, r.error);
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
}
let state = s.model.state();
for name in ["Body1", "Body1 (1)"] {
assert!(state.bodies.iter().any(|b| b.name == name), "{name}");
}
let piece = |name: &str| state.bodies.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("{name}"));
let solid = |name: &str, p: [f64; 3]| piece(name).body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2]));
assert!(solid("Body1", [20.0, 24.0, 5.0]) && solid("Body1 (1)", [20.0, 6.0, 5.0]));
assert!(s.doc.appearances.bodies.contains_key("Body1 (1)"));
}
@@ -140,7 +173,6 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
for p in corpus() {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let mut s = open(&p);
let failed_on_open: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
let state = s.model.state();
let placed = s.placed_document();
let mut picks = Vec::new();
@@ -148,7 +180,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
let Some(body) = state.bodies.iter().find(|b| b.definition_name() == definition && placed.model_body_component(b) == component) else {
continue;
};
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let mesh = body.body.tessellate(0.01).unwrap();
for index in 0..body.body.face_count() {
if body.body.planar_face(index).is_none() {
continue;
@@ -192,8 +224,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
let known = known_without_attachment(&name, &body, normal, distance);
out.push((name.clone(), body, index, known, problem));
}
let failed: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
assert_eq!(failed, failed_on_open, "{name}: undo leaves the features that failed on opening, and only those, failed");
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{name}: left with errors after undo");
}
out
}
+1 -55
View File
@@ -63,7 +63,7 @@ fn model(name: &str, s: &Session) -> Value {
.iter()
.map(|r| json!({"id": r.id, "name": r.name, "error": r.error, "warning": r.warning, "skipped": r.skipped, "state": state(name, &r.state)}))
.collect();
json!({"results": results, "world": state(name, &s.world_state().unwrap())})
json!({"results": results, "world": state(name, &s.world_state())})
}
/// The same edit on both: a parameter's value when the design has one, else a new body.
@@ -289,60 +289,6 @@ fn undoing_an_edit_after_opening_reuses_the_saved_results() {
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A saved model holds the results of the body check mode it was evaluated under: opened under
/// the same mode it is taken as it is, under another the design is evaluated again.
#[test]
fn a_model_saved_under_another_check_mode_is_evaluated_again() {
let plate = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Sample Plate").unwrap();
let mut built = Session::default();
for c in (plate.script)()["commands"].as_array().unwrap() {
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
let path = temp("mode");
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
let same = open(&path);
assert_eq!(same.model.validation(), built.model.validation());
assert_eq!(same.model.recomputed, 0, "taken from the file");
let mut other = Session::default();
other.set_validation(solidcraft_engine::kernel::Validation::EachStep);
assert_ne!(other.validation(), built.model.validation());
other.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(other.validation(), solidcraft_engine::kernel::Validation::EachStep);
assert!(other.model.recomputed > 0, "evaluated again under its own mode");
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A body made by rounds keeps, in the saved model, the rounds and the body they were made on: a
/// later round at the corner where they meet blends them again with it whether the design was
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
/// 7928.142563938 mm³, the same body as the design evaluated anew.
#[test]
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let mut built = Session::default();
built.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Box"})).unwrap();
built.execute("solid.fillet", &json!({"edges": [[20, 10, 20], [10, 0, 20]], "radius": 2})).unwrap();
let path = temp("later-round");
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
let bytes = std::fs::read(&path).unwrap();
let mut cached = open(&path);
assert_eq!(cached.model.last_recomputed, 0, "opened without evaluating");
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
for s in [&mut cached, &mut rebuilt] {
s.execute("solid.fillet", &json!({"edges": [[20, 0, 10]], "radius": 3})).unwrap();
let st = s.model.state();
let b = &st.body("Box").unwrap().body;
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
assert_eq!(b.face_count(), 15);
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
}
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A design saved before body names were pinned (#6403) pins them as it opens, as its first
/// command would, and opening it is no change. That command pinned them as part of itself, so
/// undoing it went back to the unpinned design, which no evaluated result matched: the first
-404
View File
@@ -1,404 +0,0 @@
//! Every design of the corpus loads, two builds of it name its faces and edges alike, and a
//! name it stores that no longer finds its face or edge fails its feature. The stored names are
//! the fillet and chamfer edges, the faces a feature picks, the face a sketch sits on and the
//! sheet metal edges; one that finds nothing (`gone`, a sketch's face `lost`) must leave its
//! feature failed (it asks for the reference to be picked again, never takes another face or
//! edge), and a design that no longer loads fails the test. A build's names are each face's and
//! edge's name with where it is (its area and centre, its length and middle); the second build
//! must give the same, and its stored names must find the same faces and edges.
//!
//! Face and edge indices are left out: they are the kernel's order, which a rebuild need not
//! keep (a fillet's or a boolean's faces come out in another order from one build to the next).
//! How many faces a body has and what each is called is the kernel's business, not this test's.
//!
//! The corpus is the action matrix's (the old design files in `tests/corpus`, the examples, the
//! demo and typical designs in `tests/action_matrix/designs`, the reference recipes), the sample
//! designs, the engine's design fixtures and the older designs kept by the hole matrix, taper
//! and projection tests.
use solidcraft_engine::geom::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::{FaceAttachmentStatus, Feature, FeatureKind, ModelBody, ModelState, PlaneRef, naming};
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
}
/// Where a design comes from.
enum Source {
File(PathBuf),
Sample(fn() -> Value),
}
/// Every design of the corpus: (`<group>/<design>`, where it comes from).
fn designs() -> Vec<(String, Source)> {
let root = root();
let mut out = Vec::new();
let mut add = |prefix: &str, dir: PathBuf, ext: &str| {
let Ok(rd) = std::fs::read_dir(&dir) else { return };
let mut v: Vec<PathBuf> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == ext)).collect();
v.sort();
for p in v {
let stem = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
out.push((format!("{prefix}/{stem}"), Source::File(p)));
}
};
add("corpus", root.join("apps/solidcraft-cli/tests/corpus"), "solidcraft");
add("examples", root.join("examples"), "json");
add("examples", root.join("examples/acceptance"), "json");
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");
add("fixtures", root.join("crates/engine/src/tests/fixtures"), "solidcraft");
add("hole_matrix", root.join("tests/hole_matrix/old-0.9"), "json");
add("taper", root.join("tests/taper/before-1014"), "json");
add("projection", root.join("tests/projection/before-1508"), "json");
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();
for c in cases {
out.push((format!("oracle/{}", c.file_name().unwrap_or_default().to_string_lossy()), Source::File(c.join("recipe.json"))));
}
for sample in solidcraft_engine::sample::samples() {
let stem: String = sample.name.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-");
out.push((format!("samples/{stem}"), Source::Sample(sample.script)));
}
out
}
/// Build a design: a design file (also a design saved as JSON) is opened, a script is run.
fn load(source: &Source) -> Result<Session, String> {
let mut s = Session::default();
let script = match source {
Source::Sample(script) => script(),
Source::File(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);
}
Source::File(path) if path.file_name().is_some_and(|n| n == "recipe.json") => {
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("recipe").arg(path).output().map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(format!("recipe: {}", String::from_utf8_lossy(&out.stderr).trim()));
}
serde_json::from_slice(&out.stdout).map_err(|e| e.to_string())?
}
Source::File(path) => {
let v: Value = serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
if v.get("features").is_some() {
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
return Ok(s);
}
match v.get("steps").and_then(Value::as_array) {
// The demo's steps: an MCP `execute` each.
Some(steps) => {
json!({"commands": steps.iter().map(|st| json!({"command": st["command"], "params": st["params"]})).collect::<Vec<_>>()})
}
None => v,
}
}
};
s.run_script(&script).map_err(|e| e.to_string())?;
if s.active_sketch.is_some() {
let _ = s.execute("sketch.finish", &json!({}));
}
Ok(s)
}
/// What decides a name beyond its text: `[geometric]` when it was worked out from geometry, not
/// exact history.
fn flags(list: &[(bool, &str)]) -> String {
let set: Vec<&str> = list.iter().filter(|(on, _)| *on).map(|(_, f)| *f).collect();
if set.is_empty() { String::new() } else { format!(" [{}]", set.join(", ")) }
}
/// Where each named face and edge of a body is, by name, sorted: a face's area and centre, an
/// edge's length and middle, from the body's mesh. Two builds that name their faces alike give
/// the same list.
fn placed_names(b: &ModelBody) -> Vec<(String, [f64; 4])> {
if b.body.is_mesh() {
return Vec::new();
}
let m = b.mesh();
let faces = naming::face_names(b);
let mut sums = vec![[0.0f64; 4]; faces.len()];
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let [p, q, r] = t.map(|i| m.positions.get(i as usize).copied().unwrap_or(Vec3::ZERO));
let area = (q - p).cross(r - p).len() / 2.0;
let c = (p + q + r) / 3.0;
if let Some(s) = sums.get_mut(*f as usize) {
*s = [s[0] + area, s[1] + area * c.x, s[2] + area * c.y, s[3] + area * c.z];
}
}
let mut out: Vec<(String, [f64; 4])> = faces
.iter()
.zip(&sums)
.map(|(n, s)| {
let a = s[0].max(1e-300);
(format!("{} face {n}", b.name), [s[0], s[1] / a, s[2] / a, s[3] / a])
})
.collect();
for (n, line) in naming::edge_names(b).iter().zip(&m.edges) {
let length: f64 = line.windows(2).map(|w| w[1].dist(w[0])).sum();
let mid = line.iter().fold(Vec3::ZERO, |acc, p| acc + *p) / (line.len().max(1) as f64);
out.push((format!("{} edge {n}", b.name), [length, mid.x, mid.y, mid.z]));
}
out.sort_by(|x, y| x.0.cmp(&y.0).then(x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal)));
out
}
fn face_label(st: &ModelState, body: usize, face: usize) -> String {
match st.bodies.get(body) {
Some(b) => format!("{} face {}", b.name, naming::face_names(b).get(face).map(String::as_str).unwrap_or("?")),
None => format!("body #{body} face #{face}"),
}
}
/// Every name feature `f` stores, with what it finds in the model before it, as a feature
/// rebuilding finds it, and whether it finds anything.
fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
let doc = &*s.doc;
let who = format!("ref {} ({})", f.name, f.id);
let before = s.model.state_before(f.id);
// Faces a feature picks by point and stores by name (shell, draft, hole, …).
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
if f.face_names.iter().any(|n| !n.is_empty()) {
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component).unwrap();
for (k, (p, n)) in f.kind.face_points().into_iter().zip(&f.face_names).enumerate() {
if n.is_empty() {
continue;
}
let (found, ok) = if explicit_emboss {
("not checked (a cylindrical emboss finds its face on its own body)".to_string(), true)
} else {
match naming::locate_face_on_body(&fst, f.kind.face_body(), n, p) {
Some(hit) => (format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])), true),
None => ("gone".into(), false),
}
};
out.push((format!("{who} face {k} `{n}` -> {found}"), ok));
}
}
// Edges a feature picks by point and stores by name.
if f.edge_names.iter().any(|n| !n.is_empty()) {
let edge = |out: &mut Vec<(String, bool)>, b: &ModelBody, k: usize, n: &str, p: Vec3| {
let (found, ok) = match naming::locate_edge(b, n, p) {
Ok(hit) => (
format!(
"{} edge {}{}",
b.name,
naming::edge_names(b).get(hit.edge).map(String::as_str).unwrap_or("?"),
flags(&[(!hit.verified, "geometric")])
),
true,
),
Err(e) => (format!("gone ({e})"), false),
};
out.push((format!("{who} edge {k} `{n}` -> {found}"), ok));
};
let named = |k: usize| f.edge_names.get(k).filter(|n| !n.is_empty());
match &f.kind {
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
let pts: Vec<Vec3> = solidcraft_engine::doc::plain(edges);
match solidcraft_engine::doc::blend_target(&before, body, &pts).ok().and_then(|i| before.bodies.get(i)) {
Some(b) => {
for (k, p) in pts.iter().enumerate() {
if let Some(n) = named(k) {
edge(out, b, k, n, *p);
}
}
}
None => out.push((format!("{who} edges: no body to round"), false)),
}
}
FeatureKind::SheetConvert { body, bends, .. } => match before.body(body) {
Some(b) => {
for (k, p) in solidcraft_engine::doc::plain::<Vec3, _>(bends).iter().enumerate() {
if let Some(n) = named(k) {
edge(out, b, k, n, *p);
}
}
}
None => out.push((format!("{who} bends: body `{body}` gone"), false)),
},
// Sheet metal edges are named by the sheet's panels, and found on its sheets.
FeatureKind::SheetFlange { .. } | FeatureKind::SheetHem { .. } => {
let pts: Vec<Vec3> = match &f.kind {
FeatureKind::SheetFlange { edges, .. } => edges.iter().map(|e| e.point.0).collect(),
FeatureKind::SheetHem { edges, .. } => solidcraft_engine::doc::plain(edges),
_ => Vec::new(),
};
for (k, p) in pts.iter().enumerate() {
let Some(n) = named(k) else { continue };
let on: Vec<&str> = before.sheets.iter().filter(|sh| sh.edge_by_name(n, *p).is_some()).map(|sh| sh.body.as_str()).collect();
let found = if on.is_empty() { "gone".to_string() } else { format!("sheet edge on {}", on.join(", ")) };
out.push((format!("{who} sheet edge {k} `{n}` -> {found}"), !on.is_empty()));
}
}
other => {
for (k, n) in f.edge_names.iter().enumerate().filter(|(_, n)| !n.is_empty()) {
out.push((format!("{who} edge {k} `{n}` -> not checked ({})", other.type_name()), true));
}
}
}
}
// The face a sketch sits on.
if let FeatureKind::Sketch { plane: PlaneRef::Face { name, anchor, at, .. }, .. } = &f.kind {
let mut ok = true;
let status = match doc.sketch_face_attachment(f.id, &s.model) {
FaceAttachmentStatus::Healthy { target, .. } => {
let faces = match before.body(&target.body) {
Some(b) => match doc.anchored_face_indices(&before, b, &target) {
Ok((indices, note)) => {
let names = naming::face_names(b);
let mut list: Vec<&str> = indices.iter().map(|i| names.get(*i).map(String::as_str).unwrap_or("?")).collect();
list.sort();
format!("{} faces {}{}", b.name, list.join(", "), flags(&[(note.is_some(), "changed cap")]))
}
Err(e) => {
ok = false;
format!("no faces ({e})")
}
},
None => {
ok = false;
format!("body `{}` gone", target.body)
}
};
format!("healthy: {faces}")
}
FaceAttachmentStatus::Lost { message, .. } => {
ok = false;
format!("lost: {message}")
}
FaceAttachmentStatus::Legacy { .. } => match name {
// The face its name finds, as the sketch finds it.
Some(n) => {
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component).unwrap();
match naming::locate_face(&fst, n, *at) {
Some(hit) => format!("by name: {}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])),
None => {
ok = false;
"by name: gone".into()
}
}
}
None => "by its pick".into(),
},
FaceAttachmentStatus::Sheet { .. } => "sheet face".into(),
FaceAttachmentStatus::Fixed { .. } => "fixed".into(),
};
let stored = match (anchor, name) {
(Some(a), _) => {
let kind =
a.target.certificate.as_ref().and_then(|c| serde_json::to_value(c).ok()).and_then(|v| v["kind"].as_str().map(String::from));
format!("`{}` on {} ({})", a.target.name, a.target.body, kind.unwrap_or_else(|| "no certificate".into()))
}
(None, Some(n)) => format!("`{n}`"),
(None, None) => "by its pick".into(),
};
out.push((format!("{who} plane {stored} -> {status}"), ok));
}
}
/// A build of a design: where its faces and edges are by name, and what every name it stores
/// finds.
struct Build {
names: Vec<(String, [f64; 4])>,
refs: Vec<(String, bool)>,
}
fn build(s: &Session) -> Build {
let names = s.model.state().bodies.iter().flat_map(placed_names).collect();
let mut refs = Vec::new();
for f in s.doc.features.iter().filter(|f| !f.suppressed) {
let from = refs.len();
record_refs(&mut refs, s, f);
// A name that finds nothing is sound when its feature fails: it asks for the reference
// to be picked again.
if s.model.result(f.id).is_some_and(|r| r.error.is_some()) {
refs.iter_mut().skip(from).for_each(|(_, ok)| *ok = true);
}
}
Build { names, refs }
}
/// Where two builds name their faces and edges apart (at most `cap` of them): a name only one
/// has, or a face or edge of that name somewhere else (1e-6 of its size, to the mesh's rounding).
fn differences(a: &Build, b: &Build, cap: usize) -> Vec<String> {
let mut out = Vec::new();
if a.names.len() != b.names.len() {
out.push(format!("{} named faces and edges, then {}", a.names.len(), b.names.len()));
}
for ((n, x), (m, y)) in a.names.iter().zip(&b.names) {
let scale = x.iter().chain(y).fold(1.0f64, |s, v| s.max(v.abs()));
if n != m || x.iter().zip(y).any(|(p, q)| (p - q).abs() > 1e-6 * scale) {
out.push(format!("{n} at {x:?}, then {m} at {y:?}"));
}
}
for ((r, _), (q, _)) in a.refs.iter().zip(&b.refs) {
if r != q {
out.push(format!("{r}, then {q}"));
}
}
if a.refs.len() != b.refs.len() {
out.push(format!("{} stored names, then {}", a.refs.len(), b.refs.len()));
}
out.truncate(cap);
out
}
/// Designs whose names differ from one build to the next today: (design, why). Their
/// differences are printed, not failed on, until the cause is fixed; a stored name that finds
/// nothing still fails.
const UNSTABLE: [(&str, &str); 1] = [(
"regressions/closed_sweep_torus_cut",
"the halves of each swept wall lie level along every axis, so which is `#1` and which `#2` follows the order the kernel gives the faces in, which changes from one build to the next",
)];
#[test]
fn corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone() {
let designs = designs();
let next = std::sync::atomic::AtomicUsize::new(0);
let results = std::sync::Mutex::new(Vec::new());
let workers = std::thread::available_parallelism().map_or(4, |n| n.get()).min(designs.len().max(1));
std::thread::scope(|scope| {
for _ in 0..workers {
scope.spawn(|| {
while let Some((name, source)) = designs.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
let got = load(source).and_then(|first| Ok((build(&first), build(&load(source)?))));
results.lock().unwrap_or_else(|e| e.into_inner()).push((name.clone(), got));
}
});
}
});
let mut results = results.into_inner().unwrap_or_else(|e| e.into_inner());
results.sort_by(|a, b| a.0.cmp(&b.0));
let mut problems = Vec::new();
for (name, got) in &results {
let (first, second) = match got {
Ok(builds) => builds,
Err(e) => {
problems.push(format!("{name}: does not load: {e}"));
continue;
}
};
let lost: Vec<&str> = first.refs.iter().filter(|(_, ok)| !ok).map(|(r, _)| r.as_str()).collect();
if !lost.is_empty() {
problems.push(format!("{name}: stored names that find nothing, in features that build:\n {}", lost.join("\n ")));
}
let apart = differences(first, second, 40);
if !apart.is_empty() {
match UNSTABLE.iter().find(|(design, _)| design == name) {
Some((_, why)) => eprintln!("{name} (not checked: {why}):\n {}", apart.join("\n ")),
None => problems.push(format!("{name}: two builds name it apart (first, then second):\n {}", apart.join("\n "))),
}
}
}
assert!(problems.is_empty(), "{} of {} design(s) of the corpus:\n{}", problems.len(), results.len(), problems.join("\n"));
}
+13 -13
View File
@@ -9,7 +9,6 @@ use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::prelude::*;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
@@ -32,7 +31,7 @@ struct Topology {
/// Every solid body's topology, by name.
fn topology(s: &Session) -> BTreeMap<String, Topology> {
let st = s.world_state().unwrap();
let st = s.world_state();
st.bodies
.iter()
.filter(|b| !b.body.is_mesh())
@@ -143,14 +142,16 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
/// into strips along the openings' tops and bottoms.
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
/// face count after each feature, and each outer side (the four walls and the bottom) one face
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
/// wall at 24) into strips along the openings' tops and bottoms.
#[test]
fn pi_case_sides_stay_whole_at_both_heights() {
fn pi_case_faces_per_step_at_both_heights() {
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
const FACES: [(usize, usize); 13] =
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
let dir = scratch_dir("pi");
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.sc")).unwrap()).unwrap();
let at = |height: &str, file: &str| {
@@ -173,13 +174,12 @@ fn pi_case_sides_stay_whole_at_both_heights() {
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
let mut steps = Vec::new();
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
for k in 2..=14 {
for (k, want) in FACES {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state().unwrap();
let st = t.world_state();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
// Each outer side: the faces in its plane.
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
let on = (0..body.body.face_count())
+2 -3
View File
@@ -460,13 +460,12 @@ fn snapshot_runs_its_script_on_the_in_design() {
}
}
/// An unknown command's error suggests the id that was meant first; other close ids (such as
/// `drawing.open`) may follow it.
/// An unknown command's error suggests the id that was meant.
#[test]
fn unknown_command_suggests_the_closest_id() {
let output = cli().args(["exec", "file.open"]).output().unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open`"), "{}", String::from_utf8_lossy(&output.stderr));
assert!(String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open`?"), "{}", String::from_utf8_lossy(&output.stderr));
}
/// #1833: `--timings` reports each command's time instead of the measurement, `--profile` adds
+1 -4
View File
@@ -65,10 +65,7 @@ fn allow_errors_writes_anyway_and_a_saved_design_keeps_failing() {
let output = cli().arg("run").arg(dir.broken_script()).arg("--allow-errors").arg("--out").arg(&step).arg("--save").arg(&saved).output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let measured: Value = serde_json::from_slice(&output.stdout).unwrap();
// SMLib integrates the volume (a relative accuracy of 1e-4 asked for; a box comes out within
// a few units in the last place), so the box's 4000 mm³ is checked to 1e-9, not to the bit.
let volume = measured["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume - 4000.0).abs() < 4000.0 * 1e-9, "the box without its fillet: {volume}");
assert_eq!(measured["total"]["volume_mm3"], json!(4000.0), "the box without its fillet");
assert!(std::fs::metadata(&step).unwrap().len() > 0);
// The saved design still holds the failing feature: running it fails too, `eval` reports it.
let again = dir.0.join("again.step");
+5 -5
View File
@@ -171,9 +171,9 @@ fn a_saved_view_is_its_camera_and_a_standard_view_is_unchanged() {
let top = dir.snapshot(&script, "top.png", &["--view", "top"]);
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
let scene = solidcraft_engine::view::scene(&s, &cam);
assert_same(&top, &solidcraft_engine::capture::png(&scene, &cam, SIZE.0, SIZE.1).unwrap().bytes, "the home camera turned to the top");
assert_same(&top, &dir.mcp(&script, "mcp-top.png", json!({"view": "top"})), "the MCP top view");
assert_differs(&top, &saved, "top and the saved view");
@@ -288,7 +288,7 @@ fn a_snapshot_shows_the_part_where_the_camera_puts_it() {
let vp = c.view_proj(f64::from(SIZE.0) / f64::from(SIZE.1), 1000.0);
let (mut elo, mut ehi) = ([f64::MAX; 2], [f64::MIN; 2]);
for k in 0..8 {
let p = solidcore_record::Vec3::new(
let p = solidcraft_engine::geom::Vec3::new(
corner[0] + size[0] * f64::from(k & 1),
corner[1] + size[1] * f64::from((k >> 1) & 1),
corner[2] + size[2] * f64::from((k >> 2) & 1),
@@ -335,9 +335,9 @@ fn a_forced_cpu_snapshot_is_the_cpu_rasterizers_and_says_so() {
assert!(stderr.contains("CPU rasterizer"), "{stderr}");
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
let scene = solidcraft_engine::view::scene(&s, &cam);
let cpu = solidcraft_engine::render::render_png(&scene, &cam, SIZE.0 as usize, SIZE.1 as usize).unwrap();
assert_eq!(std::fs::read(dir.0.join("cpu.png")).unwrap(), cpu);
}
@@ -5,14 +5,13 @@
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
//! as many pieces, and a sketch on such a face projected every piece.
use solidcraft_engine::geom::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde_json::json;
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::Vec3;
fn design() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
@@ -49,11 +48,11 @@ fn key(v: Vec3) -> [i64; 3] {
fn straight_edges_are_one_edge_after_many_cuts() {
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
let st = s.world_state().unwrap();
let st = s.world_state();
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
assert_eq!(bodies.len(), 1, "{what}: one body");
let body = &bodies[0].body;
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let mesh = body.tessellate(0.01).unwrap();
// Every edge is drawn as one polyline; a straight one as one segment.
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
for (i, e) in mesh.edges.iter().enumerate() {
+2 -5
View File
@@ -14,11 +14,8 @@ name = "solidcraft-web"
path = "src/main.rs"
[features]
default = ["profile"]
default = []
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
# The profiler, compiled in so any build can say where its time goes (the Profile operations
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
[dependencies]
solidcraft-engine = { workspace = true }
@@ -36,7 +33,7 @@ js-sys = "0.3"
serde_json = { workspace = true }
web-sys = { version = "0.3", features = [
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage", "StorageEvent",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
-9
View File
@@ -1,9 +0,0 @@
//! Links the browser build with an 8 MiB stack: solidcore's kernel algorithms (subdivision,
//! intersection, filleting) recurse legitimately, and wasm has no guard page to catch an
//! overflow (solidcore's README recommends 8 MiB).
fn main() {
if std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32") {
println!("cargo:rustc-link-arg=-zstack-size=8388608");
}
}
-2
View File
@@ -1,2 +0,0 @@
This build keeps its designs and settings apart per release channel (src/channel.rs).
packaging/web-channels.sh only offers a release candidate that has this file.
-10
View File
@@ -26,15 +26,6 @@
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
<!-- A drawing's Arabic, Korean and Chinese text: each fetched once, when a drawing needs it. -->
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/NotoSansArabic-Regular.ttf" data-target-path="fonts/noto-sans-arabic" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/OFL.txt" data-target-path="fonts/noto-sans-arabic" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/NotoSansKR-Regular.otf" data-target-path="fonts/noto-sans-kr" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/OFL.txt" data-target-path="fonts/noto-sans-kr" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/NotoSansSC-Regular.otf" data-target-path="fonts/noto-sans-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/OFL.txt" data-target-path="fonts/noto-sans-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/NotoSansTC-Regular.otf" data-target-path="fonts/noto-sans-tc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/OFL.txt" data-target-path="fonts/noto-sans-tc" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
@@ -43,7 +34,6 @@
<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 data-trunk rel="copy-file" href="manifest.webmanifest" />
<link data-trunk rel="copy-file" href="channel-storage.txt" />
<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" />
-61
View File
@@ -1,61 +0,0 @@
//! Release channels. The site serves the stable web app at `/app/`, the release candidate at
//! `/app-rc/` and the bleeding-edge build at `/app-edge/`: one origin, so one set of browser
//! storage. A channel that kept its designs and settings in the same places could save a design
//! in a file format the stable app can't open, or overwrite stable's settings. So each
//! pre-release channel keeps everything under its own name, and stable (and any copy served from
//! another path: the release zip, `trunk serve`) keeps the names it has always used, so what
//! people have saved at `/app/` is untouched.
//!
//! The channel comes from the directory the app is served from, not from the build, so a build
//! taken from a release zip is as separate as one built on the spot.
/// The channel the app is served as, from the page's path: `"rc"`, `"edge"`, or `""` (stable).
pub fn of_path(pathname: &str) -> &'static str {
match pathname.trim_end_matches("index.html").trim_end_matches('/').rsplit('/').next() {
Some("app-rc") => "rc",
Some("app-edge") => "edge",
_ => "",
}
}
/// `name` as this channel stores it: unchanged for stable, `rc:name` and `edge:name` otherwise.
pub fn name_in(channel: &str, name: &str) -> String {
if channel.is_empty() { name.to_owned() } else { format!("{channel}:{name}") }
}
#[cfg(target_arch = "wasm32")]
pub fn current() -> &'static str {
web_sys::window().and_then(|w| w.location().pathname().ok()).map_or("", |p| of_path(&p))
}
/// A storage key (local storage, IndexedDB database name) of the running channel.
#[cfg(target_arch = "wasm32")]
pub fn key(name: &str) -> String {
name_in(current(), name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_follows_the_directory() {
assert_eq!(of_path("/app/"), "");
assert_eq!(of_path("/app/index.html"), "");
assert_eq!(of_path("/app-rc/"), "rc");
assert_eq!(of_path("/app-rc"), "rc");
assert_eq!(of_path("/app-rc/index.html"), "rc");
assert_eq!(of_path("/app-edge/"), "edge");
assert_eq!(of_path("/"), "");
assert_eq!(of_path("/solidcraft-web-0.30.1/"), "");
assert_eq!(of_path("/app/app-rc/"), "rc", "only the directory's own name counts");
assert_eq!(of_path("/app-rc/app/"), "");
}
#[test]
fn stable_keeps_its_names() {
assert_eq!(name_in("", "solidcraft.prefs"), "solidcraft.prefs");
assert_eq!(name_in("rc", "solidcraft.prefs"), "rc:solidcraft.prefs");
assert_ne!(name_in("rc", "x"), name_in("edge", "x"));
}
}
+1 -1
View File
@@ -467,7 +467,7 @@ mod web {
async fn open_db() -> Result<IdbDatabase, String> {
let factory = web_sys::window().ok_or("no window")?.indexed_db().map_err(err)?.ok_or("this browser has no IndexedDB")?;
let request: IdbOpenDbRequest = factory.open_with_u32(&crate::channel::key(DB), 1).map_err(err)?;
let request: IdbOpenDbRequest = factory.open_with_u32(DB, 1).map_err(err)?;
let target = request.clone();
let upgrade = Closure::once_into_js(move || {
if let Ok(db) = target.result().and_then(|r| r.dyn_into::<IdbDatabase>()) {
+6 -21
View File
@@ -16,9 +16,7 @@ use solidcraft_engine::io::vfs;
use crate::disk;
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::{
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWritableFileStream,
};
use web_sys::{FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions, FileSystemWritableFileStream};
/// Where OPFS files appear to the engine.
pub const OPFS: &str = disk::OPFS_PREFIX;
@@ -113,7 +111,7 @@ thread_local! {
static LOCAL: Cell<bool> = const { Cell::new(false) };
}
/// Local storage keys of stored files (under the channel's name, see `channel.rs`).
/// Local storage keys of stored files.
const LOCAL_PREFIX: &str = "solidcraft.file.";
/// Local-storage files kept by earlier releases (compatibility shim, docs/compatibility.md):
/// still listed and opened; removing one removes either key.
@@ -171,17 +169,7 @@ fn has(sm: &web_sys::StorageManager, method: &str) -> bool {
async fn root() -> Result<FileSystemDirectoryHandle, String> {
let sm = storage_manager().filter(|sm| has(sm, "getDirectory")).ok_or("this page has no private file storage (it needs https or localhost)")?;
let dir = JsFuture::from(sm.get_directory()).await.map_err(err)?;
let dir = dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())?;
// Stable keeps its files in the root, where they have always been; a pre-release channel has a folder of its own.
match crate::channel::current() {
"" => Ok(dir),
channel => {
let opts = FileSystemGetDirectoryOptions::new();
opts.set_create(true);
let sub = JsFuture::from(dir.get_directory_handle_with_options(&format!("channel-{channel}"), &opts)).await.map_err(err)?;
sub.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "not a folder".to_string())
}
}
dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())
}
pub(crate) async fn read_handle(h: &FileSystemFileHandle) -> Result<Vec<u8>, String> {
@@ -203,7 +191,7 @@ async fn file_handle(dir: &FileSystemDirectoryHandle, name: &str, create: bool)
async fn write_file(name: &str, bytes: &[u8]) -> Result<(), String> {
if local_only() {
return local()?
.set_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}")), &encode(bytes))
.set_item(&format!("{LOCAL_PREFIX}{name}"), &encode(bytes))
.map_err(|e| format!("local storage is full or blocked: {}", err(e)));
}
let dir = root().await?;
@@ -219,7 +207,7 @@ async fn remove_file(name: &str) -> Result<(), String> {
if local_only() {
let st = local()?;
st.remove_item(&format!("{LEGACY_LOCAL_PREFIX}{name}")).map_err(err)?;
return st.remove_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}"))).map_err(err);
return st.remove_item(&format!("{LOCAL_PREFIX}{name}")).map_err(err);
}
let dir = root().await?;
JsFuture::from(dir.remove_entry(name)).await.map_err(err)?;
@@ -232,10 +220,7 @@ fn read_local() -> Result<Vec<(String, Vec<u8>, f64)>, String> {
let mut out = Vec::new();
for i in 0..st.length().unwrap_or(0).min(10_000) {
let Some(k) = st.key(i).ok().flatten() else { continue };
let own = crate::channel::key(LOCAL_PREFIX);
// The old names belong to stable; a pre-release channel has none.
let legacy = crate::channel::current().is_empty().then_some(LEGACY_LOCAL_PREFIX);
let Some(name) = k.strip_prefix(own.as_str()).or_else(|| legacy.and_then(|l| k.strip_prefix(l))) else { continue };
let Some(name) = k.strip_prefix(LOCAL_PREFIX).or_else(|| k.strip_prefix(LEGACY_LOCAL_PREFIX)) else { continue };
if let Some(v) = st.get_item(&k).ok().flatten() {
out.push((name.to_string(), decode(&v), 0.0));
}
+22 -98
View File
@@ -9,8 +9,6 @@
#[cfg(target_arch = "wasm32")]
mod api;
#[cfg(any(target_arch = "wasm32", test))]
mod channel;
#[cfg(any(target_arch = "wasm32", test))]
mod disk;
#[cfg(target_arch = "wasm32")]
mod easyprint;
@@ -23,11 +21,6 @@ mod linked;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
/// The folder the page serves its fonts from: index.html copies each there, and a drawing's text
/// fetches its fonts from it (#4890).
#[cfg(any(target_arch = "wasm32", test))]
const FONTS: &str = "fonts";
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
/// Ctrl+PageDown already reach the browser.)
@@ -159,15 +152,8 @@ impl PrefsWriter {
/// tab of the page when one tab writes). Other keys, and settings taken away (clearing the site's
/// data), leave this tab's settings as they are.
#[cfg(any(target_arch = "wasm32", test))]
fn settings_written(own_key: &str, key: Option<&str>, value: Option<String>) -> Option<String> {
// Tabs of another channel write to the same storage, under their own key.
value.filter(|_| key == Some(own_key))
}
/// The settings key of `channel` (see [`channel`]).
#[cfg(target_arch = "wasm32")]
fn prefs_key() -> String {
channel::key(solidcraft_ui_egui::prefs::STORAGE_KEY)
fn settings_written(key: Option<&str>, value: Option<String>) -> Option<String> {
value.filter(|_| key == Some(solidcraft_ui_egui::prefs::STORAGE_KEY))
}
#[cfg(target_arch = "wasm32")]
@@ -176,7 +162,6 @@ mod web {
use solidcraft_engine::Session;
use solidcraft_engine::io::vfs;
use solidcraft_ui_egui::ui_kit::BackingTooltipExt as _;
use solidcraft_ui_egui::{Services, SolveApp, ui_kit};
use wasm_bindgen::JsCast as _;
@@ -277,7 +262,7 @@ mod web {
fn hear_settings_from_other_tabs(window: &web_sys::Window, ctx: &egui::Context, inbox: PrefsInbox) {
let ctx = ctx.clone();
let on_storage = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::StorageEvent)>::new(move |e: web_sys::StorageEvent| {
if let Some(prefs) = crate::settings_written(&crate::prefs_key(), e.key().as_deref(), e.new_value()) {
if let Some(prefs) = crate::settings_written(e.key().as_deref(), e.new_value()) {
inbox.set(Some(prefs));
ctx.request_repaint();
}
@@ -292,8 +277,6 @@ mod web {
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
/// The fetches of drawing fonts that have finished, for the next frame to hand on.
type DrawingFontsDone = Rc<std::cell::RefCell<Vec<(solidcraft_engine::sketch::fallback::WebFont, Result<Vec<u8>, String>)>>>;
struct Shell {
app: SolveApp,
@@ -324,9 +307,6 @@ mod web {
font_fetch_kind: Option<&'static str>,
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
font_language: String,
/// The fonts a drawing's text asked for, other than Japanese, each fetched once a session.
drawing_fonts_asked: std::collections::HashSet<solidcraft_engine::sketch::fallback::WebFont>,
drawing_fonts_done: DrawingFontsDone,
space_mouse: SpaceMouse,
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
@@ -354,7 +334,8 @@ mod web {
}
let prefs = self.persisted_prefs();
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
let stored = web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(&crate::prefs_key(), &prefs));
let stored =
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(solidcraft_ui_egui::prefs::STORAGE_KEY, &prefs));
if !matches!(stored, Some(Ok(()))) {
log::warn!("settings: the browser's storage did not take them; they are saved again later");
self.prefs_writer = crate::PrefsWriter::default();
@@ -511,7 +492,7 @@ mod web {
files::pick_from_disk(&self.files, Intent::Open);
}
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
.clicked()
{
files::pick_from_disk(&self.files, Intent::Insert);
@@ -565,7 +546,7 @@ mod web {
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
.clicked()
{
act = Some(("insert", e.name.clone()));
@@ -575,7 +556,7 @@ mod web {
self.rename_focus = true;
}
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
.backing_on_hover_text("A copy in a file you choose; the stored design stays here")
.on_hover_text("A copy in a file you choose; the stored design stays here")
.clicked()
{
act = Some(("save_copy", e.name.clone()));
@@ -730,10 +711,6 @@ mod web {
self.chinese_kind = Some("zh-TW");
}
_ => {
solidcraft_engine::sketch::fallback::provide_web_font(
solidcraft_engine::sketch::fallback::WebFont::Japanese,
bytes.clone(),
);
self.cjk_bytes = Some(bytes);
}
}
@@ -747,9 +724,6 @@ mod web {
_ => JAPANESE_FONT_PATH,
};
log::warn!("failed to fetch script font from {path}: {err}");
if !matches!(kind, Some("zh" | "zh-TW")) {
solidcraft_engine::sketch::fallback::web_font_failed(solidcraft_engine::sketch::fallback::WebFont::Japanese);
}
false
}
};
@@ -785,12 +759,10 @@ mod web {
self.font_language = effective.to_owned();
}
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name), or a
/// drawing's text waiting for one: fetch the Japanese font, so the text does not stay tofu (#720).
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
/// fetch the Japanese font, so the text does not stay tofu (#720).
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
let japanese = solidcraft_engine::sketch::fallback::web_fonts_wanted().contains(&solidcraft_engine::sketch::fallback::WebFont::Japanese);
let wanted = solidcraft_ui_egui::fonts::painted_cjk(ctx) || japanese;
if self.font.watching_text() && wanted && self.font.text_needs_font() {
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
self.fetch_font(ctx, "ja");
}
}
@@ -811,33 +783,6 @@ mod web {
ctx.request_repaint();
});
}
/// The other fonts a drawing's text waits for (Arabic, Chinese, Korean): each fetched once,
/// and handed to the text when it comes. Japanese comes with the interface's CJK font.
fn fetch_drawing_fonts(&mut self, ctx: &egui::Context) {
use solidcraft_engine::sketch::fallback::{self, WebFont};
for (font, res) in self.drawing_fonts_done.borrow_mut().drain(..) {
match res {
Ok(bytes) => fallback::provide_web_font(font, bytes),
Err(err) => {
log::warn!("failed to fetch the drawing font {}/{}: {err}", crate::FONTS, font.file());
fallback::web_font_failed(font);
}
}
ctx.request_repaint();
}
for font in fallback::web_fonts_wanted() {
if font == WebFont::Japanese || !self.drawing_fonts_asked.insert(font) {
continue;
}
let (done, ctx) = (self.drawing_fonts_done.clone(), ctx.clone());
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(&format!("./{}/{}", crate::FONTS, font.file())).await;
done.borrow_mut().push((font, res));
ctx.request_repaint();
});
}
}
}
impl eframe::App for Shell {
@@ -845,7 +790,6 @@ mod web {
// First: a language another tab chose fetches its font this frame.
self.load_other_tabs_prefs();
self.tick_font(ctx);
self.fetch_drawing_fonts(ctx);
self.app.logic(ctx);
self.api.poll();
self.space_mouse.logic(&mut self.app, ctx);
@@ -890,15 +834,14 @@ mod web {
self.store_prefs(&ctx);
}
fn persist_egui_memory(&self) -> bool {
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
self.launch.persists() && crate::channel::current().is_empty()
self.launch.persists()
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.launch.persists() {
return;
}
let prefs = self.persisted_prefs();
storage.set_string(&crate::prefs_key(), prefs);
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs);
}
}
@@ -908,7 +851,7 @@ mod web {
/// Where `?webgl` is remembered. The installed app starts from its manifest's start URL, which
/// has no query, so the choice has to outlive the page it was made on.
const WEBGL_KEY: &str = "solidcraft.webgl"; // stored under crate::channel::key
const WEBGL_KEY: &str = "solidcraft.webgl";
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
fn webgl() -> bool {
@@ -916,17 +859,17 @@ mod web {
let q = query();
if crate::query_flag(&q, "webgl") {
if let Some(s) = &storage {
let _ = s.set_item(&crate::channel::key(WEBGL_KEY), "1");
let _ = s.set_item(WEBGL_KEY, "1");
}
return true;
}
if crate::query_flag(&q, "webgpu") {
if let Some(s) = &storage {
let _ = s.remove_item(&crate::channel::key(WEBGL_KEY));
let _ = s.remove_item(WEBGL_KEY);
}
return false;
}
storage.and_then(|s| s.get_item(&crate::channel::key(WEBGL_KEY)).ok().flatten()).is_some()
storage.and_then(|s| s.get_item(WEBGL_KEY).ok().flatten()).is_some()
}
/// Fetch font bytes asynchronously through the browser.
@@ -1025,7 +968,7 @@ mod web {
if launch.persists() {
hear_settings_from_other_tabs(&window, &cc.egui_ctx, other_tab_prefs.clone());
}
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(&crate::channel::key(k)))) {
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
@@ -1069,8 +1012,6 @@ mod web {
chinese_kind: None,
font_fetch_kind: None,
font_language,
drawing_fonts_asked: Default::default(),
drawing_fonts_done: Default::default(),
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
prefs_writer: crate::PrefsWriter::default(),
@@ -1230,26 +1171,9 @@ mod tests {
#[test]
fn only_settings_another_tab_wrote_are_loaded() {
let key = solidcraft_ui_egui::prefs::STORAGE_KEY;
assert_eq!(settings_written(key, Some(key), Some("{}".into())).as_deref(), Some("{}"));
assert_eq!(settings_written(key, Some("solidcraft.webgl"), Some("1".into())), None);
assert_eq!(settings_written(key, Some(key), None), None, "removed settings leave this tab's");
assert_eq!(settings_written(key, None, None), None, "clearing all storage leaves this tab's settings");
assert_eq!(settings_written(key, Some("rc:solidcraft.prefs"), Some("{}".into())), None, "another channel's settings are not loaded");
}
/// Each font a drawing's text can ask for on the web is one index.html copies, from an asset
/// in the repository, into the folder the app fetches it from.
#[test]
fn each_drawing_font_is_copied_where_it_is_fetched() {
let page = include_str!("../index.html");
let here = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for font in solidcraft_engine::sketch::fallback::WebFont::ALL {
let served = format!("{FONTS}/{}", font.file());
let (folder, file) = served.rsplit_once('/').unwrap_or_default();
let copy = page.lines().find(|l| l.contains("rel=\"copy-file\"") && l.contains(&format!("/{file}\""))).unwrap_or_default();
assert!(copy.contains(&format!("data-target-path=\"{folder}\"")), "index.html doesn't copy {served}: {copy}");
let from = copy.split("href=\"").nth(1).and_then(|rest| rest.split('"').next()).unwrap_or_default();
assert!(here.join(from).is_file(), "{served}: no {from}");
}
assert_eq!(settings_written(Some(key), Some("{}".into())).as_deref(), Some("{}"));
assert_eq!(settings_written(Some("solidcraft.webgl"), Some("1".into())), None);
assert_eq!(settings_written(Some(key), None), None, "removed settings leave this tab's");
assert_eq!(settings_written(None, None), None, "clearing all storage leaves this tab's settings");
}
}
+2 -6
View File
@@ -8,12 +8,9 @@ publish = false
default-run = "solidcraft"
[features]
default = ["raytrace-cpu", "bundled-render-library", "profile"]
default = ["raytrace-cpu", "bundled-render-library"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# The profiler, compiled in so any build can say where its time goes (the Profile operations
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
# (macOS file-open events), src/native_crash.rs (Windows exception filter), src/native_crash_macos.rs
@@ -81,7 +78,6 @@ windows-sys = { version = "0.61", features = [
"Win32_UI_HiDpi",
"Win32_UI_WindowsAndMessaging",
] }
winreg = "0.56"
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21.2", default-features = false }
@@ -93,7 +89,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["macos-share
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
# inset (src/main.rs); all already come with eframe and rfd.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSAccessibility", "NSApplication", "NSResponder", "NSWindow", "NSWorkspace"] }
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
# Native crash capture (src/native_crash_macos.rs): the signal handler, the faulting module
@@ -1,63 +0,0 @@
//! Cached accessibility signal, observed only while a theme offers blur. No UI-thread IO.
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU8, Ordering},
};
#[derive(Default)]
struct Reading {
needed: AtomicBool,
started: AtomicBool,
value: AtomicU8,
}
pub fn cached(ctx: &egui::Context) -> Box<dyn Fn(bool) -> Option<bool>> {
let state = Arc::new(Reading::default());
let ctx = ctx.clone();
Box::new(move |needed| {
state.needed.store(needed, Ordering::Relaxed);
if needed && !state.started.swap(true, Ordering::Relaxed) {
let weak = Arc::downgrade(&state);
let ctx = ctx.clone();
if let Err(error) = std::thread::Builder::new().name("desktop-transparency".into()).spawn(move || {
while let Some(state) = weak.upgrade() {
if state.needed.load(Ordering::Relaxed) {
let code = match read() {
Some(true) => 1,
Some(false) => 2,
None => 0,
};
if state.value.swap(code, Ordering::Relaxed) != code {
ctx.request_repaint();
}
}
drop(state);
std::thread::sleep(std::time::Duration::from_secs(5));
}
}) {
log::warn!("desktop transparency: could not observe accessibility setting: {error}");
}
}
match state.value.load(Ordering::Relaxed) {
1 => Some(true),
2 => Some(false),
_ => None,
}
})
}
#[cfg(target_os = "windows")]
fn read() -> Option<bool> {
let key =
winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER).open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").ok()?;
match key.get_value::<u32, _>("EnableTransparency").ok()? {
0 => Some(true),
1 => Some(false),
_ => None,
}
}
#[cfg(target_os = "macos")]
fn read() -> Option<bool> {
objc2::rc::autoreleasepool(|_| Some(objc2_app_kit::NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceTransparency()))
}
-8
View File
@@ -24,8 +24,6 @@ mod come_forward;
mod control_server;
#[cfg(target_os = "linux")]
mod desktop_theme;
#[cfg(any(target_os = "windows", target_os = "macos"))]
mod desktop_transparency;
mod device_lost;
#[cfg(target_os = "windows")]
mod native_crash;
@@ -541,10 +539,6 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
#[cfg(not(target_os = "linux"))]
desktop_dark: None,
#[cfg(any(target_os = "windows", target_os = "macos"))]
reduce_transparency: Some(desktop_transparency::cached(&cc.egui_ctx)),
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
reduce_transparency: None,
http_get: Some(std::sync::Arc::new(http_get)),
http_download: Some(std::sync::Arc::new(http_download)),
// Already an installed app: only the browser build offers to install.
@@ -901,8 +895,6 @@ fn main() -> eframe::Result {
if let Err(e) = startup::relaunch_with_version_compat() {
log::warn!("startup: OCLP check failed (continuing normally): {e}");
}
// SOLIDCRAFT_PROFILE=1: the profiler records from the start (diagnostics.profile reads it).
solidcraft_engine::timing::profile_from_env();
// Validate before legacy adoption, crash markers or any user preference read.
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
+2 -2
View File
@@ -22,7 +22,7 @@ assets/
appearances/library.rs # built-in visual presets
materials/library.rs # physical defaults and colours
shaders/viewport.wgsl # original viewport shader
fonts/<family>/ # unmodified fonts and OFL licences
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
locales/{en,de,ja,ko}.json # shared offline catalogs
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
@@ -79,7 +79,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `io.tangencylabs.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 the Japanese font/licence to `fonts/` and the Noto Sans Arabic, KR, SC and TC fonts with their licences to `fonts/<family>/` (fetched when a drawing's text needs them), uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
| 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 |
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
## Code and provenance
-93
View File
@@ -1,93 +0,0 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
-92
View File
@@ -1,92 +0,0 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
-92
View File
@@ -1,92 +0,0 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
-92
View File
@@ -1,92 +0,0 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+5 -13
View File
@@ -178,7 +178,6 @@ pattern_rect false
perforate false
perspective false
pipe false
placeholder true
plane false
plane_along_path false
plane_angle false
@@ -202,18 +201,6 @@ point_vertex false
polygon false
polygon_circumscribed false
polygon_edge false
prefs_appearance false
prefs_assistant false
prefs_design false
prefs_general false
prefs_graphics false
prefs_layout false
prefs_mouse3d false
prefs_navigation false
prefs_plugins false
prefs_shortcuts false
prefs_sketch false
prefs_tools false
presspull false
print_face false
project false
@@ -245,6 +232,11 @@ section false
select false
send false
settings false
settings_keyboard false
settings_monitor false
settings_mouse false
settings_palette false
settings_plug false
settings_profile false
sheet false
sheet_rules false
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Appearance</title>
<path d="M32 7.5C18 7.5 7.5 18.5 7.5 32C7.5 45.5 18.5 56.5 32 56.5C37.5 56.5 40 53 38 49C36 45 38.5 42 43 42L47.5 42C52.5 42 56.5 38 56.5 33C56.5 18.5 46 7.5 32 7.5Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><circle cx="20" cy="33" r="4" fill="currentColor"/><circle cx="28" cy="20" r="4" fill="currentColor"/><circle cx="42" cy="22" r="4" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 520 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Assistant</title>
<path d="M29 12.5Q30.2 33.8 51.5 35Q30.2 36.2 29 57.5Q27.8 36.2 6.5 35Q27.8 33.8 29 12.5Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M50 6Q50.6 12.4 57 13Q50.6 13.6 50 20Q49.4 13.6 43 13Q49.4 12.4 50 6Z" fill="currentColor" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Design</title>
<path d="M32 7.7L53.04 19.85L53.04 44.15L32 56.3L10.96 44.15L10.96 19.85Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M10.96 19.85L32 32L53.04 19.85M32 32L32 56.3" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 427 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>General</title>
<path d="M27.24 14.85L27.15 7.07L36.85 7.07L36.76 14.85L44.48 19.3L51.17 15.34L56.02 23.73L49.23 27.54L49.23 36.46L56.02 40.27L51.17 48.66L44.48 44.7L36.76 49.15L36.85 56.93L27.15 56.93L27.24 49.15L19.52 44.7L12.83 48.66L7.98 40.27L14.77 36.46L14.77 27.54L7.98 23.73L12.83 15.34L19.52 19.3Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M25 32A7 7 0 1 0 39 32A7 7 0 1 0 25 32Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 640 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Graphics</title>
<path d="M14 8L50 8A6 6 0 0 1 56 14L56 38A6 6 0 0 1 50 44L14 44A6 6 0 0 1 8 38L8 14A6 6 0 0 1 14 8ZM32 44L32 54M20 54L44 54" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 323 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Layout</title>
<path d="M14 10L50 10A6 6 0 0 1 56 16L56 48A6 6 0 0 1 50 54L14 54A6 6 0 0 1 8 48L8 16A6 6 0 0 1 14 10Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M24 10L24 54M24 39L56 39" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 436 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>3D mouse</title>
<path d="M6.5 32A25.5 25.5 0 1 0 57.5 32A25.5 25.5 0 1 0 6.5 32Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M24 32A8 8 0 1 0 40 32A8 8 0 1 0 24 32Z" fill="currentColor" stroke="currentColor" stroke-width="4.08" stroke-linejoin="round" stroke-linecap="round"/><path d="M53.4 32L45.96 35.9L45.96 28.1Z" fill="currentColor" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/><path d="M32 53.4L28.1 45.96L35.9 45.96Z" fill="currentColor" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/><path d="M10.6 32L18.04 28.1L18.04 35.9Z" fill="currentColor" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/><path d="M32 10.6L35.9 18.04L28.1 18.04Z" fill="currentColor" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Navigation</title>
<path d="M32 7A14 14 0 0 1 46 21L46 43A14 14 0 0 1 18 43L18 21A14 14 0 0 1 32 7Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M18 38L46 38" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M32 14L32 22" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 530 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Plugins</title>
<path d="M24 7L24 20M40 7L40 20M16 20L48 20L48 28A16 16 0 0 1 16 28ZM32 44L32 57" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 279 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shortcuts</title>
<path d="M12 13L52 13A6 6 0 0 1 58 19L58 45A6 6 0 0 1 52 51L12 51A6 6 0 0 1 6 45L6 19A6 6 0 0 1 12 13Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><circle cx="18" cy="26" r="3.6" fill="currentColor"/><circle cx="28" cy="26" r="3.6" fill="currentColor"/><circle cx="38" cy="26" r="3.6" fill="currentColor"/><circle cx="48" cy="26" r="3.6" fill="currentColor"/><path d="M20 40L44 40" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 639 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sketch</title>
<path d="M7 57L25.53 51.48L12.52 38.47Z" fill="currentColor" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M7 57L25.53 51.48L53.81 23.2L40.8 10.19L12.52 38.47Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/><path d="M44.62 32.39L31.61 19.38" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 545 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Tools</title>
<path d="M56.86 18.45A16.5 16.5 0 0 1 37.12 39.04L22.33 53.83A8.6 8.6 0 0 1 10.17 41.67L24.96 26.88A16.5 16.5 0 0 1 45.55 7.14L37.55 15.14L39.94 24.06L48.86 26.45Z" fill="none" stroke="currentColor" stroke-width="6.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 360 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Align drawing annotations</title>
<path d="M12 6L12 58" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 12L51.5 12A2.5 2.5 0 0 1 54 14.5L54 17.5A2.5 2.5 0 0 1 51.5 20L22.5 20A2.5 2.5 0 0 1 20 17.5L20 14.5A2.5 2.5 0 0 1 22.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 28L40.5 28A2.5 2.5 0 0 1 43 30.5L43 33.5A2.5 2.5 0 0 1 40.5 36L22.5 36A2.5 2.5 0 0 1 20 33.5L20 30.5A2.5 2.5 0 0 1 22.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 44L47.5 44A2.5 2.5 0 0 1 50 46.5L50 49.5A2.5 2.5 0 0 1 47.5 52L22.5 52A2.5 2.5 0 0 1 20 49.5L20 46.5A2.5 2.5 0 0 1 22.5 44Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 962 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Align drawing views</title>
<path d="M10.5 24L23.5 24A2.5 2.5 0 0 1 26 26.5L26 43.5A2.5 2.5 0 0 1 23.5 46L10.5 46A2.5 2.5 0 0 1 8 43.5L8 26.5A2.5 2.5 0 0 1 10.5 24Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M39.5 12L52.5 12A2.5 2.5 0 0 1 55 14.5L55 43.5A2.5 2.5 0 0 1 52.5 46L39.5 46A2.5 2.5 0 0 1 37 43.5L37 14.5A2.5 2.5 0 0 1 39.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 52L58 52" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17 29L17 40M46 18L46 39" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 846 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing balloon</title>
<path d="M9.5 37L29.5 37A2.5 2.5 0 0 1 32 39.5L32 52.5A2.5 2.5 0 0 1 29.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 39.5A2.5 2.5 0 0 1 9.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 45L35 30L40 25" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 50L19 37L28 45Z" fill="#236ead"/>
<circle cx="45" cy="17" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 651 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Base drawing view</title>
<path d="M9.5 10L54.5 10A2.5 2.5 0 0 1 57 12.5L57 51.5A2.5 2.5 0 0 1 54.5 54L9.5 54A2.5 2.5 0 0 1 7 51.5L7 12.5A2.5 2.5 0 0 1 9.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17.5 19L42.5 19A2.5 2.5 0 0 1 45 21.5L45 39.5A2.5 2.5 0 0 1 42.5 42L17.5 42A2.5 2.5 0 0 1 15 39.5L15 21.5A2.5 2.5 0 0 1 17.5 19Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="30" cy="30.5" r="6" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M47 46L53 46M50 43L50 49" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 851 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing centre mark</title>
<circle cx="32" cy="32" r="18" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 6L32 21M32 27L32 37M32 43L32 58M6 32L21 32M27 32L37 32M43 32L58 32" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 420 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Check drawing</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 19L42 19M21 29L35 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 44L37 53L57 30" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 44L37 53L57 30" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 730 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing datum</title>
<path d="M6 53L58 53" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 52L24 38L40 38Z" fill="#236ead"/>
<path d="M32 38L32 27" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21.5 7L42.5 7A2.5 2.5 0 0 1 45 9.5L45 24.5A2.5 2.5 0 0 1 42.5 27L21.5 27A2.5 2.5 0 0 1 19 24.5L19 9.5A2.5 2.5 0 0 1 21.5 7Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 631 B

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Detail drawing view</title>
<path d="M9.5 11L30.5 11A2.5 2.5 0 0 1 33 13.5L33 45.5A2.5 2.5 0 0 1 30.5 48L9.5 48A2.5 2.5 0 0 1 7 45.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13 18L23 18L23 33L29 33" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="23" cy="30" r="9" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M31 35L39 41" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47" cy="46" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M40 39L46 39L46 48L53 48" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1000 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing dimension</title>
<path d="M10 10L10 55M54 10L54 55" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 14L54 14L54 36L10 36Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 49L44 49" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M11 49L23 43L23 55Z" fill="#236ead"/>
<path d="M53 49L41 55L41 43Z" fill="#236ead"/>
</svg>

Before

Width:  |  Height:  |  Size: 598 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Edit drawing table</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25 26L54 26L54 37L25 37Z" fill="#b9ddf8"/>
<path d="M32 32L47 32M42 27L47 32L42 37" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 741 B

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Hole callout</title>
<circle cx="18" cy="43" r="13" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18" cy="43" r="6" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 35L39 22L57 22" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23 40L27 28L34 35Z" fill="#236ead"/>
<circle cx="44" cy="10" r="4.5" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M39 16L49 4" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 794 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Insert drawing image</title>
<path d="M9.5 11L54.5 11A2.5 2.5 0 0 1 57 13.5L57 50.5A2.5 2.5 0 0 1 54.5 53L9.5 53A2.5 2.5 0 0 1 7 50.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="43" cy="23" r="5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 45L24 27L36 41L42 34L54 47Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 622 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing leader</title>
<path d="M8.5 34L33.5 34A2.5 2.5 0 0 1 36 36.5L36 51.5A2.5 2.5 0 0 1 33.5 54L8.5 54A2.5 2.5 0 0 1 6 51.5L6 36.5A2.5 2.5 0 0 1 8.5 34Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18 45L39 18L56 18" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 49L19 35L26 41Z" fill="#236ead"/>
</svg>

Before

Width:  |  Height:  |  Size: 516 B

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing parts list</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.5" cy="31.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.5" cy="46.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 31L48 31M30 46L48 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 958 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Print drawing</title>
<path d="M18.5 7L45.5 7A2.5 2.5 0 0 1 48 9.5L48 29.5A2.5 2.5 0 0 1 45.5 32L18.5 32A2.5 2.5 0 0 1 16 29.5L16 9.5A2.5 2.5 0 0 1 18.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8.5 24L55.5 24A2.5 2.5 0 0 1 58 26.5L58 49.5A2.5 2.5 0 0 1 55.5 52L8.5 52A2.5 2.5 0 0 1 6 49.5L6 26.5A2.5 2.5 0 0 1 8.5 24Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="48" cy="32" r="2.5" fill="#236ead" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.5 37L45.5 37A2.5 2.5 0 0 1 48 39.5L48 55.5A2.5 2.5 0 0 1 45.5 58L18.5 58A2.5 2.5 0 0 1 16 55.5L16 39.5A2.5 2.5 0 0 1 18.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23 44L41 44M23 51L36 51" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Projected drawing view</title>
<path d="M9.5 33L31.5 33A2.5 2.5 0 0 1 34 35.5L34 52.5A2.5 2.5 0 0 1 31.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 35.5A2.5 2.5 0 0 1 9.5 33Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="20.5" cy="44" r="5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.5 8L31.5 8A2.5 2.5 0 0 1 34 10.5L34 19.5A2.5 2.5 0 0 1 31.5 22L9.5 22A2.5 2.5 0 0 1 7 19.5L7 10.5A2.5 2.5 0 0 1 9.5 8Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 25L9 30M32 25L32 30M38 44L54 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="3 5"/>
<path d="M44 28L44 13" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44 7L48 14L40 14Z" fill="#236ead"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Browse drawing resources</title>
<path d="M15 7L51 7A3 3 0 0 1 54 10L54 48A3 3 0 0 1 51 51L15 51A3 3 0 0 1 12 48L12 10A3 3 0 0 1 15 7Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L46 12A3 3 0 0 1 49 15L49 53A3 3 0 0 1 46 56L10 56A3 3 0 0 1 7 53L7 15A3 3 0 0 1 10 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 20L42 20L42 47L14 47Z" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 38L42 38M28 38L28 47" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22 27L34 27L34 36L22 36Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 933 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Add drawing revision</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 19L43 19M20 29L37 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M43 33L58 57L28 57Z" fill="#ffffff" stroke="#ffffff" stroke-width="4.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M43 33L58 57L28 57Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 745 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing revision table</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 27L10 35L20 35ZM15 42L10 50L20 50Z" fill="#236ead"/>
<path d="M31 31L49 31M31 46L49 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 752 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Section drawing view</title>
<path d="M8 15L56 15L56 50L37 50L37 27L27 27L27 50L8 50Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 38L26 22M10 25L20 15M10 50L24 36M39 35L54 20M39 48L54 33M49 50L56 43" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Space drawing annotations</title>
<path d="M28.5 9L53.5 9A2.5 2.5 0 0 1 56 11.5L56 14.5A2.5 2.5 0 0 1 53.5 17L28.5 17A2.5 2.5 0 0 1 26 14.5L26 11.5A2.5 2.5 0 0 1 28.5 9Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 28L53.5 28A2.5 2.5 0 0 1 56 30.5L56 33.5A2.5 2.5 0 0 1 53.5 36L28.5 36A2.5 2.5 0 0 1 26 33.5L26 30.5A2.5 2.5 0 0 1 28.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 47L53.5 47A2.5 2.5 0 0 1 56 49.5L56 52.5A2.5 2.5 0 0 1 53.5 55L28.5 55A2.5 2.5 0 0 1 26 52.5L26 49.5A2.5 2.5 0 0 1 28.5 47Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 19L15 25M15 38L15 44" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 14L10 21L20 21ZM15 30L10 23L20 23ZM15 33L10 40L20 40ZM15 49L10 42L20 42Z" fill="#236ead"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Update drawing</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 16L41.5 16A2.5 2.5 0 0 1 44 18.5L44 28.5A2.5 2.5 0 0 1 41.5 31L22.5 31A2.5 2.5 0 0 1 20 28.5L20 18.5A2.5 2.5 0 0 1 22.5 16Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44 40L32 37L40 28Z" fill="#236ead"/>
</svg>

Before

Width:  |  Height:  |  Size: 900 B

+8 -15
View File
@@ -1,8 +1,7 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcore_record::Vec3;
use solidcraft_engine::geom::Ring;
use solidcraft_engine::geom::{Ring, Vec3};
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Ring<Pos2>,
@@ -152,12 +151,6 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
/// The pointer target around a compass end: larger than the drawn disc, so it is easy to hit.
pub(crate) const AXIS_RADIUS: f32 = 15.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
axis_tip_scaled(painter, c, name, d, at, depth, hovered, t, 1.0);
}
/// The same compass artwork at a uniform scale, including its lettering and strokes.
#[allow(clippy::too_many_arguments)]
pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens, scale: f32) {
let col = if d.x != 0.0 {
t.axis_x
} else if d.y != 0.0 {
@@ -170,11 +163,11 @@ pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: &
// Positive shafts are the full axis colour; negative ones are a dimmed tint (and any end
// pointing away from the viewer dims further).
let ink = if !positive || depth < -0.01 { col.gamma_multiply(0.55) } else { col };
let radius = if positive { 11.0 } else { 4.8 } * scale;
let radius = if positive { 11.0 } else { 4.8 };
let direction = (at - c).normalized();
let width = 3.6 * scale;
let width = 3.6;
// A negative shaft stops at the outer edge of its ring; a positive one runs under its disc.
let end = if positive { at - direction * radius } else { at - direction * (radius + 1.2 * scale) };
let end = if positive { at - direction * radius } else { at - direction * (radius + 1.2) };
painter.line_segment([c, end], Stroke::new(width, ink));
// The round cap sits at the origin; the other end is covered by the disc or ring.
painter.circle_filled(c, width / 2.0, ink);
@@ -186,18 +179,18 @@ pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: &
// kind turns fully white.
if hovered {
// Same outer size as the resting tip: a ring's outline is half its stroke outside.
painter.circle_filled(at, if positive { radius } else { radius + 1.2 * scale }, t.on_accent);
painter.circle_filled(at, if positive { radius } else { radius + 1.2 }, t.on_accent);
// On the light theme a white disc vanishes into the compass, so it carries an outline in
// the axis colour, drawn inside the disc so it does not grow.
if light && positive {
painter.circle_stroke(at, radius - scale, Stroke::new(2.0 * scale, col));
painter.circle_stroke(at, radius - 1.0, Stroke::new(2.0, col));
}
}
if positive {
// On a hovered (white) disc the letter takes the axis colour.
let letter = if hovered { col } else { t.on_accent };
let label = name.to_uppercase();
let galley = painter.layout_no_wrap(label, crate::fonts::bold(crate::theme::Tokens::get().font_compass * scale), letter);
let galley = painter.layout_no_wrap(label, crate::fonts::bold(crate::theme::Tokens::get().font_compass), letter);
let origin = at - galley.size() / 2.0;
let ppp = painter.ctx().pixels_per_point().max(0.5);
let origin = pos2((origin.x * ppp).round() / ppp, (origin.y * ppp).round() / ppp);
@@ -208,7 +201,7 @@ pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: &
at,
radius,
Stroke::new(
2.4 * scale,
2.4,
if !hovered {
ink
} else if light {
+64 -389
View File
@@ -379,7 +379,7 @@
"Move Type": "نوع التحريك",
"Move down": "نقل للأسفل",
"Move earlier": "نقل إلى الأقدم",
"Move holes by clicking or dragging": "التحريك بالنقر أو السحب",
"Move holes by clicking or dragging": "تحريك الثقوب بالنقر أو السحب",
"Move later": "نقل إلى الأحدث",
"Move up": "نقل للأعلى",
"Name": "الاسم",
@@ -496,7 +496,6 @@
"Render Stage": "مرحلة التصيير",
"Reopen designs": "إعادة فتح التصاميم",
"Reopen the designs that were open when SolidCraft was last closed": "إعادة فتح التصاميم التي كانت مفتوحة آخر مرة",
"Repair Part": "إصلاح الجزء",
"Replace existing library file": "استبدال ملف المكتبة الموجود",
"Report a bug — review a screenshot and open a GitHub draft": "الإبلاغ عن خطأ — راجع لقطة الشاشة وافتح مسودة GitHub",
"Report copied. Keep this text to submit on GitHub when you are online.": "تم نسخ التقرير. احتفظ بهذا النص لإرساله على GitHub عند الاتصال بالإنترنت.",
@@ -585,7 +584,7 @@
"Show ghost": "إظهار الطيف",
"Show in folder": "إظهار في المجلد",
"Show the pointer an AI agent moves (#35)": "إظهار المؤشر الذي يحرّكه وكيل الذكاء الاصطناعي (#35)",
"Show tool at cursor": "الأداة عند المؤشر",
"Show tool at cursor": "إظهار الأداة عند المؤشر",
"Signed axes": "محاور بإشارة",
"Size": "الحجم",
"Sketch constraints": "قيود الرسم",
@@ -596,7 +595,7 @@
"Sketch points": "نقاط الرسم",
"Slider": "منزلق",
"Slot width": "عرض الشق",
"Snap to face and edge centres": "الالتقاط إلى المراكز",
"Snap to face and edge centres": "الالتقاط إلى مراكز الأوجه والحواف",
"SolidCraft cannot submit reports directly. Submit on GitHub; no token is needed in the app.": "لا يستطيع SolidCraft إرسال التقارير مباشرةً. أرسلها على GitHub؛ لا حاجة إلى رمز وصول في التطبيق.",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "أُغلق SolidCraft قبل حفظ هذه التصاميم. يمكن فتح آخر حفظ تلقائي لها.",
"Sort A to Z": "فرز تصاعدي",
@@ -739,34 +738,34 @@
"a picked edge's body is gone": "جسم الحافة المحددة لم يعد موجودًا",
"add a key for a joint first": "أضف أولًا مفتاحًا لمفصل",
"ai.connected_toast": "أصبح SolidCraft متاحًا في {clients}. أعد تشغيلها لاستخدامه (التفضيلات > المساعد).",
"ai.prefs.allow_control": "السماح لوكلاء الذكاء الاصطناعي بالاتصال",
"ai.prefs.allow_control_note": "قناة محلية (MCP). لا شيء يغادر هذا الحاسوب.\n\nيفتح قناة تحكم على هذا الحاسوب فقط (127.0.0.1، المنفذ 27878 أو أول منفذ متاح بعده). على البرنامج الذي يتصل أن يقدّم أيضًا رمزًا سريًا لا يقرؤه إلا حساب المستخدم الخاص بك، ويُنشأ من جديد عند كل تشغيل. يُطبّق في المرة القادمة التي يبدأ فيها SolidCraft. عند الإيقاف: لا شيء يستمع ما لم تشغّل SolidCraft مع --control.",
"ai.prefs.availability": "التوفر",
"ai.prefs.background_commands": "تشغيل الأوامر البطيئة في الخلفية",
"ai.prefs.connect_apps": "إضافة SolidCraft إلى تطبيقات الذكاء الاصطناعي",
"ai.prefs.connect_apps_note": "إيقافه يزيل الإدخالات مجددًا.\n\nعند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
"ai.prefs.connect_at_start": "توصيل آخر وكيل عند البدء",
"ai.prefs.allow_control": "السماح لوكلاء الذكاء الاصطناعي بالاتصال (قناة التحكم / MCP)",
"ai.prefs.allow_control_note": "يفتح قناة تحكم على هذا الحاسوب فقط (127.0.0.1، المنفذ 27878 أو أول منفذ متاح بعده). على البرنامج الذي يتصل أن يقدّم أيضًا رمزًا سريًا لا يقرؤه إلا حساب المستخدم الخاص بك، ويُنشأ من جديد عند كل تشغيل. يُطبّق في المرة القادمة التي يبدأ فيها SolidCraft. عند الإيقاف: لا شيء يستمع ما لم تشغّل SolidCraft مع --control.",
"ai.prefs.availability": "عناصر تحكم المساعد",
"ai.prefs.background_commands": "تشغيل أوامر الوكيل في الخلفية",
"ai.prefs.connect_apps": "إتاحة SolidCraft لتطبيقات الذكاء الاصطناعي على هذا الحاسوب",
"ai.prefs.connect_apps_note": "عند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
"ai.prefs.connect_at_start": "الاتصال بآخر وكيل مستخدم عند بدء SolidCraft",
"ai.prefs.connect_at_start_note": "يبدأ في الخلفية الوكيل الذي اتصلت به آخر مرة (أو الوكيل الافتراضي) في كل مرة يبدأ فيها SolidCraft، ليكون جاهزًا للمحادثة. لا يحدث ذلك عند بدء SolidCraft مع --control. إذا تعذّر الاتصال، يعرض المساعد «فشل الاتصال» وزر «إعادة المحاولة».",
"ai.prefs.follow_camera": "تتبّع الكاميرا",
"ai.prefs.follow_camera": "توجيه الكاميرا نحو الرسومات",
"ai.prefs.follow_camera_off": "تتبّع الكاميرا التلقائي متوقف.",
"ai.prefs.follow_camera_on": "تتجه الكاميرا نحو الرسومات التي ينشئها الوكيل.",
"ai.prefs.intro": "وكلاء الذكاء الاصطناعي في SolidCraft.",
"ai.prefs.make_default": "تعيين افتراضيًا",
"ai.prefs.intro": "الوكلاء والأذونات وطريقة ظهور عملهم.",
"ai.prefs.make_default": "الاستخدام افتراضيًا",
"ai.prefs.no_agents": "لا يوجد وكلاء بعد. أضف وكيلًا أو حدّث القائمة للعثور على الوكلاء المثبّتين.",
"ai.prefs.no_saved_approvals": "لا توجد موافقات محفوظة.",
"ai.prefs.open_at_start": "فتح المحادثة عند البدء",
"ai.prefs.open_at_start": "فتح لوحة المساعد عند بدء SolidCraft",
"ai.prefs.open_at_start_note": "يعرض لوحة المساعد (أو نافذتها المستقلة) في كل مرة يبدأ فيها SolidCraft. لا يحدث ذلك عند بدء SolidCraft مع --control.",
"ai.prefs.override_all_warning": "قد يحفظ هذا الوكيل التصميم أو يكتب فوقه أو يصدّره أو يستبدله دون أن يسأل.",
"ai.prefs.permission_all": "تنفيذ كل شيء",
"ai.prefs.permission_all_summary": "لا يسأل أبدًا",
"ai.prefs.permission_ask": "السؤال أولًا",
"ai.prefs.permission_ask_summary": "يسأل عن كل تغيير",
"ai.prefs.permission_edits": "إجراء تعديلات",
"ai.prefs.permission_edits_summary": "يعدّل بحرية ويسأل عن الباقي",
"ai.prefs.permission_all": "السماح بكل شيء",
"ai.prefs.permission_all_summary": "تعمل أدوات SolidCraft المتاحة دون سؤال.",
"ai.prefs.permission_ask": "السؤال قبل التغييرات",
"ai.prefs.permission_ask_summary": "الموافقة على كل تغيير في التصميم وكل إجراء على الملفات.",
"ai.prefs.permission_edits": "السماح بتعديلات النموذج",
"ai.prefs.permission_edits_summary": "تُنفَّذ تعديلات النموذج، وتبقى إجراءات الملفات والإجراءات المدمّرة تتطلب السؤال.",
"ai.prefs.permission_scope": "لأدوات SolidCraft فقط. أوامر الطرفية وتعديلات الملفات لدى الوكيل تتبع أذوناته الخاصة.",
"ai.prefs.pointer_preview_hidden": "المؤشر مخفي",
"ai.prefs.pointer_preview_speed": "مؤشر الوكيل · {speed}",
"ai.prefs.pointer_speed": "السرعة",
"ai.prefs.pointer_speed": "سرعة الحركة",
"ai.prefs.privacy_note": "لا يستلم المحادثة إلا الوكيل أو نقطة الاتصال التي تختارها. تُخزَّن مفاتيح نقاط الاتصال في سلسلة مفاتيح النظام.",
"ai.prefs.show_pointer": "إظهار مؤشر الوكيل",
"ai.prefs.turn_limit_hint": "الحد الأقصى لاستدعاءات الأدوات التي تغيّر التصميم في الرسالة الواحدة.",
@@ -791,7 +790,6 @@
"browser.edit_canvas_title": "تعديل اللوحة: {name}",
"browser.edit_point_cloud_title": "تحرير سحابة النقاط: {name}",
"browser.move_title": "تحريك: {name}",
"browser.needs_repair": "{name}: يحتاج إلى إصلاح",
"browser.parameters": "المعاملات ({count})",
"browser.precision": "الدقة: {precision}",
"browser.sketch_attachment_lost": "{name} (فُقد الارتباط)",
@@ -836,7 +834,6 @@
"cmd.assembly_mass_properties": "خصائص الكتلة",
"cmd.body.print_face": "وجه الطباعة",
"cmd.body_rename": "إعادة تسمية الجسم",
"cmd.body_repair": "إصلاح الجزء",
"cmd.browser_collapse_all": "طي الكل",
"cmd.browser_expand_all": "توسيع الكل",
"cmd.browser_group": "مجموعة جديدة",
@@ -921,8 +918,6 @@
"cmd.diagnostics_crash_reports": "تقارير الأعطال",
"cmd.diagnostics_crash_reports_clear": "حذف تقارير الأعطال",
"cmd.diagnostics_crash_test": "اختبار العطل",
"cmd.diagnostics_profile": "التحليل الزمني",
"cmd.diagnostics_validation": "التحقق",
"cmd.doc_recover": "استعادة التصميم",
"cmd.doc_recovery_discard": "تجاهل التصميم المستعاد",
"cmd.doc_recovery_list": "التصاميم القابلة للاستعادة",
@@ -960,7 +955,6 @@
"cmd.frame_profile_list": "مكتبة المقاطع",
"cmd.frame_profile_properties": "خصائص المقطع",
"cmd.inspect_accessibility": "تحليل إمكانية الوصول",
"cmd.inspect_analysis": "تحليل العرض",
"cmd.inspect_center_of_mass": "مركز الكتلة",
"cmd.inspect_clearance": "الخلوص",
"cmd.inspect_colored_components": "تفعيل ألوان المكوّنات",
@@ -984,7 +978,6 @@
"cmd.joint_delete": "حذف المفصل",
"cmd.joint_drive": "تحريك المفاصل",
"cmd.joint_edit": "تعديل المفصل",
"cmd.joint_find_limits": "إيجاد حدود المفصل",
"cmd.joint_gear": "ترس",
"cmd.joint_inspect": "فحص المفصل",
"cmd.joint_limits": "حدود المفصل",
@@ -997,11 +990,6 @@
"cmd.joint_screw": "لولب",
"cmd.joint_solve": "حل المفاصل",
"cmd.joint_sweep": "مفصل كنس",
"cmd.layout.apply": "تطبيق التخطيط",
"cmd.layout.get": "فحص التخطيط",
"cmd.layout.reset": "إعادة ضبط التخطيط",
"cmd.layout.save": "حفظ التخطيط",
"cmd.layout.workspace.select": "اختيار مساحة عمل التخطيط",
"cmd.library_apply": "تطبيق عنصر من المكتبة المحلية",
"cmd.library_export": "تصدير مكتبة التصميم",
"cmd.library_inspect": "فحص المكتبة المحلية",
@@ -1035,21 +1023,6 @@
"cmd.occurrence_move": "تحريك النسخة",
"cmd.occurrence_properties": "الخصائص الفيزيائية للنسخة",
"cmd.occurrence_reparent": "نقل إلى مكوّن",
"cmd.panel.activate": "تنشيط علامة تبويب اللوحة",
"cmd.panel.close": "إغلاق اللوحة",
"cmd.panel.collapse": "طي قسم اللوحة",
"cmd.panel.dismiss": "إغلاق العرض المؤقت للوحة",
"cmd.panel.float": "تعويم اللوحة",
"cmd.panel.focus": "تركيز اللوحة",
"cmd.panel.list": "سرد اللوحات",
"cmd.panel.move": "نقل اللوحة",
"cmd.panel.native_close": "إعادة إرساء قسم اللوحة من نافذته المستقلة",
"cmd.panel.open": "فتح اللوحة",
"cmd.panel.peek": "عرض اللوحة المطوية مؤقتًا",
"cmd.panel.popout": "نقل اللوحة إلى نافذة مستقلة",
"cmd.panel.reset_pair": "إعادة ضبط فاصل اللوحة",
"cmd.panel.resize": "تغيير حجم قسم اللوحة",
"cmd.panel.resize_pair": "تحريك فاصل اللوحة",
"cmd.parameters_add": "إضافة معامل مستخدم",
"cmd.parameters_change": "تغيير المعاملات",
"cmd.parameters_comment": "التعليق على المعامل",
@@ -1386,7 +1359,6 @@
"crash.unclean_explanation": "اكتُشف عند تشغيل لاحق، ولم يُسجَّل وقت الخروج بدقة. قد يسبب إيقاف تشغيل النظام أو الإيقاف القسري خروجًا غير سليم أيضًا.",
"delete.and_more": "… و{count} غيرها",
"diagnostic.empty_boolean_component": "لا يوجد جسم للقص أو التقاطع في المكوّن '{component}'؛ قد تتداخل الأداة مع '{body}' في المكوّن '{other}' — نشّط ذلك المكوّن أو اختر الجسم هدفًا",
"diagnostic.reselect_reference": "{feature}: المرجع `{reference}` لم يعد موجودًا كما اختير؛ أعد اختياره",
"dialog.bodies_count": "{count} أجسام",
"dialog.clearance_invalid": "يجب أن يكون الخلوص طولًا قيمته 0 أو أكثر",
"dialog.compute": "احسب",
@@ -1396,9 +1368,6 @@
"dialog.custom_axis": "↖ محور مخصص",
"dialog.dimension_insert_hint": "انقر على بُعد قائد آخر لإدراج اسمه",
"dialog.dimension_name": "البُعد: {name}",
"dialog.dimension_step_down": "إنقاص بمقدار {step}",
"dialog.dimension_step_expression": "يتغير الرقم فقط بالخطوات؛ يبقى التعبير كما كُتب",
"dialog.dimension_step_up": "زيادة بمقدار {step}",
"dialog.edit_title": "تعديل {title}",
"dialog.no_shown_body": "لا يوجد جسم ظاهر لتغييره: أظهر جسمًا، أو اختر «جسم جديد»",
"dialog.pattern_sketch_not_object": "اختر أجسامًا أو ميزات: الرسم ليس أيًّا منهما. كرّر منحنياته بأداة النمط أو المرآة داخل الرسم، أو ابثقه أولًا.",
@@ -1406,7 +1375,6 @@
"dialog.selected_count": "↖ {count} محدد",
"dialog.selection_constraint": "قيد {id}",
"dialog.selection_external_curve": "{sketch} · منحنى {id}",
"dialog.selection_external_point": "{point} · {sketch}",
"dialog.selection_guidance": "حدد مُدخلًا لتغييره. انقر على العناصر المحددة لإزالتها؛ Ctrl/Cmd مع النقر يبدّل تحديدها أيضًا. راجع المعاينة ثم اضغط موافق.",
"dialog.selection_midpoint": "نقطة المنتصف · {id}",
"dialog.selection_profile": "{sketch} · مقطع {number}",
@@ -1424,7 +1392,6 @@
"document.read_only_save": "فُتح هذا التصميم للقراءة فقط: احفظه كملف جديد",
"document.save_changes": "حفظ التغييرات على {name}؟",
"document.save_copy": "حفظ نسخة…",
"document.tab_unsaved_changes": "تغييرات غير محفوظة",
"document.unsaved_changes": "في {name} تغييرات غير محفوظة. إن لم تحفظها فستضيع.",
"drive.rigid": "{joint} مفصل صلب: يثبّت أجزاءه في مكانها، لذا لا يوجد ما يمكن تحريكه. لتتحرك الأجزاء، غيّر نوعه في «تعديل المفصل»: «دوراني» للدوران، «منزلق» للانزلاق، «أسطواني» لكليهما.",
"drive.stopped_by_contact": "توقف عند {value}: سيخترق {a} الجسم {b}",
@@ -1457,14 +1424,6 @@
"file_load.reading_percent": "قراءة الملف... {percent}%",
"file_load.rebuilding": "إعادة بناء الميزة {done} من {total}",
"file_load.recovering": "استعادة التصميم",
"file_load.step_build": "جارٍ بناء النموذج",
"file_load.step_feature": "الميزة {done} من {total}",
"file_load.step_part": "الجزء {done} من {total}",
"file_load.step_percent": "{percent}%",
"file_load.step_read": "جارٍ قراءة الملف",
"file_load.step_rebuild": "جارٍ إعادة بناء الميزات",
"file_load.step_repair": "جارٍ فحص الأجزاء وإصلاحها",
"file_load.step_show": "جارٍ تجهيز العرض",
"file_load.stopping": "انتظار توقف العملية السابقة...",
"file_load.updating_links": "تحديث الروابط",
"file_load.waiting": "انتظار عملية الملف",
@@ -1472,7 +1431,7 @@
"from the profile": "من المقطع",
"from the rule": "من القاعدة",
"help.apache": "رخصة Apache 2.0",
"help.appearance.assign": "يمنح الأجسام أو الأوجه أو المكوّنات المحددة لونًا أو مظهر مادة، من دون تغيير وزنها. استخدمه للتمييز بين القطع أو لمعاينة التشطيب.",
"help.appearance.assign": "يمنح الأجسام أو الأوجه أو المكونات المحددة لونًا أو مظهر مادة، من دون تغيير وزنها. استخدمه للتمييز بين القطع أو لمعاينة التشطيب.",
"help.assembly.bom": "يسرد قطع التصميم أو المكوّن مع أعدادها حسب رقم القطعة، ويمكنه حفظ القائمة بصيغة CSV أو JSON. استخدمه كقائمة قطع للطلب والتجميع.",
"help.attributions": "الإسنادات (ATTRIBUTION.md)",
"help.build": "الإصدار {version} · الإيداع {commit} · بُني في {date}",
@@ -1561,11 +1520,6 @@
"help.joint.reach": "يحرّك نقطة على قطعة نحو هدف بتغيير مفاصل سلسلتها ضمن حدودها. استخدمه لضبط وضعية آلية حسب الموضع المطلوب لنهايتها، مثل طرف ذراع روبوت.",
"help.joint.rigid_group": "يثبّت القطع المحددة معًا كما هي لتتحرك كقطعة واحدة. استخدمه للقطع التي تنتمي إلى وحدة واحدة ولا تتحرك بالنسبة إلى بعضها.",
"help.joint.screw": "يربط الدوران والانزلاق بخطوة لولبية، بحيث تتقدم القطعة بمقدار تلك المسافة في كل دورة. استخدمه للوالب القيادة والبراغي والمحاور الملولبة.",
"help.layout.apply": "تطبيق تخطيط محفوظ على مساحة العمل الحالية. تحتفظ أدوات التحرير القائمة بمسوداتها.",
"help.layout.get": "قراءة التخطيط المعتمد وعرض مساحات العمل المسجلة.",
"help.layout.reset": "استعادة التخطيط الافتراضي المسجل لمساحة العمل الحالية. تبقى إعدادات المستخدم المسبقة متاحة.",
"help.layout.save": "حفظ التخطيط الكامل باسم محدد. يتطلب استبدال إعداد مسبق للمستخدم إذنًا صريحًا بالكتابة فوقه.",
"help.layout.workspace.select": "اختيار مساحة عمل تخطيط مسجلة وتذكر التخطيط السابق.",
"help.library.apply": "يطبّق مادة أو مظهرًا من ملف مكتبة محلي على أجسام أو أوجه أو مكوّنات. استخدمه لإعادة استخدام مواد حفظتها من تصميم آخر.",
"help.library.export": "يحفظ مواد التصميم ومظاهره في ملف مكتبة. استخدمه لمشاركتها أو إعادة استخدامها في تصميمات أخرى.",
"help.library.inspect": "يفحص ملف مكتبة ويسرد موادّه ومظاهره دون تغيير التصميم. استخدمه لمعرفة ما تحويه المكتبة قبل تطبيقها.",
@@ -1579,21 +1533,6 @@
"help.notice": "NOTICE",
"help.occurrence.ground": "يثبّت قطعة في موضعها أو يحررها مجددًا، بحيث تحرّك المفاصل القطع الأخرى حولها. استخدمه للإطار أو القاعدة التي يُبنى عليها باقي التجميع.",
"help.occurrence.move": "يحرّك جزءًا في التجميع فقط ضمن درجات الحرية التي تسمح بها مفاصله. حدد وجهًا للجزء، ثم اسحب سهمًا أو قوسًا متاحًا.",
"help.panel.activate": "اختيار علامة تبويب لوحة مفتوحة دون طلب تركيز لوحة المفاتيح.",
"help.panel.close": "إغلاق اللوحة بعد التحقق من دورة حياة محتواها. يُحفظ موضعها.",
"help.panel.collapse": "طي قسم اللوحة أو توسيعه مع الاحتفاظ بأبعاده المطلوبة.",
"help.panel.dismiss": "إغلاق العرض المؤقت للوحة مطوية.",
"help.panel.float": "نقل لوحة إلى قسم عائم ذي موضع محدد صراحةً.",
"help.panel.focus": "تركيز لوحة وإعادة فتح حالتها المحفوظة وتوسيع قسمها عند الحاجة.",
"help.panel.list": "قراءة أنواع اللوحات المسجلة والمثيلات المعتمدة وعلامات التبويب النشطة والتوافر وإمكانات المضيف.",
"help.panel.move": "نقل لوحة إلى قسم جديد أو ضمها إلى مجموعة علامات تبويب موجودة.",
"help.panel.native_close": "إعادة قسم لوحة من نافذته المستقلة إلى موضع الإرساء المحفوظ دون إلغاء محتواه.",
"help.panel.open": "فتح نوع لوحة بمعرّف مثيل صريح. يتطلب المثيل الجديد تحديد موضعه.",
"help.panel.peek": "إظهار لوحة مطوية مؤقتًا دون تغيير حجمها المحفوظ أو حالة طيها.",
"help.panel.popout": "نقل لوحة إلى نافذة مستقلة عند دعم ذلك. يستخدم مضيف المتصفح قسمًا عائمًا.",
"help.panel.reset_pair": "استعادة نسبة الحجم الافتراضية لقسمي إرساء متجاورين ضمن حدودهما المشتركة.",
"help.panel.resize": "تعيين العرض والارتفاع الخارجيين لقسم اللوحة ضمن حدود جميع علامات التبويب.",
"help.panel.resize_pair": "تغيير حجم قسمي إرساء متجاورين معًا مع الحفاظ على امتدادهما الكلي المشترك.",
"help.parameters.change": "يفتح جدول القيم المسماة والتعبيرات التي تتحكم في أبعاد التصميم. استخدمه لتغيير مقاس في مكان واحد فتتبعه كل الميزات التي تستخدمه.",
"help.parameters.export": "يحفظ معاملات التصميم بصيغة CSV أو JSON. استخدمه لتعديلها في جدول بيانات أو نقلها إلى تصميم آخر.",
"help.parameters.graph": "يبيّن أي المعاملات والميزات تستخدم أيًّا من غيرها، وأي حلقات أو أخطاء بينها. استخدمه لمعرفة ما سيتأثر بتغيير معامل.",
@@ -1779,7 +1718,6 @@
"help.timeline.delete": "يحذف الميزات المحددة من السجل، وحذف رسمٍ يحذف معه الميزات المبنية عليه. استخدمه لإزالة خطوة لم تعد مطلوبة.",
"help.timeline.dependents": "يسرد الميزات التي ستُحذف مع ميزة ما، وتلك التي ستفشل بدونها. استخدمه قبل حذف ميزة أو تغييرها.",
"help.timeline.play": "أعد تشغيل الميزات المتبقية واحدة تلو الأخرى، مع توقف لمدة نصف ثانية بعد كل إعادة بناء. شغّل هذا الأمر مرة أخرى أو اضغط Escape للإيقاف؛ ينتقل «الذهاب إلى النهاية» فورًا إلى النهاية.",
"help.version": "الإصدار {version}",
"help.view.apply": "يدير العرض إلى اتجاه قياسي مثل الأمامي أو العلوي أو الأيزومتري، أو إلى عرض مسمّى محفوظ. استخدمه للنظر إلى النموذج من زاوية ثابتة.",
"help.view.delete": "يحذف عرضًا مسمّى محفوظًا. لا يتغير النموذج.",
"help.view.fit": "يضبط التكبير ويُزيح العرض حتى يملأ النموذج كله أو التحديد منطقة العرض. استخدمه حين يخرج النموذج من مجال الرؤية.",
@@ -1867,27 +1805,6 @@
"home.start_hint": "أنشئ تصميمًا، أو افتح ملفًا حديثًا، أو استكشف مثالًا.",
"home.whats_new": "ما الجديد",
"home.yesterday": "أمس",
"import_report.details": "التفاصيل:",
"import_report.file_one": "كانت هناك مشكلة أخرى واحدة في الملف.",
"import_report.file_other": "كانت هناك {count} مشكلات أخرى في الملف.",
"import_report.hint": "قد يحل ذلك تصدير هذه الأجزاء مرة أخرى من البرنامج الذي أنشأها. يحتوي «نسخ رسالة التحذير» على التفاصيل.",
"import_report.left_out_one": "تعذّر إصلاح وجه واحد وهو مفقود.",
"import_report.left_out_other": "تعذّر إصلاح {count} أوجه وهي مفقودة.",
"import_report.needs_repair_one": "يحتاج جزء واحد إلى إصلاح قبل النمذجة عليه.",
"import_report.needs_repair_other": "تحتاج {count} أجزاء إلى إصلاح قبل النمذجة عليها.",
"import_report.not_imported_one": "تعذّر استيراد جزء واحد.",
"import_report.not_imported_other": "تعذّر استيراد {count} أجزاء.",
"import_report.not_shown_one": "تعذّر رسم وجه واحد وهو مخفي.",
"import_report.not_shown_other": "تعذّر رسم {count} أوجه وهي مخفية.",
"import_report.open_one": "في جزء واحد فجوات بين أوجهه، فاستُورد كجسم مفتوح.",
"import_report.open_other": "في {count} أجزاء فجوات بين أوجهها، فاستُوردت كأجسام مفتوحة.",
"import_report.refused_one": "تعذّر تحويل جزء واحد إلى مجسم، فلم يُستورد.",
"import_report.refused_other": "تعذّر تحويل {count} أجزاء إلى مجسمات، فلم تُستورد.",
"import_report.repaired_one": "أُصلحت عيوب صغيرة في جزء واحد.",
"import_report.repaired_other": "أُصلحت عيوب صغيرة في {count} أجزاء.",
"import_report.solids": "استُورد {solids} من {parts} أجزاء كمجسمات.",
"import_report.surfaces_one": "تعذّر تحويل جزء واحد إلى مجسم، فاستُورد كأسطح.",
"import_report.surfaces_other": "تعذّر تحويل {count} أجزاء إلى مجسمات، فاستُوردت كأسطح.",
"install.button_tip": "ثبّت SolidCraft كتطبيق: يُفتح في نافذته الخاصة ويعمل دون اتصال",
"install.dock_1": "اختر ملف › إضافة إلى Dock.",
"install.dock_2": "انقر إضافة.",
@@ -1912,39 +1829,28 @@
"issue.note_save_failed": "تعذّر حفظ لقطة الشاشة: {error}",
"issue.note_saved": "حُفظت لقطة الشاشة. أرفق ملف PNG في مسودة GitHub.",
"issue.save_screenshot": "حفظ لقطة الشاشة…",
"joint.find_limits": "إيجاد الحدود",
"joint.find_limits_failed": "توقف البحث عن الحدود بسبب خطأ",
"joint.find_limits_hint": "يملأ الحدود من الموضع الذي تلمس فيه القطع التي يحرّكها هذا المفصل قطعةً أخرى لأول مرة، بالدوران أو الانزلاق في الاتجاهين من موضعه الحالي. يجري البحث على نسخة: لا يتغيّر شيء حتى الضغط على OK.",
"joint.find_limits_stale": "تغيّر المفصل أو التصميم منذ إيجاد هذه الحدود: ابحث عنها مرة أخرى",
"joint.finding_limits": "جارٍ إيجاد الحدود…",
"joint.from_bodies_hint": "ينتقل كل جسم مختار إلى مكوّن جديد خاص به ويبقى في مكانه.",
"joint.limit_contact": "{side} {value}: {a} يلمس {b}",
"joint.limit_limit": "{side} {value}: حدّ قيمة أخرى أو مفصل آخر",
"joint.limit_loop": "{side} {value}: لا يمكن لحلقة مغلقة أن تنغلق بعده",
"joint.limit_range": "{side}: لا تلامس ضمن {range}، تُرك كما كان",
"joint.limit_turn": "{side} {value}: لا تلامس ضمن دورة واحدة",
"joint.limits_overlapping": "متداخلة بالفعل هنا: {pairs}. لا توقف البحث إلا إذا ازداد تداخلها.",
"joint.one_body": "كلا الاختيارين على {body}. اختر الثاني على قطعة أخرى.",
"joint.one_component": "هذه القطع أجسام لمكوّن واحد: {bodies}. المفصل يحرّك مكوّنًا مقابل آخر، لذا تحتاج كل قطعة إلى مكوّن خاص بها.",
"joint.rigid_group_one_component": "هذه الأجسام وعددها {count} كلها في مكوّن واحد (أو في التصميم نفسه): استخدم {command} أولًا.",
"joint.rigid_group_select_components": "اختر مكوّنين أو أكثر.",
"joint_move.blocked_hint": "تحقق من نوع المفصل (مثل المفصل الثابت)، وتثبيت الجزء، وحدود المفصل، والقيم التي تتحكم بها المعاملات.",
"joint_move.blocked_title": "لا يمكن تحريك هذا الجزء",
"joint_move.coarse": "الخطوة قرب المحور",
"joint_move.coarse": "خطوة الدوران قرب المحور (°)",
"joint_move.conflict": "يتعارض أحد مفاصل هذا الجزء مع المفاصل الأخرى: {joint}.",
"joint_move.conflict_hint": "عدّل المفصل المتعارض أو عطّله أو احذفه حتى تتمكن المفاصل الأخرى من وضع الجزء.",
"joint_move.distance": "المسافة من نقطة البداية",
"joint_move.done": "تم",
"joint_move.fine": "الخطوة بعيدًا عن المحور",
"joint_move.fine": "خطوة الدوران بعيدًا عن المحور (°)",
"joint_move.fixed": "لا يملك هذا الجزء محور مفصل قابلًا للحركة.",
"joint_move.help": "تحريك — اسحب سهمًا أو قوسًا. يلغى السحب الحالي بمفتاح Esc.",
"joint_move.legacy_hint": "عند الإيقاف: لوحة التحريك السابقة.",
"joint_move.precision": "اسحب خارج الدليل المتقطع للخطوة الدقيقة.",
"joint_move.legacy_hint": "أوقف هذا الخيار لاستخدام لوحة التحريك السابقة.",
"joint_move.precision": "اسحب خارج الدليل المتقطع لضبط الدوران بدقة.",
"joint_move.rotation": "الدوران من نقطة البداية",
"joint_move.select": "اختر وجهًا أو جزءًا واحدًا من التجميع، ثم اختر تحريك.",
"joint_move.settings": "تحريك التجميع",
"joint_move.step": "الخطوة",
"joint_move.use_handles": "استخدام مقابض حركة المفاصل للتحريك",
"joint_move.use_handles": "استخدام مقابض حركة المفاصل لتحريك التجميع",
"keymap.command_id": "معرّف الأمر",
"keymap.empty": "لا توجد أوامر مطابقة",
"keymap.use_anyway": "استخدام {key} هنا على أي حال",
@@ -2004,7 +1910,6 @@
"material.selection.mixed": "مواد مختلفة",
"measure.at": "عند",
"measure.item": "العنصر {number}",
"measure.line_to_centre": "من الخط إلى المركز",
"measure.quick_copied": "تم نسخ القياس",
"measure.quick_copy": "انقر لنسخ القياس",
"measure.quick_edges": "{count} حواف",
@@ -2063,12 +1968,6 @@
"navbar.assistant": "المساعد",
"navbar.custom.add_slot": "إضافة {slot}",
"navbar.custom.always": "ظاهر دائمًا",
"navbar.custom.anchor.bottom_centre": "أسفل الوسط",
"navbar.custom.anchor.bottom_left": "أسفل اليسار",
"navbar.custom.anchor.bottom_right": "أسفل اليمين",
"navbar.custom.anchor.top_centre": "أعلى الوسط",
"navbar.custom.anchor.top_left": "أعلى اليسار",
"navbar.custom.anchor.top_right": "أعلى اليمين",
"navbar.custom.available": "{available} · اسحبها إلى الشريط، أو اضغط إضافة",
"navbar.custom.buttons": "الأزرار",
"navbar.custom.compact": "مضغوط",
@@ -2105,7 +2004,6 @@
"navbar.custom.reset_default": "إعادة الضبط إلى الافتراضي",
"navbar.custom.right": "نقل إلى اليمين",
"navbar.custom.select": "لم يتم تحديد أي خانة. حدّد خانة على الشريط لنقلها أو إزالتها أو تغيير مظهرها.",
"navbar.custom.separator_hint": "فاصل بين مجموعات الأزرار. اسحبه إلى الشريط، أو اضغط على إضافة؛ يمكنك استخدام أي عدد من الفواصل.",
"navbar.custom.separators_full": "يحتوي الشريط بالفعل على أربعة فواصل.",
"navbar.custom.show": "إظهار شريط التنقل",
"navbar.custom.shows": "يعرض",
@@ -2245,50 +2143,20 @@
"prefs.alt_frees_manipulators": "Alt / Option يحرر أسهم أدوات التحريك أيضًا",
"prefs.anti_aliasing_amd_dx12": "يستخدم تنعيم الحواف 1× لأن فحص بدء التشغيل لم يرسم شيئًا مع أخذ العينات المتعددة ونجح عند 1×. يمكنك اختيار إعداد آخر أعلاه؛ أعد التشغيل لتطبيقه إن نجح الفحص.",
"prefs.anti_aliasing_note": "ينعّم الحواف المسننة.",
"prefs.appearance.colour_vision_help": "يغيّر الألوان ذات الدلالة: حالات الرسم والمحاور والثنيات والتحليلات والأيقونات.",
"prefs.appearance.desktop_only": "لتطبيق سطح المكتب فقط",
"prefs.appearance.follow_help": "يعيد التحميل عندما تعيد أداة أخرى كتابة الملف.",
"prefs.appearance.interface_scale": "مقياس الواجهة",
"prefs.appearance.next_themes": "السمات التالية",
"prefs.appearance.option_count": "{count} خيارات",
"prefs.appearance.option_count_one": "{count} خيار",
"prefs.appearance.previous_themes": "السمات السابقة",
"prefs.appearance.theme_count": "{count} سمات",
"prefs.appearance.theme_own": "ألوان السمة",
"prefs.appearance.viewport_colours": "ألوان العرض ثلاثي الأبعاد",
"prefs.appearance.viewport_logo": "منطقة العرض والشعار",
"prefs.applies_on_restart": "يُطبَّق عند إعادة التشغيل",
"prefs.assistant": "المساعد",
"prefs.assistant_connecting": "الاتصال",
"prefs.assistant_follow_help": "النظر إلى الرسومات يحرّك العرض مع المؤشر.",
"prefs.assistant_limit": "الحد",
"prefs.assistant_limited": "محدود",
"prefs.assistant_pointer_speed": "سرعة المؤشر",
"prefs.assistant_seconds": "ثوانٍ",
"prefs.assistant_seconds_aria": "الثواني",
"prefs.assistant_use_care": "استخدم بحذر",
"prefs.auto_operation": "اختيار العملية تلقائيًا",
"prefs.auto_operation_note": "ضم أو جسم جديد أو قص، حسب ما تلامسه المادة.",
"prefs.auto_operation_note": "تختار نوافذ الميزات «ضم» أو «جسم جديد» أو «قص» بحسب ما تلامسه المادة الجديدة، إلى أن تختار عملية بنفسك. عند الإيقاف: تحتفظ بالعملية التي تُفتح بها.",
"prefs.axes": "المحاور",
"prefs.axis": "المحور",
"prefs.blur_cost": "تكلفة العرض النسبية. زيادة الأشرطة تعني عادةً مزيدًا من العمل.",
"prefs.blur_device": "غير متاح على جهاز الرسوميات أو مسار العرض هذا",
"prefs.blur_guard": "خُفّضت جودة التمويه للحفاظ على استجابة الإطارات. لم يتغير نصف القطر.",
"prefs.blur_off": "إيقاف",
"prefs.blur_reduce_transparency": "معطّل بسبب إعداد تقليل الشفافية في النظام",
"prefs.blur_software": "غير متاح مع الرسوميات البرمجية (gpu-software)",
"prefs.blur_source": "غير متاح في عرض مساحة العمل هذا",
"prefs.blur_web": "غير متاح في مسار عرض الويب هذا",
"prefs.bodies_folders": "مجلدات الأجسام",
"prefs.bodies_folders_always": "دائمًا",
"prefs.bodies_folders_never": "أبدًا",
"prefs.bodies_folders_several": "عند التعدد",
"prefs.bodies_folders_several": "عند تعدد الأجسام فقط",
"prefs.bottom": "أسفل",
"prefs.bottom_centre": "أسفل الوسط",
"prefs.bottom_right": "أسفل اليمين",
"prefs.browser": "المستعرض",
"prefs.browser_components_open": "المكوّنات مفتوحة من البداية",
"prefs.browser_components_open_note": "يبدأ كل مكوّن في المستعرض مفتوحًا كما كان من قبل، بدلًا من أن يبقى مغلقًا حتى تفتحه.",
"prefs.browser_eye_left": "أيقونة الإظهار بجانب سهم الطي",
"prefs.browser_eye_left_note": "تعرض صفوف المستعرض أيقونة الإظهار/الإخفاء بجانب سهم الطي كما كانت من قبل، بدلًا من عمود عند نهاية الصفوف.",
"prefs.browser_reveal_on_hover": "إظهار عند مرور المؤشر",
@@ -2318,32 +2186,14 @@
"prefs.dark_parts_shading_help": "تظليل العرض بإضاءة خطية مع إضاءة سماء ناعمة، لتحتفظ الأوجه الداكنة بتدرج بدل أن تصبح سوداء",
"prefs.dead_zone_note": "الشريط المخطط هو المنطقة الميتة: تُتجاهل الحركة داخلها.",
"prefs.default": "افتراضي",
"prefs.design": "التصميم",
"prefs.design_note": "ما تبدأ به التصاميم الجديدة.",
"prefs.design_tools.ask": "اسأل",
"prefs.design_tools.degrees": "درجات",
"prefs.design_tools.double_click_help": "أو لتحرير مكوّن مرتبط في موضعه.",
"prefs.design_tools.ghost": "إظهار الموضع القديم للأجزاء المنقولة كشبح",
"prefs.design_tools.hole_moves_help": "اختيار الوجه مجددًا ينقل الثقب بدلًا من إزالته.",
"prefs.design_tools.hole_snaps_help": "منتصف الوجه ومراكز حوافه الدائرية.",
"prefs.design_tools.imported_files": "الملفات المستوردة",
"prefs.design_tools.note": "ملاحظة",
"prefs.design_tools.off": "إيقاف",
"prefs.design_tools.placement_group": "مسند، نتوء، تعشيق مرن",
"prefs.design_tools.rows": "صفوف",
"prefs.design": "افتراضيات التصميم",
"prefs.design_note": "ما تبدأ به التصاميم الجديدة. التصاميم المفتوحة تحتفظ بإعداداتها.",
"prefs.devices": "الفأرة ثلاثية الأبعاد",
"prefs.dialog_buttons": "أزرار مربعات الحوار",
"prefs.dialogs": "الحوارات",
"prefs.dialogs_edge": "عند حافة العرض",
"prefs.dialogs_left_column": "في العمود الأيسر",
"prefs.dialogs_note": "مكان فتح حوارات الأوامر: مثبتة عند حافة العرض، أو في العمود الأيسر مكان المستعرض والسجل، اللذين يعودان كما كانا عند إغلاق الحوار.",
"prefs.dimension_step_angle": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الزاوية",
"prefs.dimension_step_length": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الطول",
"prefs.dimension_steps": "خطوات النافذة",
"prefs.dimension_values": "قيم الأبعاد",
"prefs.dimension_values_note": "مكان كتابة قيمة البعد. للنافذة أسهم تغيّرها بخطوات؛ تضع أداة البعد النص أولًا.",
"prefs.dimension_values_view": "في العرض",
"prefs.dimension_values_window": "في نافذة",
"prefs.display": "العرض",
"prefs.edges": "الحواف والتظليل",
"prefs.extend": "توسيع",
@@ -2359,7 +2209,7 @@
"prefs.general.active": "نشط",
"prefs.general.crash_reports": "تقارير الأعطال",
"prefs.general.edited": "معدّل",
"prefs.general.held": "المشمول",
"prefs.general.held": "المحفوظ",
"prefs.general.import": "استيراد...",
"prefs.general.last_check": "آخر تحقق: {time}",
"prefs.general.minutes": "دقائق",
@@ -2367,7 +2217,7 @@
"prefs.general.profile_name": "اسم ملف التعريف",
"prefs.general.save_new": "حفظ كجديد",
"prefs.general.step_import": "STEP والاستيراد",
"prefs.general.this_computer": "هذا الحاسوب",
"prefs.general.this_computer": "هذا الكمبيوتر",
"prefs.general_note": "ملفات التعريف واللغة والملفات والتحديثات.",
"prefs.gesture_orbit": "تدوير العرض",
"prefs.gesture_pan": "إزاحة العرض",
@@ -2379,23 +2229,6 @@
"prefs.graphics_card_restart": "يُطبَّق بعد إعادة تشغيل SolidCraft. يتجاوز WGPU_ADAPTER_NAME هذا الاختيار.",
"prefs.graphics_card_unused": "لم تُستخدم بطاقة الرسوميات المحفوظة لهذه النافذة؛ البطاقة المستخدمة تظهر في «المساعدة › حول» وفي شريط الحالة. جرّب «تلقائي»، أو عيّن WGPU_ADAPTER_NAME إلى جزء فريد من اسم البطاقة.",
"prefs.graphics_note": "وازن بين السرعة والنعومة. كل خيار يعرض أثره على عيّنة.",
"prefs.graphics_page.current_view": "العرض الحالي",
"prefs.graphics_page.fastest": "الأسرع",
"prefs.graphics_page.fold_items": "تلخيص العناصر",
"prefs.graphics_page.in_view": "داخل العرض",
"prefs.graphics_page.live_details": "تفاصيل مباشرة",
"prefs.graphics_page.own_mix": "مزيجك الخاص",
"prefs.graphics_page.quick_measure": "قياس سريع",
"prefs.graphics_page.reference_geometry_through_bodies": "الهندسة المرجعية عبر الأجسام",
"prefs.graphics_page.restart": "تلزم إعادة التشغيل",
"prefs.graphics_page.sharpest": "الأكثر وضوحًا",
"prefs.graphics_page.single_surface_analysis": "تحليل سطح واحد",
"prefs.graphics_page.status_chips": "شارات الحالة",
"prefs.graphics_page.subtitle": "وازن بين السرعة والنعومة.",
"prefs.graphics_page.supersampling": "التنعيم الفائق",
"prefs.graphics_page.supersampling_adaptive": "فقط أثناء سكون العرض",
"prefs.graphics_page.surfaces_edges": "الأسطح والحواف",
"prefs.graphics_page.view_bar": "أيضًا في شريط العرض",
"prefs.graphics_presets_note": "الإعدادات المسبقة تضبط الخيارات أدناه معًا.",
"prefs.hide_constraint_glyphs_zoomed_out": "إخفاء رموز القيود عند التصغير",
"prefs.hide_constraint_glyphs_zoomed_out_note": "تختفي رموز القيود من العرض عندما يُصغَّر الرسم بقدر يجعلها تزحمه. معطّل: تبقى بحجم ثابت على الشاشة عند أي مستوى تكبير.",
@@ -2404,7 +2237,7 @@
"prefs.history_origin_bodies_note": "يبدأ السجل على اليسار أو تحت المستعرض بمجلد الأجسام للمكوّن النشط وبالأصل، مع أيقونات الإظهار والقوائم كما في المستعرض. لا يتغير السجل في الأسفل.",
"prefs.history_play_button": "زر تشغيل السجل في عناصر تحكم السجل",
"prefs.history_play_button_note": "يضيف تشغيل السجل بين الرجوع خطوة والتقدم خطوة في عناصر تحكم السجل. يمكنك أيضًا النقر بزر الماوس الأيمن على عناصر التحكم لإظهاره أو إخفائه.",
"prefs.history_sketches": "الرسومات في السجل",
"prefs.history_sketches": "الرسومات",
"prefs.history_sketches_in_order": "بالترتيب",
"prefs.history_sketches_note": "عند ضبط الرسومات على «تحت ميزتها»، يعرض السجل على اليسار كل رسم بمستوى واحد للداخل، تحت أول ميزة بُنيت منه، متى كان عريضًا بما يكفي لإظهار الأسماء. أما الخط الزمني في الأسفل فيحتفظ دائمًا بترتيب الإنشاء.",
"prefs.history_sketches_under_feature": "تحت ميزتها",
@@ -2429,22 +2262,6 @@
"prefs.language_system": "لغة النظام",
"prefs.layout": "التخطيط",
"prefs.layout_note": "المضغوط يثبّت اللوحات على حواف النافذة. شفافية المستعرض متاحة في الوضع العائم. غيّر جهة المساعد من صفحة المساعد.",
"prefs.layout_page.active_commands": "تمييز الأوامر النشطة",
"prefs.layout_page.always": "دائمًا",
"prefs.layout_page.card": "بطاقة",
"prefs.layout_page.column": "عمود",
"prefs.layout_page.hover": "عند مرور المؤشر",
"prefs.layout_page.icons_labels": "الأيقونات + التسميات",
"prefs.layout_page.ok_cancel": "موافق وإلغاء",
"prefs.layout_page.on_hover": "عند مرور المؤشر",
"prefs.layout_page.open": "تُفتح",
"prefs.layout_page.origin_bodies": "الأصل والأجسام في الأعلى",
"prefs.layout_page.panels": "اللوحات",
"prefs.layout_page.pinned_menus": "استبعاد الأوامر المثبتة من القوائم",
"prefs.layout_page.play_history": "زر تشغيل السجل",
"prefs.layout_page.slot_count": "{count} من {max} خانة",
"prefs.layout_page.view_corner": "أيضًا في زاوية العرض",
"prefs.layout_page.window": "النافذة",
"prefs.left": "يسار",
"prefs.left_click": "نقر أيسر",
"prefs.line_drag_draws_one": "الضغط والسحب يرسم خطًا واحدًا",
@@ -2473,13 +2290,6 @@
"prefs.mouse_heading": "الفأرة",
"prefs.mouse_mapping": "تعيين أزرار الفأرة",
"prefs.navigation": "التنقل",
"prefs.navigation.axes_signed": "المحاور",
"prefs.navigation.cube_compact": "المكعب",
"prefs.navigation.device": "الجهاز",
"prefs.navigation.free": "حر",
"prefs.navigation.orbit_style": "نمط التدوير",
"prefs.navigation.radial_help": "اضغط مطولًا أو انقر سريعًا بالزر الأيمن لعرض حلقة من الأدوات.",
"prefs.navigation.turntable": "طاولة دوارة",
"prefs.navigation_note": "كيف تحرك الفأرة ولوحة اللمس والفأرة ثلاثية الأبعاد العرض.",
"prefs.navigator_fit": "ملاءمة النموذج بعد تغيير العرض من متصفح العرض",
"prefs.navigator_fit_note": "النقر على وجه أو ركن أو سهم في المكعب، أو على محور في أداة المحاور، يوجّه العرض أيضًا نحو منتصف النموذج من المسافة الواحدة التي تُظهره كاملًا من كل جهة (كرته المحيطة). يستخدم العرض الرئيسي التكبير والمركز ذاتهما ما دام هذا مفعّلًا، فلا تغيّر العروض المختارة بعده سوى الاتجاه. عند الإيقاف: يدور العرض في مكانه محتفظًا بالهدف والتكبير.",
@@ -2496,15 +2306,11 @@
"prefs.origin_plane_names_note": "الأسماء المعروضة في المستعرض والحوارات والقوائم. {top} هو XY و{front} هو XZ و{right} هو YZ، والمحور Z للأعلى. تحتفظ الأوامر والسكربتات والتصاميم المحفوظة بـ XY وXZ وYZ.",
"prefs.overall_speed": "السرعة العامة",
"prefs.parameter_list_rows": "صفوف قائمة المعاملات",
"prefs.parameter_list_rows_tip": "الحد الأقصى للمعاملات التي يسردها السهم لأسفل في مربع تعبير فارغ.",
"prefs.parameter_list_rows_tip": "يعرض السهم لأسفل في حقل قيمة فارغ المعاملات، المفضلة أولًا، حتى هذا العدد. العدد الأقل يبقي القائمة خفيفة في التصميم الكبير.",
"prefs.persistent_ribbon_pressed": "تمييز أوامر الشريط النشطة",
"prefs.persistent_ribbon_pressed_help": "تمييز أوامر الشريط أثناء تفعيل تأثيراتها. أوقف هذا الخيار للاحتفاظ بالمظهر المحايد السابق ومساعدة الأوامر.",
"prefs.perspective_fov": "مجال الرؤية",
"prefs.placement": "الموضع",
"prefs.plugins_enabled": "مفعّل",
"prefs.plugins_installed": "المثبّتة",
"prefs.plugins_intro": "إضافات معزولة.",
"prefs.plugins_show_folder": "إظهار مجلد الإضافات",
"prefs.prefer_free_sketch_points": "تفضيل نقاط الرسم القابلة للتحريك عند تداخل النقاط",
"prefs.preview_choices": "معاينة اختياراتك",
"prefs.preview_tint_new_faces": "تلوين السطح الجديد في المعاينة",
@@ -2519,8 +2325,6 @@
"prefs.profile_exported": "تم تصدير ملف التعريف.",
"prefs.profile_imported": "تم استيراد ملف التعريف.",
"prefs.profile_name_hint": "اسم ملف التعريف، مثل: حاسوب محمول",
"prefs.profile_operations": "تحليل زمن العمليات",
"prefs.profile_operations_note": "يسجّل المدة التي تستغرقها استدعاءات SMLib وعمليات النواة والميزات والأوامر وعمليات الاستيراد. يقرؤه الوكلاء عبر diagnostics.profile.",
"prefs.profile_rename": "إعادة تسمية",
"prefs.profile_renamed": "تمت إعادة تسمية ملف التعريف.",
"prefs.profile_save": "حفظ",
@@ -2550,9 +2354,6 @@
"prefs.scale_text": "الحجم والنص",
"prefs.search": "البحث في الإعدادات",
"prefs.search_empty": "لا توجد إعدادات مطابقة",
"prefs.section_hatch": "تهشير المقطع",
"prefs.section_hatch_by_part": "حسب الجزء",
"prefs.section_hatch_one": "نمط واحد",
"prefs.select": "تحديد",
"prefs.select_one_item": "تحديد عنصر واحد",
"prefs.select_other_hidden": "تحديد العناصر الهندسية المخفية باستخدام «تحديد عنصر آخر»",
@@ -2562,43 +2363,8 @@
"prefs.shift_add_ctrl_toggle": "Shift يضيف فقط، وCtrl / Cmd يبدّل",
"prefs.shift_add_note": "عند التشغيل: Shift يضيف، وCtrl / Cmd يبدّل. عند الإيقاف (الافتراضي): كلاهما يبدّل.",
"prefs.shortcuts": "الاختصارات",
"prefs.shortcuts_import": "استيراد",
"prefs.shortcuts_intro": "مفاتيح كل أمر.",
"prefs.shortcuts_not_assigned": "غير معيّن",
"prefs.shortcuts_press": "اضغط مفتاحًا...",
"prefs.single_surface_analysis": "استخدام تحليل سطح واحد",
"prefs.single_surface_analysis_help": "عند إيقاف هذا الخيار، يمكن إبقاء تظليل السطح وتحليل الاستمرارية مفعّلين معًا. عند تشغيله، يبقى آخر تحليل تم بدؤه أو تغييره فقط.",
"prefs.sketch.align_points": "المحاذاة مع نقاط الرسم الأخرى",
"prefs.sketch.align_points_note": "تتحاذى الخطوط أثناء الرسم مع النقاط على المحورين معًا.",
"prefs.sketch.auto_dimensions": "إضافة كل الأبعاد للأشكال الجديدة",
"prefs.sketch.auto_dimensions_note": "القيم المدخلة وكل بُعد يثبّت الشكل.",
"prefs.sketch.auto_locate": "تحديد الموضع من أصل الرسم",
"prefs.sketch.auto_locate_note": "مع إضافة كل الأبعاد.",
"prefs.sketch.center_arc": "القوس بنقطة مركز يتبع المؤشر",
"prefs.sketch.click_selects": "النقر يحدد",
"prefs.sketch.conflict_colour": "لون التعارض",
"prefs.sketch.drawing": "الرسم",
"prefs.sketch.exit_keep": "إبقاء",
"prefs.sketch.exit_previous": "العودة إلى العرض السابق",
"prefs.sketch.exit_unless_rotated": "إلا إذا تم التدوير",
"prefs.sketch.faces_edges": "الأوجه والحواف",
"prefs.sketch.hide_glyphs": "إخفاء الرموز عند التصغير",
"prefs.sketch.marks": "العلامات في الرسم",
"prefs.sketch.note": "الرسم والالتقاط والتحديد.",
"prefs.sketch.opening_closing": "الفتح والإغلاق",
"prefs.sketch.prefer_free_points": "تفضيل النقاط القابلة للتحريك عند تداخلها",
"prefs.sketch.project_options": "السؤال عن خيارات الإسقاط أولًا",
"prefs.sketch.recent_snap": "الالتقاط عند تقاطع دليلين",
"prefs.sketch.select_other_hidden": "تحديد آخر يشمل الهندسة المخفية",
"prefs.sketch.shortcut_cycles_modes": "الاختصار مجددًا يبدّل الوضع",
"prefs.sketch.shortcut_cycles_modes_tip": "عند الإيقاف: تبدأ الأداة مجددًا.",
"prefs.sketch.snapping_guides": "الالتقاط والأدلة",
"prefs.sketch.state_icons": "أيقونات الحالة",
"prefs.sketch.tracking_guides": "أدلة المراكز ونقاط المنتصف",
"prefs.sketch.tracking_guides_note": "أدلة أفقية وعمودية مؤقتة من المراكز ونقاط منتصف الحواف عند التحويم.",
"prefs.sketch.tracking_points": "أدلة من نقاط الرسم أيضًا",
"prefs.sketch.unless_turned_note": "العودة إذا لم يتم التدوير",
"prefs.sketch.whole_bodies": "الأجسام كاملة",
"prefs.sketch_align_points": "المحاذاة مع نقاط الرسم الأخرى أثناء الرسم",
"prefs.sketch_align_points_note": "أثناء الرسم، تتحاذى النقطة أفقيًا مع نقطة رسم وعموديًا مع نقطة أخرى في الوقت نفسه، مع خط إرشادي منقط، وتحتفظ النقرة بالمحاذاتين. عند الإيقاف: مع النقطة السابقة فقط، محور واحد في كل مرة.",
"prefs.sketch_conflict_colour": "الهندسة المتعارضة بلون التعارض",
@@ -2631,11 +2397,11 @@
"prefs.source": "المصدر",
"prefs.speed": "السرعة",
"prefs.stand_up_note": "بعد فتح ملف STEP أو IGES ومحوره Z للأعلى، يُعرض تدويره ربع دورة حول X.",
"prefs.stand_up_unknown": "ملفات من برامج غير معروفة",
"prefs.stand_up_unknown": "للملفات من برامج غير معروفة",
"prefs.stand_up_unknown.ask": "السؤال في مربع حوار",
"prefs.stand_up_unknown.note": "عرض ملاحظة",
"prefs.stand_up_unknown.off": "عدم العرض",
"prefs.stand_up_unknown_note": "كيفية عرض الاقتراح عندما لا يذكر الملف برنامجًا معروفًا.",
"prefs.stand_up_unknown_note": "طريقة العرض عندما لا يذكر رأس الملف برنامجًا معروفًا بحفظ المحور Y للأعلى. تظهر الملاحظة فقط عندما يبدو أن الملف ملقى على ظهره؛ أما «السؤال في مربع حوار» فيسأل عن كل ملف كهذا، كما كان سابقًا.",
"prefs.status_bar": "شريط الحالة",
"prefs.status_bar_note": "شريط رفيع على الحافة السفلية للنافذة يعرض ما يجري تحريره، وحالة الرسم المفتوح بلونها، ووحدات التصميم؛ النقر على الوحدات يغيّرها. عندها لا تظهر حالة الرسم في زاوية العرض.",
"prefs.status_done": "تم",
@@ -2671,10 +2437,6 @@
"prefs.theme_follow_off": "إيقاف",
"prefs.theme_follow_stop": "إيقاف الاتباع",
"prefs.theme_import_hint": "انقر لاختيار ملف سمة، أو أفلت ملفًا هنا، أو اضغط Ctrl+V (Cmd+V على Mac) بعد نسخ سمة",
"prefs.theme_notice_dismiss": "إخفاء",
"prefs.theme_option_blur": "تمويه خلف اللوحات",
"prefs.theme_option_blur_help": "تمويه خلف اللوحات شبه الشفافة، بالبكسل الفعلي. الصفر يوقف التمويه.",
"prefs.theme_option_invalid": "القيمة المحفوظة للخيار {option} غير صالحة؛ تُستخدم القيمة الافتراضية للسمة. تبقى القيمة المحفوظة كما هي.",
"prefs.theme_option_opacity": "العتامة",
"prefs.theme_option_opacity_help": "اضبط عتامة اللوحات العائمة. يُطبّق حد أدنى للعتامة للحفاظ على وضوح النص فوق ألوان منطقة العرض.",
"prefs.theme_option_tab_colours": "ألوان علامات التبويب",
@@ -2683,7 +2445,6 @@
"prefs.theme_option_tab_fade_ms_help": "تتغيّر ألوان اللوحات ومنطقة العرض تدريجيًا عند تبديل علامات التبويب. القيمة صفر تبدّلها فورًا.",
"prefs.theme_options_badge": "الخيارات",
"prefs.theme_options_more": "المزيد · {count}",
"prefs.theme_options_newer": "تتطلب هذه السمة إصدارًا أحدث من SolidCraft. بعد الترقية، أعد استيراد السمة الأصلية لاستعادة تعريفات خياراتها.",
"prefs.theme_options_reset": "إعادة تعيين",
"prefs.theme_options_unavailable": "غير متاح على هذا الجهاز",
"prefs.theme_paste": "لصق السمة",
@@ -2707,14 +2468,8 @@
"prefs.under_cursor": "تحت المؤشر",
"prefs.units_precision": "الوحدات والدقة",
"prefs.updates": "التحديثات",
"prefs.validation": "التحقق من الأجسام",
"prefs.validation.each_feature": "مرة لكل ميزة",
"prefs.validation.each_step": "بعد كل خطوة",
"prefs.validation.label": "فحص النواة الكامل",
"prefs.validation.off": "متوقف (تجريبي)",
"prefs.validation_note": "بعد كل خطوة: يفحص كل عملية كنس وعملية منطقية وتقويس.\nمرة لكل ميزة: يفحص ناتج كل ميزة مرة واحدة.\nمتوقف (تجريبي): لا فحص.",
"prefs.value_boxes_in_view": "إظهار القيم بجانب مقابضها",
"prefs.value_boxes_in_view_tip": "مسافة البثق ونصف قطر التدوير... في مربع داخل العرض.",
"prefs.value_boxes_in_view": "إظهار القيم بجانب مقابضها في العرض",
"prefs.value_boxes_in_view_tip": "قيمة الأمر (المسافة، نصف القطر…) في مربع بجانب مقبضه، فوق النموذج. عند الإيقاف: تكتبها في نافذة الأمر، التي تتلقى لوحة المفاتيح بعد التحديد، ولا شيء يغطي النموذج.",
"prefs.view": "سلوك العرض",
"prefs.view_confirm_corner": "موافق وإلغاء في زاوية العرض",
"prefs.view_confirm_corner_note": "أثناء فتح مربع حوار أو رسم، تؤدي علامة الصح وعلامة الإكس أعلى يمين العرض ما يؤديه زرّا موافق وإلغاء في مربع الحوار، أو تنهي الرسم وتلغيه. وينتقل متصفح العرض إلى أسفلهما.",
@@ -2747,17 +2502,6 @@
"recovery.just_now": "الآن",
"recovery.min_ago": "قبل {count} دقيقة",
"recovery.never_saved": "لم يُحفظ قط",
"refused.change": "تعذّر تغيير {body}.",
"refused.cut": "تعذّر قطع {body}.",
"refused.face_curved": "أحد أوجهه المنحنية لا يمكن استخدامه في النمذجة.",
"refused.face_flat": "أحد أوجهه المستوية لا يمكن استخدامه في النمذجة.",
"refused.face_freeform": "أحد أوجهه الحرة لا يمكن استخدامه في النمذجة.",
"refused.face_rounded": "أحد أوجهه المستديرة لا يمكن استخدامه في النمذجة.",
"refused.faces_apart": "أوجهه لا تلتقي بدقة كافية لاستخدامها في النمذجة.",
"refused.intersect": "تعذّر التقاطع مع {body}.",
"refused.join": "تعذّر الضم إلى {body}.",
"refused.repair": "إصلاح الجزء",
"refused.show_face": "إظهار الوجه",
"render.adaptive_absolute": "الحد الأدنى المطلق لخطأ RGB",
"render.adaptive_coverage": "هدف خطأ التغطية",
"render.adaptive_enable": "توزيع العيّنات باستخدام عيّنات استكشافية مستقلة",
@@ -4115,11 +3859,11 @@
"select two components": "حدد مكوّنين اثنين",
"set by the sketch's lines": "تحدده خطوط الرسم",
"settings.display_quality_note": "المستويات الأدق ترسم الأوجه والحواف المنحنية بنعومة أكبر وتستغرق وقتًا أطول لإنشاء الشبكة. يتغير العرض فقط، لا التصميم ولا الملفات المصدّرة.",
"settings.double_click_components": "النقر المزدوج على مكوّن لتنشيطه",
"settings.double_click_components": "انقر نقرًا مزدوجًا على مكوّن لتنشيطه، أو لتحرير مكوّن مرتبط في مكانه",
"settings.frame_rate_details_note": "يضيف رسمًا لأزمنة الإطارات الأخيرة مع المثلثات المرسومة فوقه، ومدرّجًا تكراريًا لأزمنة الإطارات، وما رسمته وحدة GPU. انقر معدل الإطارات في العرض لإظهارها أو إخفائها.",
"settings.frame_rate_note": "يعرض الإطارات في الثانية في الزاوية اليمنى السفلية من العرض. أثناء تفعيله يُعاد رسم العرض باستمرار.",
"settings.insert_lists_open_designs": "إدراج تصميم يسرد التصاميم المفتوحة أولًا",
"settings.measure_axis_distances": "مسافات المحاور في القياس",
"settings.insert_lists_open_designs": "يعرض إدراج تصميم أولًا التصاميم المفتوحة في علامات التبويب الأخرى",
"settings.measure_axis_distances": "إظهار المسافات على X/Y/Z",
"settings.measure_axis_distances_help": "إظهار مركّبات الأبعاد على محاور العالم بجانب المسافة الكلية أثناء فتح أداة القياس.",
"settings.msaa_applies_on_restart": "يُطبّق تنعيم الحواف في المرة القادمة التي يبدأ فيها SolidCraft",
"settings.msaa_reset": "{setting}: القيمة {requested}× غير مدعومة في جهاز الرسوميات هذا؛ أصبحت الآن {reset}× (المدعوم: {supported}).",
@@ -4129,7 +3873,7 @@
"settings.reset_intro": "هذه الإعدادات لا تعمل على هذا الحاسوب وكانت ستمنع SolidCraft من البدء، لذا غُيّرت:",
"settings.reset_title": "تم تغيير الإعدادات",
"settings.supersampling_note": "يرسم العرض بدقة أعلى ثم يصفّيه: حواف وخطوط وشبكة أنعم على الشاشات الأقل من 4K. يكلّف وقتًا من معالج الرسوميات: نحو 2.25× من البكسلات عند 1.5× و4× عند 2×.",
"settings.update_changed_links": "تحديث الروابط المتغيرة عند العودة إلى علامة تبويب",
"settings.update_changed_links": "تحديث التصاميم المرتبطة التي تغيّرت عند العودة إلى علامة التبويب",
"settings.window_system_details": "التفاصيل",
"settings.window_system_note_now": "يُطبَّق بعد إعادة التشغيل. الحالي: {now}.",
"settings.window_system_wayland_limited": "بعض الميزات محدودة على Wayland الأصلي.",
@@ -4225,20 +3969,11 @@
"the section's profiles must be in one sketch": "يجب أن تكون مقاطع المقطع العرضي في رسم واحد",
"the two origins are on the same component": "يقع الأصلان على المكوّن نفسه",
"thread.automatic_size": "حجم تلقائي",
"thread.chamfer_depth_needed": "مطلوب لهذا الشكل",
"thread.designation": "التسمية",
"thread.drill_range": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_custom": "المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_taper": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.end_square": "قائمة",
"thread.fade_short": "1.25 P (ISO 3508)",
"thread.fade_standard": "2.5 P (ISO 3508)",
"thread.fits_the_face": "يناسب الوجه، مثلًا {example}",
"thread.form_bspp": "سن أنابيب G (ISO 228-1)",
"thread.form_bspt": "سن أنابيب مخروطي R، Rc (ISO 7-1)",
"thread.form_npt": "سن أنابيب مخروطي NPT (ASME B1.20.1)",
"thread.form_rp": "سن أنابيب داخلي متوازٍ Rp (ISO 7-1)",
"thread.direction": "اتجاه السن اللولبي",
"thread.iso_metric": "متري ISO",
"thread.left_hand": "يساري",
"thread.right_hand": "يميني",
"thread.select_size": "اختر الحجم",
"timeline.delete_after_marker_one": "حذف ميزة واحدة بعد مؤشر السجل",
"timeline.delete_after_marker_other": "حذف {count} ميزات بعد مؤشر السجل",
@@ -4490,7 +4225,6 @@
"ui.agent_chat_no_images": "هذا الوكيل لا يقبل الصور",
"ui.agent_chat_no_picture": "لا توجد صورة في الحافظة",
"ui.agent_chat_no_tool_call": "لم يستخدم النموذج أدوات SolidCraft، لذا لم يتغيّر التصميم. اختر نموذجًا يدعم استدعاء الأدوات؛ راجع «النماذج المحلية» في دليل محادثة الوكيل.",
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط تحديث.",
"ui.agent_chat_none_found": "لم يُعثر على وكيل برمجي. ثبّت أحد هذه ثم اضغط تحديث:",
"ui.agent_chat_none_found_short": "لم يُعثر على وكيل",
"ui.agent_chat_not_installed": "غير مثبت",
@@ -4531,7 +4265,7 @@
"ui.agent_chat_send_now_tip": "إيقاف خطوة الوكيل الحالية وإرسال هذه بعدها (Ctrl+Enter)",
"ui.agent_chat_setup_body": "يتواصل SolidCraft مع وكيل برمجة مثبّت على هذا الحاسوب. اختر واحدًا من التفضيلات › الوكيل.",
"ui.agent_chat_setup_title": "إعداد مساعد",
"ui.agent_chat_side": "تثبيت المحادثة",
"ui.agent_chat_side": "جانب محادثة الوكيل",
"ui.agent_chat_signed_in": "سجّل الدخول",
"ui.agent_chat_state_auth": "يلزم تسجيل الدخول",
"ui.agent_chat_state_busy": "جارٍ العمل…",
@@ -4596,14 +4330,14 @@
"ui.ai_key_note": "يُخزّن في سلسلة مفاتيح النظام، وليس في إعدادات SolidCraft أبدًا. اتركه فارغًا لخادم محلي.",
"ui.ai_keychain_error": "رفضت سلسلة مفاتيح النظام: {error}",
"ui.ai_kind": "النوع",
"ui.ai_mode_all": "تنفيذ كل شيء",
"ui.ai_mode_all": "السماح بكل شيء",
"ui.ai_mode_all_note": "لا شيء يسأل أولًا. تبقى تقارير الأعطال محظورة وتظل حدود المجلدات سارية.",
"ui.ai_mode_all_warning": "يمكن للوكيل حفظ تصميمك والكتابة فوقه وتصديره واستبداله دون سؤال. استخدم هذا فقط مع وكيل تثق به، وعلى تصاميم حفظتها.",
"ui.ai_mode_ask": "السؤال أولًا",
"ui.ai_mode_ask": "السؤال في كل مرة",
"ui.ai_mode_ask_note": "كل تغيير في التصميم وكل إجراء على الملفات ينتظر سماحك.",
"ui.ai_mode_edits": "إجراء تعديلات",
"ui.ai_mode_edits": "السماح تلقائيًا بتعديلات النموذج",
"ui.ai_mode_edits_note": "أوامر النمذجة تعمل دون سؤال؛ أما الحفظ والفتح والتصدير وإجراءات الملفات أو الإجراءات الهدّامة الأخرى فتسأل.",
"ui.ai_mode_global": "كما في الأعلى",
"ui.ai_mode_global": "كما هو مضبوط أعلاه",
"ui.ai_model": "النموذج",
"ui.ai_model_hint": "qwen3:14b",
"ui.ai_modes_scope": "هذه الإعدادات تغطي أدوات SolidCraft. أدوات الوكيل الخاصة (الصدفة وتعديل الملفات) تتبع وضع الوكيل الخاص، الذي تختاره في رأس لوحة المحادثة عندما تكون للوكيل أوضاع.",
@@ -4629,7 +4363,7 @@
"ui.ai_save": "حفظ",
"ui.ai_settings": "إعدادات الذكاء الاصطناعي…",
"ui.ai_show_features": "إظهار ميزات الذكاء الاصطناعي",
"ui.ai_show_features_note": "إيقافه يخفي المساعد في كل مكان ولا يبدأ أي شيء. تصبح كل الخيارات أدناه باهتة.\n\nمحادثة الوكيل: لوحة يعمل فيها وكيل برمجي تستخدمه أصلًا، أو نقطة نهاية نموذج تختارها، على التصميم المفتوح. عند الإيقاف: لا زر ولا اختصار ولا عنصر قائمة ولا لوحة للذكاء الاصطناعي، ولا يُبحث عن شيء أو يُشغّل.",
"ui.ai_show_features_note": "محادثة الوكيل: لوحة يعمل فيها وكيل برمجي تستخدمه أصلًا، أو نقطة نهاية نموذج تختارها، على التصميم المفتوح. عند الإيقاف: لا زر ولا اختصار ولا عنصر قائمة ولا لوحة للذكاء الاصطناعي، ولا يُبحث عن شيء أو يُشغّل.",
"ui.ai_switch_tip": "تغيير الوكيل لهذه المحادثة",
"ui.ai_test": "اختبار",
"ui.ai_test_acp_ok": "يجيب: {agent}",
@@ -4663,7 +4397,7 @@
"ui.ai_tool_sum_then": "{first}، ثم {next}",
"ui.ai_tool_unfold": "انقر لعرض ما أعاده",
"ui.ai_tool_waiting": "بالانتظار",
"ui.ai_toolbar_button": "زر المساعد في شريط التطبيق",
"ui.ai_toolbar_button": "إظهار الزر في شريط الأدوات",
"ui.ai_toolbar_button_note": "زر الذكاء الاصطناعي في الطرف الأيمن من شريط الأدوات. يظل Ctrl+Shift+A وقائمة العرض يفتحان اللوحة.",
"ui.ai_undo_tip": "التراجع عن كل ما غيّره الوكيل لهذه الرسالة، بخطوة واحدة",
"ui.ai_url_hint": "https://example.com/v1",
@@ -4710,8 +4444,6 @@
"ui.centerline": "خط المركز",
"ui.chain": "سلسلة",
"ui.chamfer": "شطف",
"ui.chamfer_angle": "زاوية الشطف",
"ui.chamfer_depth": "عمق الشطف",
"ui.chamfer_type": "نوع الشطف",
"ui.change_workspace": "تغيير مساحة العمل",
"ui.circle": "دائرة",
@@ -4742,15 +4474,11 @@
"ui.colour_vision_sample": "عيّنة",
"ui.colour_vision_tritan": "ضعف الأزرق",
"ui.colour_vision_tritan_tip": "لضعف أو غياب الإحساس باللون الأزرق",
"ui.comb_length": "طول المشط",
"ui.comb_length_tip": "طول الأسنان: مضاعف للطول التلقائي الذي يجعل أطول سنّ خُمس حجم المنحنيات المعروضة.",
"ui.comb_teeth": "الأسنان",
"ui.comb_teeth_tip": "عدد أسنان كل مشط، موزّعة بالتساوي على طول المنحنى. كلما زادت الأسنان ظهرت تفاصيل أكثر عن تغيّر الانحناء.",
"ui.combine": "دمج",
"ui.coming_later": "قريبًا",
"ui.command_done": "تم {command}",
"ui.command_palette_hint": "اكتب أمرًا، مثل {example} — أو اضغط S للبحث",
"ui.components_from_bodies_hint": "أنشئ مكوّنًا لكل جسم محدد، باسم ذلك الجسم، ضمن المكوّن النشط.",
"ui.components_from_bodies_hint": "أنشئ مكونًا لكل جسم محدد، باسم ذلك الجسم، ضمن المكون النشط.",
"ui.components_from_bodies_required": "حدد جسمًا واحدًا على الأقل.",
"ui.concentric": "متحد المركز",
"ui.cone": "مخروط",
@@ -4770,24 +4498,11 @@
"ui.construction.ribbon_label": "خطوط مساعدة",
"ui.construction.side_hint": "خط إنشائي: انقر الجهة التي يمتد نحوها الخط؛ انقر بزر الفأرة الأيمن للبدء من جديد",
"ui.context_menu": "القائمة السياقية",
"ui.continuity_crease": "ثنية",
"ui.continuity_curvature": "الانحناء",
"ui.continuity_curvature_change": "تغيّر الانحناء",
"ui.continuity_curvature_change_tip": "تغيّر الانحناء (G3): يتغير الانحناء أيضًا بسلاسة عبر خط الالتقاء.",
"ui.continuity_curvature_tip": "متصل الانحناء (G2): تمر الانعكاسات دون انكسار.",
"ui.continuity_g0": "G0 ثنية",
"ui.continuity_g0": "G0 تجعّد",
"ui.continuity_g1": "G1 مماس",
"ui.continuity_g2": "G2 انحناء",
"ui.continuity_gap": "فجوة",
"ui.continuity_position": "الموضع",
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع ثنية عند التقائها.",
"ui.continuity_smooth_curvature_change": "تغيّر انحناء سلس",
"ui.continuity_smooth_curvature_change_tip": "تغيّر انحناء سلس (G4): حتى طريقة تغيّر الانحناء تستمر بسلاسة عبر خط الالتقاء.",
"ui.continuity_tangent": "مماس",
"ui.continuity_tangent_tip": "مماس (G1): لا ثنية، لكن الانحناء يقفز عبر خط الالتقاء.",
"ui.copy": "نسخ",
"ui.copy_details": "نسخ التفاصيل",
"ui.copy_details_tip": "انسخ الرسالة كاملة للبحث عنها أو للإبلاغ عنها.",
"ui.corner": "زاوية",
"ui.corners": "الزوايا",
"ui.cosmetic_thread_markings": "علامات السن اللولبي الشكلية",
@@ -4809,8 +4524,7 @@
"ui.dark": "داكن",
"ui.default_modelling_orientation": "اتجاه النمذجة الافتراضي",
"ui.delete": "حذف",
"ui.delete_select_first": "حدّد أولًا ما تريد حذفه: الميزات أو الأجسام أو المكوّنات أو هندسة الرسم",
"ui.dependents_select_first": "حدّد أولًا ما تريد سرد معتمداته",
"ui.delete_select_first": "حدّد أولًا ما تريد حذفه: الميزات أو الأجسام أو المكونات أو هندسة الرسم",
"ui.depth": "العمق",
"ui.design": "التصميم",
"ui.design_properties.author": "المؤلف",
@@ -4906,10 +4620,8 @@
"ui.edit_sketch_empty": "لا توجد رسومات لتعديلها",
"ui.ellipse": "قطع ناقص",
"ui.emboss_wrap_note": "تلتف ملامح الخطوط المتعامدة المماسة لأسطوانة مدعومة تلقائيًا. اختر جدارًا لتحديد الهدف.",
"ui.end": "النهاية",
"ui.end_radius": "نصف قطر النهاية",
"ui.end_scale": "مقياس النهاية",
"ui.ends": "الطرفان",
"ui.entities": "العناصر",
"ui.equal": "متساوٍ",
"ui.equation_end": "النهاية",
@@ -4929,52 +4641,19 @@
"ui.export_ellipsis_keymap": "تصدير…",
"ui.export_keymap_tip": "المفاتيح ومفضلات مربع S وشريط الأدوات، كملف",
"ui.extend": "تمديد",
"ui.extent": "الامتداد",
"ui.extent_type": "نوع الامتداد",
"ui.extract_component": "استخراج إلى ملف…",
"ui.extract_design_changed": "تغيّر التصميم أثناء اختيار الملف. ابدأ الاستخراج إلى ملف من جديد.",
"ui.extract_done": "أصبح المكوّن تصميمًا مستقلًا، مرتبطًا هنا.",
"ui.extrude": "بثق",
"ui.extrude_auto_to_object": "البثق إلى هدف مختار",
"ui.extrude_auto_to_object_tip": "وجه أو رأس أو مستوى موازٍ يغيّر الامتداد إلى «إلى كائن».",
"ui.extrude_auto_to_object_tip": "في البثق، مع اختيار مقطع، يؤدي اختيار وجه أو رأس أو مستوى موازٍ خارج مستواه إلى تبديل الامتداد إلى «إلى كائن»",
"ui.factor": "المعامل",
"ui.fade": "تلاشٍ",
"ui.fade_length": "طول التلاشي",
"ui.feature_history": "سجل الميزات",
"ui.feature_options": "خيارات الميزة",
"ui.features": "الميزات",
"ui.file": "ملف",
"ui.fillet": "تقويس",
"ui.fillet_corner_ball": "كرة",
"ui.fillet_corner_ball_tip": "جزء من سطح الكرة المتدحرجة، مماسّ (G1) لكل تقويس.",
"ui.fillet_corner_curvature_tip": "الانحناء (G2): رقعة ناعمة. تُرجَع الزاوية على طول كل حافة بمقدار الإرجاع ({factor} ضعف نصف القطر ما لم تكتب قيمة) وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار.",
"ui.fillet_corner_needs_constant": "زوايا الرقعة الناعمة (G2) تتطلب نصف قطر ثابتًا وملاءمة دقيقة لنصف القطر.",
"ui.fillet_corner_patch": "رقعة ناعمة",
"ui.fillet_corner_patch_tip": "متصل الانحناء (G2): تُرجَع الزاوية على طول كل حافة وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار.",
"ui.fillet_corner_tangent_tip": "مماس (G1): زاوية بالكرة المتدحرجة، جزء من سطح الكرة المتدحرجة مماسّ لكل تقويس.",
"ui.fillet_error_corner": "تعذّر بناء الزاوية الناعمة هنا. جرّب مسافة إرجاع أخرى، أو زوايا مماسّة.",
"ui.fillet_error_failed": "تعذّر بناء هذا التقويس على هذه الحواف. جرّب نصف قطر آخر، أو قوّس حوافّ أقل في المرة الواحدة.",
"ui.fillet_error_setback_short": "مسافة الإرجاع قصيرة جدًا على رقعة هذه الزاوية: تُبنى ابتداءً من {least}. جرّب مسافة إرجاع أطول.",
"ui.fillet_error_too_large": "نصف القطر كبير جدًا على الوجوه المجاورة لهذه الحواف. جرّب نصف قطر أصغر، أو اختر في «حدود الوجوه» الخيار «أكبر نصف قطر».",
"ui.fillet_fit": "حدود الوجوه",
"ui.fillet_fit_exact": "نصف قطر دقيق",
"ui.fillet_fit_needs_constant": "ملاءمة نصف القطر تتطلب نصف قطر ثابتًا.",
"ui.fillet_fit_reduce": "أكبر نصف قطر",
"ui.fillet_fit_taper": "نصف قطر مُضيَّق",
"ui.fillet_fit_tip": "عندما يكون نصف القطر أكبر مما تسمح به الوجوه المجاورة للحافة.\nنصف قطر دقيق: نصف القطر كما هو، أو لا تقويس ورسالة توضّح السبب.\nأكبر نصف قطر: تُقوَّس كل حافة بأكبر نصف قطر يتّسع.\nنصف قطر مُضيَّق: يبقى نصف القطر، ويُضيَّق التقويس فقط حيث تنتهي الوجوه.",
"ui.fillet_options_tip": "سلسلة مماسّة: قوّس أيضًا الحواف التي تتصل بسلاسة بالحواف المختارة.\nمعاينة: اعرض النتيجة في العرض أثناء التحرير.",
"ui.fillet_setback": "مسافة الإرجاع",
"ui.fillet_setback_hint": "{factor} × نصف القطر",
"ui.fillet_setback_least": "أكثر من {least}",
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، وهي التي تلتقي عندها التقويسات دون إرجاع: نصف القطر حين تكون الأوجه متعامدة.",
"ui.fillet_smooth_radius": "تغيّر ناعم لنصف القطر",
"ui.fillet_turn": "الالتفاف",
"ui.fillet_turn_mitre": "وصلة مائلة",
"ui.fillet_turn_swept": "ملتف",
"ui.fillet_turn_tip": "حيث يلتف تقويسان حول حافة ركن حادة بينهما، مثل التقويسات على قاعدة نتوء عند حوافه القائمة.\nوصلة مائلة: يمتد التقويسان حتى يلتقيا في وصلة مائلة.\nملتف: يتوقف التقويسان عند حافة الركن ويلتفان حولها، والركن طارة مماسة لكليهما. فقط لتقويسين بنصف قطر واحد حول حافة ركن مستقيمة ومحدبة.",
"ui.fillet_type_choices_tip": "نصف قطر ثابت: نصف قطر واحد على طول الحافة.\nطول الوتر: يبقى عرض التقويس نفسه مهما كانت الزاوية بين الوجوه.\nنصف قطر متغيّر: نصف قطر عند كل طرف، ومزيد على طول الحافة إن أضفته.\nمتصل الانحناء: مقطع بلا انكسار في الانعكاسات حيث يلتقي بالوجوه المستوية (G2).\nمنحنى شريحي: يتبع نصف القطر على طول الحافة أنصاف أقطار تحكّم تحدّدها.",
"ui.fillet_type_curvature": "متصل الانحناء",
"ui.fillet_type_tip": "المقطع عبر الحافة. متصل الانحناء: مقطع متصل الانحناء (G2) بين وجوه مستوية، فتمر الانعكاسات من الوجوه إلى التقويس دون انكسار.",
"ui.filter_commands_or_keys": "تصفية الأوامر أو المفاتيح",
"ui.findInTimeline": "البحث في الخط الزمني",
"ui.findInWindow": "البحث في النافذة",
@@ -5084,14 +4763,14 @@
"ui.hidePlanes": "إخفاء المستويات",
"ui.hideProfile": "إخفاء المقطع",
"ui.hide_component_history": "إخفاء سجله",
"ui.hide_origin_by_default": "إخفاء مجلد نقطة الأصل في التصاميم الجديدة",
"ui.hide_origin_by_default": "إخفاء نقطة الأصل افتراضيًا",
"ui.hide_origin_note": "تبدأ التصاميم الجديدة ومجلد نقطة الأصل مخفي. يحتفظ كل تصميم بإعداد نقطة الأصل الخاص به بمجرد إظهارها أو إخفائها.",
"ui.history_back": "الرجوع خطوة",
"ui.history_below_browser": "أسفل المستعرض",
"ui.history_edge_hint": "انقر مرتين على الحافة",
"ui.history_first": "الذهاب إلى البداية",
"ui.history_forward": "التقدم خطوة",
"ui.history_hide_hidden": "إخفاء المكوّنات المخفية",
"ui.history_hide_hidden": "إخفاء المكونات المخفية",
"ui.history_hide_names": "إخفاء الأسماء",
"ui.history_last": "الذهاب إلى النهاية",
"ui.history_marker_tip": "مؤشر السجل: اسحبه لإرجاع النموذج، والميزات الجديدة تُضاف هنا",
@@ -5101,7 +4780,7 @@
"ui.history_play_button_show": "إظهار زر التشغيل",
"ui.history_play_finish_action": "أكمل الإجراء الحالي قبل تشغيل السجل",
"ui.history_play_roll_back": "أعد مؤشر السجل إلى الخلف قبل التشغيل",
"ui.history_show_hidden": "إظهار المكوّنات المخفية",
"ui.history_show_hidden": "إظهار المكونات المخفية",
"ui.history_show_names": "إظهار الأسماء",
"ui.history_sketch_listed_under": "مدرج تحت {feature}",
"ui.history_title": "السجل",
@@ -5124,7 +4803,6 @@
"ui.inspect": "فحص",
"ui.instances": "النسخ",
"ui.intersect": "تقاطع",
"ui.iso_4753": "ISO 4753",
"ui.isolate": "عزل",
"ui.join": "ضم",
"ui.joint_play": "▶ تشغيل",
@@ -5227,7 +4905,6 @@
"ui.part_size_needs_a_part": "حدد قطعة قياسية مُدرجة (برغي أو صامولة أو حلقة مسطحة أو مسمار) لتغيير مقاسها. لتكبير جسم أو تصغيره، استخدم «تحجيم».",
"ui.part_size_shared": "يعيد بناء القطعة مستبدلًا أي ميزات مضافة. تتحدث كل نسخ هذا المكوّن.",
"ui.paste": "لصق",
"ui.patch_continuity_corner_tip": "تلتقي الرقعة بالأوجه المحيطة بها بالاستمرارية المختارة على طول كل جانب. وعند زاوية تلتقي فيها تلك الأوجه بعضها ببعض بانحناءات مختلفة، تبقى مماسّة (G1) هناك.",
"ui.path": "المسار",
"ui.pattern_object_type_tip": "الأجسام: أشكال مصمتة تُختار في العرض أو في المستعرض؛ يُنسخ كل منها كاملًا.\nالميزات: خطوات في السجل، مثل بثق أو ثقب، تُختار في السجل أو على وجه صنعته؛ تتكرر الخطوة.\nالرسم ليس أيًّا منهما: كرّر منحنياته بأداة النمط أو المرآة داخل الرسم، أو ابثقه أولًا.",
"ui.pattern_on_path": "نمط على مسار",
@@ -5244,7 +4921,7 @@
"ui.pitch": "الخطوة",
"ui.placement": "الموضع",
"ui.placement_handles": "مقابض الموضع",
"ui.placement_handles_tip": "علامة لسحب الميزة عبر النموذج وحلقة لتدويرها.",
"ui.placement_handles_tip": "مقابض على معاينة المسند أو النتوء أو وصلة الكبس: اسحب العلامة لتحريكها على النموذج، وأدِرها من حلقتها",
"ui.plane": "المستوى",
"ui.plane_at_angle": "مستوى بزاوية",
"ui.plugins": "الإضافات",
@@ -5404,8 +5081,6 @@
"ui.sketch_intersect_geometry": "الهندسة",
"ui.sketch_intersect_projection_link": "رابط الإسقاط",
"ui.sketch_intersect_selection_filter": "مرشح التحديد",
"ui.sketch_keys_only": "إبقاء مفاتيح النموذج خارج الرسومات",
"ui.sketch_keys_only_tip": "أثناء تحرير رسم، تعمل مفاتيح الرسم والمفاتيح التي تعمل في كل مكان فقط؛ ولا يفعل مفتاح نموذج مثل E أو F شيئًا، فلا يمكن لمفتاح مضغوط بالخطأ أن يغلق رسمًا ثقيلًا.",
"ui.sketch_modifiers": "مُعدِّلات الرسم",
"ui.sketch_project_geometry": "الهندسة",
"ui.sketch_report.all_constrained": "كل شيء مقيد بالكامل.",
@@ -5442,8 +5117,8 @@
"ui.start": "البداية",
"ui.start_over": "البدء من جديد",
"ui.start_page": "صفحة البداية",
"ui.step_values": "تغيير القيم بالأسهم والعجلة",
"ui.step_values_tip": "يغيّر السهمان لأعلى ولأسفل والعجلة فوق حقل قيمة الرقم فيه.",
"ui.step_values": "تغيير القيم بمفاتيح الأسهم وعجلة الفأرة",
"ui.step_values_tip": "في حقل قيمة تكتب فيه، يغيّر السهمان لأعلى ولأسفل الرقم العادي بمقدار 1 (Shift: 10، Alt: 0.1)، وكذلك عجلة الفأرة فوق الحقل. أما التعابير فتبقى كما كُتبت.",
"ui.stitch_tolerance_hint": "دقيقة",
"ui.stretch": "مطّ",
"ui.supersampling": "التنعيم الفائق (SSAA)",
@@ -5574,7 +5249,7 @@
"update.check_unavailable": "التحقق من التحديثات غير متاح لهذا التثبيت.",
"update.checking": "جارٍ التحقق…",
"update.copy": "نسخ",
"update.dmg_opened": "صورة القرص مفتوحة وموثّقة: أنهِ SolidCraft، ثم اسحب SolidCraft الجديد إلى مجلد التطبيقات مستبدلًا النسخة القديمة، ثم افتح SolidCraft من جديد.",
"update.dmg_opened": "صورة القرص مفتوحة وموثّقة: اسحب SolidCraft إلى مجلد التطبيقات مستبدلًا النسخة القديمة، ثم أعد تشغيل SolidCraft.",
"update.download": "تنزيل",
"update.download_open": "تنزيل وفتح",
"update.downloading": "جارٍ التنزيل والتحقق… {done} من {total} MB",
+3 -78
View File
@@ -477,7 +477,6 @@
"Rectangular": "Pravougaoni",
"Rectangular Tube": "Pravougaona cijev",
"Reference face": "Referentno lice",
"Reference plane": "Referentna ravan",
"Relief Depth": "Dubina rasterećenja",
"Relief Width": "Širina rasterećenja",
"Remove Tools": "Ukloni alate",
@@ -557,7 +556,6 @@
"Section": "Presjek",
"Section (optional)": "Presjek (neobavezno)",
"Section position": "Položaj presjeka",
"Sections": "Presjeci",
"Sections (one sketch each)": "Presjeci (po jedna skica)",
"Select Other": "Izaberi drugo",
"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).": "Izaberi drugo: držite lijevo dugme mirno na modelu (ili desni klik › Izaberi drugo…) da prikažete sve ispod pokazivača; ponovo kliknite na isto mjesto da izaberete sljedeći element. Dvaput kliknite srednjim dugmetom za Početni prikaz (Postavke › Navigacija).",
@@ -861,7 +859,6 @@
"cmd.component_extract": "Izdvoji u datoteku",
"cmd.component_finish_edit": "Završi uređivanje",
"cmd.component_from_bodies": "Napravi komponente iz tijela",
"cmd.component_hide_history": "Sakrij vremensku traku komponente",
"cmd.component_insert_design": "Umetni dizajn",
"cmd.component_links": "Popis veza",
"cmd.component_list": "Popis komponenti",
@@ -878,7 +875,6 @@
"cmd.component_rename": "Preimenuj komponentu",
"cmd.component_replace_with_link": "Zamijeni vezom",
"cmd.component_revert_position": "Vrati položaj",
"cmd.component_show_history": "Prikaži vremensku traku komponente",
"cmd.component_unlock": "Otključaj sadržaj",
"cmd.component_update_links": "Ažuriraj veze",
"cmd.config_activate": "Aktiviraj konfiguraciju",
@@ -889,7 +885,6 @@
"cmd.config_rule": "Pravilo konfiguracije",
"cmd.config_rules": "Pravila konfiguracije",
"cmd.config_table": "Prikaži tabelu konfiguracija",
"cmd.construct.plane.axis_point": "Ravan kroz osu i tačku",
"cmd.construct_axis_edge": "Osa kroz ivicu",
"cmd.construct_axis_normal_to_face": "Osa okomita na lice",
"cmd.construct_axis_through_cylinder": "Osa kroz cilindar/konus/torus",
@@ -938,11 +933,6 @@
"cmd.explode_show": "Prikaži rastavljeni prikaz",
"cmd.expr_evaluate": "Izračunaj izraz",
"cmd.face_delete": "Izbriši lice",
"cmd.file.export_3mf": "Izvezi 3MF",
"cmd.file.export_obj": "Izvezi OBJ",
"cmd.file.export_step": "Izvezi STEP",
"cmd.file.export_stl": "Izvezi STL",
"cmd.file.export_urdf": "Izvezi URDF",
"cmd.file_export": "Izvoz",
"cmd.file_insert_mesh": "Umetni mrežu",
"cmd.file_insert_step": "Umetni STEP",
@@ -960,7 +950,6 @@
"cmd.inspect_colored_components": "Omogući boje komponenti",
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
"cmd.inspect_compare": "Uporedi tijela",
"cmd.inspect_compare_picture": "Uporedi sliku",
"cmd.inspect_continuity": "Analiza kontinuiteta",
"cmd.inspect_curvature_comb": "Analiza češlja zakrivljenosti",
"cmd.inspect_curvature_map": "Analiza mape zakrivljenosti",
@@ -1051,9 +1040,6 @@
"cmd.pointcloud_edit": "Uredi oblak tačaka",
"cmd.pointcloud_insert": "Umetni oblak tačaka",
"cmd.pointcloud_list": "Oblaci tačaka",
"cmd.render.compare.divider": "Pomjeri razdjelnik poređenja renderovanja",
"cmd.render.result.discard": "Odbaci rezultat renderovanja",
"cmd.render.result.select": "Izaberi rezultat renderovanja",
"cmd.render_aging_describe": "Slojevito starenje površine",
"cmd.render_backends": "Pozadinski sistemi renderovanja",
"cmd.render_batch": "Renderuj sve kadrove",
@@ -1268,7 +1254,6 @@
"cmd.timeline_rollback": "Pomjeri oznaku vraćanja",
"cmd.timeline_stop": "Zaustavi vremensku traku",
"cmd.timeline_suppress": "Potisni operaciju",
"cmd.ui.assistant": "Prikaži ili sakrij Asistenta",
"cmd.view.render_browser": "Stablo renderovanja",
"cmd.view.render_dock": "Premjesti panel renderovanja",
"cmd.view.render_focus": "Prikaz fokusa renderovanja",
@@ -1303,11 +1288,9 @@
"cmd.view_tool": "Alat za navigaciju",
"cmd.view_top": "Pogled odozgo",
"cmd.view_visual_style": "Vizuelni stil",
"color.blue": "Plava",
"color.copy": "Kliknite da kopirate vrijednosti boje",
"color.edit": "Kliknite da uredite boju",
"color.gamma": "Prikaz vrijednosti boje u gama prostoru 0-255",
"color.green": "Zelena",
"color.hex": "Hex",
"color.hex_help": "Unesite #rrggbb ili #rrggbbaa; šest cifara zadržava neprozirnost",
"color.hex_hint": "#rrggbb",
@@ -1315,7 +1298,6 @@
"color.linear": "Prikaz vrijednosti boje u linearnom prostoru 0-1",
"color.recent": "Nedavne boje",
"color.recent_help": "Ovdje se pojavljuju boje koje izaberete u bilo kojem polju boje",
"color.red": "Crvena",
"color.saturation": "Zasićenost (procenat)",
"color.saturation_value": "Zasićenost i vrijednost",
"color.selected": "Izabrana boja",
@@ -1323,8 +1305,6 @@
"color.step_up": "Povećaj za korak",
"color.value": "Vrijednost, svjetlina (procenat)",
"config.column_filter_empty": "Nema odgovarajućih kolona",
"config.rule_parameter_columns": "Prikazani su samo parametri koji imaju kolonu u tabeli. Dodajte kolonu pomoću Dodaj kolonu (+) da biste parametar koristili ovdje.",
"config.rule_suppression_columns": "Prikazane su samo operacije koje imaju kolonu Potisni u tabeli. Dodajte kolonu pomoću Dodaj kolonu (+) da biste ovdje mogli upravljati operacijom.",
"contact.components_count": "Komponente: {count}",
"contact.enabled_label": "omogućeno",
"coupling.gear_1": "Zupčanik 1",
@@ -1373,11 +1353,7 @@
"dialog.pattern_sketch_not_object": "Izaberite tijela ili operacije: skica nije ni jedno ni drugo. Ponovite njene krive pomoću alata Niz ili Preslikaj u samoj skici ili je prvo ekstrudirajte.",
"dialog.select": "↖ Izaberi",
"dialog.selected_count": "↖ Izabrano: {count}",
"dialog.selection_constraint": "Ograničenje {id}",
"dialog.selection_external_curve": "{sketch} · kriva {id}",
"dialog.selection_guidance": "Izaberite ulaz da ga promijenite. Kliknite izabrane objekte da ih uklonite; Ctrl/Cmd-klik ih također uključuje i isključuje. Pogledajte pregled, a zatim pritisnite U redu.",
"dialog.selection_midpoint": "Središnja tačka · {id}",
"dialog.selection_profile": "{sketch} · profil {number}",
"dialog.selection_step": "{step} od {total}: Izaberite {input}",
"dialog.step": "Korak",
"document.changed_on_disk": "{name} je promijenjen na disku",
@@ -1460,7 +1436,6 @@
"help.construct.axis.two_points": "Pravi pomoćnu osu kroz dvije tačke, vrhove ili tačke skice. Koristite je za osu koja prolazi između dva poznata položaja.",
"help.construct.plane.along_path": "Pravi pomoćnu ravan okomitu na ivicu ili krivu, na nekom položaju duž nje. Koristite je da skicirate profil za povlačenje ili presjek putanje.",
"help.construct.plane.at_angle": "Pravi pomoćnu ravan zakrenutu za ugao u odnosu na ravan ili lice, oko ose ili ivice. Koristite je za nagnute skice, npr. kutni nosač.",
"help.construct.plane.axis_point": "Pravi pomoćnu ravan koja sadrži osu i tačku izvan te ose. Izaberite osu ishodišta ili pomoćnu osu, ravnu ivicu ili liniju skice, pa vrh, tačku skice ili pomoćnu tačku. Tačka na osi ne može odrediti jedinstvenu ravan.",
"help.construct.plane.midplane": "Pravi pomoćnu ravan na pola puta između dvije ravni ili ravna lica, ili koja ih raspolavlja tamo gdje se sastaju pod uglom. Koristite je kao ravan simetrije za preslikavanje.",
"help.construct.plane.offset": "Pravi pomoćnu ravan paralelnu s ravni ili ravnim licem, na određenoj udaljenosti ili kroz tačku. Koristite je da skicirate na zadatoj visini iznad lica.",
"help.construct.plane.perpendicular": "Pravi pomoćnu ravan koja sadrži liniju ili ivicu i okomita je na ravan ili lice. Koristite je da skicirate uspravno na modelu duž ivice.",
@@ -1837,8 +1812,6 @@
"joint_move.blocked_hint": "Provjerite vrstu spoja (npr. Kruti), učvršćenje, granice spoja i vrijednosti koje pokreću parametri.",
"joint_move.blocked_title": "Ovaj dio se ne može pomjeriti",
"joint_move.coarse": "Korak rotacije blizu ose (°)",
"joint_move.conflict": "Jedan spoj ovog dijela je u sukobu s ostalima: {joint}.",
"joint_move.conflict_hint": "Uredite, potisnite ili izbrišite spoj koji je u sukobu da bi ostali spojevi mogli postaviti dio.",
"joint_move.distance": "Udaljenost od početka",
"joint_move.done": "Gotovo",
"joint_move.fine": "Korak rotacije dalje od ose (°)",
@@ -1938,8 +1911,6 @@
"motion.track_name": "Staza {number}",
"nav.alias.help": "Shift+Alt+povlačenje lijevim dugmetom orbitira, Shift+Alt+srednjim pomjera, Shift+Alt+desnim zumira; povlačenje srednjim dugmetom također pomjera, a desnim orbitira",
"nav.blender.help": "Povlačenje srednjim dugmetom orbitira, Shift+srednje pomjera, Ctrl+srednje zumira; povlačenje desnim dugmetom ne dira kameru",
"nav.creo.help": "Povlačenje srednjim dugmetom orbitira, Shift+srednje pomjera, Ctrl+srednje uz vertikalno povlačenje zumira (prema dolje za približavanje); točkić također zumira",
"nav.creo.label": "Creo",
"nav.free_orbit.help": "Miš i 3D miš rotiraju kroz oba pola i čuvaju nagib. Privlačenje osa tipkom Shift je isključeno.",
"nav.free_orbit.label": "Slobodno orbitiranje",
"nav.free_orbit.snap_help": "Miš i 3D miš rotiraju kroz oba pola i čuvaju nagib. Držite Shift tokom povlačenja za orbitiranje da privučete smjer pogleda i nagib.",
@@ -2092,19 +2063,12 @@
"parts.family.button_head_cap_screw": "Vijak s poluokruglom glavom i imbusom",
"parts.family.dowel_pin": "Cilindrični zatik",
"parts.family.hex_bolt": "Vijak sa šestougaonom glavom",
"parts.family.hex_cap_screw_inch": "Vijak sa šestougaonom glavom (inč)",
"parts.family.hex_nut": "Šestougaona matica",
"parts.family.hex_nut_inch": "Šestougaona matica (inč)",
"parts.family.set_screw": "Bezglavi vijak s imbusom, ravni vrh",
"parts.family.socket_head_cap_screw": "Vijak s cilindričnom glavom i imbusom",
"parts.family.washer": "Obična podloška",
"parts.family.washer_sae": "Obična podloška, SAE (uska)",
"parts.family.washer_uss": "Obična podloška, USS (široka)",
"parts.holes": "Rupe (neobavezno)",
"parts.placement_hint": "izaberite jednu ili više rupa da u svaku postavite dio, ili ga U redu postavlja u ishodište",
"parts.system": "Sistem jedinica",
"parts.system.inch": "Inčni",
"parts.system.metric": "Metrički",
"pattern.alignment_2": "Poravnanje 2",
"pattern.alignment_hint": "Broj uključuje original. Centar ravnomjerno raspoređuje kopije; Obrni bira dodatnu stranu za parne ukupne brojeve.",
"pattern.direction_1": "Smjer 1",
@@ -2166,8 +2130,6 @@
"prefs.button_left": "Povlačenje lijevim dugmetom",
"prefs.button_middle": "Povlačenje srednjim dugmetom",
"prefs.button_right": "Povlačenje desnim dugmetom",
"prefs.carousel_next": "Sljedeće",
"prefs.carousel_previous": "Prethodno",
"prefs.choose_font": "Izaberi datoteku fonta…",
"prefs.click": "Klik",
"prefs.contact_shading": "Meko sjenčenje dodira",
@@ -2180,8 +2142,6 @@
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Zakrivljene površi",
"prefs.curves": "Zakrivljene površi",
"prefs.cut_preview_tint": "Prikaži uklonjeni materijal u crvenoj boji",
"prefs.cut_preview_tint_help": "Pregled ispunjava materijal koji operacija uklanja i oblik koji zamjenjuje crvenom prozirnom sjenom. Kada je isključeno, te ispune su potpuno prozirne i crtaju se samo njihovi obrisi.",
"prefs.dark_parts_shading": "Zadrži oblik tamnih dijelova",
"prefs.dark_parts_shading_help": "Sjenči prikaz u linearnom svjetlu s mekim dopunskim svjetlom neba, tako da tamna lica zadrže gradijent umjesto da pocrne",
"prefs.dead_zone_note": "Šrafirana traka je mrtva zona: kretanje unutar nje se zanemaruje.",
@@ -2240,8 +2200,6 @@
"prefs.interface_language": "Jezik interfejsa",
"prefs.interface_note": "Kako SolidCraft izgleda i gdje su njegovi dijelovi.",
"prefs.interval_minutes": "{n} min",
"prefs.invert_curved_roll_arrows": "Obrni zakrivljene strelice za valjanje",
"prefs.invert_curved_roll_arrows_note": "Dvije zakrivljene strelice u uglu kocke valjaju prikaz unutar ravni ekrana. Uključeno: strelica u smjeru kazaljke na satu valja prikaz suprotno od smjera kazaljke na satu, a strelica suprotno od smjera kazaljke na satu valja ga u smjeru kazaljke na satu. Isključeno: svaka strelica valja prikaz onako kako je zakrivljena.",
"prefs.invert_navigator_arrows": "Obrni strelice za rotaciju kocke prikaza",
"prefs.invert_navigator_arrows_note": "Strelice oko kocke orbitiraju kameru u smjeru u kojem pokazuju, kao u programima Fusion i Inventor: strelica s lijeve strane pokazuje udesno i zakreće kameru udesno. Isključeno: strelica okreće kocku prema strani na kojoj se nalazi, pa lijeva strelica zakreće kameru ulijevo. Postavka ne utiče na zakrivljene strelice za valjanje.",
"prefs.keep_work_on_tab_switch": "Zadrži dijaloge i alate otvorenima pri prebacivanju između dizajna",
@@ -2301,8 +2259,6 @@
"prefs.placement": "Postavljanje",
"prefs.prefer_free_sketch_points": "Daj prednost pokretnim tačkama skice kada se tačke preklapaju",
"prefs.preview_choices": "Pregled vaših izbora",
"prefs.preview_tint_new_faces": "Oboji novu površ u pregledu",
"prefs.preview_tint_new_faces_help": "Pregled operacije koja mijenja tijelo može nacrtati površ koju dodaje u boji pregleda. Kada je isključeno, tijelo se crta kakvo jest.",
"prefs.print_face_highlight": "Isticanje površine za štampu",
"prefs.print_face_highlight_tip": "Koliko se jako površina za štampu tijela boji i obrubljuje u prikazu. 0 % ne prikazuje isticanje.",
"prefs.profile_active": "Aktivni profil",
@@ -2600,10 +2556,6 @@
"render.base_color": "Osnovna boja · sRGB",
"render.base_metallic": "Metalni udio",
"render.base_opaque_only": "Ovaj uređivač čuva semantiku neprozirnih tijela. Za optičko propuštanje koristite izričite kontrole stakla.",
"render.batch_all_shots": "Svi kadrovi ({count})",
"render.batch_empty": "Sačuvajte kadar prije renderovanja svih kadrova.",
"render.batch_limit": "Renderovanje svih kadrova ograničeno je na 32 kadra.",
"render.batch_status": "U toku: {running} · Na čekanju: {waiting}",
"render.bloom_enable": "Sjaj prikaza",
"render.bloom_help": "Kreativni sjaj samo na izlazu iz pozitivnih linearnih svijetlih dijelova, raširen kroz tri normalizovana prolaza kutijastog filtera. Poluprečnik je u pikselima izlaza. Dodaje se u linearnoj svjetlosti prikaza prije sRGB-a; nije objektiv, volumetrijski efekat niti poboljšanje konvergencije. Sirovi EXR ostaje nepromijenjen.",
"render.bloom_opaque": "Sjaj zahtijeva neprozirni izlaz. Prozirni PNG se odbija, a ne spljoštava tiho.",
@@ -2714,13 +2666,6 @@
"render.choose_look": "Prvo izaberite izgled u Izgledima",
"render.chromatic_offset": "Hromatski pomak ivica",
"render.clear_stage": "Ukloni sačuvanu scenu",
"render.compare_divider": "Razdjelnik poređenja",
"render.compare_live": "Uživo",
"render.compare_live_preview": "Pregled uživo",
"render.compare_render": "Renderovanje {number}",
"render.compare_side": "Renderovanje {number} · {shot}",
"render.compare_summary": "{shot} · {width} × {height} · PNG · {seconds} s · {renderer} · zabilježeno u reviziji {revision}",
"render.compare_swap": "Zamijeni strane",
"render.copy_request": "Kopiraj JSON",
"render.cpu_aa_help": "Prostorni uzorci: više uzoraka poboljšava ivice i produžava renderovanje.",
"render.cpu_area_add": "Dodaj pravougaono CPU svjetlo",
@@ -3470,10 +3415,6 @@
"render.result_retained": "Prikazuje se posljednja završena slika; novije renderovanje je još nije zamijenilo.",
"render.result_sequence": "Kontrolna tačka",
"render.result_title": "Rezultat renderovanja",
"render.results_choose": "Zadržani rezultati",
"render.results_label": "Renderovanje {number} · {shot} · {seconds} s",
"render.results_stale_label": "{result} [zastarjelo]",
"render.results_unnamed": "Slika",
"render.return_design": "Nazad na Dizajn",
"render.review_stale": "Ovaj pregled je zastario. Riješite ga ili ponovo provjerite prije primjene.",
"render.ribbon_aging": "Starenje",
@@ -3957,7 +3898,10 @@
"the two origins are on the same component": "oba ishodišta su na istoj komponenti",
"thread.automatic_size": "Automatska veličina",
"thread.designation": "Oznaka",
"thread.direction": "Smjer navoja",
"thread.iso_metric": "ISO metrički",
"thread.left_hand": "Lijevi",
"thread.right_hand": "Desni",
"thread.select_size": "Izaberite veličinu",
"timeline.delete_after_marker_one": "Izbriši 1 operaciju nakon oznake vraćanja",
"timeline.delete_after_marker_other": "Izbriši operacije nakon oznake vraćanja: {count}",
@@ -4131,7 +4075,6 @@
"ui.agent_chat_auth_terminal": "Pokrenite ovo u terminalu, a zatim pritisnite Poveži ponovo:",
"ui.agent_chat_auth_title": "Prijavi se kod agenta",
"ui.agent_chat_cancel": "Otkaži",
"ui.agent_chat_cancel_timeout": "Agent nije odgovorio na Zaustavi pa je veza s njim prekinuta. Povežite se ponovo da biste nastavili.",
"ui.agent_chat_cannot_start": "Instalirano, ali se ne pokreće: {error}",
"ui.agent_chat_changes": "Promjene: {n}, jedan korak poništavanja",
"ui.agent_chat_changes_n": "Promjene: {n}",
@@ -4141,13 +4084,11 @@
"ui.agent_chat_command": "Komanda agenta",
"ui.agent_chat_connect": "Poveži",
"ui.agent_chat_copy": "Kopiraj",
"ui.agent_chat_copy_message": "Kopiraj poruku",
"ui.agent_chat_ctx_bodies": "Tijela",
"ui.agent_chat_ctx_elements": "Elementi",
"ui.agent_chat_ctx_features": "Operacije",
"ui.agent_chat_ctx_more": "+još {n}",
"ui.agent_chat_ctx_other": "Ostalo",
"ui.agent_chat_custom_heading": "Prilagođeni model",
"ui.agent_chat_custom_hint": "Bilo koji program koji govori Agent Client Protocol na stdin i stdout. Ne pokreće ga nikakva ljuska.",
"ui.agent_chat_denied": "Odbijeno",
"ui.agent_chat_deny": "Odbij",
@@ -4191,16 +4132,8 @@
"ui.agent_chat_mode_all": "Sve",
"ui.agent_chat_mode_ask": "Pitaj",
"ui.agent_chat_mode_edits": "Izmjene",
"ui.agent_chat_model_custom": "Koristi prilagođeni: {model}",
"ui.agent_chat_model_no_tools": "{model} ne može koristiti SolidCraft alate, pa može samo razgovarati. Izaberite model s podrškom za alate.",
"ui.agent_chat_model_rejected": "Agent nije prihvatio model {model}: {error}",
"ui.agent_chat_model_search": "Pretraži modele…",
"ui.agent_chat_model_tip": "Model koji agent koristi. Vaš izbor se pamti za ovog agenta.",
"ui.agent_chat_models_failed": "Popis modela nije moguće učitati: {error}",
"ui.agent_chat_models_heading": "Dostupni modeli",
"ui.agent_chat_models_listing": "Učitavanje popisa modela…",
"ui.agent_chat_models_none": "Krajnja tačka ne navodi nijedan model",
"ui.agent_chat_models_not_a_list": "Odgovor nije popis modela",
"ui.agent_chat_new": "Novi razgovor",
"ui.agent_chat_new_confirm": "Započeti novi razgovor? Trenutni će biti obrisan.",
"ui.agent_chat_next_picture": "Sljedeća slika",
@@ -4474,11 +4407,9 @@
"ui.construction.contact_hint": "Tangenta: izaberite krug/luk ili otvoreni splajn/koniku, pa tačku dodira (krive slobodnog oblika: krajnje tačke)",
"ui.construction.external_hint": "Tangenta: izaberite krug ili luk, vanjsku tačku, pa željeno rješenje",
"ui.construction.free_hint": "Pomoćna prava: kliknite dvije tačke; desnim klikom počnite ispočetka",
"ui.construction.normal_needs_length": "Beskonačna linija je uvijek pomoćna: upišite dužinu da biste nacrtali običnu liniju",
"ui.construction.point_hint": "Pomoćna prava: kliknite tačku kroz koju prolazi",
"ui.construction.reference_hint": "Pomoćna prava: izaberite pravu liniju, pa tačku; desnim klikom promijenite referencu",
"ui.construction.ribbon_label": "Pomoćne",
"ui.construction.side_hint": "Pomoćna prava: kliknite stranu prema kojoj linija ide; desnim klikom počnite ispočetka",
"ui.context_menu": "Kontekstni meni",
"ui.continuity_g0": "G0 pregib",
"ui.continuity_g1": "G1 tangentno",
@@ -4744,7 +4675,6 @@
"ui.hideOrigin": "Sakrij ishodište",
"ui.hidePlanes": "Sakrij ravni",
"ui.hideProfile": "Sakrij profil",
"ui.hide_component_history": "Sakrij njenu vremensku traku",
"ui.hide_origin_by_default": "Podrazumijevano sakrij ishodište",
"ui.hide_origin_note": "Novi dizajni počinju sa skrivenim folderom Ishodište. Dizajn zadržava vlastitu postavku Ishodišta kada ga prikažete ili sakrijete.",
"ui.history_back": "Idi korak nazad",
@@ -4752,7 +4682,6 @@
"ui.history_edge_hint": "dvaput kliknite ivicu",
"ui.history_first": "Idi na početak",
"ui.history_forward": "Idi korak naprijed",
"ui.history_hide_hidden": "Sakrij skrivene komponente",
"ui.history_hide_names": "Sakrij nazive",
"ui.history_last": "Idi na kraj",
"ui.history_marker_tip": "Oznaka vraćanja: povucite da vratite model unazad; nove operacije dolaze ovdje",
@@ -4762,12 +4691,10 @@
"ui.history_play_button_show": "Prikaži dugme za reprodukciju",
"ui.history_play_finish_action": "Završite trenutnu radnju prije reprodukcije vremenske trake",
"ui.history_play_roll_back": "Pomjerite oznaku vraćanja unazad prije reprodukcije",
"ui.history_show_hidden": "Prikaži skrivene komponente",
"ui.history_show_names": "Prikaži nazive",
"ui.history_sketch_listed_under": "Navedeno ispod {feature}",
"ui.history_title": "Vremenska traka",
"ui.history_toggle_names": "Prikaži ili sakrij nazive operacija",
"ui.history_waits_for_editor": "Poništi, Ponovi i vremenska traka čekaju dok se ne otvori uređivač operacije",
"ui.hole": "Rupa",
"ui.hole_tap_type": "Vrsta navoja rupe",
"ui.hole_type": "Vrsta rupe",
@@ -5048,7 +4975,6 @@
"ui.showDimension": "Prikaži kotu",
"ui.showOrigin": "Prikaži ishodište",
"ui.showPlanes": "Prikaži ravni",
"ui.show_component_history": "Prikaži njenu vremensku traku",
"ui.show_constraints": "Ograničenja",
"ui.show_construction_geometries": "Pomoćna geometrija",
"ui.show_dimensions": "Kote",
@@ -5275,7 +5201,6 @@
"web.open_link": "Otvoriti povezani dizajn?",
"web.open_link_confirm": "Postoje nesačuvane promjene. Otvoriti povezani dizajn u novoj kartici?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ izaberi · Enter / Tab dovrši · Esc odbaci",
"↑ ↓ choose · Enter applies this value": "↑ ↓ izbor · Enter primjenjuje ovu vrijednost",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ izaberi · Enter primjenjuje ono što ste upisali · Esc odbaci",
"⌘Enter continues on GitHub.": "⌘Enter nastavlja na GitHubu."
}
+4 -84
View File
@@ -477,7 +477,6 @@
"Rectangular": "Obdélníkový",
"Rectangular Tube": "Obdélníková trubka",
"Reference face": "Referenční plocha",
"Reference plane": "Referenční rovina",
"Relief Depth": "Hloubka odlehčení",
"Relief Width": "Šířka odlehčení",
"Remove Tools": "Odstranit nástroje",
@@ -557,7 +556,6 @@
"Section": "Řez",
"Section (optional)": "Průřez (volitelné)",
"Section position": "Poloha průřezu",
"Sections": "Průřezy",
"Sections (one sketch each)": "Průřezy (každý jeden náčrt)",
"Select Other": "Vybrat jiné",
"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).": "Vybrat jiné: podržte levé tlačítko na modelu (nebo pravé tlačítko › Vybrat jiné…) a zobrazí se vše pod kurzorem; dalším kliknutím na stejné místo vyberete další prvek. Dvojklikem prostředním tlačítkem přejdete na výchozí pohled (Předvolby › Navigace).",
@@ -861,7 +859,6 @@
"cmd.component_extract": "Extrahovat do souboru",
"cmd.component_finish_edit": "Dokončit úpravu",
"cmd.component_from_bodies": "Vytvořit komponenty z těles",
"cmd.component_hide_history": "Skrýt historii komponenty",
"cmd.component_insert_design": "Vložit návrh",
"cmd.component_links": "Seznam odkazů",
"cmd.component_list": "Seznam komponent",
@@ -878,7 +875,6 @@
"cmd.component_rename": "Přejmenovat komponentu",
"cmd.component_replace_with_link": "Nahradit odkazem",
"cmd.component_revert_position": "Vrátit polohu",
"cmd.component_show_history": "Zobrazit historii komponenty",
"cmd.component_unlock": "Odemknout obsah",
"cmd.component_update_links": "Aktualizovat odkazy",
"cmd.config_activate": "Aktivovat konfiguraci",
@@ -889,7 +885,6 @@
"cmd.config_rule": "Pravidlo konfigurace",
"cmd.config_rules": "Pravidla konfigurace",
"cmd.config_table": "Zobrazit tabulku konfigurací",
"cmd.construct.plane.axis_point": "Rovina osou a bodem",
"cmd.construct_axis_edge": "Osa hranou",
"cmd.construct_axis_normal_to_face": "Osa kolmá k ploše",
"cmd.construct_axis_through_cylinder": "Osa válce/kužele/anuloidu",
@@ -938,11 +933,6 @@
"cmd.explode_show": "Zobrazit rozložený pohled",
"cmd.expr_evaluate": "Vyhodnotit výraz",
"cmd.face_delete": "Smazat plochu",
"cmd.file.export_3mf": "Exportovat 3MF",
"cmd.file.export_obj": "Exportovat OBJ",
"cmd.file.export_step": "Exportovat STEP",
"cmd.file.export_stl": "Exportovat STL",
"cmd.file.export_urdf": "Exportovat URDF",
"cmd.file_export": "Exportovat",
"cmd.file_insert_mesh": "Vložit síť",
"cmd.file_insert_step": "Vložit STEP",
@@ -960,7 +950,6 @@
"cmd.inspect_colored_components": "Zapnout barvy komponent",
"cmd.inspect_colored_components_disable": "Vypnout barvy komponent",
"cmd.inspect_compare": "Porovnat tělesa",
"cmd.inspect_compare_picture": "Porovnat obrázek",
"cmd.inspect_continuity": "Analýza spojitosti",
"cmd.inspect_curvature_comb": "Analýza hřebenu křivosti",
"cmd.inspect_curvature_map": "Mapa křivosti",
@@ -1051,9 +1040,6 @@
"cmd.pointcloud_edit": "Upravit mračno bodů",
"cmd.pointcloud_insert": "Vložit mračno bodů",
"cmd.pointcloud_list": "Mračna bodů",
"cmd.render.compare.divider": "Posunout dělicí čáru porovnání renderů",
"cmd.render.result.discard": "Zahodit výsledek renderu",
"cmd.render.result.select": "Vybrat výsledek renderu",
"cmd.render_aging_describe": "Vrstvené stárnutí povrchu",
"cmd.render_backends": "Renderovací jádra",
"cmd.render_batch": "Renderovat všechny záběry",
@@ -1268,7 +1254,6 @@
"cmd.timeline_rollback": "Přesunout značku časové osy",
"cmd.timeline_stop": "Zastavit historii",
"cmd.timeline_suppress": "Potlačit prvek",
"cmd.ui.assistant": "Zobrazit nebo skrýt Asistenta",
"cmd.view.render_browser": "Prohlížeč renderu",
"cmd.view.render_dock": "Přesunout panel renderu",
"cmd.view.render_focus": "Soustředěný pohled na render",
@@ -1303,11 +1288,9 @@
"cmd.view_tool": "Navigační nástroj",
"cmd.view_top": "Pohled shora",
"cmd.view_visual_style": "Vizuální styl",
"color.blue": "Modrá",
"color.copy": "Kliknutím zkopírujete hodnoty barvy",
"color.edit": "Kliknutím upravíte barvu",
"color.gamma": "Hodnoty barvy v gama prostoru 0–255",
"color.green": "Zelená",
"color.hex": "Hex",
"color.hex_help": "Zadejte #rrggbb nebo #rrggbbaa; šest číslic zachová neprůhlednost",
"color.hex_hint": "#rrggbb",
@@ -1315,7 +1298,6 @@
"color.linear": "Hodnoty barvy v lineárním prostoru 0–1",
"color.recent": "Nedávné barvy",
"color.recent_help": "Barvy, které vyberete v libovolném poli barvy, se zobrazí zde",
"color.red": "Červená",
"color.saturation": "Sytost (v procentech)",
"color.saturation_value": "Sytost a jas",
"color.selected": "Vybraná barva",
@@ -1323,8 +1305,6 @@
"color.step_up": "Zvýšit o krok",
"color.value": "Hodnota, tedy jas (v procentech)",
"config.column_filter_empty": "Žádné odpovídající sloupce",
"config.rule_parameter_columns": "Uvedeny jsou jen parametry, které mají v tabulce sloupec. Sloupec přidáte pomocí Přidat sloupec (+), abyste parametr mohli použít zde.",
"config.rule_suppression_columns": "Uvedeny jsou jen prvky, které mají v tabulce sloupec Potlačit. Sloupec přidáte pomocí Přidat sloupec (+), abyste mohli prvek řídit zde.",
"contact.components_count": "Počet komponent: {count}",
"contact.enabled_label": "povoleno",
"coupling.gear_1": "Kolo 1",
@@ -1373,11 +1353,7 @@
"dialog.pattern_sketch_not_object": "Vyberte tělesa nebo prvky: náčrt není ani jedno. Opakujte jeho křivky vlastním nástrojem Pole nebo Zrcadlit v náčrtu, nebo jej nejprve vysuňte.",
"dialog.select": "↖ Vybrat",
"dialog.selected_count": "↖ Vybráno: {count}",
"dialog.selection_constraint": "Vazba {id}",
"dialog.selection_external_curve": "{sketch} · křivka {id}",
"dialog.selection_guidance": "Vstup změníte jeho výběrem. Kliknutím na vybraný objekt jej odeberete; Ctrl/Cmd+klik výběr také přepíná. Zkontrolujte náhled a stiskněte OK.",
"dialog.selection_midpoint": "Střed · {id}",
"dialog.selection_profile": "{sketch} · profil {number}",
"dialog.selection_step": "{step} z {total}: Vyberte {input}",
"dialog.step": "Krok",
"document.changed_on_disk": "Soubor {name} byl změněn na disku",
@@ -1460,7 +1436,6 @@
"help.construct.axis.two_points": "Vytvoří konstrukční osu procházející dvěma body, vrcholy nebo body náčrtu. Použijte ji pro osu vedoucí mezi dvěma známými polohami.",
"help.construct.plane.along_path": "Vytvoří konstrukční rovinu kolmou k hraně nebo křivce v zadané poloze podél ní. Použijte to k náčrtu profilu pro tažení nebo příčného řezu podél cesty.",
"help.construct.plane.at_angle": "Vytvoří konstrukční rovinu natočenou o úhel od roviny nebo plochy kolem osy nebo hrany. Použijte ji pro šikmé náčrty, například u úhlového držáku.",
"help.construct.plane.axis_point": "Vytvoří konstrukční rovinu obsahující osu a bod mimo tuto osu. Vyberte osu počátku nebo konstrukční osu, přímou hranu nebo úsečku náčrtu a pak vrchol, bod náčrtu nebo konstrukční bod. Bod ležící na ose nemůže určit jednoznačnou rovinu.",
"help.construct.plane.midplane": "Vytvoří konstrukční rovinu uprostřed mezi dvěma rovinami nebo rovinnými plochami, nebo rovinu půlící jejich úhel, když se protínají pod úhlem. Použijte ji jako rovinu souměrnosti pro zrcadlení.",
"help.construct.plane.offset": "Vytvoří konstrukční rovinu rovnoběžnou s rovinou nebo rovinnou plochou ve zvolené vzdálenosti nebo procházející bodem. Použijte ji k náčrtu v dané výšce nad plochou.",
"help.construct.plane.perpendicular": "Vytvoří konstrukční rovinu, která obsahuje přímku nebo hranu a je kolmá k rovině nebo ploše. Použijte ji k náčrtu kolmo na model podél hrany.",
@@ -1837,8 +1812,6 @@
"joint_move.blocked_hint": "Zkontrolujte typ kloubu (např. Pevný), ukotvení, limity kloubu a hodnoty řízené parametry.",
"joint_move.blocked_title": "Tento díl nelze posunout",
"joint_move.coarse": "Krok otáčení blízko osy (°)",
"joint_move.conflict": "Kloub tohoto dílu je v konfliktu s ostatními: {joint}.",
"joint_move.conflict_hint": "Upravte, potlačte nebo smažte kloub v konfliktu, aby mohly díl umístit ostatní klouby.",
"joint_move.distance": "Vzdálenost od začátku",
"joint_move.done": "Hotovo",
"joint_move.fine": "Krok otáčení daleko od osy (°)",
@@ -1938,8 +1911,6 @@
"motion.track_name": "Stopa {number}",
"nav.alias.help": "Shift+Alt+tažení levým tlačítkem otáčí, Shift+Alt+tažení prostředním posouvá, Shift+Alt+tažení pravým přibližuje; tažení prostředním tlačítkem také posouvá a pravým otáčí",
"nav.blender.help": "Tažení prostředním tlačítkem otáčí, Shift+prostřední posouvá, Ctrl+prostřední přibližuje; tažení pravým tlačítkem kameru nemění",
"nav.creo.help": "Tažení prostředním tlačítkem otáčí, Shift+prostřední posouvá, Ctrl+prostřední se svislým tažením přibližuje (tažením dolů přiblížíte); přibližuje i kolečko",
"nav.creo.label": "Creo",
"nav.free_orbit.help": "Myš a 3D myš otáčejí přes oba póly a zachovávají natočení. Přichytávání os klávesou Shift je vypnuto.",
"nav.free_orbit.label": "Volné otáčení",
"nav.free_orbit.snap_help": "Myš a 3D myš otáčejí přes oba póly a zachovávají natočení. Podržením klávesy Shift při otáčení tažením přichytíte směr pohledu a natočení.",
@@ -2092,19 +2063,12 @@
"parts.family.button_head_cap_screw": "Šroub s půlkulovou hlavou a vnitřním šestihranem",
"parts.family.dowel_pin": "Válcový kolík (lícovaný)",
"parts.family.hex_bolt": "Šroub se šestihrannou hlavou",
"parts.family.hex_cap_screw_inch": "Šroub se šestihrannou hlavou (palcový)",
"parts.family.hex_nut": "Šestihranná matice",
"parts.family.hex_nut_inch": "Šestihranná matice (palcová)",
"parts.family.set_screw": "Stavěcí šroub s vnitřním šestihranem, s plochým koncem",
"parts.family.socket_head_cap_screw": "Šroub s válcovou hlavou a vnitřním šestihranem",
"parts.family.washer": "Plochá podložka",
"parts.family.washer_sae": "Plochá podložka, SAE (úzká)",
"parts.family.washer_uss": "Plochá podložka, USS (široká)",
"parts.holes": "Otvory (volitelné)",
"parts.placement_hint": "vyberte jeden nebo více otvorů, aby se do každého usadil díl, nebo OK umístí díl do počátku",
"parts.system": "Soustava jednotek",
"parts.system.inch": "Palcová",
"parts.system.metric": "Metrická",
"pattern.alignment_2": "Zarovnání 2",
"pattern.alignment_hint": "Počty zahrnují originál. Na střed rozloží kopie rovnoměrně; Převrátit volí stranu navíc u sudých počtů.",
"pattern.direction_1": "Směr 1",
@@ -2166,8 +2130,6 @@
"prefs.button_left": "Tažení levým tlačítkem",
"prefs.button_middle": "Tažení prostředním tlačítkem",
"prefs.button_right": "Tažení pravým tlačítkem",
"prefs.carousel_next": "Další",
"prefs.carousel_previous": "Předchozí",
"prefs.choose_font": "Vybrat soubor písma…",
"prefs.click": "Klik",
"prefs.contact_shading": "Měkké stínování kontaktů",
@@ -2180,8 +2142,6 @@
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Zakřivené plochy",
"prefs.curves": "Zakřivené plochy",
"prefs.cut_preview_tint": "Zobrazit odebraný materiál červeně",
"prefs.cut_preview_tint_help": "Náhled vyplní materiál, který prvek odebírá, i tvar, který nahrazuje, červeným průhledným stínem. Když je vypnuto, jsou tyto výplně zcela průhledné a kreslí se jen jejich obrysy.",
"prefs.dark_parts_shading": "Zachovat tvar tmavých dílů",
"prefs.dark_parts_shading_help": "Stínuje pohled v lineárním světle s jemným světlem oblohy, takže tmavé plochy zachovají přechod a nezčernají",
"prefs.dead_zone_note": "Šrafované pásmo je mrtvá zóna: pohyb uvnitř něj se ignoruje.",
@@ -2240,8 +2200,6 @@
"prefs.interface_language": "Jazyk rozhraní",
"prefs.interface_note": "Vzhled aplikace SolidCraft a rozmístění jejích částí.",
"prefs.interval_minutes": "{n} min",
"prefs.invert_curved_roll_arrows": "Obrátit zakřivené šipky natočení",
"prefs.invert_curved_roll_arrows_note": "Dvě zakřivené šipky v rohu krychle natáčejí pohled v rovině obrazovky. Zapnuto: šipka po směru hodinových ručiček natáčí pohled proti směru hodinových ručiček a šipka proti směru hodinových ručiček ho natáčí po směru. Vypnuto: každá šipka natáčí pohled tak, jak je zakřivená.",
"prefs.invert_navigator_arrows": "Obrátit šipky otáčení krychle pohledu",
"prefs.invert_navigator_arrows_note": "Šipky kolem krychle otáčejí kameru tím směrem, kam míří, jako ve Fusion a Inventor: šipka vlevo míří doprava a stáčí kameru doprava. Vypnuto: šipka otáčí krychli na stranu, na které leží, takže šipka vlevo stáčí kameru doleva. Zakřivené šipky natočení tím nejsou dotčeny.",
"prefs.keep_work_on_tab_switch": "Ponechat dialogy a nástroje otevřené při přepínání návrhů",
@@ -2301,8 +2259,6 @@
"prefs.placement": "Umístění",
"prefs.prefer_free_sketch_points": "Při překrytí bodů upřednostnit pohyblivé body náčrtu",
"prefs.preview_choices": "Náhled vašich voleb",
"prefs.preview_tint_new_faces": "Podbarvit nový povrch v náhledu",
"prefs.preview_tint_new_faces_help": "Náhled prvku, který mění těleso, může vykreslit povrch, který prvek přidává, barvou náhledu. Když je vypnuto, těleso se vykreslí samo o sobě.",
"prefs.print_face_highlight": "Zvýraznění tiskové plochy",
"prefs.print_face_highlight_tip": "Jak výrazně se tisková plocha tělesa v pohledu obarví a obtáhne. 0 % nezobrazí žádné zvýraznění.",
"prefs.profile_active": "Aktivní profil",
@@ -2600,10 +2556,6 @@
"render.base_color": "Základní barva · sRGB",
"render.base_metallic": "Kovový podíl",
"render.base_opaque_only": "Tento editor zachovává sémantiku neprůhledných těles. Pro optický průchod světla použijte samostatné ovládací prvky skla.",
"render.batch_all_shots": "Všechny záběry ({count})",
"render.batch_empty": "Před renderováním všech záběrů nejprve uložte záběr.",
"render.batch_limit": "Renderování všech záběrů je omezeno na 32 záběrů.",
"render.batch_status": "Probíhá: {running} · Čeká: {waiting}",
"render.bloom_enable": "Záře ve výstupu",
"render.bloom_help": "Kreativní záře z lineárních jasů se třemi normalizovanými průchody. Poloměr v pixelech výstupu, přičtení před sRGB. Nejde o simulaci čočky, objemu ani zlepšení konvergence. Surové EXR se nemění.",
"render.bloom_opaque": "Záře vyžaduje neprůhledný výstup. Průhledné PNG se odmítne, neprovede se skryté sloučení.",
@@ -2714,13 +2666,6 @@
"render.choose_look": "Nejprve vyberte styl ve Vzhledech",
"render.chromatic_offset": "Chromatický posun hran",
"render.clear_stage": "Odebrat uloženou scénografii",
"render.compare_divider": "Dělicí čára porovnání",
"render.compare_live": "Živě",
"render.compare_live_preview": "Živý náhled",
"render.compare_render": "Render {number}",
"render.compare_side": "Render {number} · {shot}",
"render.compare_summary": "{shot} · {width} × {height} · PNG · {seconds} s · {renderer} · zachyceno ve revizi {revision}",
"render.compare_swap": "Prohodit",
"render.copy_request": "Kopírovat JSON",
"render.cpu_aa_help": "Prostorové vzorky: více vzorků zlepší hrany a prodlouží vykreslení.",
"render.cpu_area_add": "Přidat obdélníkové světlo CPU",
@@ -3470,10 +3415,6 @@
"render.result_retained": "Zobrazuje se poslední hotový obrázek; novější vykreslení jej zatím nenahradilo.",
"render.result_sequence": "Kontrolní bod",
"render.result_title": "Výsledek vykreslení",
"render.results_choose": "Ponechané výsledky",
"render.results_label": "Render {number} · {shot} · {seconds} s",
"render.results_stale_label": "{result} [zastaralý]",
"render.results_unnamed": "Obrázek",
"render.return_design": "Zpět k návrhu",
"render.review_stale": "Tato kontrola je zastaralá. Před použitím ji vyřešte nebo proveďte znovu.",
"render.ribbon_aging": "Stárnutí",
@@ -3957,7 +3898,10 @@
"the two origins are on the same component": "oba počátky jsou na téže komponentě",
"thread.automatic_size": "Automatická velikost",
"thread.designation": "Označení",
"thread.direction": "Směr závitu",
"thread.iso_metric": "Metrický ISO",
"thread.left_hand": "Levotočivý",
"thread.right_hand": "Pravotočivý",
"thread.select_size": "Vybrat velikost",
"timeline.delete_after_marker_one": "Smazat 1 prvek za značkou historie",
"timeline.delete_after_marker_other": "Smazat prvky za značkou historie: {count}",
@@ -4131,7 +4075,6 @@
"ui.agent_chat_auth_terminal": "Spusťte toto v terminálu a pak znovu stiskněte Připojit:",
"ui.agent_chat_auth_title": "Přihlásit se k agentovi",
"ui.agent_chat_cancel": "Zrušit",
"ui.agent_chat_cancel_timeout": "Agent na Zastavit neodpověděl a byl odpojen. Pro pokračování se připojte znovu.",
"ui.agent_chat_cannot_start": "Nainstalováno, ale nespouští se: {error}",
"ui.agent_chat_changes": "Změny: {n}, jeden krok zpět",
"ui.agent_chat_changes_n": "Změn: {n}",
@@ -4141,13 +4084,11 @@
"ui.agent_chat_command": "Příkaz agenta",
"ui.agent_chat_connect": "Připojit",
"ui.agent_chat_copy": "Kopírovat",
"ui.agent_chat_copy_message": "Kopírovat zprávu",
"ui.agent_chat_ctx_bodies": "Tělesa",
"ui.agent_chat_ctx_elements": "Elementy",
"ui.agent_chat_ctx_features": "Prvky",
"ui.agent_chat_ctx_more": "+{n} dalších",
"ui.agent_chat_ctx_other": "Jiné",
"ui.agent_chat_custom_heading": "Vlastní model",
"ui.agent_chat_custom_hint": "Jakýkoli program, který komunikuje protokolem Agent Client Protocol přes stdin a stdout. Nespouští se přes shell.",
"ui.agent_chat_denied": "Zamítnuto",
"ui.agent_chat_deny": "Zamítnout",
@@ -4191,16 +4132,8 @@
"ui.agent_chat_mode_all": "Vše",
"ui.agent_chat_mode_ask": "Ptát se",
"ui.agent_chat_mode_edits": "Úpravy",
"ui.agent_chat_model_custom": "Použít vlastní: {model}",
"ui.agent_chat_model_no_tools": "{model} nemůže používat nástroje SolidCraftu, takže umí jen chatovat. Vyberte model s podporou nástrojů.",
"ui.agent_chat_model_rejected": "Agent nepřijal model {model}: {error}",
"ui.agent_chat_model_search": "Hledat modely…",
"ui.agent_chat_model_tip": "Model, který agent používá. Vaše volba se pro tohoto agenta zapamatuje.",
"ui.agent_chat_models_failed": "Seznam modelů se nepodařilo načíst: {error}",
"ui.agent_chat_models_heading": "Dostupné modely",
"ui.agent_chat_models_listing": "Načítání seznamu modelů…",
"ui.agent_chat_models_none": "Endpoint neuvádí žádné modely",
"ui.agent_chat_models_not_a_list": "Odpověď nebyla seznam modelů",
"ui.agent_chat_new": "Nový chat",
"ui.agent_chat_new_confirm": "Začít nový chat? Aktuální bude vymazán.",
"ui.agent_chat_next_picture": "Další obrázek",
@@ -4474,21 +4407,14 @@
"ui.construction.contact_hint": "Tečna: vyberte kružnici/oblouk nebo otevřený splajn/kuželosečku, pak bod dotyku (volné křivky: koncové body)",
"ui.construction.external_hint": "Tečna: vyberte kružnici nebo oblouk, vnější bod, pak požadované řešení",
"ui.construction.free_hint": "Konstrukční přímka: klikněte na dva body; pravým tlačítkem začnete znovu",
"ui.construction.normal_needs_length": "Nekonečná přímka je vždy konstrukční: zadejte délku, abyste nakreslili běžnou úsečku",
"ui.construction.point_hint": "Konstrukční přímka: klikněte na bod, kterým má procházet",
"ui.construction.reference_hint": "Konstrukční přímka: vyberte přímku, pak bod; pravým tlačítkem změníte referenci",
"ui.construction.ribbon_label": "Pom. přímky",
"ui.construction.side_hint": "Konstrukční přímka: klikněte na stranu, kterým směrem přímka vede; pravým tlačítkem začnete znovu",
"ui.context_menu": "Místní nabídka",
"ui.continuity_crease": "Hrana",
"ui.continuity_curvature": "Křivost",
"ui.continuity_curvature_change": "Změna křivosti",
"ui.continuity_g0": "G0 hrana",
"ui.continuity_g1": "G1 tečně",
"ui.continuity_g2": "G2 křivost",
"ui.continuity_gap": "Mezera",
"ui.continuity_position": "Poloha",
"ui.continuity_tangent": "Tečnost",
"ui.copy": "Kopírovat",
"ui.corner": "Roh",
"ui.corners": "Rohy",
@@ -4749,7 +4675,6 @@
"ui.hideOrigin": "Skrýt počátek",
"ui.hidePlanes": "Skrýt roviny",
"ui.hideProfile": "Skrýt profil",
"ui.hide_component_history": "Skrýt její historii",
"ui.hide_origin_by_default": "Ve výchozím stavu skrýt počátek",
"ui.hide_origin_note": "Nové návrhy začínají se skrytou složkou Počátek. Návrh si ponechá vlastní nastavení počátku, jakmile ho zobrazíte nebo skryjete.",
"ui.history_back": "O krok zpět",
@@ -4757,7 +4682,6 @@
"ui.history_edge_hint": "dvakrát klikněte na hranu",
"ui.history_first": "Přejít na začátek",
"ui.history_forward": "O krok vpřed",
"ui.history_hide_hidden": "Skrýt skryté komponenty",
"ui.history_hide_names": "Skrýt názvy",
"ui.history_last": "Přejít na konec",
"ui.history_marker_tip": "Značka historie: přetažením vrátíte model zpět; nové prvky se vkládají sem",
@@ -4767,12 +4691,10 @@
"ui.history_play_button_show": "Zobrazit tlačítko Přehrát",
"ui.history_play_finish_action": "Před přehráním historie dokončete aktuální akci",
"ui.history_play_roll_back": "Před přehráním posuňte značku historie zpět",
"ui.history_show_hidden": "Zobrazit skryté komponenty",
"ui.history_show_names": "Zobrazit názvy",
"ui.history_sketch_listed_under": "Uvedeno pod prvkem {feature}",
"ui.history_title": "Historie",
"ui.history_toggle_names": "Zobrazit nebo skrýt názvy prvků",
"ui.history_waits_for_editor": "Zpět, Znovu a Historie počkají, dokud se neotevře editor prvku",
"ui.hole": "Otvor",
"ui.hole_tap_type": "Typ závitu otvoru",
"ui.hole_type": "Typ otvoru",
@@ -5053,7 +4975,6 @@
"ui.showDimension": "Zobrazit kótu",
"ui.showOrigin": "Zobrazit počátek",
"ui.showPlanes": "Zobrazit roviny",
"ui.show_component_history": "Zobrazit její historii",
"ui.show_constraints": "Vazby",
"ui.show_construction_geometries": "Konstrukční geometrie",
"ui.show_dimensions": "Kóty",
@@ -5236,7 +5157,7 @@
"update.check_unavailable": "Pro tuto instalaci není kontrola aktualizací dostupná.",
"update.checking": "Kontrola…",
"update.copy": "Kopírovat",
"update.dmg_opened": "Obraz disku je otevřený a ověřený: ukončete SolidCraft, přetáhněte nový SolidCraft do Aplikací místo staré kopie a poté SolidCraft znovu otevřete.",
"update.dmg_opened": "Obraz disku je otevřený a ověřený: přetáhněte SolidCraft do Aplikací místo staré kopie a poté SolidCraft restartujte.",
"update.download": "Stáhnout",
"update.download_open": "Stáhnout a otevřít",
"update.downloading": "Stahování a ověřování… {done} z {total} MB",
@@ -5280,7 +5201,6 @@
"web.open_link": "Otevřít propojený návrh?",
"web.open_link_confirm": "Existují neuložené změny. Otevřít propojený návrh na nové kartě?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vybrat · Enter / Tab doplnit · Esc zavřít",
"↑ ↓ choose · Enter applies this value": "↑ ↓ výběr · Enter použije tuto hodnotu",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ výběr · Enter použije, co jste napsali · Esc zavře",
"⌘Enter continues on GitHub.": "⌘Enter pokračuje na GitHubu."
}

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