mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ceb187308 | |||
| 8263253ad4 | |||
| 77d3eb5cd8 | |||
| 7207472fab | |||
| 2c9d3bfa4b | |||
| a920fa2625 | |||
| 058c05c686 | |||
| 741a2308f4 |
@@ -10,9 +10,6 @@ Changes a default: no
|
||||
Changes a default: yes — <what>, old behaviour kept behind preference <name>
|
||||
Docs-only and bot PRs are exempt. -->
|
||||
|
||||
<!-- A kernel change (solidcore/, crates/smlib/) that changes or deletes lines of an existing test or golden expectation needs a line saying why the old test was wrong (adding tests needs nothing):
|
||||
Test change: <reason> -->
|
||||
|
||||
## Validation
|
||||
|
||||
What did you run, and what were the results? Include expected and measured values for geometry changes.
|
||||
|
||||
@@ -446,11 +446,7 @@ jobs:
|
||||
- name: Design format pending changes
|
||||
run: bash scripts/test-format-pending.sh
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
# Not blocking while #14591 is open: the check "the open v3 page was given another build's
|
||||
# file after the update" fails in about one queue batch in a few, with no PR to blame. A
|
||||
# failure is reported as a warning (and still printed in this step's log), not as a failure.
|
||||
run: |
|
||||
cargo xtask web-offline-test || echo "::warning title=Web app offline test failed (non-blocking, #14591)::cargo xtask web-offline-test failed; see this step's log. Tracked in #14591."
|
||||
run: cargo xtask web-offline-test
|
||||
- name: Affected-package selection self-test
|
||||
run: python3 -I scripts/ci/test_affected.py
|
||||
- name: Merge-of-main detection self-test
|
||||
|
||||
@@ -84,8 +84,7 @@ jobs:
|
||||
# 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
|
||||
# The checkout fetches history without file contents (below), so this takes well under a minute.
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
@@ -94,11 +93,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# A pull request's diff needs its base's history: the commits and trees, not every file
|
||||
# version. A full clone of every blob took over the 5-minute timeout on a 2-vCPU runner and
|
||||
# cancelled the merge queue's rig selection (Select the rig areas) again and again.
|
||||
# A pull request's diff needs its base's history.
|
||||
fetch-depth: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && '0' || '1' }}
|
||||
filter: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && 'blob:none' || '' }}
|
||||
- name: Selection self-test
|
||||
run: python3 -I scripts/ci/test_kernel_rig_plan.py
|
||||
- id: plan
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Four quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, the "Changes a default:" line is filled in, a PR
|
||||
# adding to the showcase shows a picture, and a kernel fix that changes or deletes existing test or
|
||||
# golden lines says why on a "Test change:" line. None is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
# Three quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, the "Changes a default:" line is filled in, and
|
||||
# a PR adding to the showcase shows a picture. None is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
name: PR checks
|
||||
|
||||
on:
|
||||
@@ -64,21 +63,6 @@ jobs:
|
||||
const r = await checks.checkPicture({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
test-change:
|
||||
name: Test change check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
const r = await checks.checkTestChange({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
# Labelling needs write access, which a fork PR's token never has, so it is its own job and best
|
||||
# effort: a fork PR just doesn't get the `changes-default` label from here.
|
||||
label:
|
||||
|
||||
Generated
+38
-139
@@ -730,42 +730,18 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"polling",
|
||||
"rustix 1.1.6",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
dependencies = [
|
||||
"calloop 0.13.0",
|
||||
"calloop",
|
||||
"rustix 0.38.44",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
|
||||
dependencies = [
|
||||
"calloop 0.14.5",
|
||||
"rustix 1.1.6",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -1306,7 +1282,6 @@ dependencies = [
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"smithay-clipboard",
|
||||
"web-time",
|
||||
"webbrowser",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -4198,8 +4173,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"calloop 0.13.0",
|
||||
"calloop-wayland-source 0.3.0",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4216,44 +4191,6 @@ dependencies = [
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"calloop 0.14.5",
|
||||
"calloop-wayland-source 0.4.1",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"rustix 1.1.6",
|
||||
"thiserror 2.0.21",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-csd-frame",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-experimental",
|
||||
"wayland-protocols-misc",
|
||||
"wayland-protocols-wlr",
|
||||
"wayland-scanner",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-clipboard"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"smithay-client-toolkit 0.20.0",
|
||||
"wayland-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.2"
|
||||
@@ -4322,7 +4259,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4365,11 +4302,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4385,11 +4322,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-color"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-control"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"serde",
|
||||
@@ -4398,7 +4335,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-control-egui"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"image",
|
||||
@@ -4408,7 +4345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4417,7 +4354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4427,7 +4364,6 @@ dependencies = [
|
||||
"solidcore-tess",
|
||||
"solidcraft-color",
|
||||
"solidcraft-derive",
|
||||
"solidcraft-format",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
@@ -4440,7 +4376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-dynamics"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4451,7 +4387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4475,18 +4411,9 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-format"
|
||||
version = "0.40.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4499,7 +4426,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4524,7 +4451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4542,7 +4469,6 @@ dependencies = [
|
||||
"solidcore-record",
|
||||
"solidcraft-color",
|
||||
"solidcraft-doc",
|
||||
"solidcraft-format",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
@@ -4553,7 +4479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4574,7 +4500,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-look"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"log",
|
||||
@@ -4586,7 +4512,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
@@ -4600,14 +4526,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp-server"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-profile"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"web-time",
|
||||
@@ -4615,7 +4541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4638,7 +4564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"harfrust",
|
||||
"serde",
|
||||
@@ -4652,7 +4578,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"log",
|
||||
@@ -4666,18 +4592,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-testkit"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-theme"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
@@ -4687,7 +4613,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4729,7 +4655,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-kit"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4752,14 +4678,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-registry"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-text"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
@@ -4769,7 +4695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-viewport"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -4781,7 +4707,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-viewport-egui"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
@@ -4792,7 +4718,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-viewport-gpu"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4808,7 +4734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5126,7 +5052,6 @@ version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -5527,32 +5452,6 @@ dependencies = [
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-experimental"
|
||||
version = "20250721.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-misc"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.12"
|
||||
@@ -6158,7 +6057,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.5.1",
|
||||
"bytemuck",
|
||||
"calloop 0.13.0",
|
||||
"calloop",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -6179,7 +6078,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.44",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
@@ -6321,7 +6220,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"proc-macro2",
|
||||
|
||||
+1
-3
@@ -6,7 +6,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["solidcore", "vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.40.1"
|
||||
version = "0.40.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -25,8 +25,6 @@ solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-dynamics = { path = "crates/dynamics" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
# Versioned file formats: the format tag, the version registry and its upgrade, a newer file told
|
||||
solidcraft-format = { path = "crates/format" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
solidcraft-engine = { path = "crates/engine" }
|
||||
solidcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
|
||||
@@ -1750,31 +1750,3 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
assert_eq!(fingerprint(), first);
|
||||
}
|
||||
}
|
||||
|
||||
// #13295: a circle sketched on the plane at 45 degrees through a bar's step edge, 7.9e-6 off
|
||||
// that edge (as the reporter's sketch had it), cut through all across a hole. The cut wall's
|
||||
// seam edge ends on the bar's edge a few micrometres across the seam, inside its vertex's
|
||||
// tolerance, and the check refused the result ("Face Near missing Seam").
|
||||
#[test]
|
||||
fn a_circle_cut_from_a_slanted_plane_through_a_step_edge_beside_a_hole() {
|
||||
let mut s = Session::default();
|
||||
let (y0, z1, yc, z2) = (16.423877938511588, 13.06859205815335, 12.527075811882755, 19.566787029154483);
|
||||
let p = [[0.0, 0.0], [y0, 0.0], [y0, z1], [yc, z1], [yc, z2], [0.0, z2]];
|
||||
s.execute("sketch.create", &json!({"plane": "YZ"})).unwrap();
|
||||
for i in 0..6 {
|
||||
s.execute("sketch.line", &json!({"points": [p[i], p[(i + 1) % 6]]})).unwrap();
|
||||
}
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
s.execute("solid.extrude", &json!({"distance": 100, "operation": "new"})).unwrap();
|
||||
let start = s.doc.features.len();
|
||||
let hole = json!({"base": [20.421614627576027, y0 + 5.0, 6.534296029076675], "axis": [0, -1, 0], "radius": 3.5355339059327378, "height": y0 + 10.0, "operation": "cut", "targets": ["Body1"]});
|
||||
s.execute("solid.cylinder", &hole).unwrap();
|
||||
let r = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let plane = json!({"origin": [0, yc - yc * r, z2 + yc * r], "x_dir": [1, 0, 0], "y_dir": [0, r, -r]});
|
||||
s.execute("sketch.create", &json!({"plane": plane, "name": "Slant"})).unwrap();
|
||||
s.execute("sketch.circle.center", &json!({"center": [20.4216146277246, 12.527083680740917], "radius": 4.716990566008734})).unwrap();
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
s.execute("solid.extrude", &json!({"sketch": "Slant", "through_all": true, "direction": "negative", "operation": "cut", "targets": ["Body1"]}))
|
||||
.unwrap();
|
||||
check(&s, start, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@ solidcraft-look = { workspace = true }
|
||||
solidcraft-ui-text = { workspace = true }
|
||||
solidcraft-theme = { workspace = true }
|
||||
solidcraft-space-mouse = { workspace = true }
|
||||
# "clipboard" is not a default here: eframe 0.37 builds egui-winit without it, and then every
|
||||
# Copy and Paste on the desktop silently does nothing (see src/clipboard_tests.rs).
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links", "clipboard"] }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
|
||||
egui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//! The desktop build keeps the system clipboard.
|
||||
//!
|
||||
//! eframe 0.37 builds egui-winit without its clipboard unless asked, and the workspace turns
|
||||
//! eframe's defaults off: every Copy (the error text, Copy diagnostic information, Ctrl+C in a
|
||||
//! field) then reaches the platform as a command that nothing carries out. The UI tests see the
|
||||
//! copied text in egui's output and still pass, so this is checked where the build is described.
|
||||
|
||||
#[test]
|
||||
fn the_desktop_build_enables_eframes_clipboard() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let eframe = manifest.lines().find(|line| line.trim_start().starts_with("eframe =")).expect("the eframe dependency");
|
||||
assert!(eframe.contains("\"clipboard\""), "eframe without its clipboard feature drops every copy on the desktop: {eframe}");
|
||||
}
|
||||
@@ -20,8 +20,6 @@
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
#[cfg(test)]
|
||||
mod clipboard_tests;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
+8
-190
@@ -247,7 +247,6 @@
|
||||
"Drag to move the hole; click to remove it": "Arrossegueu per moure el forat; feu clic per treure'l",
|
||||
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Arrossegueu per moure el recolzament pel model. S'ajusta als punts del bocet de la cara.",
|
||||
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Arrossegueu per moure l'encaix a pressió pel model. S'ajusta als punts del bocet de la cara.",
|
||||
"Drag to place it on another face.": "Arrossegueu-lo per col·locar-lo en una altra cara.",
|
||||
"Drag to point the catch in another direction.": "Arrossegueu per orientar el retén en una altra direcció.",
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Arrossegueu per girar el recolzament de 5° en 5°. Manteniu Alt o Ctrl per girar-lo lliurement.",
|
||||
"Drive Joint": "Accionar la unió",
|
||||
@@ -514,7 +513,6 @@
|
||||
"Offset Y": "Desplaçament Y",
|
||||
"Offset Z": "Desplaçament Z",
|
||||
"On your computer": "Al vostre equip",
|
||||
"One more stretch of the picked edge, each with its own angle, height and turns": "Un tram més de l'aresta triada, cadascun amb el seu angle, altura i girs",
|
||||
"Opacity": "Opacitat",
|
||||
"Open GitHub": "Obrir GitHub",
|
||||
"Open GitHub draft": "Obrir l'esborrany de GitHub",
|
||||
@@ -636,7 +634,6 @@
|
||||
"Remove the track": "Eliminar la pista",
|
||||
"Remove this bend": "Treure aquest plegat",
|
||||
"Remove this point": "Eliminar aquest punt",
|
||||
"Remove this segment": "Eliminar aquest segment",
|
||||
"Rename Feature": "Canviar el nom de l'operació",
|
||||
"Rename Folder": "Canviar el nom de la carpeta",
|
||||
"Render Stage": "Escenari de renderitzat",
|
||||
@@ -726,7 +723,6 @@
|
||||
"Section position": "Posició de la secció",
|
||||
"Sections": "Seccions",
|
||||
"Sections (one sketch each)": "Seccions (un bocet cadascuna)",
|
||||
"Segments": "Segments",
|
||||
"Select Features": "Seleccionar les operacions",
|
||||
"Select Other": "Seleccionar-ne un altre",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar-ne un altre: manteniu el botó esquerre quiet sobre el model (o feu clic amb el botó dret › Seleccionar-ne un altre…) per llistar tot el que hi ha sota el punter; torneu a fer clic al mateix lloc per seleccionar el següent element. Feu doble clic amb el botó central per tornar a Inici (Preferències › Navegació).",
|
||||
@@ -776,7 +772,6 @@
|
||||
"Spacing must be a non-zero number": "L'espaiat ha de ser un nombre diferent de zero",
|
||||
"Specialise": "Especialitzar",
|
||||
"Speed": "Velocitat",
|
||||
"Split Edge": "Dividir aresta",
|
||||
"Splitting tool": "Eina de divisió",
|
||||
"Square": "Quadrat",
|
||||
"Stainless Steel": "Acer inoxidable",
|
||||
@@ -937,20 +932,13 @@
|
||||
"ai.http.confirm": "Permetre per a aquest servei",
|
||||
"ai.http.warning": "La clau de l'API i la conversa s'enviaran sense xifrar. Qualsevol persona amb accés a la xarxa les pot llegir o canviar. Feu-ho servir només en una xarxa de confiança.",
|
||||
"ai.prefs.allow_control": "Permetre que es connectin agents d'IA",
|
||||
"ai.prefs.allow_control_note": "Un canal local (MCP). No surt res d'aquest ordinador.\n\nObre un canal de control només en aquest equip (127.0.0.1, al port de sota; 27878 pren el següent lliure quan està en ús). El programa que es connecti també ha de presentar un token secret que només pot llegir el vostre compte d'usuari, generat de nou a cada arrencada. S'aplica a l'instant. Desactivat: no s'escolta res tret que inicieu SolidCraft amb --control.",
|
||||
"ai.prefs.allow_control_note": "Un canal local (MCP). No surt res d'aquest ordinador.\n\nObre un canal de control només en aquest equip (127.0.0.1, port 27878 o el següent lliure). El programa que es connecti també ha de presentar un token secret que només pot llegir el vostre compte d'usuari, generat de nou a cada arrencada. S'aplica la propera vegada que s'iniciï SolidCraft. Desactivat: no s'escolta res tret que inicieu SolidCraft amb --control.",
|
||||
"ai.prefs.availability": "Disponibilitat",
|
||||
"ai.prefs.background_commands": "Executar les ordres de l'agent en segon pla",
|
||||
"ai.prefs.connect_apps": "Posar SolidCraft a disposició de les aplicacions d'IA d'aquest equip",
|
||||
"ai.prefs.connect_apps_note": "Desactivat: treu les entrades que havia afegit.\n\nEn iniciar SolidCraft, l'afegeix com a servidor MCP a les aplicacions d'IA que trobi en aquest equip (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), perquè un agent el pugui fer servir sense configurar res. Només s'afegeix l'entrada solidcraft o se n'actualitza el camí; una entrada que hàgiu canviat es deixa intacta, i el fitxer es copia abans. Desactivat: s'eliminen les entrades que havia afegit. Ajuda › Connectar assistents d'IA… ho fa a mà.",
|
||||
"ai.prefs.connect_at_start": "Connectar el darrer agent en iniciar",
|
||||
"ai.prefs.connect_at_start_note": "Inicia en segon pla l'agent que vau connectar per última vegada (o el predeterminat) cada vegada que s'inicia SolidCraft, perquè estigui a punt per xatejar. No quan SolidCraft s'inicia amb --control. Si no es pot connectar, l'Assistent mostra Connexió fallida i un botó Reintentar.",
|
||||
"ai.prefs.control_failed": "No s'ha pogut iniciar el canal de control: {error}",
|
||||
"ai.prefs.control_from_launch": "Iniciat amb --control al port {port} per a aquesta execució. Els canvis d'aquí s'apliquen la propera vegada que SolidCraft s'iniciï sense --control.",
|
||||
"ai.prefs.control_listening": "Escoltant a 127.0.0.1:{port}",
|
||||
"ai.prefs.control_not_listening": "No està escoltant",
|
||||
"ai.prefs.control_port": "Port",
|
||||
"ai.prefs.control_port_note": "El port al qual es connecten els agents d'IA en aquest equip (127.0.0.1). 27878 és el predeterminat: quan està en ús, es pren el següent lliure. Qualsevol altre port s'usa exactament. Apunteu el vostre client MCP al mateix port, per exemple solidcraft-cli mcp --connect 127.0.0.1:7878.",
|
||||
"ai.prefs.control_port_taken": "El port {port} està en ús per un altre programa (o una altra finestra de SolidCraft). Trieu un altre port.",
|
||||
"ai.prefs.follow_camera": "Girar la càmera cap als bocets",
|
||||
"ai.prefs.follow_camera_off": "El seguiment automàtic de la càmera està desactivat.",
|
||||
"ai.prefs.follow_camera_on": "La càmera gira cap als bocets que crea l'agent.",
|
||||
@@ -1011,13 +999,6 @@
|
||||
"browser.sketches_to_parts_not_on_face": "no en una cara de peça",
|
||||
"browser.sketches_to_parts_part_missing": "la seva peça ha desaparegut",
|
||||
"browser.sketches_to_parts_result": "S'han mogut {moved} bocets a la peça on són; {stayed} s'han quedat: {why}",
|
||||
"browser.straighten_angle": "Direcció de l'aresta",
|
||||
"browser.straighten_choose": "Trieu Horitzontal, Vertical o Angle per a l'aresta i premeu Redreçar.",
|
||||
"browser.straighten_click_a": "Feu clic a la imatge en un extrem d'una aresta que hagi de quedar recta, o escriviu-ne les coordenades.",
|
||||
"browser.straighten_click_b": "Feu clic a l'altre extrem de l'aresta, o escriviu-ne les coordenades.",
|
||||
"browser.straighten_horizontal": "Horitzontal",
|
||||
"browser.straighten_title": "Redreçar: {name}",
|
||||
"browser.straighten_vertical": "Vertical",
|
||||
"browser.units": "Unitats: {units}",
|
||||
"cam.flat": "Cara plana",
|
||||
"cam.follower": "Seguidor",
|
||||
@@ -1047,8 +1028,6 @@
|
||||
"cmd.appearance_pick_up": "Agafar l'aparença",
|
||||
"cmd.appearance_replace": "Substituir l'aparença",
|
||||
"cmd.assembly_bom": "Llista de materials",
|
||||
"cmd.assembly_constraint_create": "Restringir",
|
||||
"cmd.assembly_constraint_edit": "Editar la restricció",
|
||||
"cmd.assembly_mass_properties": "Propietats de massa de l'ensamblatge",
|
||||
"cmd.body.print_face": "Cara d'impressió",
|
||||
"cmd.body_rename": "Canviar el nom del cos",
|
||||
@@ -1067,7 +1046,6 @@
|
||||
"cmd.canvas_delete": "Eliminar el llenç",
|
||||
"cmd.canvas_edit": "Editar el llenç",
|
||||
"cmd.canvas_insert": "Llenç",
|
||||
"cmd.canvas_straighten": "Redreçar el llenç",
|
||||
"cmd.canvas_to_sketch": "Llenç a bocet",
|
||||
"cmd.component_activate": "Activar el component",
|
||||
"cmd.component_break_link": "Trencar el vincle",
|
||||
@@ -1109,10 +1087,7 @@
|
||||
"cmd.config_rule": "Regla de configuració",
|
||||
"cmd.config_rules": "Regles de configuració",
|
||||
"cmd.config_table": "Mostrar la taula de configuracions",
|
||||
"cmd.construct.axis": "Eix",
|
||||
"cmd.construct.plane": "Pla",
|
||||
"cmd.construct.plane.axis_point": "Pla per eix i punt",
|
||||
"cmd.construct.point": "Punt",
|
||||
"cmd.construct_axis_edge": "Eix per una aresta",
|
||||
"cmd.construct_axis_normal_to_face": "Eix perpendicular a una cara",
|
||||
"cmd.construct_axis_through_cylinder": "Eix per un cilindre, con o tor",
|
||||
@@ -1175,7 +1150,6 @@
|
||||
"cmd.file_save_mesh": "Desar com a malla",
|
||||
"cmd.file_send_to_slicer": "Enviar al laminador",
|
||||
"cmd.file_slicers": "Laminadors instal·lats",
|
||||
"cmd.frame_end_cap": "Tapa d'extrem",
|
||||
"cmd.frame_member": "Element estructural",
|
||||
"cmd.frame_profile_insert": "Inserir un perfil",
|
||||
"cmd.frame_profile_list": "Biblioteca de perfils",
|
||||
@@ -1196,7 +1170,6 @@
|
||||
"cmd.inspect_isocurve": "Anàlisi d'isocorbes",
|
||||
"cmd.inspect_minimum_radius": "Anàlisi de radi mínim",
|
||||
"cmd.inspect_parameter_sweep": "Escombrat de paràmetres",
|
||||
"cmd.inspect_parameter_variants": "Comparar variants de paràmetres",
|
||||
"cmd.inspect_silhouette": "Silueta",
|
||||
"cmd.inspect_zebra": "Anàlisi de franges de zebra",
|
||||
"cmd.joint_as_built": "Unió segons la posició actual",
|
||||
@@ -1250,10 +1223,6 @@
|
||||
"cmd.motion_trace": "Traça del moviment",
|
||||
"cmd.motion_trace_edit": "Editar la traça del moviment",
|
||||
"cmd.motion_trace_keep": "Conservar la traça del moviment",
|
||||
"cmd.note_add": "Afegir una nota",
|
||||
"cmd.note_delete": "Eliminar la nota",
|
||||
"cmd.note_list": "Llistar les notes",
|
||||
"cmd.note_update": "Editar la nota",
|
||||
"cmd.occurrence_align": "Alinear la instància",
|
||||
"cmd.occurrence_copy": "Enganxar (instància)",
|
||||
"cmd.occurrence_drag": "Arrossegar la instància",
|
||||
@@ -1379,7 +1348,6 @@
|
||||
"cmd.render_start": "Renderitzar la imatge",
|
||||
"cmd.render_status": "Progrés del renderitzat",
|
||||
"cmd.select_clear": "Esborrar la selecció",
|
||||
"cmd.selection_get": "Obtenir la selecció",
|
||||
"cmd.settings_profile_apply": "Aplicar el perfil d'ajustos",
|
||||
"cmd.settings_profile_delete": "Eliminar el perfil d'ajustos",
|
||||
"cmd.settings_profile_export": "Exportar el perfil d'ajustos",
|
||||
@@ -1543,7 +1511,6 @@
|
||||
"cmd.timeline_suppress": "Suprimir l'operació",
|
||||
"cmd.ui.assistant": "Alternar l'Assistent",
|
||||
"cmd.ui.palette": "Cercar ordres",
|
||||
"cmd.ui.toggle_visibility": "Mostrar/amagar la selecció",
|
||||
"cmd.view.navigator": "Navegador de vistes personalitzat",
|
||||
"cmd.view.render_browser": "Explorador de render",
|
||||
"cmd.view.render_dock": "Moure l'acoblament del render",
|
||||
@@ -1601,7 +1568,6 @@
|
||||
"color.step_down": "Baixar un pas",
|
||||
"color.step_up": "Pujar un pas",
|
||||
"color.value": "Valor, la brillantor (percentatge)",
|
||||
"component.error.ambiguous_name": "Diversos components es diuen `{name}`. Feu servir un ID numèric de component: {ids}.",
|
||||
"config.column_filter_empty": "No hi ha columnes coincidents",
|
||||
"config.rule_parameter_columns": "Només es llisten els paràmetres amb una columna a la taula. Afegiu-ne una amb Afegir una columna (+) per fer-la servir aquí.",
|
||||
"config.rule_suppression_columns": "Només es llisten les operacions amb una columna Suprimir a la taula. Afegiu-ne una amb Afegir una columna (+) per controlar-la aquí.",
|
||||
@@ -1642,7 +1608,6 @@
|
||||
"diagnostic.reselect_reference": "{feature}: la referència `{reference}` ja no existeix tal com es va triar; torneu-la a seleccionar",
|
||||
"dialog.bodies_count": "{count} cossos",
|
||||
"dialog.clearance_invalid": "la folgança ha de ser una longitud de 0 o més",
|
||||
"dialog.component_origin": "{placement} · {item}",
|
||||
"dialog.compute": "Calcular",
|
||||
"dialog.compute_again": "Calcular de nou",
|
||||
"dialog.computing": "Calculant… {seconds} s",
|
||||
@@ -1655,12 +1620,10 @@
|
||||
"dialog.dimension_step_expression": "Només s'avança amb un nombre; una expressió es queda tal com s'escriu",
|
||||
"dialog.dimension_step_up": "Pujar {step}",
|
||||
"dialog.edit_title": "EDITAR {title}",
|
||||
"dialog.hole_held_off_its_line": "L'aresta de referència manté aquest forat sobre una línia que surt de la cara abans del punt triat. Trieu on la línia travessa la cara, o traieu l'aresta per moure el forat lliurement.",
|
||||
"dialog.no_shown_body": "No hi ha cap cos mostrat per canviar: mostreu-ne un o trieu Cos nou",
|
||||
"dialog.pattern_sketch_not_object": "Trieu cossos o operacions: un bocet no és cap de les dues coses. Repetiu les seves corbes amb el Patró o la Simetria del propi bocet, o extruïu-lo primer.",
|
||||
"dialog.primitive_point_tip": "Un punt que omple X, Y i Z: feu clic aquí i després feu clic en un vèrtex, una aresta o una cara del model, un punt d'un bocet o l'origen.",
|
||||
"dialog.reference_edge_tip": "Una aresta recta de la cara triada per mesurar-hi la distància de sota: feu clic aquí i després feu clic a l'aresta.",
|
||||
"dialog.right_click_pick": "Seleccioneu {input} primer",
|
||||
"dialog.select": "↖ Seleccionar",
|
||||
"dialog.selected_count": "↖ {count} seleccionats",
|
||||
"dialog.selection_constraint": "Restricció {id}",
|
||||
@@ -1674,12 +1637,9 @@
|
||||
"document.changed_on_disk": "{name} ha canviat al disc",
|
||||
"document.changed_on_disk_detail": "Un altre programa o un altre equip ha modificat {name} des que el vau obrir o desar per última vegada. Sobreescriure substitueix aquests canvis pels vostres; Desar una còpia conserva tots dos.",
|
||||
"document.copy_name": "Còpia de {name}",
|
||||
"document.discard_row": "Descartar",
|
||||
"document.finish_save_prompt": "Acabeu de respondre l'avís de desar abans d'obrir un altre disseny.",
|
||||
"document.newer_file": "Desat amb una versió més recent de SolidCraft (format {format}; aquesta versió llegeix fins a {reads}). Si falta alguna cosa, actualitzeu.",
|
||||
"document.newer_file_change": "Desat amb una versió més recent de SolidCraft (format {format} amb `{change}`, que aquesta versió no coneix). Si falta alguna cosa, actualitzeu.",
|
||||
"document.newer_file_not_read": "No llegit: {what}. En desar es conserva.",
|
||||
"document.not_saved_yet": "Encara sense desar",
|
||||
"document.open_anyway": "Obrir de tota manera",
|
||||
"document.open_elsewhere": "{name} ja està obert",
|
||||
"document.open_elsewhere_detail": "{name} està obert a SolidCraft {app} a {who}, obert {when}. Si deseu tots dos, un de vosaltres perdrà la feina. Obriu-lo en només lectura per veure'l sense sobreescriure'l.",
|
||||
@@ -1689,9 +1649,6 @@
|
||||
"document.read_only_save": "Aquest disseny s'ha obert en només lectura: deseu-lo com a fitxer nou",
|
||||
"document.save_changes": "Voleu desar els canvis de {name}?",
|
||||
"document.save_copy": "Desar una còpia…",
|
||||
"document.save_many": "Voleu desar els canvis de {n} dissenys?",
|
||||
"document.save_row": "Desar",
|
||||
"document.save_selected": "Desar els seleccionats",
|
||||
"document.tab_unsaved_changes": "Canvis sense desar",
|
||||
"document.unsaved_changes": "{name} té canvis sense desar. Si no els deseu, es perdran.",
|
||||
"drive.rigid": "{joint} és una unió rígida: manté les seves peces quietes, així que no té res a accionar. Per deixar-les moure, canvieu-ne el tipus a Editar la unió: Revolució per girar, Lliscador per lliscar, Cilíndrica per totes dues.",
|
||||
@@ -1711,8 +1668,6 @@
|
||||
"dxf.tolerance": "Tolerància de corba",
|
||||
"dxf.tolerance_help": "Només cossos visibles i bocets mostrats. Les corbes fan servir segments rectes. Una tolerància més petita dóna corbes més fines i consumeix més temps i memòria.",
|
||||
"each rail's curves must be in one sketch": "les corbes de cada guia han de ser en un sol bocet",
|
||||
"error.end_cap_faces": "Trieu almenys una cara d'extrem oberta d'un element estructural",
|
||||
"error.end_cap_unresolved": "L'extrem {ends} d'aquesta tapa ja no és al model. Torneu a triar les cares d'extrem",
|
||||
"feature computation ended without a result": "el càlcul de l'operació s'ha acabat sense resultat",
|
||||
"file.open.drawing_filter": "Dissenys, fitxers CAD, malles i dibuixos",
|
||||
"file.open.portal_fallback.no_portal_service": "El selector de fitxers que s'ha obert no és el propi de l'escriptori: el servei del portal de l'escriptori no està disponible.",
|
||||
@@ -1724,7 +1679,6 @@
|
||||
"file_load.inserting": "Inserint la geometria",
|
||||
"file_load.link_item": "{name} ({done} de {total})",
|
||||
"file_load.opened": "S'ha obert {name}",
|
||||
"file_load.opened_behind": "{name} s'ha obert en una pestanya nova",
|
||||
"file_load.opened_in_tab": "S'ha obert {name} en una pestanya nova",
|
||||
"file_load.opening": "Obrint el disseny",
|
||||
"file_load.preparing_thumbnail": "Preparant la miniatura…",
|
||||
@@ -1772,13 +1726,11 @@
|
||||
"help.config.table": "Mostra la taula de configuració: una fila per variant, una columna per paràmetre o operació, i si el disseny encara coincideix amb la fila activa. Feu-ho servir per revisar les variants d'un disseny.",
|
||||
"help.connect_note": "Reinicieu cada assistent perquè carregui el servidor de SolidCraft. Cada fitxer s'ha desat com a .bak abans de modificar-lo.",
|
||||
"help.connect_title": "Connectar assistents d'IA",
|
||||
"help.construct.axis": "Crea un eix de construcció amb el mètode que demanin les vostres seleccions: al llarg d'una aresta o línia, per un forat o una cara rodona, on es troben dos plans, per dos punts, o perpendicular a una cara per un punt. Feu-lo servir quan conegueu les referències i no el mètode.",
|
||||
"help.construct.axis.edge": "Crea un eix de construcció al llarg d'una aresta recta o una línia de bocet. Feu-lo servir com a eix per a una revolució, un patró circular o una unió.",
|
||||
"help.construct.axis.normal_to_face": "Crea un eix de construcció per un punt, perpendicular a una cara. Feu-lo servir com a eix que surt recte d'una superfície.",
|
||||
"help.construct.axis.through_cylinder": "Crea un eix de construcció pel centre d'una cara de cilindre, con o tor, o una aresta circular. Feu-lo servir per girar o fer un patró d'operacions al voltant d'una peça rodona.",
|
||||
"help.construct.axis.two_planes": "Crea un eix de construcció on es troben dos plans o cares planes. Feu-lo servir quan l'eix que necessiteu és la línia entre dues cares.",
|
||||
"help.construct.axis.two_points": "Crea un eix de construcció per dos punts, vèrtexs o punts de bocet. Feu-lo servir per a un eix que va entre dues posicions conegudes.",
|
||||
"help.construct.plane": "Crea un pla de construcció amb el mètode que demanin les vostres seleccions: desplaçat respecte d'un pla, per un punt, entre dos plans, per línies o punts, tangent a una cara corba, o perpendicular a una trajectòria. Feu-lo servir quan conegueu les referències i no el mètode.",
|
||||
"help.construct.plane.along_path": "Crea un pla de construcció perpendicular a una aresta o corba, en una posició al llarg seu. Feu-lo servir per bocetar un perfil per a un escombrat o una secció d'una trajectòria.",
|
||||
"help.construct.plane.at_angle": "Crea un pla de construcció girat un angle des d'un pla o cara, al voltant d'un eix o aresta. Activeu «Fer servir només la direcció de l'eix» per projectar l'eix sobre la base i pivotar al voltant d'un punt d'aquest pla (l'origen de la base quan no se'n tria cap). Feu-lo servir per a bocets inclinats, com un suport angular.",
|
||||
"help.construct.plane.axis_point": "Crea un pla de construcció que conté un eix i un punt fora d'aquest eix. Trieu un origen o eix de construcció, una aresta recta o línia de bocet, i després un vèrtex, punt de bocet o punt de construcció. Un punt a l'eix no pot definir un pla únic.",
|
||||
@@ -1788,7 +1740,6 @@
|
||||
"help.construct.plane.tangent": "Crea un pla de construcció que toca un cilindre, con, esfera o tor en un punt. Feu-lo servir per bocetar al costat d'una peça rodona.",
|
||||
"help.construct.plane.three_points": "Crea un pla de construcció per tres punts. Feu-lo servir per a un pla definit per posicions del model, com tres cantonades.",
|
||||
"help.construct.plane.two_edges": "Crea un pla de construcció per dues arestes rectes o línies que són en un mateix pla. Feu-lo servir per a un pla a través d'una peça, com per dues arestes oposades.",
|
||||
"help.construct.point": "Crea un punt de construcció amb el mètode que demanin les vostres seleccions: en un vèrtex, al centre d'un cercle o una esfera, al llarg d'una aresta, on es troben dues línies o tres plans, o on una línia travessa un pla. Feu-lo servir quan conegueu les referències i no el mètode.",
|
||||
"help.construct.point.along_path": "Crea un punt de construcció en una posició al llarg d'una aresta o corba. Feu-lo servir com a referència a mig camí d'una trajectòria.",
|
||||
"help.construct.point.center": "Crea un punt de construcció al centre d'una aresta circular, cercle o arc de bocet, esfera o tor. Feu-lo servir com a referència per a forats, eixos i unions.",
|
||||
"help.construct.point.edge_plane": "Crea un punt de construcció on una aresta o línia travessa un pla o cara plana. Feu-lo servir per marcar on una trajectòria creua un pla.",
|
||||
@@ -1814,7 +1765,6 @@
|
||||
"help.file.send_to_slicer": "Desa els cossos seleccionats com a fitxer 3MF i obre'l al laminador que trieu. Feu-ho servir per passar del model a la impressió en un sol pas.",
|
||||
"help.font_details": "La interfície fa servir les fonts integrades d'egui (Ubuntu Light i Hack, sota la Ubuntu Font Licence i la llicència MIT/Bitstream Vera; Noto Emoji i la font d'icones emoji, sota l'OFL i MIT). El text dels bocets fa servir Liberation Sans (SIL OFL 1.1). El paquet opcional de fonts de The Crafting Apps, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), no s'inclou a les compilacions de SolidCraft.",
|
||||
"help.fonts": "Fonts",
|
||||
"help.frame.end_cap": "Tanca els extrems oberts dels elements estructurals amb plaques amb la forma del contorn del perfil. Feu-lo servir per segellar tubs, angulars i bigues, a sobre de l'extrem o encastat-hi.",
|
||||
"help.frame.member": "Col·loca un perfil, com un tub o una extrusió d'alumini, al llarg de les línies d'un bocet d'estructura, un element per línia. Feu-ho servir per construir bastiments, prestatges i estructures soldades.",
|
||||
"help.inspect.accessibility": "Coloreja les cares que una eina pot assolir des d'una direcció i les que no. Feu-ho servir per comprovar una peça per a mecanitzat o motlle des d'aquell costat.",
|
||||
"help.inspect.center_of_mass": "Mostra el centre de masses dels cossos, cadascun per separat i tots junts, amb densitat uniforme. Feu-ho servir per comprovar l'equilibri i on bolcarà una peça.",
|
||||
@@ -1941,13 +1891,13 @@
|
||||
"help.sketch.construction_line": "Crea una línia de construcció infinita que passa per dos punts. Feu-la servir per estructurar un bocet sense partir perfils.",
|
||||
"help.sketch.construction_line.angle": "Crea una línia de construcció infinita amb un angle respecte de l'eix X del bocet. Trieu el punt i escriviu l'angle, o trieu-ne la direcció.",
|
||||
"help.sketch.construction_line.bisector": "Trieu dues línies rectes del bocet que es tallin i després una de les dues bisectrius de l'angle. El resultat segueix les seves línies d'origen.",
|
||||
"help.sketch.construction_line.horizontal": "Crea una línia de construcció horitzontal infinita que passi per un punt, o escriviu-ne la distància a l'eix X del bocet. Una distància escrita es converteix en una cota controladora. La seva restricció horitzontal segueix les edicions posteriors.",
|
||||
"help.sketch.construction_line.parallel": "Trieu una línia recta de bocet i després col·loqueu una línia de construcció paral·lela infinita o escriviu-ne el desplaçament. Un desplaçament escrit es converteix en una distància controladora. Feu clic amb el botó dret per triar una altra referència.",
|
||||
"help.sketch.construction_line.horizontal": "Crea una línia de construcció horitzontal infinita que passa per un punt. La seva restricció horitzontal segueix les edicions posteriors.",
|
||||
"help.sketch.construction_line.parallel": "Trieu una línia recta del bocet i col·loqueu una línia de construcció paral·lela infinita. Feu clic amb el botó dret per triar una altra referència.",
|
||||
"help.sketch.construction_line.perpendicular": "Trieu una línia recta del bocet i col·loqueu una línia de construcció perpendicular infinita. Feu clic amb el botó dret per triar una altra referència.",
|
||||
"help.sketch.construction_line.tangent_at": "Crea una tangent en un punt d'un cercle o arc circular, o en un extrem d'una spline o cònica oberta. Trieu la corba i després el punt de contacte.",
|
||||
"help.sketch.construction_line.tangent_external": "Trieu un cercle o arc circular, després un punt exterior i una solució de tangent. Les previsualitzacions només mostren contactes a l'arc dibuixat.",
|
||||
"help.sketch.construction_line.tangent_two": "Trieu dos cercles o arcs circulars i després una tangent comuna a la previsualització. Els contactes han de ser a les corbes dibuixades.",
|
||||
"help.sketch.construction_line.vertical": "Crea una línia de construcció vertical infinita que passi per un punt, o escriviu-ne la distància a l'eix Y del bocet. Una distància escrita es converteix en una cota controladora. La seva restricció vertical segueix les edicions posteriors.",
|
||||
"help.sketch.construction_line.vertical": "Crea una línia de construcció vertical infinita que passa per un punt. La seva restricció vertical segueix les edicions posteriors.",
|
||||
"help.sketch.copy": "Copia les corbes i punts de bocet seleccionats, juntament amb les restriccions i cotes que hi ha entre ells. Enganxeu-los en aquest bocet o en un altre.",
|
||||
"help.sketch.create": "Inicia un bocet 2D nou en un pla o en una cara plana del model. Cada extrusió, revolució i escombrat comença amb un bocet.",
|
||||
"help.sketch.cut": "Copia la geometria de bocet seleccionada i l'esborra del bocet, en un sol pas de desfer. Enganxeu-la en un altre lloc d'aquest bocet o en un altre.",
|
||||
@@ -2291,9 +2241,6 @@
|
||||
"measure.at": "A",
|
||||
"measure.item": "Element {number}",
|
||||
"measure.line_to_centre": "Línia al centre",
|
||||
"measure.perimeter": "Perímetre",
|
||||
"measure.perimeter_inner": "Perímetre interior",
|
||||
"measure.perimeter_outer": "Perímetre exterior",
|
||||
"measure.quick_copied": "Mesura copiada",
|
||||
"measure.quick_copy": "Feu clic per copiar la mesura",
|
||||
"measure.quick_edges": "{count} arestes",
|
||||
@@ -2335,8 +2282,6 @@
|
||||
"nav.hfss.help": "Alt+arrossegar esquerre desplaça, Majúscules+Alt+arrossegar esquerre fa zoom, Ctrl+arrossegar esquerre en un espai buit orbita",
|
||||
"nav.inventor.help": "Arrossegar amb el botó central desplaça, Majúscules+central orbita, Ctrl+central fa zoom i arrossegar amb el dret orbita",
|
||||
"nav.maya.help": "Alt+arrossegar esquerre orbita, Alt+arrossegar central desplaça, Alt+arrossegar dret fa zoom; arrossegar central també desplaça",
|
||||
"nav.maya_horizontal_zoom.help": "Alt+arrossegar amb el botó dret: cap a la dreta apropa, cap a l'esquerra allunya. Desactiveu-ho per tornar al zoom arrossegant en vertical.",
|
||||
"nav.maya_horizontal_zoom.label": "Maya: zoom arrossegant en horitzontal",
|
||||
"nav.nx.help": "Arrossegar amb el botó central orbita, Majúscules+central desplaça (central+dret de NX), Ctrl+central fa zoom (central+esquerre de NX); arrossegar amb el dret deixa la càmera quieta",
|
||||
"nav.openscad.help": "Arrossegar amb el botó esquerre en un espai buit orbita, Majúscules+esquerre selecciona amb un rectangle i arrossegar geometria del bocet l'edita; arrossegar amb el dret o el central desplaça, i Majúscules+dret o Majúscules+central fa zoom",
|
||||
"nav.orbit_cursor_wrap.help": "Un arrossegament d'òrbita que arriba a la vora de la vista mou el cursor a la vora oposada i continua girant. Desactivat: l'arrossegament s'atura a la vora de la pantalla. No té efecte a Wayland ni al navegador.",
|
||||
@@ -2467,16 +2412,6 @@
|
||||
"not a move": "no és un moviment",
|
||||
"not a number": "no és un nombre",
|
||||
"open a sketch first": "obriu primer un bocet",
|
||||
"operations.cancel": "Cancel·lar {name}",
|
||||
"operations.cancelled": "{name} cancel·lat",
|
||||
"operations.cancelling": "Cancel·lant…",
|
||||
"operations.cannot_cancel": "{name} no es pot cancel·lar",
|
||||
"operations.feature": "Operació",
|
||||
"operations.feature_edit": "Editar l'operació",
|
||||
"operations.meshing": "Carregant les peces",
|
||||
"operations.queued": "A la cua",
|
||||
"operations.stopped": "Cancel·lat",
|
||||
"operations.waiting": "A punt",
|
||||
"parameters.apply_live": "Aplicar cada canvi en sortir del camp",
|
||||
"parameters.apply_live_tip": "Desactivat: els canvis esperen fins que premeu Aplicar o D'acord, i llavors el disseny es reconstrueix una sola vegada per a tots. Cancel·lar o Escape els descarta.",
|
||||
"parameters.held": "{count} canvis esperant Aplicar",
|
||||
@@ -2565,7 +2500,6 @@
|
||||
"prefs.appearance.viewport_colours": "Colors de la vista 3D",
|
||||
"prefs.appearance.viewport_logo": "Visor i logotip",
|
||||
"prefs.applies_on_restart": "S'aplica en reiniciar",
|
||||
"prefs.assemblies": "Ensamblatges",
|
||||
"prefs.assistant": "Assistent",
|
||||
"prefs.assistant_connecting": "Connectant",
|
||||
"prefs.assistant_follow_help": "Els bocets Mirar cap a mouen la vista amb el punter.",
|
||||
@@ -2618,10 +2552,6 @@
|
||||
"prefs.choose_font": "Triar un fitxer de tipus de lletra…",
|
||||
"prefs.click": "Clic",
|
||||
"prefs.command_dialog_values": "Diàlegs d'ordres",
|
||||
"prefs.constraint_glyph_size": "Mida dels glifs de restricció",
|
||||
"prefs.constraint_glyph_size_note": "Com de grans es dibuixen els glifs de restricció i les seves lletres o icones en un bocet. El 100 % és la mida habitual; els glifs conserven aquesta mida en pantalla amb qualsevol zoom.",
|
||||
"prefs.construct_menu_groups": "Agrupar les eines de Construir per tipus d'entitat",
|
||||
"prefs.construct_menu_groups_help": "Mostra els encapçalaments Plans, Eixos i Punts. Desactivat, es manté l'ordre original del menú.",
|
||||
"prefs.contact_shading": "Ombrejat de contacte suau",
|
||||
"prefs.contact_shading_note": "Ombrejat suau on les peces s'acosten.",
|
||||
"prefs.context_menu": "Menú contextual",
|
||||
@@ -2657,8 +2587,6 @@
|
||||
"prefs.design_tools.rows": "Files",
|
||||
"prefs.devices": "Ratolí 3D",
|
||||
"prefs.dialog_buttons": "Botons de diàleg",
|
||||
"prefs.dialog_first_letter_buttons": "Els botons de diàleg responen a la seva primera lletra",
|
||||
"prefs.dialog_first_letter_buttons_help": "Amb un diàleg obert, escriure la primera lletra de l'etiqueta d'un botó el prem: O per a OK, C per a Cancel·la. Una lletra que encaixa en dos botons no fa res, i un camp de text amb el focus conserva la pulsació. Desactivat: escriure no afecta els botons, com abans.",
|
||||
"prefs.dialogs": "Diàlegs",
|
||||
"prefs.dialogs_edge": "A la vora de la vista",
|
||||
"prefs.dialogs_left_column": "A la columna esquerra",
|
||||
@@ -2672,23 +2600,12 @@
|
||||
"prefs.dimension_values_window": "En una finestra",
|
||||
"prefs.display": "Pantalla",
|
||||
"prefs.edges": "Arestes i ombrejat",
|
||||
"prefs.export": "Exportar",
|
||||
"prefs.export_name": "Nom del fitxer d'exportació",
|
||||
"prefs.export_name.body": "Nom del cos exportat (el primer quan n'hi ha diversos)",
|
||||
"prefs.export_name.design": "Nom del disseny",
|
||||
"prefs.export_name_hint": "p. ex., {design}-{body}",
|
||||
"prefs.export_name_sample": "En exportar aquest disseny s'ofereix: {name}",
|
||||
"prefs.export_name_tip": "El nom de fitxer que ofereixen Fitxer › Exportar i Desar com a malla. Se substitueixen aquests marcadors:\n{placeholders}\nUna propietat del disseny que no estigui definida s'omet. Deixeu el camp buit per oferir el nom del disseny o, en una malla, el del cos.",
|
||||
"prefs.extend": "Extensions",
|
||||
"prefs.extra_navigation": "Ctrl + arrossegar amb el botó esquerre sobre un espai buit orbita; manteniu l'Espai per desplaçar.",
|
||||
"prefs.extrude_arrow_face_stops": "La fletxa d'extrusió s'atura a les cares",
|
||||
"prefs.extrude_arrow_face_stops_note": "En arrossegar la fletxa de distància d'Extruir, s'atura a les cares planes paral·leles al perfil, com el costat oposat d'un bloc que s'està tallant, i ressegueix la cara. Desactivat: la fletxa només es mou a passos rodons.",
|
||||
"prefs.extrude_view_isometric": "Girar la vista a isomètrica en obrir Extruir",
|
||||
"prefs.extrude_view_return": "Tornar a la vista anterior en tancar Extruir",
|
||||
"prefs.file_operations_wait": "Esperar que s'obrin els fitxers",
|
||||
"prefs.file_operations_wait_note": "Mentre s'obre o s'insereix un fitxer, o es tornen a llegir els vincles, el disseny espera darrere una targeta amb el progrés del fitxer i Cancel·la. Desactivat: el fitxer apareix al panell d'operacions i podeu seguir treballant; una inserció s'aplica quan acabeu el que esteu fent, i un fitxer obert mentrestant va a una pestanya nova.",
|
||||
"prefs.fillet_legacy_radius": "Usar el radi predeterminat anterior d'Arrodoniment de sòlid",
|
||||
"prefs.fillet_legacy_radius.help": "Desactivat de manera predeterminada. Activat, un nou Arrodoniment de sòlid s'obre amb el número 2 llegit en les unitats del disseny («2 in» en un disseny en polzades), com abans; desactivat, s'obre amb el radi que porta l'eina, 2 mm, mostrat en les unitats del disseny («0.07874 in» en un disseny en polzades).",
|
||||
"prefs.follows_os": "Segueix el sistema operatiu",
|
||||
"prefs.font": "Tipus de lletra",
|
||||
"prefs.font_fallback": "Els glifs que no té el tipus de lletra triat es prenen del tipus integrat, així cap etiqueta no mostra un requadre buit.",
|
||||
@@ -2712,17 +2629,6 @@
|
||||
"prefs.gesture_pan": "Desplaçar",
|
||||
"prefs.gesture_zoom": "Zoom",
|
||||
"prefs.graphics": "Gràfics",
|
||||
"prefs.graphics_api": "API gràfica",
|
||||
"prefs.graphics_api_auto": "Automàtica",
|
||||
"prefs.graphics_api_help": "Trieu l'API gràfica que s'usa per obrir SolidCraft. Si el proper inici no pot obrir la finestra, es restaura la tria anterior.",
|
||||
"prefs.graphics_api_override": "Una opció d'inici controla l'API gràfica. La tria desada s'aplicarà quan SolidCraft s'obri normalment.",
|
||||
"prefs.graphics_api_restart_note": "S'aplica després de reiniciar. Els dissenys sense desar es gestionen abans de tancar.",
|
||||
"prefs.graphics_api_restart_unavailable": "Reinicieu SolidCraft per aplicar l'API gràfica desada.",
|
||||
"prefs.graphics_api_running": "Actual: {current}. Triada per al proper inici: {selected}.",
|
||||
"prefs.graphics_api_save_failed": "No s'ha pogut desar la tria d'API gràfica: {error}",
|
||||
"prefs.graphics_api_start_failed": "No s'ha pogut preparar l'API gràfica triada: {error}",
|
||||
"prefs.graphics_api_start_reverted": "L'inici anterior amb {requested} no va obrir la finestra. SolidCraft ha restaurat {previous}.",
|
||||
"prefs.graphics_api_unavailable": "La selecció de l'API gràfica està disponible a l'aplicació d'escriptori.",
|
||||
"prefs.graphics_card": "Targeta gràfica",
|
||||
"prefs.graphics_card_auto": "Automàtica",
|
||||
"prefs.graphics_card_missing": "La targeta gràfica desada no està disponible; s'usa Automàtica.",
|
||||
@@ -2766,8 +2672,6 @@
|
||||
"prefs.history_sketches_in_order": "En ordre",
|
||||
"prefs.history_sketches_note": "Amb Bocets a Sota la seva operació, l'historial de l'esquerra llista cada bocet un nivell endins, sota la primera operació construïda a partir d'ell, sempre que hi hagi prou amplada per als noms. La línia de temps de baix manté sempre l'ordre de creació.",
|
||||
"prefs.history_sketches_under_feature": "Sota la seva operació",
|
||||
"prefs.history_steps_component": "Passos de l'historial per component",
|
||||
"prefs.history_steps_component_note": "Amb un component actiu o seleccionat, els controls de l'Historial recorren les operacions d'aquest component, tal com les mostra l'Historial de baix. Desactivat: recorren totes les operacions del disseny. L'Historial de l'esquerra o sota el navegador sempre recorre totes les operacions.",
|
||||
"prefs.hold_stop_snapping": "Mantenir premut per aturar l'ajust",
|
||||
"prefs.home_fit_size": "Mida d'Inici i Ajustar",
|
||||
"prefs.home_fit_size_note": "Quant de la vista omple el model quan Inici, Ajustar, un disseny obert o un canvi de vista del navegador l'enquadren, tant si l'ajust de sota està activat com si no. 100 %: omple la vista entre els panells, la brúixola i la barra de navegació.",
|
||||
@@ -2783,8 +2687,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "Les dues fletxes corbes a la cantonada del cub giren la vista dins del pla de la pantalla. Activades: la fletxa horària gira en sentit antihorari i l'antihorària gira en sentit horari. Desactivades: cada fletxa gira la vista cap on es corba.",
|
||||
"prefs.invert_navigator_arrows": "Invertir les fletxes de rotació del cub de vista",
|
||||
"prefs.invert_navigator_arrows_note": "Les fletxes al voltant del cub orbiten la càmera cap on apunten, com a Fusion i Inventor: la fletxa de l'esquerra apunta a la dreta i gira la càmera a la dreta. Desactivades: una fletxa gira el cub cap al costat on és, així la fletxa de l'esquerra gira la càmera a l'esquerra. Les fletxes de gir corbes no en queden afectades.",
|
||||
"prefs.joint_drag_trackball": "Girar les peces d'una unió de ròtula com un trackball en arrossegar-les",
|
||||
"prefs.joint_drag_trackball_help": "Arrossegar una peça que subjecta una unió de ròtula la gira amb el gir més petit possible al voltant de la unió, com si rodés una bola de la mida de la peça. Desactivat: el punt que heu agafat segueix exactament el punter, i la peça també pot girar sobre ell.",
|
||||
"prefs.keep_unsaved_also_asks": "Continuar preguntant quan es tanca la pestanya",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Amb això desactivat, la pestanya es tanca sense la finestra del navegador un cop la còpia de cada disseny sense desar està al dia.",
|
||||
"prefs.keep_unsaved_in_browser": "Conservar la feina sense desar en aquest navegador",
|
||||
@@ -2860,8 +2762,6 @@
|
||||
"prefs.not_on_gpu": "no en aquesta GPU",
|
||||
"prefs.on": "Activat",
|
||||
"prefs.opaque": "Opac",
|
||||
"prefs.operations_panel": "Mostrar les operacions a la vista",
|
||||
"prefs.operations_panel_note": "Les operacions llargues (obrir un fitxer, calcular una operació, una ordre de l'agent) es mostren a la cantonada inferior dreta de la vista amb el seu progrés i un botó per cancel·lar-les. Desactivat: un avís de «Calculant», com abans.",
|
||||
"prefs.orbit_snap": "Manteniu el Majúscules mentre orbiteu per ajustar a un eix.",
|
||||
"prefs.orientation": "Orientació",
|
||||
"prefs.origin_plane_names": "Noms dels plans d'origen",
|
||||
@@ -2942,8 +2842,6 @@
|
||||
"prefs.ribbon_tabs_only": "Mostrar només les pestanyes de la cinta",
|
||||
"prefs.ribbon_tabs_only_note": "La cinta es plega fins a la seva fila de pestanyes. En fer clic en una pestanya es mostren els seus panells sobre la vista fins que comença una ordre o el punter en surt. «Només les pestanyes de la cinta», a la pestanya Vista o amb la seva drecera, ho commuta.",
|
||||
"prefs.right_click": "Clic dret",
|
||||
"prefs.save_prompt_one_at_a_time": "Preguntar per cada disseny sense desar per separat en tancar-ne diversos",
|
||||
"prefs.save_prompt_one_at_a_time_help": "Mostrar un avís de desar per cada disseny sense desar en lloc d'una llista combinada.",
|
||||
"prefs.saving": "Inici i desament",
|
||||
"prefs.scale_note": "També Ctrl + / Ctrl − a la finestra. Els botons fan 28 px al 100 %.",
|
||||
"prefs.scale_text": "Escala i text",
|
||||
@@ -2980,8 +2878,6 @@
|
||||
"prefs.sketch.center_arc": "L'arc pel centre segueix el cursor",
|
||||
"prefs.sketch.click_selects": "El clic selecciona",
|
||||
"prefs.sketch.conflict_colour": "Color de conflicte",
|
||||
"prefs.sketch.cursor_coordinates": "Mostrar les coordenades del cursor en un bocet",
|
||||
"prefs.sketch.cursor_coordinates_note": "Al costat del punter, la seva X i Y des de l'origen del bocet, també sense cap eina en marxa. Desactivat: només mentre una eina col·loca un punt.",
|
||||
"prefs.sketch.drawing": "Dibuix",
|
||||
"prefs.sketch.ellipse_axes": "Dibuixar les el·lipses amb els seus eixos",
|
||||
"prefs.sketch.ellipse_axes_note": "Els eixos major i menor com a línies de construcció, per poder acotar els semieixos i els eixos. Desactivat: només l'el·lipse, i D a sobre seu dóna el radi menor allà on es tiri el valor, dibuixat i etiquetat R com abans.",
|
||||
@@ -2993,20 +2889,14 @@
|
||||
"prefs.sketch.faces_edges": "Cares i arestes",
|
||||
"prefs.sketch.hide_glyphs": "Amagar els glifs en allunyar-se",
|
||||
"prefs.sketch.marks": "Marques al bocet",
|
||||
"prefs.sketch.max_points": "Bocet més gran",
|
||||
"prefs.sketch.max_points_note": "El màxim de punts o corbes que admet un bocet: 20000 de manera predeterminada, com a molt 200000. Inserir SVG i Inserir DXF refusen un dibuix més gran. Un bocet gran es resol i es dibuixa més lentament.",
|
||||
"prefs.sketch.max_points_unit": "punts",
|
||||
"prefs.sketch.note": "Dibuix, ajust i selecció.",
|
||||
"prefs.sketch.opening_closing": "Obertura i tancament",
|
||||
"prefs.sketch.prefer_free_points": "Preferir punts mòbils quan se superposen",
|
||||
"prefs.sketch.project_options": "Demanar primer les opcions de Projectar",
|
||||
"prefs.sketch.recent_snap": "Ajustar on es creuen dues guies",
|
||||
"prefs.sketch.select_3d_curve_points": "Seleccionar punts de corbes de bocet 3D",
|
||||
"prefs.sketch.select_3d_curve_points_help": "Mostra i permet seleccionar punts de les corbes de bocet 3D dibuixades com a referències de construcció.",
|
||||
"prefs.sketch.select_other_hidden": "Seleccionar-ne un altre arriba a la geometria oculta",
|
||||
"prefs.sketch.shortcut_cycles_modes": "La drecera canvia de mode en tornar-la a prémer",
|
||||
"prefs.sketch.shortcut_cycles_modes_tip": "Desactivat: torna a iniciar l'eina.",
|
||||
"prefs.sketch.snap_to_visible_grid": "Ajustar als encreuaments visibles de la quadrícula",
|
||||
"prefs.sketch.snapping_guides": "Ajust i guies",
|
||||
"prefs.sketch.state_icons": "Icones d'estat",
|
||||
"prefs.sketch.tracking_guides": "Guies de centre i punt mitjà",
|
||||
@@ -3034,7 +2924,6 @@
|
||||
"prefs.sketch_selection": "Bocet i selecció",
|
||||
"prefs.sketch_selection_heading": "Bocet i selecció",
|
||||
"prefs.sketch_selection_note": "Com s'ajusten els bocets i com seleccionen els clics.",
|
||||
"prefs.sketch_snap_to_visible_grid_note": "Un punt d'un bocet s'ajusta als encreuaments de les línies que dibuixa la quadrícula quan el punter n'apunta un, i si no, a la retícula més fina entre ells. Desactivat: el punt s'ajusta només a la retícula més fina, com abans.",
|
||||
"prefs.sketch_state_icons": "Icones dels bocets amb el color del seu estat",
|
||||
"prefs.sketch_state_icons_note": "A l'historial i a l'explorador, la icona d'un bocet pren el color del seu estat (insuficientment restringit, completament restringit o en conflicte) i la seva informació en diu l'estat. Desactivat: tots els bocets tenen la mateixa icona.",
|
||||
"prefs.sketch_tab_always": "Mantenir visible la pestanya Bocet",
|
||||
@@ -3556,12 +3445,12 @@
|
||||
"render.cpu_subsurface_denoise_reason": "La dispersió subsuperficial de CPU necessita la reducció de soroll desactivada. Es conserven els vostres ajustos.",
|
||||
"render.cpu_subsurface_help": "La dispersió redirigeix la llum dins del vidre tancat. Les taxes positives necessiten il·luminació indirecta amb la reducció de soroll desactivada. La taxa és de col·lisions per mm; 0 no afegeix dispersió. Un biaix de fase positiu afavoreix les direccions cap endavant; un de negatiu, les cap enrere.",
|
||||
"render.cpu_summary": "Ray tracing de CPU · reflexos · HDR · llums direccionals i puntuals",
|
||||
"render.cpu_threads": "Workers de CPU",
|
||||
"render.cpu_threads": "Processos de CPU",
|
||||
"render.cpu_threads_auto": "Automàtic",
|
||||
"render.cpu_threads_custom": "Personalitzat",
|
||||
"render.cpu_threads_help": "Automàtic fa servir els workers de CPU disponibles. Un worker s'executa en sèrie. Més workers poden necessitar més memòria.",
|
||||
"render.cpu_threads_help": "Automàtic fa servir els processos de CPU disponibles. Un procés s'executa en sèrie. Més processos poden necessitar més memòria.",
|
||||
"render.cpu_threads_single": "Un",
|
||||
"render.cpu_threads_web": "El renderitzat al navegador fa servir un worker cooperatiu. Es conserva la tria de workers desada per a l'escriptori.",
|
||||
"render.cpu_threads_web": "El renderitzat al navegador fa servir un procés cooperatiu. Es conserva l'elecció de processos desada per a l'escriptori.",
|
||||
"render.cpu_transparent_help": "El PNG transparent treu el fons i conserva la cobertura de les vores. L'opacitat del cos i de la cara ha de mantenir-se al 100 %.",
|
||||
"render.cpu_volume_enable_path": "Activar la il·luminació indirecta",
|
||||
"render.cpu_volume_enable_path_plan": "Ray tracing de CPU · profunditat {depth} · {spp} mostres/píxel · llavor {seed} · ombres traçades. Conserva l'atmosfera definida i el pressupost d'imatge.",
|
||||
@@ -3736,7 +3625,6 @@
|
||||
"render.error_pbr_opacity": "el PBR opac requereix opacitat 1",
|
||||
"render.error_physical_glass": "la recepta triada no és vidre físic",
|
||||
"render.error_preview_device": "la vista prèvia del producte requereix un dispositiu instal·lat amb búfer de profunditat",
|
||||
"render.error_product_placement": "col·locació de producte no vàlida",
|
||||
"render.error_product_triangle": "triangle de producte no vàlid",
|
||||
"render.error_provisional_identity": "identitat provisional de renderitzat o nombre de mostres no vàlida",
|
||||
"render.error_quality_bounds": "la projecció de qualitat supera els límits natius",
|
||||
@@ -3820,10 +3708,6 @@
|
||||
"render.gpu_path_retry": "Torna-ho a provar",
|
||||
"render.gpu_seed": "Llavor aleatòria",
|
||||
"render.gpu_unavailable": "El path tracing de GPU encara no està a punt en aquest dispositiu.",
|
||||
"render.graphics_api_restart_vulkan": "Reiniciar amb Vulkan",
|
||||
"render.graphics_api_restart_warning": "Canviar l'API gràfica reinicia SolidCraft i us demana de desar els dissenys sense desar. Si la nova API no pot obrir la finestra, es restaura la tria anterior al proper inici.",
|
||||
"render.graphics_api_settings": "Configuració gràfica",
|
||||
"render.graphics_api_vulkan_required": "El path tracing a la GPU necessita Vulkan en aquesta sessió. Reinicieu amb Vulkan o seguiu amb un mètode d'imatge per CPU.",
|
||||
"render.ground_apply": "Ajustar i desar el terra",
|
||||
"render.ground_gap": "Espai lliure explícit",
|
||||
"render.ground_linear_rgb": "Reflectància difusa · RGB lineal",
|
||||
@@ -4825,8 +4709,6 @@
|
||||
"sketch.hint.spline3d": "Spline 3D: feu clic als punts d'ajust en 3D; clic dret o Esc per acabar",
|
||||
"sketch.hint.trim": "Retallar: feu clic al tros de corba que voleu treure, o arrossegueu pels trossos que voleu treure",
|
||||
"sketch.make_selection": "Convertir la selecció en {linetype}",
|
||||
"sketch.move_released": "Mogut: s'han deixat anar {count} restriccions que el lligaven al que es queda ({names}).",
|
||||
"sketch.move_released_one": "Mogut: s'ha deixat anar {count} restricció que el lligava al que es queda ({names}).",
|
||||
"sketch.open_ends": "Oberts {count}",
|
||||
"sketch.status_conflict": "restriccions en conflicte",
|
||||
"sketch.status_fully_constrained": "totalment restringit",
|
||||
@@ -5061,7 +4943,6 @@
|
||||
"ui.agent_chat_allow_once": "Permetre una vegada",
|
||||
"ui.agent_chat_allowed": "Permès",
|
||||
"ui.agent_chat_answered": "Resposta: {option}",
|
||||
"ui.agent_chat_answered_in": "Respost en {time}",
|
||||
"ui.agent_chat_approval": "Permetre que l'agent faci això?",
|
||||
"ui.agent_chat_attach_selection": "Adjuntar la selecció",
|
||||
"ui.agent_chat_attach_selection_tip": "Envia amb el missatge el que estigui seleccionat al model (cares, arestes, operacions, cossos) perquè l'agent sàpiga a què us referiu",
|
||||
@@ -5090,7 +4971,6 @@
|
||||
"ui.agent_chat_ctx_bodies": "Cossos",
|
||||
"ui.agent_chat_ctx_elements": "Elements",
|
||||
"ui.agent_chat_ctx_features": "Operacions",
|
||||
"ui.agent_chat_ctx_gone": "Ja no és al disseny: s'ha canviat o eliminat.",
|
||||
"ui.agent_chat_ctx_more": "+{n} més",
|
||||
"ui.agent_chat_ctx_other": "Altres",
|
||||
"ui.agent_chat_custom_heading": "Model personalitzat",
|
||||
@@ -5104,7 +4984,6 @@
|
||||
"ui.agent_chat_dock_right": "Acoblar a la dreta",
|
||||
"ui.agent_chat_drop_hint": "Deixeu anar per adjuntar-ho al proper missatge",
|
||||
"ui.agent_chat_edit": "Editar",
|
||||
"ui.agent_chat_ended_after": "Acabat després de {time}",
|
||||
"ui.agent_chat_exited": "L'agent s'ha aturat.",
|
||||
"ui.agent_chat_failed_start": "No s'ha pogut iniciar l'agent: {error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}: un fitxer CAD s'importa al disseny, no es llegeix",
|
||||
@@ -5142,7 +5021,6 @@
|
||||
"ui.agent_chat_model_no_tools": "{model} no pot usar les eines de SolidCraft, així que només pot xatejar. Trieu un model amb suport d'eines.",
|
||||
"ui.agent_chat_model_rejected": "L'agent no ha acceptat {model}: {error}",
|
||||
"ui.agent_chat_model_search": "Cercar models…",
|
||||
"ui.agent_chat_model_search_or_id": "Cerqueu o escriviu qualsevol ID de model…",
|
||||
"ui.agent_chat_model_tip": "El model que usa l'agent. La vostra tria es recorda per a aquest agent.",
|
||||
"ui.agent_chat_models_failed": "No s'han pogut llistar els models: {error}",
|
||||
"ui.agent_chat_models_heading": "Models disponibles",
|
||||
@@ -5154,7 +5032,6 @@
|
||||
"ui.agent_chat_next_picture": "Imatge següent",
|
||||
"ui.agent_chat_no_cli": "No s'ha trobat solidcraft-cli al costat de l'aplicació, així que l'agent no pot accedir al disseny.",
|
||||
"ui.agent_chat_no_control": "El punt de connexió del xat de l'agent no s'ha iniciat, així que l'agent no pot accedir al disseny.",
|
||||
"ui.agent_chat_no_design_hint": "L'agent s'executa sense les eines de SolidCraft, així que no pot llegir ni canviar el disseny: no s'ha trobat solidcraft-cli al costat de l'aplicació. Instal·leu SolidCraft amb solidcraft-cli al costat (a Windows, el MSI o el zip portàtil) o definiu SOLIDCRAFT_CLI amb la seva ruta, i torneu a connectar.",
|
||||
"ui.agent_chat_no_images": "Aquest agent no admet imatges",
|
||||
"ui.agent_chat_no_picture": "No hi ha cap imatge al porta-retalls",
|
||||
"ui.agent_chat_no_tool_call": "El model no ha fet servir les eines de SolidCraft, així que el disseny no ha canviat. Trieu un model que admeti crides a eines; vegeu Models locals a la guia del xat de l'agent.",
|
||||
@@ -5169,7 +5046,7 @@
|
||||
"ui.agent_chat_permission": "L'agent demana executar: {title}",
|
||||
"ui.agent_chat_pick": "Agent",
|
||||
"ui.agent_chat_pick_context": "Triar context",
|
||||
"ui.agent_chat_pick_context_tip": "Trieu cares, arestes, operacions i cossos del model i premeu OK: s'afegeixen al missatge com una fitxa on és el cursor (escriure @ fa el mateix)",
|
||||
"ui.agent_chat_pick_context_tip": "Trieu cares, arestes, operacions i cossos del model perquè l'agent els miri i premeu D'acord",
|
||||
"ui.agent_chat_placeholder": "Demaneu a l'agent que modifiqui el disseny…",
|
||||
"ui.agent_chat_placeholder_busy": "Posa un missatge a la cua per quan l'agent acabi (Ctrl+Retorn: enviar ara)",
|
||||
"ui.agent_chat_pop_out": "Obrir en una finestra pròpia, que podeu moure a una altra pantalla",
|
||||
@@ -5201,26 +5078,16 @@
|
||||
"ui.agent_chat_setup_title": "Configurar un assistent",
|
||||
"ui.agent_chat_side": "Costat del xat de l'agent",
|
||||
"ui.agent_chat_signed_in": "sessió iniciada",
|
||||
"ui.agent_chat_snap_none_found": "No s'ha trobat cap agent de codi. El paquet Snap està confinat, així que no pot veure ni iniciar els agents de codi instal·lats en aquest equip. Per usar-los, instal·leu SolidCraft des de l'AppImage o el paquet .deb o .rpm. Un agent que usa una API compatible amb OpenAI també funciona al paquet Snap: configureu-ne un amb Afegir agent.",
|
||||
"ui.agent_chat_span_h": "{h} h {min} min",
|
||||
"ui.agent_chat_span_min": "{n} min",
|
||||
"ui.agent_chat_span_s": "{n} s",
|
||||
"ui.agent_chat_state_auth": "Cal iniciar sessió",
|
||||
"ui.agent_chat_state_busy": "Treballant…",
|
||||
"ui.agent_chat_state_exited": "Agent aturat",
|
||||
"ui.agent_chat_state_failed": "Connexió fallida",
|
||||
"ui.agent_chat_state_no_design": "Sense accés al disseny",
|
||||
"ui.agent_chat_state_off": "Sense connexió",
|
||||
"ui.agent_chat_state_ready": "A punt",
|
||||
"ui.agent_chat_state_starting": "Iniciant…",
|
||||
"ui.agent_chat_stop": "Aturar",
|
||||
"ui.agent_chat_stop_tip": "Aturar l'agent i tot el que hagi iniciat",
|
||||
"ui.agent_chat_stop_turn_tip": "Atura l'agent (Esc). Els missatges a la cua es queden a la cua.",
|
||||
"ui.agent_chat_thinking_for": "Pensant durant {time}",
|
||||
"ui.agent_chat_tool_running_for": "en execució durant {time}",
|
||||
"ui.agent_chat_tools_not_started": "L'agent s'executa sense les eines de SolidCraft, així que no pot llegir ni canviar el disseny: s'ha trobat solidcraft-cli, però les seves eines no s'han iniciat (l'error del xat explica per què). Torneu a connectar; si continua passant, reinstal·leu SolidCraft.",
|
||||
"ui.agent_chat_transcript_assistant": "Assistent:",
|
||||
"ui.agent_chat_transcript_user": "Usuari:",
|
||||
"ui.agent_chat_turn_limit": "Canvis per missatge",
|
||||
"ui.agent_chat_turn_unlimited": "Sense límit",
|
||||
"ui.agent_chat_undo_changes": "Desfer aquests canvis",
|
||||
@@ -5377,9 +5244,7 @@
|
||||
"ui.browser_bodies_one": "{count} cos",
|
||||
"ui.browser_bodies_other": "{count} cossos",
|
||||
"ui.browser_collapse": "Contraure l'explorador",
|
||||
"ui.browser_collapse_below": "Contraure a sota",
|
||||
"ui.browser_expand": "Expandir l'explorador",
|
||||
"ui.browser_expand_below": "Expandir a sota",
|
||||
"ui.browser_title": "Explorador",
|
||||
"ui.calibrate": "Calibrar",
|
||||
"ui.cancel": "Cancel·la",
|
||||
@@ -5409,7 +5274,6 @@
|
||||
"ui.collapse": "Contraure",
|
||||
"ui.collinear": "Colineal",
|
||||
"ui.colour_vision": "Visió del color",
|
||||
"ui.colour_vision_bends": "Plecs",
|
||||
"ui.colour_vision_deutan": "Deuteranòpsia",
|
||||
"ui.colour_vision_deutan_tip": "Per a una percepció del verd feble o absent",
|
||||
"ui.colour_vision_failed": "Fallit",
|
||||
@@ -5439,24 +5303,14 @@
|
||||
"ui.conic_curve": "Corba cònica",
|
||||
"ui.constant": "Constant",
|
||||
"ui.constraints_conflict": "Conflicte de restriccions",
|
||||
"ui.construct_auto.chosen": "Triat a partir de les vostres seleccions. Trieu una altra referència, o traieu-ne una, per canviar el mètode.",
|
||||
"ui.construct_auto.no_fit": "Trieu altres referències: cap mètode es pot construir amb aquestes.",
|
||||
"ui.construct_auto.pick": "Trieu amb què construir-lo: el mètode es dedueix de les vostres seleccions.",
|
||||
"ui.construct_auto.refs": "Referències",
|
||||
"ui.construct_auto.select": "Seleccioneu primer les referències amb què construir",
|
||||
"ui.construct_axes": "Eixos",
|
||||
"ui.construct_planes": "Plans",
|
||||
"ui.construct_points": "Punts",
|
||||
"ui.construction": "Construcció",
|
||||
"ui.construction.angle_hint": "Línia de construcció: feu clic en un punt i escriviu un angle o trieu una direcció",
|
||||
"ui.construction.axis_hint": "Línia de construcció: feu clic en un punt o escriviu la seva distància a l'eix del bocet",
|
||||
"ui.construction.bisector_hint": "Bisectriu: trieu dues línies rectes que es tallin i després la solució volguda",
|
||||
"ui.construction.common_hint": "Tangent comuna: trieu dos cercles o arcs i després la solució volguda",
|
||||
"ui.construction.contact_hint": "Tangent: trieu un cercle, arc o spline o cònica oberta i després el punt de contacte (corbes lliures: extrems)",
|
||||
"ui.construction.external_hint": "Tangent: trieu un cercle o arc, un punt exterior i després la solució volguda",
|
||||
"ui.construction.free_hint": "Línia de construcció: feu clic en dos punts; feu clic amb el botó dret per reiniciar",
|
||||
"ui.construction.normal_needs_length": "Una línia infinita és sempre de construcció: escriviu una longitud per dibuixar una línia normal",
|
||||
"ui.construction.parallel_hint": "Línia de construcció: trieu una línia recta i després feu clic o escriviu el desplaçament; feu clic amb el botó dret per canviar la referència",
|
||||
"ui.construction.point_hint": "Línia de construcció: feu clic en el punt per on ha de passar",
|
||||
"ui.construction.reference_hint": "Línia de construcció: trieu una línia recta i després un punt; feu clic amb el botó dret per canviar la referència",
|
||||
"ui.construction.ribbon_label": "Línies X",
|
||||
@@ -5599,17 +5453,6 @@
|
||||
"ui.ellipse": "El·lipse",
|
||||
"ui.emboss_wrap_note": "Els perfils de línies ortogonals tangents a un cilindre compatible s'embolcallen automàticament. Trieu una paret per triar un destí.",
|
||||
"ui.end": "Fi",
|
||||
"ui.end_cap.chamfer": "Xamfrà de cantonada",
|
||||
"ui.end_cap.direction": "Direcció",
|
||||
"ui.end_cap.faces": "Cares d'extrem",
|
||||
"ui.end_cap.inset": "Reducció",
|
||||
"ui.end_cap.inset_kind": "Reduir per",
|
||||
"ui.end_cap.inset_ratio": "Proporció de reducció",
|
||||
"ui.end_cap.inward": "Cap endins",
|
||||
"ui.end_cap.length": "Longitud",
|
||||
"ui.end_cap.outward": "Cap enfora",
|
||||
"ui.end_cap.ratio": "Proporció de gruix",
|
||||
"ui.end_cap_title": "TAPA D'EXTREM",
|
||||
"ui.end_radius": "Radi final",
|
||||
"ui.end_scale": "Escala final",
|
||||
"ui.ends": "Extrems",
|
||||
@@ -5630,10 +5473,7 @@
|
||||
"ui.explode_needs_two_components": "La vista explosionada separa components, així que el disseny en necessita dos o més (Component nou, Inserir un disseny o Crear components a partir de cossos)",
|
||||
"ui.exportDxf": "Exportar DXF…",
|
||||
"ui.export_ellipsis_keymap": "Exportar…",
|
||||
"ui.export_glb": "Exportar GLB",
|
||||
"ui.export_keymap_tip": "Les tecles, els favorits de la cerca S i la barra d'eines, com a fitxer",
|
||||
"ui.export_selects_only_body": "Seleccionar l'únic cos en exportar una malla",
|
||||
"ui.export_selects_only_body_tip": "Exportar STL, Exportar 3MF i Enviar al laminador comencen amb el cos del disseny seleccionat quan només n'hi ha un.",
|
||||
"ui.extend": "Estendre",
|
||||
"ui.extent": "Extensió",
|
||||
"ui.extent_type": "Tipus d'extensió",
|
||||
@@ -5693,8 +5533,6 @@
|
||||
"ui.finest": "Molt fina",
|
||||
"ui.finish_sketch": "Acabar el bocet",
|
||||
"ui.first_dimension_scales": "La primera cota escala el dibuix",
|
||||
"ui.first_dimension_scales_about_origin": "Escalar respecte de l'origen del bocet",
|
||||
"ui.first_dimension_scales_about_origin_tip": "Activat: el dibuix sempre creix o s'encongeix respecte de l'origen del bocet, així que una forma dibuixada lluny de l'origen es mou. Desactivat: un dibuix que res no lliga a l'origen s'escala respecte del seu propi centre i es queda on es va dibuixar.",
|
||||
"ui.first_dimension_scales_tip": "Activat: la primera cota de longitud d'un bocet escala el dibuix que mesura, de manera que la seva forma no canvia. Desactivat: la cota només acota allò que mesura.",
|
||||
"ui.fit": "Ajustar",
|
||||
"ui.fix_unfix": "Fixar/alliberar",
|
||||
@@ -6117,7 +5955,6 @@
|
||||
"ui.project_references_construction_note": "Projectar, Intersecar, Incloure geometria 3D i les arestes de cara d'un bocet nou sobre una cara creen geometria de construcció: referències sobre les quals dibuixar, excloses dels perfils. Continuen vinculades al model.",
|
||||
"ui.properties": "Propietats",
|
||||
"ui.properties.copy_matrix": "Copiar la matriu",
|
||||
"ui.properties.copy_value": "Feu clic per copiar",
|
||||
"ui.properties.first_moments": "Primers moments",
|
||||
"ui.properties.inertia": "Moment d'inèrcia",
|
||||
"ui.properties.inertia_note": "Respecte al centre de masses, en eixos globals (g·{unit}²)",
|
||||
@@ -6130,7 +5967,6 @@
|
||||
"ui.properties.measuring_failed": "El mesurament s'ha aturat abans d'acabar.",
|
||||
"ui.properties.principal_axis": "Eix principal {number}",
|
||||
"ui.properties.principal_moments": "Moments principals",
|
||||
"ui.properties.value_copied": "Valor copiat",
|
||||
"ui.pull_direction": "Direcció d'estirada",
|
||||
"ui.quality": "Qualitat",
|
||||
"ui.quantity": "Quantitat",
|
||||
@@ -6151,14 +5987,11 @@
|
||||
"ui.relink_design_changed": "L'ensamblatge ha canviat mentre trieu un origen. Torneu a iniciar Revincular l'origen.",
|
||||
"ui.relink_done": "Origen revinculat a totes les instàncies.",
|
||||
"ui.relink_not_linked": "Aquest component no està vinculat a cap disseny d'origen.",
|
||||
"ui.relink_one_done": "Origen revinculat en aquesta instància.",
|
||||
"ui.relink_select_one": "Seleccioneu primer un component vinculat o una de les seves instàncies.",
|
||||
"ui.relink_source_all": "Revincular l'origen (totes les instàncies)…",
|
||||
"ui.relink_source_one": "Revincular l'origen…",
|
||||
"ui.removeFromQuickAccess": "Treure de l'accés ràpid",
|
||||
"ui.removeFromToolbar": "Treure de la barra d'eines",
|
||||
"ui.removeSection": "Treure la secció",
|
||||
"ui.renameSelectedBodies": "Canviar el nom dels cossos seleccionats",
|
||||
"ui.render": "Renderitzar",
|
||||
"ui.renderer": "Motor de renderitzat",
|
||||
"ui.repeat": "Repetir",
|
||||
@@ -6189,8 +6022,6 @@
|
||||
"ui.ribbon_pin_footer": "Les ordres fixades es mostren a la cinta. Passeu el punter per una fila per fixar-la o deixar-la de fixar.",
|
||||
"ui.ribbon_workspace": "Espai de treball",
|
||||
"ui.right": "Dreta",
|
||||
"ui.right_click_confirms": "El botó dret confirma les entrades dels diàlegs",
|
||||
"ui.right_click_confirms_tip": "Amb un diàleg obert, un clic amb el botó dret a la vista passa a la seva propera entrada buida, i a l'última executa OK",
|
||||
"ui.save": "Desar",
|
||||
"ui.save_as": "Desar com a",
|
||||
"ui.save_mesh_no_slicer_open": "No s'ha trobat cap laminador: trieu Personalitzat… i trieu el vostre programa de laminatge.",
|
||||
@@ -6250,16 +6081,6 @@
|
||||
"ui.sketch_intersect_selection_filter": "Filtre de selecció",
|
||||
"ui.sketch_keys_only": "Mantenir les tecles del model fora dels bocets",
|
||||
"ui.sketch_keys_only_tip": "Mentre editeu un bocet, només s'executen les tecles de bocet i les que funcionen a tot arreu; una tecla de model com E o F no fa res, així una tecla perduda no pot tancar un bocet pesat.",
|
||||
"ui.sketch_links_added_curve": "{command} ha afegit 1 corba al bocet.",
|
||||
"ui.sketch_links_added_curves": "{command} ha afegit {count} corbes al bocet.",
|
||||
"ui.sketch_links_added_curves_and_points": "{command} ha afegit {curves} i {points} al bocet.",
|
||||
"ui.sketch_links_added_point": "{command} ha afegit 1 punt al bocet.",
|
||||
"ui.sketch_links_added_points": "{command} ha afegit {count} punts al bocet.",
|
||||
"ui.sketch_links_curve_count": "{count} corbes",
|
||||
"ui.sketch_links_curve_count_one": "1 corba",
|
||||
"ui.sketch_links_hidden": "El resultat s'amaga perquè {option} està desactivat a la paleta del bocet.",
|
||||
"ui.sketch_links_point_count": "{count} punts",
|
||||
"ui.sketch_links_point_count_one": "1 punt",
|
||||
"ui.sketch_modifiers": "Modificadors del bocet",
|
||||
"ui.sketch_project_geometry": "Geometria",
|
||||
"ui.sketch_report.all_constrained": "Tot està totalment restringit.",
|
||||
@@ -6299,7 +6120,6 @@
|
||||
"ui.step_values": "Incrementar valors amb les fletxes i la roda",
|
||||
"ui.step_values_tip": "En un camp de valor on esteu escrivint, Amunt/Avall canvien un nombre simple d'1 en 1 (Majúscules: 10, Alt: 0,1), i la roda del ratolí sobre el camp fa el mateix. Les expressions es deixen tal com s'escriuen.",
|
||||
"ui.stitch_tolerance_hint": "estricta",
|
||||
"ui.straighten": "Redreçar",
|
||||
"ui.stretch": "Estirar",
|
||||
"ui.supersampling": "Supermostreig (SSAA)",
|
||||
"ui.supersampling_adaptive": "Només quan la vista està quieta",
|
||||
@@ -6373,8 +6193,6 @@
|
||||
"ui.view_angles_yaw": "Rumb",
|
||||
"ui.view_angles_zero": "Posa {axis} a 0°",
|
||||
"ui.view_centre": "Centre de la vista",
|
||||
"ui.view_compass_tip": "Vista des de {axis}. Torneu-hi a fer clic per invertir-la. Arrossegueu per orbitar.",
|
||||
"ui.view_compass_tip_snap": "Vista des de {axis}. Torneu-hi a fer clic per invertir-la. Arrossegueu per orbitar; manteniu Majúscules per ajustar.",
|
||||
"ui.viewport_status_clear": "Desactivar {name}",
|
||||
"ui.viewport_status_clear_hint": "Desactivar {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} arestes",
|
||||
|
||||
+31
-170
@@ -247,7 +247,6 @@
|
||||
"Drag to move the hole; click to remove it": "Arrastra para mover el agujero; haz clic para quitarlo",
|
||||
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Arrastra para mover el refuerzo por el modelo. Se ajusta a los puntos del boceto en la cara.",
|
||||
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Arrastra para mover el encaje a presión por el modelo. Se ajusta a los puntos del boceto en la cara.",
|
||||
"Drag to place it on another face.": "Arrastra para colocarlo en otra cara.",
|
||||
"Drag to point the catch in another direction.": "Arrastra para orientar el retén en otra dirección.",
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Arrastra para girar el refuerzo en pasos de 5°. Mantén Alt o Ctrl para girarlo libremente.",
|
||||
"Drive Joint": "Accionar unión",
|
||||
@@ -261,8 +260,8 @@
|
||||
"EMBOSS": "REALCE",
|
||||
"EXPLODED VIEW": "VISTA EXPLOSIONADA",
|
||||
"EXTEND": "EXTENDER",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada forma nueva también se acota desde el origen del boceto hasta que no quede nada libre. Una forma ya unida al origen o a geometría situada no recibe ninguna.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que dibujas recibe todas sus cotas con los tamaños dibujados (los valores escritos tienen prioridad). Se omite un tamaño que el boceto ya fija.",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada forma nueva también se acota desde el origen del croquis hasta que no quede nada libre. Una forma ya unida al origen o a geometría situada no recibe ninguna.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que dibujas recibe todas sus cotas con los tamaños dibujados (los valores escritos tienen prioridad). Se omite un tamaño que el croquis ya fija.",
|
||||
"Edge": "Arista",
|
||||
"Edges": "Aristas",
|
||||
"Edit": "Editar",
|
||||
@@ -514,7 +513,6 @@
|
||||
"Offset Y": "Desfase Y",
|
||||
"Offset Z": "Desfase Z",
|
||||
"On your computer": "En tu equipo",
|
||||
"One more stretch of the picked edge, each with its own angle, height and turns": "Otro tramo más de la arista seleccionada, cada uno con su propio ángulo, altura y giros",
|
||||
"Opacity": "Opacidad",
|
||||
"Open GitHub": "Abrir GitHub",
|
||||
"Open GitHub draft": "Abrir borrador de GitHub",
|
||||
@@ -636,7 +634,6 @@
|
||||
"Remove the track": "Eliminar la pista",
|
||||
"Remove this bend": "Quitar este pliegue",
|
||||
"Remove this point": "Eliminar este punto",
|
||||
"Remove this segment": "Quitar este segmento",
|
||||
"Rename Feature": "Cambiar nombre de la operación",
|
||||
"Rename Folder": "Cambiar nombre de la carpeta",
|
||||
"Render Stage": "Escenario de renderizado",
|
||||
@@ -726,7 +723,6 @@
|
||||
"Section position": "Posición de la sección",
|
||||
"Sections": "Secciones",
|
||||
"Sections (one sketch each)": "Secciones (un boceto cada una)",
|
||||
"Segments": "Segmentos",
|
||||
"Select Features": "Seleccionar operaciones",
|
||||
"Select Other": "Seleccionar otro",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar otro: mantén pulsado el botón izquierdo sobre el modelo (o haz clic derecho › Seleccionar otro…) para ver todo lo que hay bajo el cursor; vuelve a pulsar en el mismo lugar para seleccionar el siguiente elemento. Haz doble clic con el botón central para volver a Inicio (Preferencias › Navegación).",
|
||||
@@ -776,7 +772,6 @@
|
||||
"Spacing must be a non-zero number": "El espaciado debe ser un número distinto de cero",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidad",
|
||||
"Split Edge": "Dividir arista",
|
||||
"Splitting tool": "Herramienta de división",
|
||||
"Square": "Cuadrado",
|
||||
"Stainless Steel": "Acero inoxidable",
|
||||
@@ -937,20 +932,13 @@
|
||||
"ai.http.confirm": "Permitir para este servicio",
|
||||
"ai.http.warning": "La clave de API y la conversación se enviarán sin cifrar. Cualquiera con acceso a la red podría leerlas o modificarlas. Úsalo solo en una red de confianza.",
|
||||
"ai.prefs.allow_control": "Permitir que se conecten agentes de IA (canal de control / MCP)",
|
||||
"ai.prefs.allow_control_note": "Un canal local (MCP). Nada sale de este equipo.\n\nAbre un canal de control solo en este equipo (127.0.0.1, en el puerto de abajo; 27878 toma el siguiente libre cuando está en uso). El programa que se conecte también debe presentar un token secreto que solo puede leer tu cuenta de usuario, generado de nuevo en cada arranque. Se aplica al momento. Desactivado: no se escucha nada salvo que inicies SolidCraft con --control.",
|
||||
"ai.prefs.allow_control_note": "Abre un canal de control solo en este equipo (127.0.0.1, puerto 27878 o el siguiente libre). El programa que se conecte también debe presentar un token secreto que solo puede leer tu cuenta de usuario, generado de nuevo en cada arranque. Se aplica la próxima vez que se inicie SolidCraft. Desactivado: no se escucha nada salvo que inicies SolidCraft con --control.",
|
||||
"ai.prefs.availability": "Controles del Asistente",
|
||||
"ai.prefs.background_commands": "Ejecutar los comandos del agente en segundo plano",
|
||||
"ai.prefs.connect_apps": "Poner SolidCraft a disposición de las aplicaciones de IA de este equipo",
|
||||
"ai.prefs.connect_apps_note": "Al iniciar SolidCraft, lo añade como servidor MCP a las aplicaciones de IA que encuentre en este equipo (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), para que un agente pueda usarlo sin configurar nada. Solo se añade la entrada solidcraft o se actualiza su ruta; una entrada que hayas cambiado se deja intacta y el archivo se copia antes. Desactivado: se eliminan las entradas que añadió. Ayuda › Conectar asistentes de IA… lo hace a mano.",
|
||||
"ai.prefs.connect_at_start": "Conectar el último agente usado al iniciar SolidCraft",
|
||||
"ai.prefs.connect_at_start_note": "Inicia el agente que conectaste por última vez (o el predeterminado) en segundo plano cada vez que se abre SolidCraft",
|
||||
"ai.prefs.control_failed": "No se ha podido iniciar el canal de control: {error}",
|
||||
"ai.prefs.control_from_launch": "Iniciado con --control en el puerto {port} para esta ejecución. Los cambios de aquí se aplican la próxima vez que SolidCraft se inicie sin --control.",
|
||||
"ai.prefs.control_listening": "Escuchando en 127.0.0.1:{port}",
|
||||
"ai.prefs.control_not_listening": "No está escuchando",
|
||||
"ai.prefs.control_port": "Puerto",
|
||||
"ai.prefs.control_port_note": "El puerto al que se conectan los agentes de IA en este equipo (127.0.0.1). 27878 es el predeterminado: cuando está en uso, se toma el siguiente libre. Cualquier otro puerto se usa exactamente. Apunta tu cliente MCP al mismo puerto, por ejemplo solidcraft-cli mcp --connect 127.0.0.1:7878.",
|
||||
"ai.prefs.control_port_taken": "El puerto {port} está en uso por otro programa (u otra ventana de SolidCraft). Elige otro puerto.",
|
||||
"ai.prefs.follow_camera": "Girar la cámara hacia los bocetos",
|
||||
"ai.prefs.follow_camera_off": "El seguimiento automático de la cámara está desactivado.",
|
||||
"ai.prefs.follow_camera_on": "La cámara gira hacia los bocetos que crea el agente.",
|
||||
@@ -1011,13 +999,6 @@
|
||||
"browser.sketches_to_parts_not_on_face": "no en la cara de una pieza",
|
||||
"browser.sketches_to_parts_part_missing": "falta su pieza",
|
||||
"browser.sketches_to_parts_result": "Se movieron {moved} bocetos a la pieza en la que están; {stayed} se quedaron: {why}",
|
||||
"browser.straighten_angle": "Dirección de la arista",
|
||||
"browser.straighten_choose": "Elige Horizontal, Vertical o Ángulo para la arista y pulsa Enderezar.",
|
||||
"browser.straighten_click_a": "Haz clic en la imagen en un extremo de una arista que deba quedar recta, o escribe sus coordenadas.",
|
||||
"browser.straighten_click_b": "Haz clic en el otro extremo de la arista, o escribe sus coordenadas.",
|
||||
"browser.straighten_horizontal": "Horizontal",
|
||||
"browser.straighten_title": "Enderezar: {name}",
|
||||
"browser.straighten_vertical": "Vertical",
|
||||
"browser.units": "Unidades: {units}",
|
||||
"cam.flat": "Cara plana",
|
||||
"cam.follower": "Seguidor",
|
||||
@@ -1047,8 +1028,6 @@
|
||||
"cmd.appearance_pick_up": "Tomar apariencia",
|
||||
"cmd.appearance_replace": "Reemplazar apariencia",
|
||||
"cmd.assembly_bom": "Lista de materiales",
|
||||
"cmd.assembly_constraint_create": "Restringir",
|
||||
"cmd.assembly_constraint_edit": "Editar restricción",
|
||||
"cmd.assembly_mass_properties": "Propiedades de masa",
|
||||
"cmd.body.print_face": "Cara de impresión",
|
||||
"cmd.body_rename": "Cambiar nombre del cuerpo",
|
||||
@@ -1067,7 +1046,6 @@
|
||||
"cmd.canvas_delete": "Eliminar lienzo",
|
||||
"cmd.canvas_edit": "Editar lienzo",
|
||||
"cmd.canvas_insert": "Lienzo",
|
||||
"cmd.canvas_straighten": "Enderezar lienzo",
|
||||
"cmd.canvas_to_sketch": "Lienzo a boceto",
|
||||
"cmd.component_activate": "Activar componente",
|
||||
"cmd.component_break_link": "Romper vínculo",
|
||||
@@ -1109,10 +1087,7 @@
|
||||
"cmd.config_rule": "Regla de configuración",
|
||||
"cmd.config_rules": "Reglas de configuración",
|
||||
"cmd.config_table": "Mostrar tabla de configuraciones",
|
||||
"cmd.construct.axis": "Eje",
|
||||
"cmd.construct.plane": "Plano",
|
||||
"cmd.construct.plane.axis_point": "Plano por eje y punto",
|
||||
"cmd.construct.point": "Punto",
|
||||
"cmd.construct_axis_edge": "Eje por arista",
|
||||
"cmd.construct_axis_normal_to_face": "Eje perpendicular a cara",
|
||||
"cmd.construct_axis_through_cylinder": "Eje por cilindro/cono/toro",
|
||||
@@ -1250,10 +1225,6 @@
|
||||
"cmd.motion_trace": "Trayectoria de movimiento",
|
||||
"cmd.motion_trace_edit": "Editar traza de movimiento",
|
||||
"cmd.motion_trace_keep": "Conservar trayectoria de movimiento",
|
||||
"cmd.note_add": "Añadir nota",
|
||||
"cmd.note_delete": "Eliminar nota",
|
||||
"cmd.note_list": "Listar notas",
|
||||
"cmd.note_update": "Editar nota",
|
||||
"cmd.occurrence_align": "Alinear instancia",
|
||||
"cmd.occurrence_copy": "Pegar (instancia)",
|
||||
"cmd.occurrence_drag": "Arrastrar instancia",
|
||||
@@ -1379,7 +1350,6 @@
|
||||
"cmd.render_start": "Renderizar imagen",
|
||||
"cmd.render_status": "Progreso del renderizado",
|
||||
"cmd.select_clear": "Borrar selección",
|
||||
"cmd.selection_get": "Obtener selección",
|
||||
"cmd.settings_profile_apply": "Aplicar perfil de ajustes",
|
||||
"cmd.settings_profile_delete": "Eliminar perfil de ajustes",
|
||||
"cmd.settings_profile_export": "Exportar perfil de ajustes",
|
||||
@@ -1543,7 +1513,6 @@
|
||||
"cmd.timeline_suppress": "Suprimir operación",
|
||||
"cmd.ui.assistant": "Alternar Asistente",
|
||||
"cmd.ui.palette": "Buscar comandos",
|
||||
"cmd.ui.toggle_visibility": "Mostrar/ocultar selección",
|
||||
"cmd.view.navigator": "Navegador de vistas personalizado",
|
||||
"cmd.view.render_browser": "Explorador de render",
|
||||
"cmd.view.render_dock": "Mover acople de render",
|
||||
@@ -1601,7 +1570,6 @@
|
||||
"color.step_down": "Bajar un paso",
|
||||
"color.step_up": "Subir un paso",
|
||||
"color.value": "Valor, el brillo (porcentaje)",
|
||||
"component.error.ambiguous_name": "Varios componentes se llaman `{name}`. Usa un ID numérico de componente: {ids}.",
|
||||
"config.column_filter_empty": "Sin columnas coincidentes",
|
||||
"config.rule_parameter_columns": "Solo se listan los parámetros con una columna en la tabla. Añade una con Añadir columna (+) para usarla aquí.",
|
||||
"config.rule_suppression_columns": "Solo se listan las operaciones con una columna Suprimir en la tabla. Añade una con Añadir columna (+) para controlarla aquí.",
|
||||
@@ -1642,7 +1610,6 @@
|
||||
"diagnostic.reselect_reference": "{feature}: la referencia `{reference}` ya no existe como se seleccionó; vuélvela a seleccionar",
|
||||
"dialog.bodies_count": "{count} cuerpos",
|
||||
"dialog.clearance_invalid": "la holgura debe ser una longitud de 0 o más",
|
||||
"dialog.component_origin": "{placement} · {item}",
|
||||
"dialog.compute": "Calcular",
|
||||
"dialog.compute_again": "Calcular de nuevo",
|
||||
"dialog.computing": "Calculando… {seconds} s",
|
||||
@@ -1655,7 +1622,6 @@
|
||||
"dialog.dimension_step_expression": "Solo se puede avanzar con un número; una expresión se queda como se escribe",
|
||||
"dialog.dimension_step_up": "Subir {step}",
|
||||
"dialog.edit_title": "EDITAR {title}",
|
||||
"dialog.hole_held_off_its_line": "La arista de referencia mantiene este agujero sobre una línea que sale de la cara antes del punto seleccionado. Elige dónde cruza la línea la cara, o quita la arista para mover el agujero libremente.",
|
||||
"dialog.no_shown_body": "No hay cuerpo mostrado para cambiar: muestra uno, o elige Nuevo cuerpo",
|
||||
"dialog.pattern_sketch_not_object": "Selecciona cuerpos o operaciones: un boceto no es ninguno. Repite sus curvas con el Patrón o Espejo del propio boceto, o extrúdelas primero.",
|
||||
"dialog.primitive_point_tip": "Un punto que rellena X, Y y Z: haz clic aquí y después en un vértice, una arista o una cara del modelo, en un punto de un boceto o en el origen.",
|
||||
@@ -1674,12 +1640,9 @@
|
||||
"document.changed_on_disk": "{name} ha cambiado en el disco",
|
||||
"document.changed_on_disk_detail": "Otro programa u otro equipo ha modificado {name} desde que lo abriste o lo guardaste por última vez. Sobrescribir sustituye esos cambios por los tuyos; Guardar una copia conserva ambos.",
|
||||
"document.copy_name": "Copia de {name}",
|
||||
"document.discard_row": "Descartar",
|
||||
"document.finish_save_prompt": "Termina de responder al aviso de guardado antes de abrir otro diseño.",
|
||||
"document.newer_file": "Guardado con una versión más reciente de SolidCraft (formato {format}; esta versión lee hasta {reads}). Si falta algo, actualiza.",
|
||||
"document.newer_file_change": "Guardado con una versión más reciente de SolidCraft (formato {format} con `{change}`, que esta versión no conoce). Si falta algo, actualiza.",
|
||||
"document.newer_file_not_read": "No leído: {what}. Al guardar se conserva.",
|
||||
"document.not_saved_yet": "Aún sin guardar",
|
||||
"document.open_anyway": "Abrir de todos modos",
|
||||
"document.open_elsewhere": "{name} ya está abierto",
|
||||
"document.open_elsewhere_detail": "{name} está abierto en SolidCraft {app} en {who}, abierto {when}. Si guardáis los dos, uno de vosotros perderá su trabajo. Ábrelo en solo lectura para verlo sin sobrescribirlo.",
|
||||
@@ -1689,9 +1652,6 @@
|
||||
"document.read_only_save": "Este diseño se abrió en solo lectura: guárdalo como un archivo nuevo",
|
||||
"document.save_changes": "¿Guardar los cambios de {name}?",
|
||||
"document.save_copy": "Guardar una copia…",
|
||||
"document.save_many": "¿Guardar los cambios de {n} diseños?",
|
||||
"document.save_row": "Guardar",
|
||||
"document.save_selected": "Guardar seleccionados",
|
||||
"document.tab_unsaved_changes": "Cambios sin guardar",
|
||||
"document.unsaved_changes": "{name} tiene cambios sin guardar. Si no los guardas, se perderán.",
|
||||
"drive.rigid": "{joint} es una junta rígida: mantiene sus partes quietas, así que no tiene nada que impulsar.",
|
||||
@@ -1724,7 +1684,6 @@
|
||||
"file_load.inserting": "Insertando geometría",
|
||||
"file_load.link_item": "{name} ({done} de {total})",
|
||||
"file_load.opened": "Se ha abierto {name}",
|
||||
"file_load.opened_behind": "{name} se ha abierto en una pestaña nueva",
|
||||
"file_load.opened_in_tab": "Se ha abierto {name} en una pestaña nueva",
|
||||
"file_load.opening": "Abriendo diseño",
|
||||
"file_load.preparing_thumbnail": "Preparando la miniatura...",
|
||||
@@ -1772,28 +1731,25 @@
|
||||
"help.config.table": "Muestra la tabla de configuración: una fila por variante, una columna por parámetro u operación, y si el diseño sigue coincidiendo con la fila activa. Úsalo para revisar las variantes de un diseño.",
|
||||
"help.connect_note": "Reinicia cada asistente para que cargue el servidor de SolidCraft. Cada archivo se guardó como .bak antes de modificarlo.",
|
||||
"help.connect_title": "Conectar asistentes de IA",
|
||||
"help.construct.axis": "Crea un eje de construcción con el método que pidan tus selecciones: a lo largo de una arista o línea, por un agujero o una cara redonda, donde se encuentran dos planos, por dos puntos, o perpendicular a una cara por un punto. Úsalo cuando conozcas las referencias y no el método.",
|
||||
"help.construct.axis.edge": "Crea un eje de construcción a lo largo de una arista recta o de una línea de boceto. Úsalo como eje de una revolución, un patrón circular o una unión.",
|
||||
"help.construct.axis.normal_to_face": "Crea un eje de construcción que pasa por un punto, perpendicular a una cara. Úsalo como eje que sale recto de una cara.",
|
||||
"help.construct.axis.normal_to_face": "Crea un eje de construcción que pasa por un punto, perpendicular a una cara. Úsalo como un eje que sale recto de una superficie.",
|
||||
"help.construct.axis.through_cylinder": "Crea un eje de construcción por el centro de una cara cilíndrica, cónica o toroidal, o de una arista circular. Úsalo para girar o repetir operaciones alrededor de una pieza redonda.",
|
||||
"help.construct.axis.two_planes": "Crea un eje de construcción donde se cortan dos planos o caras planas. Úsalo cuando el eje que necesitas sea la línea entre dos caras.",
|
||||
"help.construct.axis.two_points": "Crea un eje de construcción por dos puntos, vértices o puntos de boceto. Úsalo para un eje que va entre dos posiciones conocidas.",
|
||||
"help.construct.plane": "Crea un plano de construcción con el método que pidan tus selecciones: con desfase respecto a un plano, por un punto, entre dos planos, por líneas o puntos, tangente a una cara curva, o perpendicular a una trayectoria. Úsalo cuando conozcas las referencias y no el método.",
|
||||
"help.construct.plane.along_path": "Crea un plano de construcción perpendicular a una arista o curva, en una posición a lo largo de ella. Úsalo para dibujar el perfil de un barrido o una sección de una trayectoria.",
|
||||
"help.construct.plane.at_angle": "Crea un plano de construcción girado un ángulo respecto a un plano o cara, alrededor de un eje o arista. Activa «Usar solo la dirección del eje» para proyectar el eje sobre la base y pivotar alrededor de un punto de ese plano (el origen de la base cuando no se elige ninguno). Úsalo para bocetos inclinados, como un soporte angular.",
|
||||
"help.construct.plane.axis_point": "Crea un plano de construcción que contiene un eje y un punto fuera de ese eje. Elige un origen o un eje de construcción, una arista recta o una línea de boceto, y después un vértice, un punto de boceto o un punto de construcción. Un punto del eje no define un plano único.",
|
||||
"help.construct.plane.midplane": "Crea un plano de construcción a medio camino entre dos planos o caras planas, o que los biseca donde se encuentran en ángulo. Úsalo como plano de simetría al reflejar.",
|
||||
"help.construct.plane.midplane": "Crea un plano de construcción a medio camino entre dos planos o caras planas, o que los biseca donde se encuentran en ángulo. Úsalo como plano de simetría para un espejo.",
|
||||
"help.construct.plane.offset": "Crea un plano de construcción paralelo a un plano o una cara plana, a una distancia o por un punto. Úsalo para dibujar a una altura fija sobre una cara.",
|
||||
"help.construct.plane.perpendicular": "Crea un plano de construcción que contiene una línea o arista y es perpendicular a un plano o una cara. Úsalo para dibujar en vertical sobre un modelo a lo largo de una arista.",
|
||||
"help.construct.plane.tangent": "Crea un plano de construcción que toca un cilindro, cono, esfera o toro en un punto. Úsalo para dibujar en el lado de una pieza redonda.",
|
||||
"help.construct.plane.three_points": "Crea un plano de construcción por tres puntos. Úsalo para un plano definido por posiciones del modelo, como tres esquinas.",
|
||||
"help.construct.plane.two_edges": "Crea un plano de construcción por dos aristas rectas o líneas que están en un mismo plano. Úsalo para un plano que cruza una pieza, por ejemplo por dos aristas opuestas.",
|
||||
"help.construct.point": "Crea un punto de construcción con el método que pidan tus selecciones: en un vértice, en el centro de un círculo o una esfera, a lo largo de una arista, donde se encuentran dos líneas o tres planos, o donde una línea cruza un plano. Úsalo cuando conozcas las referencias y no el método.",
|
||||
"help.construct.point.along_path": "Crea un punto de construcción en una posición a lo largo de una arista o curva. Úsalo como referencia a mitad de una trayectoria.",
|
||||
"help.construct.point.center": "Crea un punto de construcción en el centro de una arista circular, un círculo o arco de boceto, una esfera o un toro. Úsalo como referencia para agujeros, ejes y uniones.",
|
||||
"help.construct.point.edge_plane": "Crea un punto de construcción donde una arista o línea atraviesa un plano o una cara plana. Úsalo para marcar dónde una trayectoria cruza un plano.",
|
||||
"help.construct.point.three_planes": "Crea un punto de construcción donde se encuentran tres planos o caras planas. Úsalo como referencia de esquina, incluso donde el modelo no tiene vértice.",
|
||||
"help.construct.point.two_edges": "Crea un punto de construcción donde se cortan dos aristas o líneas, o donde más se acercan. Úsalo como referencia en una esquina virtual.",
|
||||
"help.construct.point.two_edges": "Crea un punto de construcción donde se cortan dos aristas o líneas, o donde están más cerca. Úsalo como referencia en una esquina virtual.",
|
||||
"help.construct.point.vertex": "Crea un punto de construcción sobre un vértice o un punto de boceto. Úsalo como referencia que permanece cuando se oculta el boceto o el cuerpo que lo rodea.",
|
||||
"help.contact.check": "Lista los pares de piezas de los conjuntos de contacto que se atraviesan en su posición actual. Úsalo para encontrar colisiones antes de accionar uniones o ejecutar un estudio de movimiento.",
|
||||
"help.contact.create": "Agrupa dos o más piezas en un conjunto de contacto, cuyos cuerpos no pueden atravesarse mientras las uniones se mueven. Úsalo para piezas que deben detenerse al tocarse, como una palanca contra su tope.",
|
||||
@@ -1815,7 +1771,7 @@
|
||||
"help.font_details": "La interfaz utiliza las fuentes integradas de egui (Ubuntu Light y Hack, bajo las licencias Ubuntu Font Licence y MIT/Bitstream Vera; Noto Emoji y la fuente de iconos emoji, bajo OFL y MIT). El texto de los bocetos utiliza Liberation Sans (SIL OFL 1.1). El paquete opcional de fuentes de The Crafting Apps, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), no se incluye en las compilaciones de SolidCraft.",
|
||||
"help.fonts": "Fuentes",
|
||||
"help.frame.end_cap": "Cierra los extremos abiertos de los miembros de una estructura con placas con la forma del contorno del perfil. Úsalo para tapar tubos, angulares y vigas, sobre el extremo o embutidas en él.",
|
||||
"help.frame.member": "Coloca un perfil, como un tubo o una extrusión de aluminio, a lo largo de las líneas de un boceto de estructura, un miembro por línea. Úsalo para construir bastidores, estanterías y estructuras soldadas.",
|
||||
"help.frame.member": "Coloca un perfil, como un tubo o un perfil de aluminio extruido, a lo largo de las líneas de un boceto de estructura, un miembro por línea. Úsalo para construir bastidores, estanterías y estructuras soldadas.",
|
||||
"help.inspect.accessibility": "Colorea las caras a las que una herramienta puede llegar desde una dirección y las que no. Úsalo para comprobar una pieza para mecanizado o moldeo desde ese lado.",
|
||||
"help.inspect.center_of_mass": "Muestra el centro de masa de los cuerpos, cada uno por separado y todos juntos, con densidad uniforme. Úsalo para comprobar el equilibrio y dónde volcará una pieza.",
|
||||
"help.inspect.colored_components": "Da a cada componente su propio color temporal, sin cambiar los aspectos. Úsalo para distinguir de un vistazo las piezas de un ensamblaje.",
|
||||
@@ -1886,11 +1842,11 @@
|
||||
"help.parts.library": "Abre las piezas estándar: tornillos, pernos, tuercas, arandelas, tornillos sin cabeza, pasadores cilíndricos y rodamientos en tamaños estándar. Úsalo para añadir una pieza estándar en lugar de modelarla tú.",
|
||||
"help.parts.resize": "Cambia el tamaño o la longitud de una pieza estándar insertada y la reconstruye; sus uniones la mantienen asentada. Úsalo cuando un tornillo resulta demasiado largo o un agujero necesita otro tamaño.",
|
||||
"help.plastic.assign_rule": "Asigna una regla de plástico a cuerpos, de modo que sus operaciones de plástico toman su desmoldeo y holgura como valores por defecto. Úsalo para configurar una pieza moldeada una sola vez para todas sus operaciones.",
|
||||
"help.plastic.boss": "Añade un saliente para tornillo en una cara: un poste con agujero opcional, desmoldeo, redondeo en la base y nervios de apoyo. Úsalo para los puntos de tornillo en carcasas moldeadas o impresas.",
|
||||
"help.plastic.boss": "Añade una torreta para tornillo en una cara: un poste con agujero opcional, desmoldeo, redondeo en la base y nervios de apoyo. Úsalo para los puntos de tornillo en carcasas moldeadas o impresas.",
|
||||
"help.plastic.lip": "Adds a lip or a matching groove along the rim of a shelled body. Use it so that two housing halves locate on each other and close tightly.",
|
||||
"help.plastic.manage_rules": "Crea y edita reglas de plástico: material, espesor de pared, radios, holgura y desmoldeo. Úsalas para mantener coherentes las operaciones de una pieza moldeada.",
|
||||
"help.plastic.rest": "Añade un apoyo en una cara: un taco redondo o rectangular, macizo o hueco, con desmoldeo. Úsalo como soporte o separador en una carcasa moldeada.",
|
||||
"help.plastic.snap_fit": "Añade un gancho de encaje a presión en una cara, con su longitud, espesor y retención. Úsalo para unir piezas sin tornillos.",
|
||||
"help.plastic.snap_fit": "Añade un gancho de encaje a presión en una cara, con su longitud, espesor y resalte. Úsalo para unir piezas sin tornillos.",
|
||||
"help.pointcloud.insert": "Inserta un escaneo PLY, XYZ o PTS como nube de puntos junto al modelo. Úsala como",
|
||||
"help.sheet.convert": "Convierte una placa o un cuerpo vaciado de espesor uniforme en un cuerpo de chapa metálica, con pliegues en las aristas que elijas. Úsalo para hacer desplegable una pieza existente.",
|
||||
"help.sheet.export_bend_table": "Guarda la tabla de plegado de un cuerpo de chapa metálica como CSV o JSON: ángulos, radios y posiciones de los pliegues en el desarrollo. Úsalo para la plegadora o el taller.",
|
||||
@@ -1901,22 +1857,22 @@
|
||||
"help.sheet.hem": "Dobla el borde de una chapa sobre sí mismo. Úsalo para bordes seguros y rígidos, sin rebabas cortantes.",
|
||||
"help.sheet.manage_rules": "Crea y edita reglas de chapa metálica: espesor, radio de plegado, factor K, alivios y holguras. Úsalas para ajustarte al material y a las herramientas de tu taller.",
|
||||
"help.sheet.path_flange": "Añade una cadena de pestañas a lo largo del borde de una chapa, siguiendo una cadena abierta de líneas de boceto. Úsalo para perfiles como una U o una Z en un solo paso.",
|
||||
"help.sheet.perforate": "Añade un campo de agujeros sobre toda la chapa plana, en patrón al tresbolillo o en cuadrícula. Úsalo para rejillas, ventilaciones y paneles más ligeros.",
|
||||
"help.sheet.perforate": "Añade un campo de agujeros sobre toda la chapa plana, en patrón al tresbolillo o cuadrado. Úsalo para rejillas, ventilaciones y paneles más ligeros.",
|
||||
"help.sheet.refold": "Vuelve a plegar un cuerpo de chapa metálica desplegado. Úsalo tras añadir operaciones a la chapa plana.",
|
||||
"help.sheet.unfold": "Despliega todos los pliegues de un cuerpo de chapa metálica. Úsalo para añadir agujeros o cortes que deban cruzar un pliegue, y luego vuelve a plegar.",
|
||||
"help.sketch.arc.center_point": "Dibuja un arco circular alrededor de un punto central, de un punto inicial a un punto final. Úsalo cuando se conoce el centro del arco, como un radio alrededor de un agujero o de un pivote.",
|
||||
"help.sketch.arc.tangent": "Dibuja un arco que empieza en el extremo de una línea o arco y continúa tangente a él. Úsalo para seguir una cadena con una curva suave en lugar de una esquina.",
|
||||
"help.sketch.arc.three_point": "Dibuja un arco de un punto inicial a un punto final pasando por un tercer punto. Úsalo cuando el arco deba pasar por puntos dados y no alrededor de un centro conocido.",
|
||||
"help.sketch.arc3d": "Dibuja un arco circular exacto en el espacio por tres puntos, con el Boceto 3D activado. Úsalo para curvas en trayectorias y guías que salen del plano del boceto.",
|
||||
"help.sketch.auto_constrain": "Añade las restricciones que el boceto ya casi cumple, como horizontal, paralela o coincidente, y luego lo acota hasta definirlo por completo. Úsalo para fijar en un solo paso un boceto dibujado a mano alzada.",
|
||||
"help.sketch.auto_constrain_from_datum": "Funciona como Restringir automáticamente, pero mide las posiciones desde un punto de referencia que eliges en lugar del origen. Úsalo cuando la pieza se organiza a partir de un punto de referencia, como una esquina o un agujero.",
|
||||
"help.sketch.auto_constrain": "Añade las restricciones que el boceto ya casi cumple, como horizontal, paralela o coincidente, y luego lo acota hasta definirlo por completo. Úsalo para fijar de un paso un boceto dibujado a mano alzada.",
|
||||
"help.sketch.auto_constrain_from_datum": "Funciona como Restricción automática, pero mide las posiciones desde un punto de referencia que eliges en lugar del origen. Úsalo cuando la pieza se organiza a partir de un punto de referencia, como una esquina o un agujero.",
|
||||
"help.sketch.blend_curve": "Une los extremos de dos curvas con una spline suave tangente a ambas, o también de curvatura continua si lo eliges. Úsalo para salvar un hueco entre curvas sin quiebros.",
|
||||
"help.sketch.break": "Divide una curva en trozos donde otras curvas la cruzan, o en puntos que colocas sobre ella. Úsalo cuando una parte de una curva necesite su propia restricción o deba borrarse por separado.",
|
||||
"help.sketch.cancel": "Sale del boceto y descarta todo lo cambiado desde que se creó o se abrió para editarlo. Deshacer recupera los cambios.",
|
||||
"help.sketch.centerline": "Cambia las líneas entre líneas normales y líneas de eje, ejes que los perfiles dejan fuera. Usa una línea de eje como eje de una revolución, una simetría o una restricción de simetría.",
|
||||
"help.sketch.chamfer.distance_angle": "Bisela la esquina entre dos líneas, definida por una distancia a lo largo de la primera línea y un ángulo. Úsalo cuando un plano da el chaflán como una longitud y un ángulo, como 2 mm × 30°.",
|
||||
"help.sketch.centerline": "Cambia las líneas entre líneas normales y líneas de eje, ejes que los perfiles dejan fuera. Usa una línea de eje como eje de una revolución, un espejo o una restricción de simetría.",
|
||||
"help.sketch.chamfer.distance_angle": "Bisela la esquina entre dos líneas, definido por una distancia a lo largo de la primera línea y un ángulo. Úsalo cuando un plano da el chaflán como una longitud y un ángulo, como 2 mm × 30°.",
|
||||
"help.sketch.chamfer.equal_distance": "Bisela la esquina entre dos líneas la misma distancia hacia atrás en ambas. Úsalo para el chaflán habitual de 45° en un contorno.",
|
||||
"help.sketch.chamfer.two_distance": "Bisela la esquina entre dos líneas, definida por una distancia distinta a lo largo de cada línea. Úsalo para chaflanes que no son de 45°, dados como dos longitudes.",
|
||||
"help.sketch.chamfer.two_distance": "Bisela la esquina entre dos líneas, definido por una distancia distinta a lo largo de cada línea. Úsalo para chaflanes que no son de 45°, dados como dos longitudes.",
|
||||
"help.sketch.circle.center": "Dibuja un círculo a partir de su centro y su diámetro. Úsalo para agujeros, salientes y otras formas redondas cuyo centro se conoce.",
|
||||
"help.sketch.circle.three_point": "Dibuja un círculo por tres puntos. Úsalo cuando el círculo deba pasar por puntos dados, por ejemplo de geometría existente.",
|
||||
"help.sketch.circle.three_tangent": "Dibuja un círculo que toca tres líneas, círculos o arcos. Úsalo para ajustar un círculo en el espacio entre tres curvas, como el círculo inscrito de un triángulo.",
|
||||
@@ -1941,13 +1897,13 @@
|
||||
"help.sketch.construction_line": "Crea una línea de construcción infinita a través de dos puntos. Úsala para organizar un boceto.",
|
||||
"help.sketch.construction_line.angle": "Crea una línea de construcción infinita en un ángulo desde el eje X del boceto. Elige el punto a",
|
||||
"help.sketch.construction_line.bisector": "Elige dos líneas rectas de boceto que se intersecten, luego elige una de las dos bisectrices del ángulo.",
|
||||
"help.sketch.construction_line.horizontal": "Crea una línea de construcción horizontal infinita que pase por un punto, o escribe su distancia al eje X del boceto. Una distancia escrita se convierte en una cota controladora. Su restricción horizontal sigue las ediciones posteriores.",
|
||||
"help.sketch.construction_line.parallel": "Elige una línea recta de boceto y luego coloca una línea de construcción paralela infinita o escribe su desfase. Un desfase escrito se convierte en una distancia controladora. Haz clic con el botón derecho para elegir otra referencia.",
|
||||
"help.sketch.construction_line.horizontal": "Crea una línea de construcción horizontal infinita a través de un punto. Su dirección horizontal",
|
||||
"help.sketch.construction_line.parallel": "Elige una línea recta de boceto, luego coloca una línea de construcción paralela infinita. R",
|
||||
"help.sketch.construction_line.perpendicular": "Elige una línea recta de boceto, luego coloca una línea de construcción perpendicular infinita.",
|
||||
"help.sketch.construction_line.tangent_at": "Crea una tangente en un punto de un círculo o arco circular, o en un extremo de una curva abierta.",
|
||||
"help.sketch.construction_line.tangent_external": "Elige un círculo o arco circular, luego un punto externo y una solución de tangente. Th",
|
||||
"help.sketch.construction_line.tangent_two": "Elige dos círculos o arcos circulares, luego elige una tangente común en la previsualización.",
|
||||
"help.sketch.construction_line.vertical": "Crea una línea de construcción vertical infinita que pase por un punto, o escribe su distancia al eje Y del boceto. Una distancia escrita se convierte en una cota controladora. Su restricción vertical sigue las ediciones posteriores.",
|
||||
"help.sketch.construction_line.vertical": "Crea una línea de construcción vertical infinita a través de un punto. Su dirección vertical",
|
||||
"help.sketch.copy": "Copia las curvas y puntos de boceto seleccionados, junto con las restricciones y cotas entre ellos. Pégalos en este boceto o en otro.",
|
||||
"help.sketch.create": "Empieza un nuevo boceto 2D en un plano o en una cara plana del modelo. Toda extrusión, revolución y barrido empieza con un boceto.",
|
||||
"help.sketch.cut": "Copia la geometría de boceto seleccionada y la borra del boceto, en un solo paso de deshacer. Pégala en otro lugar de este boceto o en otro.",
|
||||
@@ -1958,10 +1914,10 @@
|
||||
"help.sketch.extend": "Alarga una línea o un arco hasta la curva más cercana que encuentre, o hace crecer dos curvas hasta que se encuentran. Úsalo para cerrar huecos de un perfil tras recortar o editar.",
|
||||
"help.sketch.fillet": "Redondea la esquina donde se encuentran dos líneas o arcos con un arco de un radio dado. Varias esquinas redondeadas a la vez comparten un radio.",
|
||||
"help.sketch.finish": "Sale del boceto y vuelve al modelo. Sus perfiles cerrados quedan listos para Extrusión, Revolución y otras operaciones.",
|
||||
"help.sketch.finish_auto_constrain": "Ejecuta Restringir automáticamente en el boceto y luego lo finaliza. Úsalo para dejar un boceto totalmente definido en un solo paso.",
|
||||
"help.sketch.finish_auto_constrain": "Ejecuta Restricción automática en el boceto y luego lo finaliza. Úsalo para dejar un boceto totalmente definido de un paso.",
|
||||
"help.sketch.fit_curves_to_section": "Corta un cuerpo de malla o sólido con el plano del boceto y ajusta líneas y arcos a la sección. Úsalo para reconstruir como boceto editable una pieza escaneada o importada.",
|
||||
"help.sketch.include_3d": "Trae aristas del modelo, vértices y curvas de otros bocetos a este boceto como referencias vinculadas, aplanando sobre él las que no están en su plano. Úsalo para construir sobre geometría que no está en el plano del boceto.",
|
||||
"help.sketch.infinite": "Cambia las líneas entre líneas normales y líneas de construcción infinitas que cruzan todo el visor. Úsalas como referencias de disposición que nunca se quedan cortas.",
|
||||
"help.sketch.infinite": "Cambia las líneas entre líneas normales y líneas de construcción infinitas que cruzan toda la vista. Úsalas como referencias de disposición que nunca se quedan cortas.",
|
||||
"help.sketch.insert_dxf": "Lee las curvas de un dibujo DXF en un boceto, escaladas, giradas y colocadas donde indiques. Úsalo para construir sobre un dibujo 2D de otro programa, como un contorno para corte láser.",
|
||||
"help.sketch.insert_svg": "Lee los trazados de un dibujo SVG en un boceto, escalados, girados y colocados donde indiques. Úsalo para convertir un logotipo o un contorno vectorial en curvas que puedas extruir.",
|
||||
"help.sketch.inspect": "Abre un informe sobre el boceto abierto: cuántos grados de libertad quedan y qué",
|
||||
@@ -2066,12 +2022,12 @@
|
||||
"help.view.apply": "Gira la vista a una dirección estándar como frontal, superior o isométrica, o a una vista con nombre guardada. Úsalo para mirar el modelo desde un ángulo fijo.",
|
||||
"help.view.delete": "Borra una vista con nombre guardada. El modelo no cambia.",
|
||||
"help.view.fit": "Acerca y desplaza para que todo el modelo, o la selección, llene la vista. Úsalo cuando el modelo se ha salido de la vista.",
|
||||
"help.view.grid": "Muestra u oculta la cuadrícula en el visor. Úsala como guía de tamaño y orientación al modelar.",
|
||||
"help.view.grid": "Muestra u oculta la cuadrícula en la vista. Úsala como guía de tamaño y orientación al modelar.",
|
||||
"help.view.ground_shadow": "Muestra u oculta la sombra del modelo en el suelo. Úsalo para juzgar con más facilidad la altura y la posición.",
|
||||
"help.view.home": "Devuelve la vista a la dirección de inicio y encuadra el modelo. Úsalo para volver rápido a una vista conocida.",
|
||||
"help.view.list": "Lista las vistas con nombre guardadas en el diseño. Úsalo para encontrar una vista que guardaste antes.",
|
||||
"help.view.navigate": "Gira, desplaza o acerca la vista por pasos, como un cuarto de vuelta a la izquierda. Úsalo para mover la cámara con precisión en lugar de arrastrar.",
|
||||
"help.view.navigation_bar": "Muestra u oculta la barra de navegación en la parte inferior del visor. Ocúltala para tener más espacio, muéstrala para llegar a las herramientas de vista con el ratón.",
|
||||
"help.view.navigation_bar": "Muestra u oculta la barra de navegación en la parte inferior de la vista. Ocúltala para tener más espacio, muéstrala para llegar a las herramientas de vista con el ratón.",
|
||||
"help.view.rename": "Da un nombre nuevo a una vista con nombre guardada. La vista en sí no cambia.",
|
||||
"help.view.ribbon_tabs_only": "Pliega la cinta hasta su fila de pestañas para que la vista recupere el espacio de los paneles. Haz clic en una pestaña para mostrar sus paneles sobre la vista; vuelve a elegir esta opción para mantener toda la cinta visible.",
|
||||
"help.view.save": "Guarda la posición actual de la cámara como una vista con nombre en el diseño. Úsalo para volver al mismo ángulo más tarde, por ejemplo para capturas de pantalla.",
|
||||
@@ -2291,9 +2247,6 @@
|
||||
"measure.at": "En",
|
||||
"measure.item": "Elemento {number}",
|
||||
"measure.line_to_centre": "Línea al centro",
|
||||
"measure.perimeter": "Perímetro",
|
||||
"measure.perimeter_inner": "Perímetro interior",
|
||||
"measure.perimeter_outer": "Perímetro exterior",
|
||||
"measure.quick_copied": "Medición copiada",
|
||||
"measure.quick_copy": "Haz clic para copiar la medición",
|
||||
"measure.quick_edges": "{count} aristas",
|
||||
@@ -2335,8 +2288,6 @@
|
||||
"nav.hfss.help": "Alt+arrastre izq. desplaza, Mayús+Alt+arrastre izq. hace zoom, Ctrl+arrastre izq. en espacio vacío orbita",
|
||||
"nav.inventor.help": "Arrastrar con el botón central desplaza, Mayús+central orbita, Ctrl+central hace zoom y arrastrar con el derecho orbita",
|
||||
"nav.maya.help": "Alt+arrastre izq. orbita, Alt+arrastre central desplaza, Alt+arrastre der. hace zoom; arrastre central también desplaza",
|
||||
"nav.maya_horizontal_zoom.help": "Alt+arrastrar con el botón derecho: hacia la derecha acerca, hacia la izquierda aleja. Desactívalo para volver al zoom arrastrando en vertical.",
|
||||
"nav.maya_horizontal_zoom.label": "Maya: zoom arrastrando en horizontal",
|
||||
"nav.nx.help": "Arrastrar con botón central orbita, Shift+central desplaza (central + derecho de NX), Ctrl+central hace zoom (central + izquierdo de NX); arrastrar con derecho deja la cámara",
|
||||
"nav.openscad.help": "Arrastrar con el botón izquierdo sobre un espacio vacío orbita, Mayús+izquierdo selecciona con un recuadro y arrastrar geometría del boceto la edita; arrastrar con el derecho o el central desplaza, y Mayús+derecho o Mayús+central hace zoom",
|
||||
"nav.orbit_cursor_wrap.help": "Un arrastre de órbita que llega al borde de la vista mueve el cursor al borde opuesto y sigue girando. Desactivado: el arrastre se detiene en el borde de la pantalla. No tiene efecto en Wayland ni en el navegador.",
|
||||
@@ -2467,16 +2418,6 @@
|
||||
"not a move": "no es un movimiento",
|
||||
"not a number": "no es un número",
|
||||
"open a sketch first": "abre primero un boceto",
|
||||
"operations.cancel": "Cancelar {name}",
|
||||
"operations.cancelled": "{name} cancelado",
|
||||
"operations.cancelling": "Cancelando…",
|
||||
"operations.cannot_cancel": "{name} no se puede cancelar",
|
||||
"operations.feature": "Operación",
|
||||
"operations.feature_edit": "Editar operación",
|
||||
"operations.meshing": "Cargando piezas",
|
||||
"operations.queued": "En cola",
|
||||
"operations.stopped": "Cancelado",
|
||||
"operations.waiting": "Listo",
|
||||
"parameters.apply_live": "Aplicar en vivo",
|
||||
"parameters.apply_live_tip": "Aplica los cambios de parámetros mientras los editas",
|
||||
"parameters.held": "Mantenidos ({count})",
|
||||
@@ -2565,7 +2506,6 @@
|
||||
"prefs.appearance.viewport_colours": "Colores de vista 3D",
|
||||
"prefs.appearance.viewport_logo": "Visor y logo",
|
||||
"prefs.applies_on_restart": "Se aplica al reiniciar",
|
||||
"prefs.assemblies": "Ensamblajes",
|
||||
"prefs.assistant": "Asistente",
|
||||
"prefs.assistant_connecting": "Conectando",
|
||||
"prefs.assistant_follow_help": "Los bocetos Mirar mueven la vista con el puntero.",
|
||||
@@ -2618,10 +2558,6 @@
|
||||
"prefs.choose_font": "Elegir archivo de fuente…",
|
||||
"prefs.click": "Clic",
|
||||
"prefs.command_dialog_values": "Diálogos de comando",
|
||||
"prefs.constraint_glyph_size": "Tamaño de los glifos de restricción",
|
||||
"prefs.constraint_glyph_size_note": "Cómo de grandes se dibujan los glifos de restricción y sus letras o iconos en un boceto. El 100 % es el tamaño habitual; los glifos conservan este tamaño en pantalla con cualquier zoom.",
|
||||
"prefs.construct_menu_groups": "Agrupar las herramientas de Construir por tipo de entidad",
|
||||
"prefs.construct_menu_groups_help": "Muestra los encabezados Planos, Ejes y Puntos. Desactivado, se mantiene el orden original del menú.",
|
||||
"prefs.contact_shading": "Sombreado de contacto suave",
|
||||
"prefs.contact_shading_note": "Sombreado suave donde las piezas se acercan.",
|
||||
"prefs.context_menu": "Menú contextual",
|
||||
@@ -2653,12 +2589,10 @@
|
||||
"prefs.design_tools.imported_files": "Archivos importados",
|
||||
"prefs.design_tools.note": "Nota",
|
||||
"prefs.design_tools.off": "Desactivado",
|
||||
"prefs.design_tools.placement_group": "Apoyo, Saliente, Encaje",
|
||||
"prefs.design_tools.placement_group": "Apoyo, Resalte, Encaje",
|
||||
"prefs.design_tools.rows": "Filas",
|
||||
"prefs.devices": "Ratón 3D",
|
||||
"prefs.dialog_buttons": "Botones de diálogo",
|
||||
"prefs.dialog_first_letter_buttons": "Los botones de diálogo responden a su primera letra",
|
||||
"prefs.dialog_first_letter_buttons_help": "Con un diálogo abierto, escribir la primera letra de la etiqueta de un botón lo pulsa: O para OK, C para Cancelar. Una letra que encaja en dos botones no hace nada, y un campo de texto con el foco conserva la pulsación. Desactivado: escribir no afecta a los botones, como antes.",
|
||||
"prefs.dialogs": "Diálogos",
|
||||
"prefs.dialogs_edge": "En el borde de la vista",
|
||||
"prefs.dialogs_left_column": "En la columna izquierda",
|
||||
@@ -2672,23 +2606,12 @@
|
||||
"prefs.dimension_values_window": "En una ventana",
|
||||
"prefs.display": "Pantalla",
|
||||
"prefs.edges": "Aristas y sombreado",
|
||||
"prefs.export": "Exportar",
|
||||
"prefs.export_name": "Nombre del archivo de exportación",
|
||||
"prefs.export_name.body": "Nombre del cuerpo exportado (el primero cuando hay varios)",
|
||||
"prefs.export_name.design": "Nombre del diseño",
|
||||
"prefs.export_name_hint": "p. ej., {design}-{body}",
|
||||
"prefs.export_name_sample": "Al exportar este diseño se ofrece: {name}",
|
||||
"prefs.export_name_tip": "El nombre de archivo que ofrecen Archivo › Exportar y Guardar como malla. Se sustituyen estos marcadores:\n{placeholders}\nUna propiedad del diseño que no esté definida se omite. Deja el campo vacío para ofrecer el nombre del diseño o, en una malla, el del cuerpo.",
|
||||
"prefs.extend": "Extensiones",
|
||||
"prefs.extra_navigation": "Ctrl + arrastrar con el botón izquierdo sobre un espacio vacío orbita; mantén Espacio para desplazar.",
|
||||
"prefs.extrude_arrow_face_stops": "La flecha de Extruir se detiene en las caras",
|
||||
"prefs.extrude_arrow_face_stops_note": "Al arrastrar la flecha de distancia de Extruir, se detiene en las caras planas paralelas al perfil, como el lado opuesto de un bloque que se corta, y resalta la cara. Desactivado: la flecha solo avanza en pasos redondos.",
|
||||
"prefs.extrude_view_isometric": "Girar la vista a isométrica al abrir Extrusión",
|
||||
"prefs.extrude_view_return": "Volver a la vista anterior al cerrar Extrusión",
|
||||
"prefs.file_operations_wait": "Esperar a que se abran los archivos",
|
||||
"prefs.file_operations_wait_note": "Mientras se abre o se inserta un archivo, o se vuelven a leer los vínculos, el diseño espera tras una tarjeta con el progreso del archivo y Cancelar. Desactivado: el archivo aparece en el panel de operaciones y puedes seguir trabajando; una inserción se aplica cuando termines lo que estás haciendo, y un archivo abierto mientras tanto va a una pestaña nueva.",
|
||||
"prefs.fillet_legacy_radius": "Usar el radio predeterminado anterior de Redondeo de sólido",
|
||||
"prefs.fillet_legacy_radius.help": "Desactivado de forma predeterminada. Activado, un nuevo Redondeo de sólido se abre con el número 2 leído en las unidades del diseño («2 in» en un diseño en pulgadas), como antes; desactivado, se abre con el radio que trae la herramienta, 2 mm, mostrado en las unidades del diseño («0.07874 in» en un diseño en pulgadas).",
|
||||
"prefs.follows_os": "Sigue al sistema operativo",
|
||||
"prefs.font": "Fuente",
|
||||
"prefs.font_fallback": "Los glifos que faltan en la fuente elegida se toman de la fuente integrada, así que ninguna etiqueta muestra un recuadro vacío.",
|
||||
@@ -2766,8 +2689,6 @@
|
||||
"prefs.history_sketches_in_order": "En orden",
|
||||
"prefs.history_sketches_note": "Con Bocetos en Debajo de su operación, el Historial de la izquierda lista cada boceto un nivel dentro, bajo la primera operación construida a partir de él, siempre que haya ancho para nombres. La línea de tiempo en la parte inferior mantiene siempre el orden de creación.",
|
||||
"prefs.history_sketches_under_feature": "Debajo de su operación",
|
||||
"prefs.history_steps_component": "Pasos del historial por componente",
|
||||
"prefs.history_steps_component_note": "Con un componente activo o seleccionado, los controles del Historial recorren las operaciones de ese componente, tal como las muestra el Historial de abajo. Desactivado: recorren todas las operaciones del diseño. El Historial de la izquierda o bajo el navegador siempre recorre todas las operaciones.",
|
||||
"prefs.hold_stop_snapping": "Mantén pulsado para desactivar el ajuste",
|
||||
"prefs.home_fit_size": "Tamaño de Inicio y Ajustar",
|
||||
"prefs.home_fit_size_note": "Cuánto de la vista ocupa el modelo al usar Inicio, Ajustar, un diseño abierto o un cambio de vista del navegador, independientemente de la configuración siguiente. 100 %: llena la vista entre los paneles, la brújula y la barra de navegación.",
|
||||
@@ -2783,8 +2704,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "Las dos flechas curvas en la esquina del cubo giran la vista en el plano de la pantalla. Activado: la flecha horaria gira antihoraria y la antihoraria gira horaria. Desactivado: cada flecha gira la vista hacia donde se curva.",
|
||||
"prefs.invert_navigator_arrows": "Invertir las flechas de rotación del cubo de vista",
|
||||
"prefs.invert_navigator_arrows_note": "Las flechas alrededor del cubo orbitan la cámara hacia donde apuntan, como en Fusion e Inventor: la flecha de la izquierda apunta a la derecha y gira la cámara a la derecha. Desactivado: una flecha rota el cubo hacia el lado en que está, así la flecha izquierda gira la cámara a la izquierda. Las flechas de giro curvo no se ven afectadas.",
|
||||
"prefs.joint_drag_trackball": "Girar las piezas de una unión de rótula como un trackball al arrastrarlas",
|
||||
"prefs.joint_drag_trackball_help": "Arrastrar una pieza que sujeta una unión de rótula la gira con el menor giro posible alrededor de la unión, como si rodara una bola del tamaño de la pieza. Desactivado: el punto que agarraste sigue exactamente al puntero, y la pieza también puede girar sobre él.",
|
||||
"prefs.keep_unsaved_also_asks": "Preguntar igualmente al cerrar la pestaña",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Con esto desactivado, la pestaña se cierra sin el cuadro del navegador en cuanto la copia de cada diseño sin guardar está al día.",
|
||||
"prefs.keep_unsaved_in_browser": "Conservar el trabajo sin guardar en este navegador",
|
||||
@@ -2860,8 +2779,6 @@
|
||||
"prefs.not_on_gpu": "no en esta GPU",
|
||||
"prefs.on": "Activado",
|
||||
"prefs.opaque": "Opaco",
|
||||
"prefs.operations_panel": "Mostrar las operaciones en la vista",
|
||||
"prefs.operations_panel_note": "Las operaciones largas (abrir un archivo, calcular una operación, un comando del agente) se muestran en la esquina inferior derecha de la vista con su progreso y un botón para cancelarlas. Desactivado: un aviso de «Calculando», como antes.",
|
||||
"prefs.orbit_snap": "Mantén Mayús mientras orbitas para ajustar a un eje.",
|
||||
"prefs.orientation": "Orientación",
|
||||
"prefs.origin_plane_names": "Nombres de los planos de origen",
|
||||
@@ -2942,8 +2859,6 @@
|
||||
"prefs.ribbon_tabs_only": "Mostrar solo las pestañas de la cinta",
|
||||
"prefs.ribbon_tabs_only_note": "La cinta se pliega hasta su fila de pestañas. Al hacer clic en una pestaña se muestran sus paneles sobre la vista hasta que empieza un comando o el puntero sale de ellos. «Solo pestañas de la cinta», en la pestaña Vista o con su atajo, alterna esta opción.",
|
||||
"prefs.right_click": "Clic derecho",
|
||||
"prefs.save_prompt_one_at_a_time": "Preguntar por cada diseño sin guardar por separado al cerrar varios",
|
||||
"prefs.save_prompt_one_at_a_time_help": "Mostrar un aviso de guardado por cada diseño sin guardar en lugar de una lista combinada.",
|
||||
"prefs.saving": "Guardado automático y recuperación",
|
||||
"prefs.scale_note": "También Ctrl + / Ctrl − en la ventana. Los botones miden 28 px al 100%.",
|
||||
"prefs.scale_text": "Escala y texto",
|
||||
@@ -2980,8 +2895,8 @@
|
||||
"prefs.sketch.center_arc": "El arco por centro sigue al cursor",
|
||||
"prefs.sketch.click_selects": "El clic selecciona",
|
||||
"prefs.sketch.conflict_colour": "Color de conflicto",
|
||||
"prefs.sketch.cursor_coordinates": "Mostrar las coordenadas del cursor en un boceto",
|
||||
"prefs.sketch.cursor_coordinates_note": "Junto al puntero, su X e Y desde el origen del boceto, también sin ninguna herramienta activa. Desactivado: solo mientras una herramienta coloca un punto.",
|
||||
"prefs.sketch.cursor_coordinates": "Mostrar las coordenadas del cursor en un croquis",
|
||||
"prefs.sketch.cursor_coordinates_note": "Junto al puntero, su X e Y desde el origen del croquis, también sin ninguna herramienta activa. Desactivado: solo mientras una herramienta coloca un punto.",
|
||||
"prefs.sketch.drawing": "Dibujo",
|
||||
"prefs.sketch.ellipse_axes": "Dibujar elipses con sus ejes",
|
||||
"prefs.sketch.ellipse_axes_note": "Los ejes mayor y menor como líneas de construcción, para poder acotar los semiejes y los ejes. Desactivado: solo la elipse, y al pulsar D sobre ella se da su radio menor allí donde se arrastre el valor, dibujado y etiquetado como R, como antes.",
|
||||
@@ -2993,20 +2908,14 @@
|
||||
"prefs.sketch.faces_edges": "Caras y aristas",
|
||||
"prefs.sketch.hide_glyphs": "Ocultar glifos al alejar",
|
||||
"prefs.sketch.marks": "Marcas en el boceto",
|
||||
"prefs.sketch.max_points": "Boceto más grande",
|
||||
"prefs.sketch.max_points_note": "El máximo de puntos o curvas que admite un boceto: 20000 de forma predeterminada, como mucho 200000. Insertar SVG e Insertar DXF rechazan un dibujo mayor. Un boceto grande se resuelve y se dibuja más despacio.",
|
||||
"prefs.sketch.max_points_unit": "puntos",
|
||||
"prefs.sketch.note": "Dibujo, ajuste y selección.",
|
||||
"prefs.sketch.opening_closing": "Apertura y cierre",
|
||||
"prefs.sketch.prefer_free_points": "Preferir puntos móviles donde se solapen",
|
||||
"prefs.sketch.project_options": "Preguntar primero las opciones de Proyectar",
|
||||
"prefs.sketch.recent_snap": "Ajustar a donde se cruzan dos guías",
|
||||
"prefs.sketch.select_3d_curve_points": "Seleccionar puntos de curvas de boceto 3D",
|
||||
"prefs.sketch.select_3d_curve_points_help": "Muestra y permite seleccionar puntos de las curvas de boceto 3D dibujadas como referencias de construcción.",
|
||||
"prefs.sketch.select_other_hidden": "Seleccionar otro llega a la geometría oculta",
|
||||
"prefs.sketch.shortcut_cycles_modes": "El atajo cambia de modo al pulsarlo otra vez",
|
||||
"prefs.sketch.shortcut_cycles_modes_tip": "Desactivado: vuelve a iniciar la herramienta.",
|
||||
"prefs.sketch.snap_to_visible_grid": "Ajustar a los cruces visibles de la cuadrícula",
|
||||
"prefs.sketch.snapping_guides": "Ajuste y guías",
|
||||
"prefs.sketch.state_icons": "Iconos de estado",
|
||||
"prefs.sketch.tracking_guides": "Guías de centro y punto medio",
|
||||
@@ -3034,7 +2943,6 @@
|
||||
"prefs.sketch_selection": "Boceto y selección",
|
||||
"prefs.sketch_selection_heading": "Boceto y selección",
|
||||
"prefs.sketch_selection_note": "Cómo se ajustan los bocetos y cómo seleccionan los clics.",
|
||||
"prefs.sketch_snap_to_visible_grid_note": "Un punto de un boceto se ajusta a los cruces de las líneas que dibuja la cuadrícula cuando el puntero apunta a uno, y si no, a la retícula más fina entre ellos. Desactivado: el punto se ajusta solo a la retícula más fina, como antes.",
|
||||
"prefs.sketch_state_icons": "Iconos de boceto en el color de su estado",
|
||||
"prefs.sketch_state_icons_note": "En Historial y el explorador, el icono de un boceto toma el color de su estado (bajo restricción, totalmente restringido o en conflicto) y su info muestra el estado. Desactivado: todos los bocetos tienen el mismo icono.",
|
||||
"prefs.sketch_tab_always": "Mantener la pestaña Boceto visible",
|
||||
@@ -3556,12 +3464,12 @@
|
||||
"render.cpu_subsurface_denoise_reason": "La dispersión subsuperficial por CPU necesita la reducción de ruido desactivada. Se conservan tus ajustes.",
|
||||
"render.cpu_subsurface_help": "La dispersión redirige la luz dentro del vidrio cerrado. Las tasas positivas necesitan iluminación indirecta con la reducción de ruido desactivada. La tasa es de colisiones por mm; 0 no añade dispersión. Un sesgo de fase positivo favorece las direcciones hacia delante; uno negativo, las hacia atrás.",
|
||||
"render.cpu_summary": "Trazado CPU · reflejos · HDR · luces direccionales y puntuales",
|
||||
"render.cpu_threads": "Workers de CPU",
|
||||
"render.cpu_threads": "Procesos de CPU",
|
||||
"render.cpu_threads_auto": "Automático",
|
||||
"render.cpu_threads_custom": "Personalizado",
|
||||
"render.cpu_threads_help": "Automático usa los workers de CPU disponibles. Un worker se ejecuta en serie. Más workers pueden necesitar más memoria.",
|
||||
"render.cpu_threads_help": "Automático usa los procesos de CPU disponibles. Un proceso se ejecuta en serie. Más procesos pueden necesitar más memoria.",
|
||||
"render.cpu_threads_single": "Uno",
|
||||
"render.cpu_threads_web": "El renderizado en el navegador usa un worker cooperativo. Se conserva la elección de workers guardada para el escritorio.",
|
||||
"render.cpu_threads_web": "El renderizado en el navegador usa un proceso cooperativo. Se conserva la elección de procesos guardada para el escritorio.",
|
||||
"render.cpu_transparent_help": "El PNG transparente quita el fondo y conserva la cobertura de los bordes. La opacidad del cuerpo y de la cara debe mantenerse al 100 %.",
|
||||
"render.cpu_volume_enable_path": "Activar iluminación indirecta",
|
||||
"render.cpu_volume_enable_path_plan": "Ray tracing en CPU · profundidad {depth} · {spp} muestras/píxel · semilla {seed} · sombras trazadas. Conserva la atmósfera definida y el presupuesto de imagen.",
|
||||
@@ -3736,7 +3644,6 @@
|
||||
"render.error_pbr_opacity": "PBR opaco requiere opacidad 1",
|
||||
"render.error_physical_glass": "la receta seleccionada no es vidrio físico",
|
||||
"render.error_preview_device": "la previsualización de producto requiere un dispositivo instalado con búfer de profundidad",
|
||||
"render.error_product_placement": "colocación de producto no válida",
|
||||
"render.error_product_triangle": "triángulo de producto no válido",
|
||||
"render.error_provisional_identity": "identidad provisional de render o cuenta de muestras no válida",
|
||||
"render.error_quality_bounds": "la proyección de calidad supera los límites nativos",
|
||||
@@ -5061,7 +4968,6 @@
|
||||
"ui.agent_chat_allow_once": "Permitir una vez",
|
||||
"ui.agent_chat_allowed": "Permitido",
|
||||
"ui.agent_chat_answered": "Respuesta: {option}",
|
||||
"ui.agent_chat_answered_in": "Respondido en {time}",
|
||||
"ui.agent_chat_approval": "¿Permitir que el agente haga esto?",
|
||||
"ui.agent_chat_attach_selection": "Adjuntar selección",
|
||||
"ui.agent_chat_attach_selection_tip": "Envía con el mensaje lo que esté seleccionado en el modelo (caras, aristas, operaciones, cuerpos) para que el agente sepa a qué te refieres",
|
||||
@@ -5090,7 +4996,6 @@
|
||||
"ui.agent_chat_ctx_bodies": "Cuerpos",
|
||||
"ui.agent_chat_ctx_elements": "Elementos",
|
||||
"ui.agent_chat_ctx_features": "Operaciones",
|
||||
"ui.agent_chat_ctx_gone": "Ya no está en el diseño: se ha cambiado o eliminado.",
|
||||
"ui.agent_chat_ctx_more": "+{n} más",
|
||||
"ui.agent_chat_ctx_other": "Otros",
|
||||
"ui.agent_chat_custom_heading": "Modelo personalizado",
|
||||
@@ -5104,7 +5009,6 @@
|
||||
"ui.agent_chat_dock_right": "Acoplar a la derecha",
|
||||
"ui.agent_chat_drop_hint": "Suelta para adjuntar a tu próximo mensaje",
|
||||
"ui.agent_chat_edit": "Editar",
|
||||
"ui.agent_chat_ended_after": "Finalizado tras {time}",
|
||||
"ui.agent_chat_exited": "El agente se ha detenido.",
|
||||
"ui.agent_chat_failed_start": "No se ha podido iniciar el agente: {error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}: un archivo CAD se importa al diseño, no se lee",
|
||||
@@ -5142,7 +5046,6 @@
|
||||
"ui.agent_chat_model_no_tools": "{model} no puede usar las herramientas de SolidCraft, así que solo puede chatear. Elige un modelo con soporte de herramientas.",
|
||||
"ui.agent_chat_model_rejected": "El agente no aceptó {model}: {error}",
|
||||
"ui.agent_chat_model_search": "Buscar modelos…",
|
||||
"ui.agent_chat_model_search_or_id": "Busca o escribe cualquier ID de modelo…",
|
||||
"ui.agent_chat_model_tip": "Modelo que utiliza el agente. Se recuerda tu elección para este agente.",
|
||||
"ui.agent_chat_models_failed": "No se pudieron listar modelos: {error}",
|
||||
"ui.agent_chat_models_heading": "Modelos disponibles",
|
||||
@@ -5154,7 +5057,6 @@
|
||||
"ui.agent_chat_next_picture": "Imagen siguiente",
|
||||
"ui.agent_chat_no_cli": "No se ha encontrado solidcraft-cli junto a la aplicación, por lo que el agente no puede acceder al diseño.",
|
||||
"ui.agent_chat_no_control": "No se ha iniciado el punto de conexión del chat del agente, por lo que no puede acceder al diseño.",
|
||||
"ui.agent_chat_no_design_hint": "El agente se ejecuta sin las herramientas de SolidCraft, así que no puede leer ni cambiar el diseño: no se ha encontrado solidcraft-cli junto a la aplicación. Instala SolidCraft con solidcraft-cli al lado (en Windows, el MSI o el zip portátil) o define SOLIDCRAFT_CLI con su ruta, y vuelve a conectar.",
|
||||
"ui.agent_chat_no_images": "Este agente no admite imágenes",
|
||||
"ui.agent_chat_no_picture": "No hay ninguna imagen en el portapapeles",
|
||||
"ui.agent_chat_no_tool_call": "El modelo no usó las herramientas de SolidCraft, así que el diseño no cambió. Elige un modelo que soporte llamada a herramientas; véase Modelos locales en la guía del chat del agente.",
|
||||
@@ -5169,7 +5071,7 @@
|
||||
"ui.agent_chat_permission": "El agente solicita ejecutar: {title}",
|
||||
"ui.agent_chat_pick": "Agente",
|
||||
"ui.agent_chat_pick_context": "Elegir contexto",
|
||||
"ui.agent_chat_pick_context_tip": "Elige caras, aristas, operaciones y cuerpos del modelo y pulsa OK: se añaden al mensaje como una ficha donde está el cursor (escribir @ hace lo mismo)",
|
||||
"ui.agent_chat_pick_context_tip": "Elige caras, aristas, operaciones y cuerpos del modelo para que el agente los examine y pulsa Aceptar",
|
||||
"ui.agent_chat_placeholder": "Pide al agente que modifique el diseño…",
|
||||
"ui.agent_chat_placeholder_busy": "Pon un mensaje en cola para cuando termine el agente (Ctrl+Intro: enviar ahora)",
|
||||
"ui.agent_chat_pop_out": "Abrir en su propia ventana, que puedes mover a otra pantalla",
|
||||
@@ -5202,25 +5104,16 @@
|
||||
"ui.agent_chat_side": "Lado del chat del agente",
|
||||
"ui.agent_chat_signed_in": "sesión iniciada",
|
||||
"ui.agent_chat_snap_none_found": "No se ha encontrado ningún agente de programación. El paquete Snap está confinado, así que no puede ver ni iniciar los agentes de programación instalados en este equipo. Para usarlos, instala SolidCraft desde el paquete AppImage, .deb o .rpm. Un agente que use una API compatible con OpenAI también funciona en el paquete Snap: configúralo con Añadir agente.",
|
||||
"ui.agent_chat_span_h": "{h} h {min} min",
|
||||
"ui.agent_chat_span_min": "{n} min",
|
||||
"ui.agent_chat_span_s": "{n} s",
|
||||
"ui.agent_chat_state_auth": "Se requiere iniciar sesión",
|
||||
"ui.agent_chat_state_busy": "Trabajando…",
|
||||
"ui.agent_chat_state_exited": "Agente detenido",
|
||||
"ui.agent_chat_state_failed": "Conexión fallida",
|
||||
"ui.agent_chat_state_no_design": "Sin acceso al diseño",
|
||||
"ui.agent_chat_state_off": "Sin conexión",
|
||||
"ui.agent_chat_state_ready": "Listo",
|
||||
"ui.agent_chat_state_starting": "Iniciando…",
|
||||
"ui.agent_chat_stop": "Detener",
|
||||
"ui.agent_chat_stop_tip": "Detener el agente y todo lo que haya iniciado",
|
||||
"ui.agent_chat_stop_turn_tip": "Detener el agente (Esc). Los mensajes en cola permanecen en cola.",
|
||||
"ui.agent_chat_thinking_for": "Pensando durante {time}",
|
||||
"ui.agent_chat_tool_running_for": "en ejecución durante {time}",
|
||||
"ui.agent_chat_tools_not_started": "El agente se ejecuta sin las herramientas de SolidCraft, así que no puede leer ni cambiar el diseño: se ha encontrado solidcraft-cli, pero sus herramientas no se han iniciado (el error del chat explica por qué). Vuelve a conectar; si sigue pasando, reinstala SolidCraft.",
|
||||
"ui.agent_chat_transcript_assistant": "Asistente:",
|
||||
"ui.agent_chat_transcript_user": "Usuario:",
|
||||
"ui.agent_chat_turn_limit": "Cambios por mensaje",
|
||||
"ui.agent_chat_turn_unlimited": "Sin límite",
|
||||
"ui.agent_chat_undo_changes": "Deshacer estos cambios",
|
||||
@@ -5409,7 +5302,6 @@
|
||||
"ui.collapse": "Contraer",
|
||||
"ui.collinear": "Colineal",
|
||||
"ui.colour_vision": "Visión del color",
|
||||
"ui.colour_vision_bends": "Pliegues",
|
||||
"ui.colour_vision_deutan": "Deuteranopía",
|
||||
"ui.colour_vision_deutan_tip": "Para personas con sensibilidad reducida o ausente al verde",
|
||||
"ui.colour_vision_failed": "Fallido",
|
||||
@@ -5439,24 +5331,14 @@
|
||||
"ui.conic_curve": "Curva cónica",
|
||||
"ui.constant": "Constante",
|
||||
"ui.constraints_conflict": "Conflicto entre restricciones",
|
||||
"ui.construct_auto.chosen": "Elegido a partir de tus selecciones. Elige otra referencia, o quita una, para cambiar el método.",
|
||||
"ui.construct_auto.no_fit": "Elige otras referencias: ningún método se puede construir con estas.",
|
||||
"ui.construct_auto.pick": "Elige con qué construirlo: el método se deduce de tus selecciones.",
|
||||
"ui.construct_auto.refs": "Referencias",
|
||||
"ui.construct_auto.select": "Selecciona primero las referencias con las que construir",
|
||||
"ui.construct_axes": "Ejes",
|
||||
"ui.construct_planes": "Planos",
|
||||
"ui.construct_points": "Puntos",
|
||||
"ui.construction": "Construcción",
|
||||
"ui.construction.angle_hint": "Línea de construcción: haz clic en un punto, luego escribe un ángulo o elige una dirección",
|
||||
"ui.construction.axis_hint": "Línea de construcción: haz clic en un punto o escribe su distancia al eje del boceto",
|
||||
"ui.construction.bisector_hint": "Bisectriz: elige dos líneas rectas que se intersecten, luego la solución deseada",
|
||||
"ui.construction.common_hint": "Tangente común: elige dos círculos/arcos, luego la solución deseada",
|
||||
"ui.construction.contact_hint": "Tangente: elige un círculo/arco o spline/cónica abierta, luego el punto de contacto (libre)",
|
||||
"ui.construction.external_hint": "Tangente: elige un círculo o arco, un punto externo, luego la solución deseada",
|
||||
"ui.construction.free_hint": "Línea de construcción: haz clic en dos puntos; clic derecho para reiniciar",
|
||||
"ui.construction.normal_needs_length": "Una línea infinita es siempre de construcción: escribe una longitud para dibujar una línea normal",
|
||||
"ui.construction.parallel_hint": "Línea de construcción: elige una línea recta y luego haz clic o escribe el desfase; haz clic con el botón derecho para cambiar la referencia",
|
||||
"ui.construction.point_hint": "Línea de construcción: haz clic en el punto por el que debe pasar",
|
||||
"ui.construction.reference_hint": "Línea de construcción: elige una línea recta, luego un punto; clic derecho para cambiar la",
|
||||
"ui.construction.ribbon_label": "X-Líneas",
|
||||
@@ -5630,10 +5512,7 @@
|
||||
"ui.explode_needs_two_components": "Vista explosada separa los componentes, así el diseño necesita dos o más (Nuevo componente, Insertar diseño o Crear componentes desde cuerpos)",
|
||||
"ui.exportDxf": "Exportar DXF…",
|
||||
"ui.export_ellipsis_keymap": "Exportar…",
|
||||
"ui.export_glb": "Exportar GLB",
|
||||
"ui.export_keymap_tip": "Exportar las teclas, los favoritos de la búsqueda S y la barra de herramientas como archivo",
|
||||
"ui.export_selects_only_body": "Seleccionar el único cuerpo al exportar una malla",
|
||||
"ui.export_selects_only_body_tip": "Exportar STL, Exportar 3MF y Enviar al laminador empiezan con el cuerpo del diseño seleccionado cuando solo hay uno.",
|
||||
"ui.extend": "Extender",
|
||||
"ui.extent": "Extensión",
|
||||
"ui.extent_type": "Tipo de extensión",
|
||||
@@ -5693,8 +5572,6 @@
|
||||
"ui.finest": "Muy fina",
|
||||
"ui.finish_sketch": "Finalizar boceto",
|
||||
"ui.first_dimension_scales": "La primera cota escala el dibujo",
|
||||
"ui.first_dimension_scales_about_origin": "Escalar respecto al origen del boceto",
|
||||
"ui.first_dimension_scales_about_origin_tip": "Activado: el dibujo siempre crece o se encoge respecto al origen del boceto, así que una forma dibujada lejos del origen se mueve. Desactivado: un dibujo que nada ata al origen se escala respecto a su propio centro y se queda donde se dibujó.",
|
||||
"ui.first_dimension_scales_tip": "Activado: la primera cota de longitud de un boceto escala el dibujo que mide, de modo que su forma no cambia. Desactivado: la cota solo dimensiona lo que mide.",
|
||||
"ui.fit": "Ajustar",
|
||||
"ui.fix_unfix": "Fijar/liberar",
|
||||
@@ -5950,7 +5827,7 @@
|
||||
"ui.origin": "Origen",
|
||||
"ui.palette_construction_mode": "Modo de construcción (X)",
|
||||
"ui.palette_more": "Más",
|
||||
"ui.palette_sketch_options": "Opciones de boceto",
|
||||
"ui.palette_sketch_options": "Opciones de croquis",
|
||||
"ui.pan": "Desplazar",
|
||||
"ui.pan_zoom_orbit": "Desplazar, zoom y orbitar",
|
||||
"ui.panel_fixture.apply": "Aplicar",
|
||||
@@ -6107,7 +5984,7 @@
|
||||
"ui.print_face_cleared": "{body}: cara de impresión limpiada",
|
||||
"ui.print_face_input": "Cara de impresión",
|
||||
"ui.print_face_pick": "Selecciona una cara plana de un cuerpo sólido para hacerla la cara de impresión",
|
||||
"ui.print_face_set": "{body}: cara de impresión establecida; Enviar al laminador coloca el cuerpo sobre ella",
|
||||
"ui.print_face_set": "{body}: cara de impresión establecida; Enviar a Laminador coloca el cuerpo sobre ella",
|
||||
"ui.profile": "Perfil",
|
||||
"ui.profile_plane": "Plano de perfil",
|
||||
"ui.profiles": "Perfiles",
|
||||
@@ -6151,14 +6028,11 @@
|
||||
"ui.relink_design_changed": "El ensamblaje cambió mientras se elegía un origen. Inicia Revincular origen de nuevo.",
|
||||
"ui.relink_done": "Origen revinculado en todas las instancias.",
|
||||
"ui.relink_not_linked": "Este componente no está vinculado a un diseño de origen.",
|
||||
"ui.relink_one_done": "Origen revinculado en esta instancia.",
|
||||
"ui.relink_select_one": "Selecciona primero un componente vinculado o una de sus instancias.",
|
||||
"ui.relink_source_all": "Revincular origen (todas las instancias)…",
|
||||
"ui.relink_source_one": "Revincular origen…",
|
||||
"ui.removeFromQuickAccess": "Quitar de Acceso rápido",
|
||||
"ui.removeFromToolbar": "Quitar de la barra de herramientas",
|
||||
"ui.removeSection": "Quitar sección",
|
||||
"ui.renameSelectedBodies": "Cambiar nombre de los cuerpos seleccionados",
|
||||
"ui.render": "Renderizar",
|
||||
"ui.renderer": "Motor de renderizado",
|
||||
"ui.repeat": "Repetir",
|
||||
@@ -6250,16 +6124,6 @@
|
||||
"ui.sketch_intersect_selection_filter": "Filtro de selección",
|
||||
"ui.sketch_keys_only": "Mantener las teclas del modelo fuera de los bocetos",
|
||||
"ui.sketch_keys_only_tip": "Mientras editas un boceto, solo se ejecutan las teclas de boceto y las que funcionan en todas partes; una tecla de modelo como E o F no hace nada, así que una tecla perdida no puede cerrar un boceto pesado.",
|
||||
"ui.sketch_links_added_curve": "{command} ha añadido 1 curva al boceto.",
|
||||
"ui.sketch_links_added_curves": "{command} ha añadido {count} curvas al boceto.",
|
||||
"ui.sketch_links_added_curves_and_points": "{command} ha añadido {curves} y {points} al boceto.",
|
||||
"ui.sketch_links_added_point": "{command} ha añadido 1 punto al boceto.",
|
||||
"ui.sketch_links_added_points": "{command} ha añadido {count} puntos al boceto.",
|
||||
"ui.sketch_links_curve_count": "{count} curvas",
|
||||
"ui.sketch_links_curve_count_one": "1 curva",
|
||||
"ui.sketch_links_hidden": "El resultado está oculto porque {option} está desactivado en la paleta del boceto.",
|
||||
"ui.sketch_links_point_count": "{count} puntos",
|
||||
"ui.sketch_links_point_count_one": "1 punto",
|
||||
"ui.sketch_modifiers": "Modificadores del boceto",
|
||||
"ui.sketch_project_geometry": "Geometría",
|
||||
"ui.sketch_report.all_constrained": "Todo está totalmente restringido.",
|
||||
@@ -6299,7 +6163,6 @@
|
||||
"ui.step_values": "Incrementar valores con las flechas y la rueda",
|
||||
"ui.step_values_tip": "En un campo de valor en el que estés escribiendo, Arriba/Abajo cambian un número simple de 1 en 1 (Mayús: 10, Alt: 0,1), y la rueda del ratón sobre el campo hace lo mismo. Las expresiones se dejan tal como se escriben.",
|
||||
"ui.stitch_tolerance_hint": "estricta",
|
||||
"ui.straighten": "Enderezar",
|
||||
"ui.stretch": "Estirar",
|
||||
"ui.supersampling": "Supermuestreo (SSAA)",
|
||||
"ui.supersampling_adaptive": "Solo cuando la vista está inmóvil",
|
||||
@@ -6373,8 +6236,6 @@
|
||||
"ui.view_angles_yaw": "Rumbo",
|
||||
"ui.view_angles_zero": "Poner {axis} a 0°",
|
||||
"ui.view_centre": "Centro de la vista",
|
||||
"ui.view_compass_tip": "Vista desde {axis}. Haz clic otra vez para invertir. Arrastra para orbitar.",
|
||||
"ui.view_compass_tip_snap": "Vista desde {axis}. Haz clic otra vez para invertir. Arrastra para orbitar; mantén Mayús para ajustar.",
|
||||
"ui.viewport_status_clear": "Desactivar {name}",
|
||||
"ui.viewport_status_clear_hint": "Desactivar {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} aristas",
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"prefs.keep_print_faces": "Keep print faces",
|
||||
"prefs.keep_print_faces_tip": "On: the print face of each body is tinted in the view at all times and saved in the design. Off: it is tinted only while Send to Slicer is open, and designs are saved and opened without print faces."
|
||||
},
|
||||
"ar": {
|
||||
"prefs.keep_print_faces": "الاحتفاظ بأوجه الطباعة",
|
||||
"prefs.keep_print_faces_tip": "تشغيل: يُلوَّن وجه الطباعة لكل جسم في العرض دائمًا ويُحفظ في التصميم. إيقاف: يُلوَّن فقط أثناء فتح نافذة إرسال إلى برنامج التقطيع، وتُحفظ التصاميم وتُفتح بدون أوجه الطباعة."
|
||||
},
|
||||
"de": {
|
||||
"prefs.keep_print_faces": "Auflageflächen behalten",
|
||||
"prefs.keep_print_faces_tip": "Ein: Die Auflagefläche jedes Körpers wird in der Ansicht immer eingefärbt und in der Konstruktion gespeichert. Aus: Sie wird nur eingefärbt, solange An Slicer senden geöffnet ist, und Konstruktionen werden ohne Auflageflächen gespeichert und geöffnet."
|
||||
},
|
||||
"es": {
|
||||
"prefs.keep_print_faces": "Conservar caras de impresión",
|
||||
"prefs.keep_print_faces_tip": "Activado: la cara de impresión de cada cuerpo se tiñe siempre en la vista y se guarda en el diseño. Desactivado: solo se tiñe mientras Enviar al laminador está abierto, y los diseños se guardan y se abren sin caras de impresión."
|
||||
},
|
||||
"fr": {
|
||||
"prefs.keep_print_faces": "Conserver les faces d’impression",
|
||||
"prefs.keep_print_faces_tip": "Activé : la face d’impression de chaque corps est toujours teintée dans la vue et enregistrée dans la conception. Désactivé : elle n'est teintée que tant qu'Envoyer au slicer est ouvert, et les conceptions sont enregistrées et ouvertes sans faces d’impression."
|
||||
},
|
||||
"he": {
|
||||
"prefs.keep_print_faces": "שמירת פאות הדפסה",
|
||||
"prefs.keep_print_faces_tip": "מופעל: פאת ההדפסה של כל גוף נצבעת בתצוגה תמיד ונשמרת בתכנון. כבוי: היא נצבעת רק כל עוד חלון שליחה לסלייסר פתוח, ותכנונים נשמרים ונפתחים ללא פאות הדפסה."
|
||||
},
|
||||
"ja": {
|
||||
"prefs.keep_print_faces": "造形面を保持",
|
||||
"prefs.keep_print_faces_tip": "オン: 各ボディの造形面をビューで常に色付けし、デザインに保存します。オフ: 「スライサーに送る」が開いている間だけ色付けし、デザインは造形面なしで保存・オープンされます。"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.keep_print_faces": "출력 바닥면 유지",
|
||||
"prefs.keep_print_faces_tip": "켜기: 각 바디의 출력 바닥면을 뷰에 항상 색으로 표시하고 디자인에 저장합니다. 끄기: 슬라이서로 보내기가 열려 있는 동안만 표시하며, 디자인은 출력 바닥면 없이 저장되고 열립니다."
|
||||
},
|
||||
"vi": {
|
||||
"prefs.keep_print_faces": "Giữ mặt in",
|
||||
"prefs.keep_print_faces_tip": "Bật: mặt in của mỗi khối luôn được tô màu trong khung nhìn và được lưu trong thiết kế. Tắt: mặt in chỉ được tô màu khi cửa sổ Gửi sang phần mềm cắt lớp (Slicer) đang mở, và thiết kế được lưu và mở mà không có mặt in."
|
||||
},
|
||||
"zh": {
|
||||
"prefs.keep_print_faces": "保留打印底面",
|
||||
"prefs.keep_print_faces_tip": "开启:每个实体的打印底面始终在视图中着色,并保存在设计中。关闭:仅在“发送到切片软件”打开时着色,设计保存和打开时不含打印底面。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"operations.failed": "Failed"
|
||||
},
|
||||
"de": {
|
||||
"operations.failed": "Fehlgeschlagen"
|
||||
},
|
||||
"ja": {
|
||||
"operations.failed": "失敗"
|
||||
},
|
||||
"ko": {
|
||||
"operations.failed": "실패"
|
||||
},
|
||||
"zh": {
|
||||
"operations.failed": "失败"
|
||||
},
|
||||
"ar": {
|
||||
"operations.failed": "فشل"
|
||||
},
|
||||
"he": {
|
||||
"operations.failed": "נכשל"
|
||||
},
|
||||
"vi": {
|
||||
"operations.failed": "Thất bại"
|
||||
}
|
||||
}
|
||||
+18
-318
@@ -20,7 +20,7 @@
|
||||
"ABS - White": "ABS – biały",
|
||||
"ALIGN": "WYRÓWNAJ",
|
||||
"ARRANGE": "UKŁADANIE",
|
||||
"AS-BUILT JOINT": "ZŁĄCZE W BIEŻĄCYM POŁOŻENIU",
|
||||
"AS-BUILT JOINT": "ZŁĄCZE W STANIE ZASTANYM",
|
||||
"ASSIGN PLASTIC RULE": "PRZYPISZ REGUŁĘ TWORZYWA",
|
||||
"AXIS PERPENDICULAR TO FACE": "OŚ PROSTOPADŁA DO ŚCIANY",
|
||||
"AXIS THROUGH CYLINDER": "OŚ PRZEZ WALEC",
|
||||
@@ -75,7 +75,7 @@
|
||||
"Arc length": "Długość łuku",
|
||||
"Area": "Pole powierzchni",
|
||||
"As Picked": "Jak wybrano",
|
||||
"As-Built": "W bieżącym położeniu",
|
||||
"As-Built": "W stanie zastanym",
|
||||
"Assign Rule": "Przypisz regułę",
|
||||
"Attachment": "Zaczepienie",
|
||||
"BLEND SURFACE": "POWIERZCHNIA PRZEJŚCIOWA",
|
||||
@@ -266,7 +266,6 @@
|
||||
"Edge": "Krawędź",
|
||||
"Edges": "Krawędzie",
|
||||
"Edit": "Edytuj",
|
||||
"Edit Holes": "Edytuj otwory",
|
||||
"Edit Profile Sketch": "Edytuj szkic profilu",
|
||||
"Edit shortcuts": "Edytuj skróty",
|
||||
"Enable rule": "Włącz regułę",
|
||||
@@ -514,7 +513,6 @@
|
||||
"Offset Y": "Odsunięcie Y",
|
||||
"Offset Z": "Odsunięcie Z",
|
||||
"On your computer": "Na tym komputerze",
|
||||
"One more stretch of the picked edge, each with its own angle, height and turns": "Kolejny odcinek wybranej krawędzi, każdy z własnym kątem, wysokością i stroną gięcia",
|
||||
"Opacity": "Nieprzezroczystość",
|
||||
"Open GitHub": "Otwórz GitHub",
|
||||
"Open GitHub draft": "Otwórz szkic zgłoszenia na GitHubie",
|
||||
@@ -636,7 +634,6 @@
|
||||
"Remove the track": "Usuń ścieżkę",
|
||||
"Remove this bend": "Usuń to gięcie",
|
||||
"Remove this point": "Usuń ten punkt",
|
||||
"Remove this segment": "Usuń ten odcinek",
|
||||
"Rename Feature": "Zmień nazwę operacji",
|
||||
"Rename Folder": "Zmień nazwę folderu",
|
||||
"Render Stage": "Scenografia renderowania",
|
||||
@@ -695,7 +692,7 @@
|
||||
"SNAP FIT": "ZATRZASK",
|
||||
"SPLIT FACE": "PODZIEL ŚCIANĘ",
|
||||
"STITCH": "ZSZYJ",
|
||||
"STOCK": "PÓŁFABRYKAT",
|
||||
"STOCK": "NADDATEK",
|
||||
"SURFACE EXTRUDE": "WYCIĄGNIĘCIE POWIERZCHNI",
|
||||
"SURFACE LOFT": "WYCIĄGNIĘCIE ZŁOŻONE POWIERZCHNI",
|
||||
"SURFACE QUALITY": "JAKOŚĆ POWIERZCHNI",
|
||||
@@ -726,7 +723,6 @@
|
||||
"Section position": "Położenie przekroju",
|
||||
"Sections": "Przekroje",
|
||||
"Sections (one sketch each)": "Przekroje (każdy w osobnym szkicu)",
|
||||
"Segments": "Odcinki",
|
||||
"Select Features": "Zaznacz operacje",
|
||||
"Select Other": "Wybierz inny",
|
||||
"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).": "Wybierz inny: przytrzymaj nieruchomo lewy przycisk na modelu (lub prawy przycisk › Wybierz inny…), aby wyświetlić wszystko pod kursorem; kliknij ponownie w tym samym miejscu, aby zaznaczyć kolejny obiekt. Dwukrotne kliknięcie środkowym przyciskiem przechodzi do widoku głównego (Preferencje › Nawigacja).",
|
||||
@@ -776,7 +772,6 @@
|
||||
"Spacing must be a non-zero number": "Odstęp musi być liczbą różną od zera",
|
||||
"Specialise": "Uszczegółów",
|
||||
"Speed": "Prędkość",
|
||||
"Split Edge": "Podziel krawędź",
|
||||
"Splitting tool": "Narzędzie podziału",
|
||||
"Square": "Kwadrat",
|
||||
"Stainless Steel": "Stal nierdzewna",
|
||||
@@ -944,13 +939,6 @@
|
||||
"ai.prefs.connect_apps_note": "Przy uruchomieniu SolidCraft dodaje go jako serwer MCP do aplikacji AI znalezionych na tym komputerze (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), aby agent mógł go używać bez konfiguracji. Dodawany jest tylko wpis solidcraft lub odświeżana jego ścieżka; zmieniony przez ciebie wpis pozostaje nietknięty, a plik jest najpierw kopiowany. Wyłączone: dodane wpisy są usuwane. Pomoc > Połącz asystentów AI… robi to ręcznie.",
|
||||
"ai.prefs.connect_at_start": "Łącz ostatnio używanego agenta przy uruchomieniu SolidCraft",
|
||||
"ai.prefs.connect_at_start_note": "Przy każdym uruchomieniu SolidCraft uruchamia w tle ostatnio połączonego agenta (lub domyślnego), aby był gotowy do rozmowy. Nie działa, gdy SolidCraft jest uruchomiony z --control. Jeśli nie uda się połączyć, Asystent pokaże komunikat „Połączenie nieudane” i przycisk Ponów.",
|
||||
"ai.prefs.control_failed": "Nie udało się uruchomić kanału sterowania: {error}",
|
||||
"ai.prefs.control_from_launch": "Uruchomiono z --control na porcie {port} na czas tej sesji. Zmiany tutaj zadziałają przy następnym uruchomieniu SolidCraft bez --control.",
|
||||
"ai.prefs.control_listening": "Nasłuchuje na 127.0.0.1:{port}",
|
||||
"ai.prefs.control_not_listening": "Nie nasłuchuje",
|
||||
"ai.prefs.control_port": "Port",
|
||||
"ai.prefs.control_port_note": "Port, z którym łączą się agenty AI na tym komputerze (127.0.0.1). Domyślny to 27878: gdy jest zajęty, używany jest następny wolny port. Każdy inny port jest używany bez zmian. Ustaw ten sam port w kliencie MCP, np. solidcraft-cli mcp --connect 127.0.0.1:7878.",
|
||||
"ai.prefs.control_port_taken": "Port {port} jest zajęty przez inny program (lub inne okno SolidCraft). Wybierz inny port.",
|
||||
"ai.prefs.follow_camera": "Obracaj kamerę do szkiców",
|
||||
"ai.prefs.follow_camera_off": "Automatyczne podążanie kamery jest wyłączone.",
|
||||
"ai.prefs.follow_camera_on": "Kamera obraca się przodem do szkiców tworzonych przez agenta.",
|
||||
@@ -990,12 +978,11 @@
|
||||
"browser.calibrate_click_a": "Kliknij obraz, aby wskazać punkt A, lub wpisz jego współrzędne.",
|
||||
"browser.calibrate_click_b": "Kliknij obraz, aby wskazać punkt B, lub wpisz jego współrzędne.",
|
||||
"browser.calibrate_title": "Kalibruj: {name}",
|
||||
"browser.canvas_locked": "Płótno jest zablokowane. Najpierw je odblokuj.",
|
||||
"browser.capture_many": "Przesunięto komponenty ({count}), ale ich położenia nie zostały zapamiętane.",
|
||||
"browser.capture_note": "Zapamiętaj położenie zachowuje nowe położenie, a Przywróć położenie wraca do poprzedniego.",
|
||||
"browser.capture_one": "Przesunięto komponent, ale jego położenie nie zostało zapamiętane.",
|
||||
"browser.component_name": "Komponent{number}",
|
||||
"browser.edit_canvas_title": "Edytuj płótno: {name}",
|
||||
"browser.edit_canvas_title": "Edytuj kanwę: {name}",
|
||||
"browser.edit_point_cloud_title": "Edytuj chmurę punktów: {name}",
|
||||
"browser.move_title": "Przesuń: {name}",
|
||||
"browser.needs_repair": "{name}: wymaga naprawy",
|
||||
@@ -1011,13 +998,6 @@
|
||||
"browser.sketches_to_parts_not_on_face": "nie na ścianie części",
|
||||
"browser.sketches_to_parts_part_missing": "ich część już nie istnieje",
|
||||
"browser.sketches_to_parts_result": "Przeniesione szkice do części, na których leżą: {moved}; pozostały: {stayed}: {why}",
|
||||
"browser.straighten_angle": "Kierunek krawędzi",
|
||||
"browser.straighten_choose": "Wybierz dla krawędzi opcję Pozioma, Pionowa lub Kąt, a potem naciśnij Wyprostuj.",
|
||||
"browser.straighten_click_a": "Kliknij obraz na jednym końcu krawędzi, która powinna być prosta, lub wpisz współrzędne tego końca.",
|
||||
"browser.straighten_click_b": "Kliknij drugi koniec krawędzi lub wpisz jego współrzędne.",
|
||||
"browser.straighten_horizontal": "Pozioma",
|
||||
"browser.straighten_title": "Wyprostuj: {name}",
|
||||
"browser.straighten_vertical": "Pionowa",
|
||||
"browser.units": "Jednostki: {units}",
|
||||
"cam.flat": "Płaska ściana",
|
||||
"cam.follower": "Popychacz",
|
||||
@@ -1047,8 +1027,6 @@
|
||||
"cmd.appearance_pick_up": "Pobierz wygląd",
|
||||
"cmd.appearance_replace": "Zastąp wygląd",
|
||||
"cmd.assembly_bom": "Zestawienie materiałowe",
|
||||
"cmd.assembly_constraint_create": "Wiązanie",
|
||||
"cmd.assembly_constraint_edit": "Edytuj wiązanie",
|
||||
"cmd.assembly_mass_properties": "Właściwości masowe",
|
||||
"cmd.body.print_face": "Ściana podstawy druku",
|
||||
"cmd.body_rename": "Zmień nazwę bryły",
|
||||
@@ -1067,7 +1045,6 @@
|
||||
"cmd.canvas_delete": "Usuń płótno",
|
||||
"cmd.canvas_edit": "Edytuj płótno",
|
||||
"cmd.canvas_insert": "Płótno",
|
||||
"cmd.canvas_straighten": "Wyprostuj płótno",
|
||||
"cmd.canvas_to_sketch": "Szkic z płótna",
|
||||
"cmd.component_activate": "Aktywuj komponent",
|
||||
"cmd.component_break_link": "Zerwij łącze",
|
||||
@@ -1109,10 +1086,7 @@
|
||||
"cmd.config_rule": "Reguła konfiguracji",
|
||||
"cmd.config_rules": "Reguły konfiguracji",
|
||||
"cmd.config_table": "Pokaż tabelę konfiguracji",
|
||||
"cmd.construct.axis": "Oś",
|
||||
"cmd.construct.plane": "Płaszczyzna",
|
||||
"cmd.construct.plane.axis_point": "Płaszczyzna przez oś i punkt",
|
||||
"cmd.construct.point": "Punkt",
|
||||
"cmd.construct_axis_edge": "Oś przez krawędź",
|
||||
"cmd.construct_axis_normal_to_face": "Oś prostopadła do ściany",
|
||||
"cmd.construct_axis_through_cylinder": "Oś przez walec/stożek/torus",
|
||||
@@ -1175,7 +1149,6 @@
|
||||
"cmd.file_save_mesh": "Zapisz jako siatkę",
|
||||
"cmd.file_send_to_slicer": "Wyślij do slicera",
|
||||
"cmd.file_slicers": "Zainstalowane slicery",
|
||||
"cmd.frame_end_cap": "Zaślepka",
|
||||
"cmd.frame_member": "Element ramy",
|
||||
"cmd.frame_profile_insert": "Wstaw profil",
|
||||
"cmd.frame_profile_list": "Biblioteka profili",
|
||||
@@ -1196,7 +1169,6 @@
|
||||
"cmd.inspect_isocurve": "Analiza izokrzywych",
|
||||
"cmd.inspect_minimum_radius": "Analiza promienia minimalnego",
|
||||
"cmd.inspect_parameter_sweep": "Przegląd wartości parametru",
|
||||
"cmd.inspect_parameter_variants": "Porównaj warianty parametrów",
|
||||
"cmd.inspect_silhouette": "Sylwetka",
|
||||
"cmd.inspect_zebra": "Analiza zebry",
|
||||
"cmd.joint_as_built": "Złącze w bieżącym położeniu",
|
||||
@@ -1235,7 +1207,6 @@
|
||||
"cmd.material_properties_directional": "Kierunkowy moduł sprężystości",
|
||||
"cmd.material_properties_validate": "Sprawdź dane materiału fizycznego",
|
||||
"cmd.mesh_convert": "Konwertuj siatkę",
|
||||
"cmd.model_body_features": "Lista operacji bryły",
|
||||
"cmd.model_edges": "Lista krawędzi",
|
||||
"cmd.model_faces": "Lista ścian",
|
||||
"cmd.model_threads": "Lista gwintów",
|
||||
@@ -1250,10 +1221,6 @@
|
||||
"cmd.motion_trace": "Ślad ruchu",
|
||||
"cmd.motion_trace_edit": "Edytuj ślad ruchu",
|
||||
"cmd.motion_trace_keep": "Zachowaj ślad ruchu",
|
||||
"cmd.note_add": "Dodaj notatkę",
|
||||
"cmd.note_delete": "Usuń notatkę",
|
||||
"cmd.note_list": "Lista notatek",
|
||||
"cmd.note_update": "Edytuj notatkę",
|
||||
"cmd.occurrence_align": "Wyrównaj wystąpienie",
|
||||
"cmd.occurrence_copy": "Wklej (wystąpienie)",
|
||||
"cmd.occurrence_drag": "Przeciągnij wystąpienie",
|
||||
@@ -1268,11 +1235,8 @@
|
||||
"cmd.panel.dismiss": "Zamknij podgląd panelu",
|
||||
"cmd.panel.float": "Odepnij panel",
|
||||
"cmd.panel.focus": "Ustaw fokus na panelu",
|
||||
"cmd.panel.focus.next": "Ustaw fokus na następnym panelu",
|
||||
"cmd.panel.focus.previous": "Ustaw fokus na poprzednim panelu",
|
||||
"cmd.panel.list": "Lista paneli",
|
||||
"cmd.panel.move": "Przenieś panel",
|
||||
"cmd.panel.move.next_zone": "Przenieś panel do następnej strefy",
|
||||
"cmd.panel.native_close": "Zadokuj ponownie sekcję panelu",
|
||||
"cmd.panel.open": "Otwórz panel",
|
||||
"cmd.panel.peek": "Podejrzyj zwinięty panel",
|
||||
@@ -1280,12 +1244,6 @@
|
||||
"cmd.panel.reset_pair": "Resetuj podział paneli",
|
||||
"cmd.panel.resize": "Zmień rozmiar sekcji panelu",
|
||||
"cmd.panel.resize_pair": "Zmień podział paneli",
|
||||
"cmd.panel.sash.down": "Przesuń podział paneli w dół",
|
||||
"cmd.panel.sash.left": "Przesuń podział paneli w lewo",
|
||||
"cmd.panel.sash.reset": "Resetuj podział panelu z fokusem",
|
||||
"cmd.panel.sash.right": "Przesuń podział paneli w prawo",
|
||||
"cmd.panel.sash.up": "Przesuń podział paneli w górę",
|
||||
"cmd.panel.toggle": "Przełącz panel z fokusem",
|
||||
"cmd.parameters_add": "Dodaj parametr użytkownika",
|
||||
"cmd.parameters_change": "Zmień parametry",
|
||||
"cmd.parameters_comment": "Komentarz parametru",
|
||||
@@ -1379,7 +1337,6 @@
|
||||
"cmd.render_start": "Renderuj obraz",
|
||||
"cmd.render_status": "Postęp renderowania",
|
||||
"cmd.select_clear": "Wyczyść zaznaczenie",
|
||||
"cmd.selection_get": "Pobierz zaznaczenie",
|
||||
"cmd.settings_profile_apply": "Zastosuj profil ustawień",
|
||||
"cmd.settings_profile_delete": "Usuń profil ustawień",
|
||||
"cmd.settings_profile_export": "Eksportuj profil ustawień",
|
||||
@@ -1400,17 +1357,16 @@
|
||||
"cmd.sheet_refold": "Zegnij ponownie",
|
||||
"cmd.sheet_rules": "Lista reguł blach",
|
||||
"cmd.sheet_unfold": "Rozegnij",
|
||||
"cmd.sketch.construction_line": "Linia konstrukcyjna",
|
||||
"cmd.sketch.construction_line.angle": "Linia konstrukcyjna pod kątem",
|
||||
"cmd.sketch.construction_line": "Prosta konstrukcyjna",
|
||||
"cmd.sketch.construction_line.angle": "Prosta konstrukcyjna pod kątem",
|
||||
"cmd.sketch.construction_line.bisector": "Dwusieczna kąta",
|
||||
"cmd.sketch.construction_line.horizontal": "Pozioma linia konstrukcyjna",
|
||||
"cmd.sketch.construction_line.parallel": "Równoległa linia konstrukcyjna",
|
||||
"cmd.sketch.construction_line.perpendicular": "Prostopadła linia konstrukcyjna",
|
||||
"cmd.sketch.construction_line.horizontal": "Pozioma prosta konstrukcyjna",
|
||||
"cmd.sketch.construction_line.parallel": "Równoległa prosta konstrukcyjna",
|
||||
"cmd.sketch.construction_line.perpendicular": "Prostopadła prosta konstrukcyjna",
|
||||
"cmd.sketch.construction_line.tangent_at": "Styczna w punkcie krzywej",
|
||||
"cmd.sketch.construction_line.tangent_external": "Styczna przez punkt zewnętrzny",
|
||||
"cmd.sketch.construction_line.tangent_two": "Wspólna styczna",
|
||||
"cmd.sketch.construction_line.vertical": "Pionowa linia konstrukcyjna",
|
||||
"cmd.sketch.replace": "Zastąp szkic",
|
||||
"cmd.sketch.construction_line.vertical": "Pionowa prosta konstrukcyjna",
|
||||
"cmd.sketch_arc3d": "Łuk 3D",
|
||||
"cmd.sketch_arc_center_point": "Łuk ze środka",
|
||||
"cmd.sketch_arc_tangent": "Łuk styczny",
|
||||
@@ -1542,8 +1498,6 @@
|
||||
"cmd.timeline_stop": "Zatrzymaj historię",
|
||||
"cmd.timeline_suppress": "Wygaś operację",
|
||||
"cmd.ui.assistant": "Przełącz Asystenta",
|
||||
"cmd.ui.palette": "Szukaj poleceń",
|
||||
"cmd.ui.toggle_visibility": "Pokaż/ukryj zaznaczenie",
|
||||
"cmd.view.navigator": "Własny nawigator widoku",
|
||||
"cmd.view.render_browser": "Przeglądarka renderowania",
|
||||
"cmd.view.render_dock": "Przenieś dok renderowania",
|
||||
@@ -1601,7 +1555,6 @@
|
||||
"color.step_down": "Zmniejsz",
|
||||
"color.step_up": "Zwiększ",
|
||||
"color.value": "Jasność (procent)",
|
||||
"component.error.ambiguous_name": "Kilka komponentów ma nazwę `{name}`. Użyj liczbowego identyfikatora komponentu: {ids}.",
|
||||
"config.column_filter_empty": "Brak pasujących kolumn",
|
||||
"config.rule_parameter_columns": "Wymienione są tylko parametry mające kolumnę w tabeli. Dodaj ją przyciskiem Dodaj kolumnę (+), aby użyć jej tutaj.",
|
||||
"config.rule_suppression_columns": "Wymienione są tylko operacje mające w tabeli kolumnę Wygaś. Dodaj ją przyciskiem Dodaj kolumnę (+), aby nią tutaj sterować.",
|
||||
@@ -1638,11 +1591,10 @@
|
||||
"crash.report": "Zgłoś…",
|
||||
"crash.unclean_explanation": "Wykryto przy późniejszym uruchomieniu; dokładny czas zamknięcia nie został zapisany. Nieprawidłowe zamknięcie może też wynikać z wyłączenia systemu lub wymuszonego zatrzymania.",
|
||||
"delete.and_more": "… i jeszcze {count}",
|
||||
"diagnostic.empty_boolean_component": "w komponencie '{component}' nie ma bryły do wycięcia lub przecięcia; narzędzie może nachodzić na '{body}' w komponencie '{other}' – aktywuj ten komponent lub wybierz tę bryłę jako cel",
|
||||
"diagnostic.empty_boolean_component": "w komponencie '{component}' nie ma bryły do wycięcia lub przycięcia; narzędzie może nachodzić na '{body}' w komponencie '{other}' – aktywuj ten komponent lub wybierz tę bryłę jako cel",
|
||||
"diagnostic.reselect_reference": "{feature}: odniesienie `{reference}` już nie istnieje w postaci, w jakiej je wybrano; wybierz je ponownie",
|
||||
"dialog.bodies_count": "Bryły: {count}",
|
||||
"dialog.clearance_invalid": "luz musi być długością równą 0 lub większą",
|
||||
"dialog.component_origin": "{placement} · {item}",
|
||||
"dialog.compute": "Oblicz",
|
||||
"dialog.compute_again": "Oblicz ponownie",
|
||||
"dialog.computing": "Obliczanie… {seconds} s",
|
||||
@@ -1655,7 +1607,6 @@
|
||||
"dialog.dimension_step_expression": "Zmieniać można tylko liczbę; wyrażenie pozostaje tak, jak je wpisano",
|
||||
"dialog.dimension_step_up": "W górę o {step}",
|
||||
"dialog.edit_title": "EDYCJA: {title}",
|
||||
"dialog.hole_held_off_its_line": "Krawędź odniesienia trzyma ten otwór na linii, która opuszcza ścianę przed wskazanym punktem. Wskaż miejsce, w którym linia przecina ścianę, albo usuń krawędź, aby swobodnie przesuwać otwór.",
|
||||
"dialog.no_shown_body": "Brak widocznej bryły do zmiany: pokaż którąś lub wybierz opcję Nowa bryła",
|
||||
"dialog.pattern_sketch_not_object": "Wybierz bryły lub operacje: szkic nie jest ani jednym, ani drugim. Powiel jego krzywe własnym szykiem lub lustrem szkicu albo najpierw go wyciągnij.",
|
||||
"dialog.primitive_point_tip": "Punkt, który wypełnia X, Y i Z: kliknij tutaj, a potem kliknij wierzchołek, krawędź lub ścianę modelu, punkt szkicu albo początek układu.",
|
||||
@@ -1674,12 +1625,9 @@
|
||||
"document.changed_on_disk": "{name} zmienił się na dysku",
|
||||
"document.changed_on_disk_detail": "{name} został zmieniony przez inny program lub komputer od chwili otwarcia lub ostatniego zapisu. Nadpisz zastępuje te zmiany twoimi, a Zapisz kopię zachowuje obie wersje.",
|
||||
"document.copy_name": "{name} — kopia",
|
||||
"document.discard_row": "Odrzuć",
|
||||
"document.finish_save_prompt": "Zanim otworzysz kolejny projekt, odpowiedz na pytanie o zapisanie.",
|
||||
"document.newer_file": "Zapisano w nowszym SolidCraft (format {format}, ta wersja czyta do {reads}). Jeśli czegoś brakuje, zaktualizuj program.",
|
||||
"document.newer_file_change": "Zapisano w nowszym SolidCraft (format {format} ze zmianą `{change}`, której ta wersja nie zna). Jeśli czegoś brakuje, zaktualizuj program.",
|
||||
"document.newer_file_not_read": "Nie odczytano: {what}. Zapisanie to zachowa.",
|
||||
"document.not_saved_yet": "Jeszcze nie zapisano",
|
||||
"document.open_anyway": "Otwórz mimo to",
|
||||
"document.open_elsewhere": "{name} jest już otwarty",
|
||||
"document.open_elsewhere_detail": "{name} jest otwarty w SolidCraft {app} na {who}, otwarty {when}. Jeśli oboje zapiszecie, ktoś straci swoją pracę. Otwórz go tylko do odczytu, aby go obejrzeć bez nadpisywania.",
|
||||
@@ -1689,9 +1637,6 @@
|
||||
"document.read_only_save": "Ten projekt otwarto tylko do odczytu: zapisz go jako nowy plik",
|
||||
"document.save_changes": "Zapisać zmiany w {name}?",
|
||||
"document.save_copy": "Zapisz kopię…",
|
||||
"document.save_many": "Zapisać zmiany w projektach ({n})?",
|
||||
"document.save_row": "Zapisz",
|
||||
"document.save_selected": "Zapisz zaznaczone",
|
||||
"document.tab_unsaved_changes": "Niezapisane zmiany",
|
||||
"document.unsaved_changes": "{name} ma niezapisane zmiany. Jeśli ich nie zapiszesz, zostaną utracone.",
|
||||
"drive.rigid": "{joint} to złącze sztywne: utrzymuje swoje części nieruchomo, więc nie ma czym sterować. Aby mogły się poruszać, zmień jego typ w oknie Edytuj złącze: Obrotowe do obrotu, Przesuwne do przesuwu, Walcowe do obu.",
|
||||
@@ -1711,8 +1656,6 @@
|
||||
"dxf.tolerance": "Tolerancja krzywych",
|
||||
"dxf.tolerance_help": "Tylko widoczne bryły i pokazane szkice. Krzywe są przybliżane odcinkami. Mniejsza tolerancja daje dokładniejsze krzywe, ale zajmuje więcej czasu i pamięci.",
|
||||
"each rail's curves must be in one sketch": "krzywe każdej prowadnicy muszą leżeć w jednym szkicu",
|
||||
"error.end_cap_faces": "Wybierz co najmniej jedną otwartą ścianę końcową elementu ramy",
|
||||
"error.end_cap_unresolved": "Nie ma już w modelu końców tej zaślepki (nr {ends}). Wybierz ponownie ściany końcowe",
|
||||
"feature computation ended without a result": "obliczanie operacji zakończyło się bez wyniku",
|
||||
"file.open.drawing_filter": "Projekty, pliki CAD, siatki i rysunki",
|
||||
"file.open.portal_fallback.no_portal_service": "Otwarte okno wyboru pliku nie pochodzi z pulpitu: usługa portalu pulpitu jest niedostępna.",
|
||||
@@ -1724,7 +1667,6 @@
|
||||
"file_load.inserting": "Wstawianie geometrii",
|
||||
"file_load.link_item": "{name} ({done} z {total})",
|
||||
"file_load.opened": "Otwarto {name}",
|
||||
"file_load.opened_behind": "Otwarto {name} w nowej karcie",
|
||||
"file_load.opened_in_tab": "Otwarto {name} w nowej karcie",
|
||||
"file_load.opening": "Otwieranie projektu",
|
||||
"file_load.preparing_thumbnail": "Przygotowywanie miniatury…",
|
||||
@@ -1772,13 +1714,11 @@
|
||||
"help.config.table": "Pokazuje tabelę konfiguracji: jeden wiersz na wariant, jedną kolumnę na parametr lub operację oraz to, czy projekt nadal odpowiada aktywnemu wierszowi. Służy do przeglądu wariantów projektu.",
|
||||
"help.connect_note": "Uruchom ponownie każdego asystenta, aby wczytał serwer SolidCraft. Przed zmianą każdy plik zapisano jako kopię .bak.",
|
||||
"help.connect_title": "Połącz asystentów AI",
|
||||
"help.construct.axis": "Tworzy oś konstrukcyjną metodą wynikającą z wybranych elementów: wzdłuż krawędzi lub linii, przez otwór lub okrągłą ścianę, na przecięciu dwóch płaszczyzn, przez dwa punkty albo prostopadle do ściany przez punkt. Służy wtedy, gdy znasz elementy odniesienia, a nie metodę.",
|
||||
"help.construct.axis.edge": "Tworzy oś konstrukcyjną wzdłuż prostej krawędzi lub linii szkicu. Służy jako oś obrotu, szyku kołowego lub złącza.",
|
||||
"help.construct.axis.normal_to_face": "Tworzy oś konstrukcyjną przez punkt, prostopadłą do ściany. Daje oś wychodzącą prostopadle z powierzchni.",
|
||||
"help.construct.axis.through_cylinder": "Tworzy oś konstrukcyjną przez środek ściany walcowej, stożkowej lub torusa albo okrągłej krawędzi. Służy do obracania lub powielania operacji wokół okrągłej części.",
|
||||
"help.construct.axis.two_planes": "Tworzy oś konstrukcyjną w miejscu spotkania dwóch płaszczyzn lub płaskich ścian. Użyj, gdy potrzebna oś to linia między dwiema ścianami.",
|
||||
"help.construct.axis.two_points": "Tworzy oś konstrukcyjną przez dwa punkty, wierzchołki lub punkty szkicu. Przydaje się, gdy oś ma biec między dwoma znanymi położeniami.",
|
||||
"help.construct.plane": "Tworzy płaszczyznę konstrukcyjną metodą wynikającą z wybranych elementów: odsuniętą od płaszczyzny, przez punkt, między dwiema płaszczyznami, przez linie lub punkty, styczną do zakrzywionej ściany albo prostopadłą do ścieżki. Służy wtedy, gdy znasz elementy odniesienia, a nie metodę.",
|
||||
"help.construct.plane.along_path": "Tworzy płaszczyznę konstrukcyjną prostopadłą do krawędzi lub krzywej, w wybranym miejscu wzdłuż niej. Służy do szkicowania profilu przeciągnięcia lub przekroju ścieżki.",
|
||||
"help.construct.plane.at_angle": "Tworzy płaszczyznę konstrukcyjną obróconą o kąt względem płaszczyzny lub ściany, wokół osi lub krawędzi. Włącz opcję Użyj tylko kierunku osi, aby zrzutować oś na bazę i obracać wokół punktu na tej płaszczyźnie (początku bazy, gdy żaden nie jest wybrany). Służy do szkiców pochylonych, np. ukośnego wspornika.",
|
||||
"help.construct.plane.axis_point": "Tworzy płaszczyznę konstrukcyjną zawierającą oś i punkt poza nią. Wybierz oś początku układu lub konstrukcyjną, prostą krawędź lub linię szkicu, a potem wierzchołek, punkt szkicu lub punkt konstrukcyjny. Punkt leżący na osi nie wyznacza jednoznacznie płaszczyzny.",
|
||||
@@ -1788,7 +1728,6 @@
|
||||
"help.construct.plane.tangent": "Tworzy płaszczyznę konstrukcyjną styczną do walca, stożka, sfery lub torusa w punkcie. Służy do szkicowania na boku okrągłej części.",
|
||||
"help.construct.plane.three_points": "Tworzy płaszczyznę konstrukcyjną przez trzy punkty. Przydaje się, gdy płaszczyznę wyznaczają punkty modelu, np. trzy narożniki.",
|
||||
"help.construct.plane.two_edges": "Tworzy płaszczyznę konstrukcyjną przez dwie proste krawędzie lub linie leżące w jednej płaszczyźnie. Pozwala poprowadzić płaszczyznę w poprzek części, np. przez dwie przeciwległe krawędzie.",
|
||||
"help.construct.point": "Tworzy punkt konstrukcyjny metodą wynikającą z wybranych elementów: w wierzchołku, w środku okręgu lub sfery, wzdłuż krawędzi, na przecięciu dwóch linii lub trzech płaszczyzn albo tam, gdzie linia przecina płaszczyznę. Służy wtedy, gdy znasz elementy odniesienia, a nie metodę.",
|
||||
"help.construct.point.along_path": "Tworzy punkt konstrukcyjny w wybranym miejscu wzdłuż krawędzi lub krzywej. Służy jako odniesienie w dowolnym miejscu ścieżki.",
|
||||
"help.construct.point.center": "Tworzy punkt konstrukcyjny w środku okrągłej krawędzi, okręgu lub łuku szkicu, sfery albo torusa. Służy jako odniesienie dla otworów, osi i złączy.",
|
||||
"help.construct.point.edge_plane": "Tworzy punkt konstrukcyjny tam, gdzie krawędź lub linia przechodzi przez płaszczyznę lub płaską ścianę. Służy do oznaczenia miejsca, w którym ścieżka przecina płaszczyznę.",
|
||||
@@ -1814,7 +1753,6 @@
|
||||
"help.file.send_to_slicer": "Zapisuje zaznaczone bryły jako plik 3MF i otwiera go w wybranym slicerze. Służy do przejścia od modelu do druku jednym krokiem.",
|
||||
"help.font_details": "Interfejs używa wbudowanych czcionek egui (Ubuntu Light i Hack na licencjach Ubuntu Font Licence oraz MIT/Bitstream Vera; Noto Emoji i czcionka ikon emoji na licencjach OFL i MIT). Tekst szkicu używa Liberation Sans (SIL OFL 1.1). Opcjonalny pakiet czcionek The Crafting Apps, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), nie jest dołączany do kompilacji SolidCraft.",
|
||||
"help.fonts": "Czcionki",
|
||||
"help.frame.end_cap": "Zamyka otwarte końce elementów ramy płytkami w kształcie obrysu profilu, nałożonymi na koniec lub w niego wpuszczonymi. Służy do zaślepiania rur, kątowników i belek.",
|
||||
"help.frame.member": "Umieszcza profil, np. rurę lub kształtownik aluminiowy, wzdłuż linii szkicu ramy, jeden element na linię. Służy do budowy ram, regałów i konstrukcji spawanych.",
|
||||
"help.inspect.accessibility": "Koloruje ściany, do których narzędzie dociera z jednego kierunku, i te, do których nie. Służy do sprawdzenia części pod kątem obróbki lub formowania z tej strony.",
|
||||
"help.inspect.center_of_mass": "Pokazuje środek masy brył, każdej osobno i wszystkich razem, przy jednorodnej gęstości. Służy do sprawdzenia wyważenia i miejsca, w którym część się przechyli.",
|
||||
@@ -2080,7 +2018,6 @@
|
||||
"help.view.visual_style": "Przełącza sposób rysowania modelu: cieniowany z krawędziami lub bez, krawędziowy albo z krawędziami niewidocznymi. Użyj modelu krawędziowego, aby zajrzeć do wnętrza, a cieniowanego dla czystego wyglądu.",
|
||||
"history.sketch_under_constrained": "Niedostatecznie związany (stopnie swobody: {count})",
|
||||
"history.sketch_under_constrained_one": "Niedostatecznie związany (stopnie swobody: {count})",
|
||||
"hole_folder.applies_to": "Zmiany dotyczą otworów w folderze {folder} ({count}); każdy zachowuje swoje miejsce.",
|
||||
"hole_presets.confirm_delete": "Potwierdź usunięcie",
|
||||
"hole_presets.delete": "Usuń",
|
||||
"hole_presets.delete_prompt": "Usunąć ten własny szablon? Istniejące otwory pozostaną bez zmian.",
|
||||
@@ -2291,9 +2228,6 @@
|
||||
"measure.at": "W punkcie",
|
||||
"measure.item": "Element {number}",
|
||||
"measure.line_to_centre": "Linia do środka",
|
||||
"measure.perimeter": "Obwód",
|
||||
"measure.perimeter_inner": "Obwód wewnętrzny",
|
||||
"measure.perimeter_outer": "Obwód zewnętrzny",
|
||||
"measure.quick_copied": "Skopiowano pomiar",
|
||||
"measure.quick_copy": "Kliknij, aby skopiować pomiar",
|
||||
"measure.quick_edges": "Krawędzie: {count}",
|
||||
@@ -2335,8 +2269,6 @@
|
||||
"nav.hfss.help": "Alt+przeciąganie lewym przesuwa, Shift+Alt+lewy powiększa, Ctrl+przeciąganie lewym na pustym miejscu obraca widok",
|
||||
"nav.inventor.help": "Przeciąganie środkowym przesuwa, Shift+środkowy obraca widok, Ctrl+środkowy powiększa, przeciąganie prawym obraca widok",
|
||||
"nav.maya.help": "Alt+przeciąganie lewym obraca widok, Alt+środkowy przesuwa, Alt+prawy powiększa; przeciąganie środkowym także przesuwa",
|
||||
"nav.maya_horizontal_zoom.help": "Alt + przeciąganie prawym przyciskiem: w prawo przybliża, w lewo oddala. Wyłącz, aby przywrócić powiększanie przeciąganiem w pionie.",
|
||||
"nav.maya_horizontal_zoom.label": "Maya: powiększanie przeciąganiem w poziomie",
|
||||
"nav.nx.help": "Przeciąganie środkowym obraca widok, Shift+środkowy przesuwa (w NX środkowy + prawy), Ctrl+środkowy powiększa (w NX środkowy + lewy); przeciąganie prawym nie rusza kamery",
|
||||
"nav.openscad.help": "Przeciąganie lewym na pustym miejscu obraca widok, Shift+przeciąganie lewym zaznacza prostokątem, przeciąganie geometrii szkicu ją edytuje; przeciąganie prawym lub środkowym przesuwa, Shift+prawy lub Shift+środkowy powiększa",
|
||||
"nav.orbit_cursor_wrap.help": "Gdy obracanie przeciąganiem dojdzie do krawędzi widoku, kursor przeskakuje na przeciwną krawędź, a obrót trwa dalej. Wyłączone: przeciąganie zatrzymuje się na krawędzi ekranu. Nie działa w Wayland ani w przeglądarce.",
|
||||
@@ -2467,16 +2399,6 @@
|
||||
"not a move": "to nie jest przesunięcie",
|
||||
"not a number": "to nie jest liczba",
|
||||
"open a sketch first": "najpierw otwórz szkic",
|
||||
"operations.cancel": "Anuluj: {name}",
|
||||
"operations.cancelled": "Anulowano: {name}",
|
||||
"operations.cancelling": "Anulowanie…",
|
||||
"operations.cannot_cancel": "Nie można anulować: {name}",
|
||||
"operations.feature": "Operacja",
|
||||
"operations.feature_edit": "Edycja operacji",
|
||||
"operations.meshing": "Wczytywanie części",
|
||||
"operations.queued": "W kolejce",
|
||||
"operations.stopped": "Anulowano",
|
||||
"operations.waiting": "Gotowe",
|
||||
"parameters.apply_live": "Stosuj każdą zmianę po opuszczeniu pola",
|
||||
"parameters.apply_live_tip": "Wyłączone: zmiany czekają na kliknięcie Zastosuj lub OK, a potem projekt przebudowuje się raz dla wszystkich. Anuluj lub Escape je odrzuca.",
|
||||
"parameters.held": "Zmiany do zastosowania: {count}",
|
||||
@@ -2498,13 +2420,12 @@
|
||||
"parts.dimension.width": "Szerokość",
|
||||
"parts.family.bearing": "Łożysko kulkowe zwykłe (obrys)",
|
||||
"parts.family.button_head_cap_screw": "Śruba z łbem półokrągłym z gniazdem sześciokątnym",
|
||||
"parts.family.countersunk_socket_screw": "Śruba z łbem stożkowym z gniazdem sześciokątnym",
|
||||
"parts.family.dowel_pin": "Kołek walcowy",
|
||||
"parts.family.hex_bolt": "Śruba z łbem sześciokątnym",
|
||||
"parts.family.hex_cap_screw_inch": "Śruba z łbem sześciokątnym (calowa)",
|
||||
"parts.family.hex_nut": "Nakrętka sześciokątna",
|
||||
"parts.family.hex_nut_inch": "Nakrętka sześciokątna (calowa)",
|
||||
"parts.family.set_screw": "Wkręt dociskowy z gniazdem sześciokątnym z końcem płaskim",
|
||||
"parts.family.set_screw": "Wkręt dociskowy z gniazdem sześciokątnym, płaski",
|
||||
"parts.family.socket_head_cap_screw": "Śruba z łbem walcowym z gniazdem sześciokątnym",
|
||||
"parts.family.washer": "Podkładka zwykła",
|
||||
"parts.family.washer_sae": "Podkładka zwykła, SAE (wąska)",
|
||||
@@ -2565,7 +2486,6 @@
|
||||
"prefs.appearance.viewport_colours": "Kolory widoku 3D",
|
||||
"prefs.appearance.viewport_logo": "Widok i logo",
|
||||
"prefs.applies_on_restart": "Działa po ponownym uruchomieniu",
|
||||
"prefs.assemblies": "Złożenia",
|
||||
"prefs.assistant": "Asystent",
|
||||
"prefs.assistant_connecting": "Łączenie",
|
||||
"prefs.assistant_follow_help": "Patrz na szkice przesuwa widok razem ze wskaźnikiem.",
|
||||
@@ -2598,8 +2518,6 @@
|
||||
"prefs.bottom_centre": "Dół pośrodku",
|
||||
"prefs.bottom_right": "Dół z prawej",
|
||||
"prefs.browser": "Przeglądarka",
|
||||
"prefs.browser_body_features": "Operacje pod bryłami",
|
||||
"prefs.browser_body_features_note": "Każda bryła w przeglądarce rozwija się na operacje z Historii, które ją zbudowały, w kolejności Historii.",
|
||||
"prefs.browser_components_open": "Komponenty startują rozwinięte",
|
||||
"prefs.browser_components_open_note": "Każdy komponent w przeglądarce startuje rozwinięty, jak dotąd, zamiast zwinięty do czasu otwarcia.",
|
||||
"prefs.browser_eye_left": "Oko obok strzałki zwijania",
|
||||
@@ -2618,10 +2536,6 @@
|
||||
"prefs.choose_font": "Wybierz plik czcionki…",
|
||||
"prefs.click": "Kliknięcie",
|
||||
"prefs.command_dialog_values": "Okna poleceń",
|
||||
"prefs.constraint_glyph_size": "Rozmiar symboli wiązań",
|
||||
"prefs.constraint_glyph_size_note": "Jak duże są rysowane w szkicu symbole wiązań oraz ich litery lub ikony. 100% to zwykły rozmiar; symbole zachowują ten rozmiar na ekranie przy każdym powiększeniu.",
|
||||
"prefs.construct_menu_groups": "Grupuj narzędzia Konstrukcja według rodzaju elementu",
|
||||
"prefs.construct_menu_groups_help": "Pokazuje nagłówki Płaszczyzny, Osie i Punkty. Wyłączone: menu zachowuje pierwotną kolejność.",
|
||||
"prefs.contact_shading": "Miękkie cieniowanie kontaktowe",
|
||||
"prefs.contact_shading_note": "Miękkie cieniowanie tam, gdzie części są blisko siebie.",
|
||||
"prefs.context_menu": "Menu kontekstowe",
|
||||
@@ -2644,11 +2558,7 @@
|
||||
"prefs.design_tools.degrees": "Stopnie",
|
||||
"prefs.design_tools.double_click_help": "Lub aby edytować połączony komponent w miejscu.",
|
||||
"prefs.design_tools.ghost": "Pokazuj poprzednie miejsce przesuniętych części jako widmo",
|
||||
"prefs.design_tools.hole_group": "Grupuj otwory utworzone razem",
|
||||
"prefs.design_tools.hole_group_help": "Otwory utworzone za jednym razem trafiają do wspólnego folderu w Historii, na początku zwiniętego; rozwiń go, aby edytować pojedynczy otwór.",
|
||||
"prefs.design_tools.hole_moves_help": "Ponowne wybranie ściany przesuwa otwór zamiast go usuwać.",
|
||||
"prefs.design_tools.hole_several": "Kilka otworów na jednej ścianie",
|
||||
"prefs.design_tools.hole_several_help": "Wybranie innego miejsca na ścianie dodaje kolejny otwór; wybranie otworu go usuwa.",
|
||||
"prefs.design_tools.hole_snaps_help": "Środek ściany i środki jej okrągłych krawędzi.",
|
||||
"prefs.design_tools.imported_files": "Zaimportowane pliki",
|
||||
"prefs.design_tools.note": "Uwaga",
|
||||
@@ -2657,8 +2567,6 @@
|
||||
"prefs.design_tools.rows": "Wiersze",
|
||||
"prefs.devices": "Mysz 3D",
|
||||
"prefs.dialog_buttons": "Przyciski okien dialogowych",
|
||||
"prefs.dialog_first_letter_buttons": "Przyciski okien dialogowych reagują na pierwszą literę",
|
||||
"prefs.dialog_first_letter_buttons_help": "Gdy okno dialogowe jest otwarte, wpisanie pierwszej litery etykiety przycisku naciska go: O dla OK, A dla Anuluj. Litera pasująca do dwóch przycisków nic nie robi, a aktywne pole tekstowe zachowuje naciśnięty klawisz. Wyłączone: pisanie nie wpływa na przyciski, jak dotąd.",
|
||||
"prefs.dialogs": "Okna dialogowe",
|
||||
"prefs.dialogs_edge": "Przy krawędzi widoku",
|
||||
"prefs.dialogs_left_column": "W lewej kolumnie",
|
||||
@@ -2672,23 +2580,12 @@
|
||||
"prefs.dimension_values_window": "W oknie",
|
||||
"prefs.display": "Wyświetlanie",
|
||||
"prefs.edges": "Krawędzie i cieniowanie",
|
||||
"prefs.export": "Eksport",
|
||||
"prefs.export_name": "Nazwa eksportowanego pliku",
|
||||
"prefs.export_name.body": "Nazwa eksportowanej bryły (pierwszej, gdy jest ich kilka)",
|
||||
"prefs.export_name.design": "Nazwa projektu",
|
||||
"prefs.export_name_hint": "np. {design}-{body}",
|
||||
"prefs.export_name_sample": "Eksport tego projektu zaproponuje: {name}",
|
||||
"prefs.export_name_tip": "Nazwa pliku proponowana przez Plik › Eksportuj i Zapisz jako siatkę. Te symbole zastępcze są zamieniane:\n{placeholders}\nWłaściwość projektu, która nie jest ustawiona, jest pomijana. Zostaw pole puste, aby proponować nazwę projektu, a dla siatki – nazwę bryły.",
|
||||
"prefs.extend": "Rozszerz",
|
||||
"prefs.extra_navigation": "Ctrl+przeciąganie lewym na pustym miejscu obraca widok; przytrzymaj Spację, aby przesuwać.",
|
||||
"prefs.extrude_arrow_face_stops": "Strzałka wyciągnięcia zatrzymuje się na ścianach",
|
||||
"prefs.extrude_arrow_face_stops_note": "Przeciąganie strzałki odległości w wyciągnięciu zatrzymuje się na płaskich ścianach równoległych do profilu, np. na drugiej stronie wycinanego bloku, i obrysowuje ścianę. Wyłączone: strzałka przesuwa się tylko o okrągłe kroki.",
|
||||
"prefs.extrude_view_isometric": "Obracaj widok do izometrycznego przy otwarciu wyciągnięcia",
|
||||
"prefs.extrude_view_return": "Wracaj do poprzedniego widoku po zamknięciu wyciągnięcia",
|
||||
"prefs.file_operations_wait": "Czekaj na otwarcie plików",
|
||||
"prefs.file_operations_wait_note": "Gdy plik jest otwierany lub wstawiany albo łącza są ponownie odczytywane, projekt czeka pod nakładką z postępem pliku i przyciskiem Anuluj. Wyłączone: plik pojawia się w panelu zadań i możesz dalej pracować; wstawienie nastąpi, gdy skończysz bieżącą czynność, a plik otwarty w międzyczasie trafi do nowej karty.",
|
||||
"prefs.fillet_legacy_radius": "Używaj poprzedniego domyślnego promienia zaokrąglenia bryły",
|
||||
"prefs.fillet_legacy_radius.help": "Domyślnie wyłączone. Gdy włączone, nowe zaokrąglenie bryły otwiera się z liczbą 2 odczytaną w jednostkach projektu („2 in” w projekcie calowym), jak dotąd; gdy wyłączone, otwiera się z domyślnym promieniem narzędzia, 2 mm, pokazanym w jednostkach projektu („0.07874 in” w projekcie calowym).",
|
||||
"prefs.follows_os": "Zgodnie z systemem",
|
||||
"prefs.font": "Czcionka",
|
||||
"prefs.font_fallback": "Znaki, których brakuje w wybranej czcionce, są brane z czcionki wbudowanej, więc żadna etykieta nie pokaże pustego prostokąta.",
|
||||
@@ -2712,17 +2609,6 @@
|
||||
"prefs.gesture_pan": "Przesuwanie",
|
||||
"prefs.gesture_zoom": "Powiększanie",
|
||||
"prefs.graphics": "Grafika",
|
||||
"prefs.graphics_api": "API grafiki",
|
||||
"prefs.graphics_api_auto": "Automatycznie",
|
||||
"prefs.graphics_api_help": "Wybierz API grafiki, z którym uruchamia się SolidCraft. Jeśli przy następnym uruchomieniu okno się nie otworzy, przywracany jest poprzedni wybór.",
|
||||
"prefs.graphics_api_override": "API grafiki jest wymuszone przy uruchomieniu. Zapisany wybór zadziała, gdy SolidCraft zostanie uruchomiony normalnie.",
|
||||
"prefs.graphics_api_restart_note": "Działa po ponownym uruchomieniu. Przed zamknięciem pojawi się pytanie o niezapisane projekty.",
|
||||
"prefs.graphics_api_restart_unavailable": "Uruchom ponownie SolidCraft, aby zastosować zapisane API grafiki.",
|
||||
"prefs.graphics_api_running": "Bieżące: {current}. Wybrane na następne uruchomienie: {selected}.",
|
||||
"prefs.graphics_api_save_failed": "Nie udało się zapisać wyboru API grafiki: {error}",
|
||||
"prefs.graphics_api_start_failed": "Nie udało się przygotować wybranego API grafiki: {error}",
|
||||
"prefs.graphics_api_start_reverted": "Poprzednie uruchomienie z {requested} nie otworzyło okna. SolidCraft przywrócił {previous}.",
|
||||
"prefs.graphics_api_unavailable": "Wybór API grafiki jest dostępny w aplikacji desktopowej.",
|
||||
"prefs.graphics_card": "Karta graficzna",
|
||||
"prefs.graphics_card_auto": "Automatycznie",
|
||||
"prefs.graphics_card_missing": "Zapisana karta graficzna jest niedostępna; używane jest ustawienie Automatycznie.",
|
||||
@@ -2754,8 +2640,6 @@
|
||||
"prefs.history": "Historia",
|
||||
"prefs.history_bar_groups": "Zwijaj pasek Historii w grupy",
|
||||
"prefs.history_bar_groups_note": "W długiej historii ciąg operacji jednego komponentu zwija się w jedną etykietę, a foldery operacji są na początku zwinięte. Wstawione złożenie STEP trafia do folderu Grupa. Wyłączone: widać każdą operację, foldery są na początku rozwinięte, a folder nie powstaje.",
|
||||
"prefs.history_folder_kind_icon": "Ikona folderu według jego operacji",
|
||||
"prefs.history_folder_kind_icon_note": "Folder Historii, którego wszystkie operacje są jednego rodzaju, pokazuje ich ikonę.",
|
||||
"prefs.history_group_joints": "Grupuj złącza w Historii",
|
||||
"prefs.history_group_joints_note": "Dwa lub więcej złączy są pokazywane jako jeden przycisk z ich liczbą, a kliknięcie pokazuje każde z nich. Wyłączone pokazuje każde złącze.",
|
||||
"prefs.history_origin_bodies": "Pokazuj początek układu i bryły na górze",
|
||||
@@ -2766,8 +2650,6 @@
|
||||
"prefs.history_sketches_in_order": "W kolejności",
|
||||
"prefs.history_sketches_note": "Gdy ustawienie Szkice ma wartość „Pod swoją operacją”, Historia po lewej pokazuje każdy szkic o poziom głębiej, pod pierwszą operacją z niego zbudowaną, o ile panel jest dość szeroki na nazwy. Oś czasu na dole zawsze zachowuje kolejność tworzenia.",
|
||||
"prefs.history_sketches_under_feature": "Pod swoją operacją",
|
||||
"prefs.history_steps_component": "Kroki Historii według komponentu",
|
||||
"prefs.history_steps_component_note": "Gdy komponent jest aktywny lub wybrany, elementy sterujące Historii przechodzą przez operacje tego komponentu, tak jak pokazuje je Historia na dole. Wyłączone: przechodzą przez wszystkie operacje projektu. Historia po lewej lub pod przeglądarką zawsze przechodzi przez wszystkie operacje.",
|
||||
"prefs.hold_stop_snapping": "Przytrzymaj, aby wyłączyć przyciąganie",
|
||||
"prefs.home_fit_size": "Rozmiar przy poleceniach Widok główny i Dopasuj",
|
||||
"prefs.home_fit_size_note": "Jaką część widoku wypełnia model, gdy kadrują go polecenia Widok główny i Dopasuj, otwarcie projektu lub zmiana widoku w nawigatorze, niezależnie od ustawienia poniżej. 100%: wypełnia widok między panelami, kompasem i paskiem nawigacji.",
|
||||
@@ -2783,8 +2665,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "Dwie zakrzywione strzałki w rogu kostki przechylają widok w płaszczyźnie ekranu. Włączone: strzałka zgodna z ruchem wskazówek zegara przechyla przeciwnie, a przeciwna – zgodnie. Wyłączone: każda strzałka przechyla widok w stronę, w którą jest zakrzywiona.",
|
||||
"prefs.invert_navigator_arrows": "Odwróć strzałki obrotu kostki widoku",
|
||||
"prefs.invert_navigator_arrows_note": "Strzałki wokół kostki obracają kamerę w kierunku, w który wskazują, jak w Fusion i Inventor: strzałka po lewej wskazuje w prawo i obraca kamerę w prawo. Wyłączone: strzałka obraca kostkę w stronę, po której się znajduje, więc lewa strzałka obraca kamerę w lewo. Zakrzywione strzałki przechylenia nie są zmieniane.",
|
||||
"prefs.joint_drag_trackball": "Obracaj części na złączu kulistym jak trackball przy przeciąganiu",
|
||||
"prefs.joint_drag_trackball_help": "Przeciąganie części trzymanej przez złącze kuliste obraca ją o najmniejszy obrót wokół złącza, jak toczenie kuli wielkości części. Wyłączone: chwycony punkt dokładnie podąża za wskaźnikiem, a część może się też wokół niego obracać.",
|
||||
"prefs.keep_unsaved_also_asks": "Mimo to pytaj przy zamykaniu karty",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Gdy wyłączone, karta zamyka się bez okna przeglądarki, jeśli kopia każdego niezapisanego projektu jest aktualna.",
|
||||
"prefs.keep_unsaved_in_browser": "Przechowuj niezapisaną pracę w tej przeglądarce",
|
||||
@@ -2860,14 +2740,11 @@
|
||||
"prefs.not_on_gpu": "niedostępne na tym GPU",
|
||||
"prefs.on": "Włączone",
|
||||
"prefs.opaque": "Nieprzezroczysta",
|
||||
"prefs.operations_panel": "Pokazuj trwające zadania w widoku",
|
||||
"prefs.operations_panel_note": "Długie zadania (otwieranie pliku, obliczanie operacji, polecenie agenta) pojawiają się w prawym dolnym rogu widoku z postępem i przyciskiem, który je anuluje. Wyłączone: komunikat Obliczanie, jak dotąd.",
|
||||
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
|
||||
"prefs.orientation": "Orientacja",
|
||||
"prefs.origin_plane_names": "Nazwy płaszczyzn początku układu",
|
||||
"prefs.origin_plane_names_note": "Nazwy pokazywane w przeglądarce, oknach dialogowych i listach. {top} to XY, {front} to XZ, a {right} to YZ, przy osi Z w górę. Polecenia, skrypty i zapisane projekty zachowują XY, XZ i YZ.",
|
||||
"prefs.overall_speed": "Szybkość ogólna",
|
||||
"prefs.panel_layout.recovery": "Nie udało się przywrócić zapisanych układów paneli. Zapisane dane są zachowane; układ Klasyczny pozostaje dostępny.",
|
||||
"prefs.parameter_list_rows": "Wiersze listy parametrów",
|
||||
"prefs.parameter_list_rows_tip": "Maksymalna liczba parametrów, które strzałka w dół wyświetla w pustym polu wyrażenia.",
|
||||
"prefs.persistent_ribbon_pressed": "Wyróżniaj aktywne polecenia wstążki",
|
||||
@@ -2929,8 +2806,6 @@
|
||||
"prefs.remember_dialog_values.help": "Domyślnie wyłączone. Gdy włączone, po udanym poleceniu jego bezpieczne wartości i opcje są używane ponownie przy następnym otwarciu tego samego polecenia, aż do zamknięcia SolidCraft.",
|
||||
"prefs.render_legacy_image_defaults": "Używaj poprzednich ustawień domyślnych obrazu renderowania",
|
||||
"prefs.render_legacy_image_defaults_help": "Zachowuje dotychczasowy ręczny tryb pracy: nowe ustawienia używają rasteryzacji CPU, śledzenie promieni na CPU zachowuje stałe próbkowanie, a wyrenderowane wyniki otwierasz ręcznie. Dopasowuje widok przy wejściu do środowiska Renderowanie, przełącza ujęcia bez przywracania ich kamer i używa poprzedniego kadrowania i nawigacji. Wyłącza automatyczny podgląd obrazu na żywo. Zapisane ustawienia pozostają bez zmian.",
|
||||
"prefs.render_legacy_memory_admission": "Używaj poprzednich kontroli pamięci renderowania",
|
||||
"prefs.render_legacy_memory_admission_help": "Dopuszcza natywne renderowania w stałych limitach pamięci współdzielonej i hosta, nawet gdy ich oszacowanie przekracza dostępną pamięć RAM. Limity urządzenia i kontrole przydziału nadal obowiązują.",
|
||||
"prefs.render_previous_geometry_budget": "Używaj poprzedniego budżetu geometrii podglądu renderowania",
|
||||
"prefs.render_previous_geometry_budget_help": "Przywraca poprzedni stały limit geometrii (512 MiB na 64-bitowym komputerze stacjonarnym, 128 MiB w innych przypadkach) zamiast budżetu zależnego od urządzenia.",
|
||||
"prefs.reports_saved": "Zapisane: {n}",
|
||||
@@ -2942,8 +2817,6 @@
|
||||
"prefs.ribbon_tabs_only": "Pokazuj tylko karty wstążki",
|
||||
"prefs.ribbon_tabs_only_note": "Wstążka zwija się do wiersza kart. Kliknięcie karty pokazuje jej panele nad widokiem, dopóki nie uruchomisz polecenia lub wskaźnik ich nie opuści. Włącza się to i wyłącza poleceniem Tylko karty wstążki na karcie Widok lub jego skrótem.",
|
||||
"prefs.right_click": "Kliknięcie prawym",
|
||||
"prefs.save_prompt_one_at_a_time": "Pytaj osobno o każdy niezapisany projekt przy zamykaniu kilku",
|
||||
"prefs.save_prompt_one_at_a_time_help": "Pokazuje osobne pytanie o zapisanie dla każdego niezapisanego projektu zamiast wspólnej listy.",
|
||||
"prefs.saving": "Autozapis i odzyskiwanie",
|
||||
"prefs.scale_note": "Także Ctrl + / Ctrl − w oknie. Przyciski mają 28 px przy 100%.",
|
||||
"prefs.scale_text": "Skala i tekst",
|
||||
@@ -2959,8 +2832,6 @@
|
||||
"prefs.select_other_hidden": "Zaznaczaj ukrytą geometrię poleceniem Wybierz inny",
|
||||
"prefs.select_other_hidden_help": "Włączone: uwzględnia ukryte bryły, szkice i zasłonięte krawędzie w pobliżu wskaźnika. Wyłączone: używa pierwotnej listy widocznej geometrii pod wskaźnikiem.",
|
||||
"prefs.selecting": "Zaznaczanie",
|
||||
"prefs.shared_panel_layout": "Używaj wspólnego układu paneli",
|
||||
"prefs.shared_panel_layout_help": "Pokazuje istniejące panele Przeglądarka, Historia i Asystent jako przesuwalne panele. Zmiana działa po zastosowaniu preferencji.",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift tylko dodaje; Ctrl / Cmd przełącza",
|
||||
"prefs.shift_add_note": "Włączone: Shift dodaje, Ctrl / Cmd przełącza. Wyłączone (domyślnie): oba przełączają.",
|
||||
@@ -2985,28 +2856,20 @@
|
||||
"prefs.sketch.drawing": "Rysowanie",
|
||||
"prefs.sketch.ellipse_axes": "Rysuj elipsy z osiami",
|
||||
"prefs.sketch.ellipse_axes_note": "Oś wielka i mała jako linie konstrukcyjne, aby można było wymiarować półosie i osie. Wyłączone: sama elipsa, a D na niej daje promień małej półosi, gdziekolwiek przeciągnąć wartość, rysowany i oznaczony R jak dotąd.",
|
||||
"prefs.sketch.escape_twice_finishes": "Podwójne Esc kończy szkic",
|
||||
"prefs.sketch.escape_twice_finishes_note": "Dwa szybkie naciśnięcia Esc kończą szkic. Wyłączone: Esc tylko anuluje narzędzie lub zaznaczenie, a szkic kończy polecenie Zakończ szkic.",
|
||||
"prefs.sketch.exit_keep": "Zachowaj",
|
||||
"prefs.sketch.exit_previous": "Wróć do poprzedniego widoku",
|
||||
"prefs.sketch.exit_unless_rotated": "Chyba że obrócono",
|
||||
"prefs.sketch.faces_edges": "Ściany i krawędzie",
|
||||
"prefs.sketch.hide_glyphs": "Ukrywaj symbole przy oddaleniu",
|
||||
"prefs.sketch.marks": "Oznaczenia w szkicu",
|
||||
"prefs.sketch.max_points": "Największy szkic",
|
||||
"prefs.sketch.max_points_note": "Najwięcej punktów lub krzywych w jednym szkicu: domyślnie 20000, maksymalnie 200000. Wstaw SVG i Wstaw DXF odrzucają większy rysunek. Duży szkic wolniej się rozwiązuje i rysuje.",
|
||||
"prefs.sketch.max_points_unit": "punktów",
|
||||
"prefs.sketch.note": "Rysowanie, przyciąganie i zaznaczanie.",
|
||||
"prefs.sketch.opening_closing": "Otwieranie i zamykanie",
|
||||
"prefs.sketch.prefer_free_points": "Preferuj ruchome punkty tam, gdzie się nakładają",
|
||||
"prefs.sketch.project_options": "Najpierw pytaj o opcje rzutowania",
|
||||
"prefs.sketch.recent_snap": "Przyciągaj do przecięcia dwóch linii pomocniczych",
|
||||
"prefs.sketch.select_3d_curve_points": "Zaznaczaj punkty na krzywych szkicu 3D",
|
||||
"prefs.sketch.select_3d_curve_points_help": "Pokazuje punkty na narysowanych krzywych szkicu 3D i pozwala je zaznaczać jako odniesienia konstrukcyjne.",
|
||||
"prefs.sketch.select_other_hidden": "Wybierz inny sięga do ukrytej geometrii",
|
||||
"prefs.sketch.shortcut_cycles_modes": "Ponowny skrót przełącza tryb",
|
||||
"prefs.sketch.shortcut_cycles_modes_tip": "Wyłączone: uruchamia narzędzie od nowa.",
|
||||
"prefs.sketch.snap_to_visible_grid": "Przyciągaj do widocznych przecięć siatki",
|
||||
"prefs.sketch.snapping_guides": "Przyciąganie i linie pomocnicze",
|
||||
"prefs.sketch.state_icons": "Ikony stanu",
|
||||
"prefs.sketch.tracking_guides": "Linie pomocnicze do środków i punktów środkowych",
|
||||
@@ -3034,7 +2897,6 @@
|
||||
"prefs.sketch_selection": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_heading": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_note": "Jak działa przyciąganie w szkicach i jak kliknięcia zaznaczają.",
|
||||
"prefs.sketch_snap_to_visible_grid_note": "Punkt w szkicu przyciąga się do przecięć linii rysowanych przez siatkę, gdy wskaźnik jest blisko takiego przecięcia, a w przeciwnym razie do gęstszej siatki między nimi. Wyłączone: punkt przyciąga się tylko do gęstszej siatki, jak dotąd.",
|
||||
"prefs.sketch_state_icons": "Ikony szkiców w kolorze ich stanu",
|
||||
"prefs.sketch_state_icons_note": "W Historii i przeglądarce ikona szkicu przyjmuje kolor jego stanu (niedostatecznie związany, w pełni związany lub w konflikcie), a podpowiedź podaje ten stan. Wyłączone: każdy szkic ma tę samą ikonę.",
|
||||
"prefs.sketch_tab_always": "Zawsze pokazuj kartę Szkic",
|
||||
@@ -3179,7 +3041,7 @@
|
||||
"refused.face_freeform": "Jedna z jej ścian o dowolnym kształcie nie nadaje się do modelowania.",
|
||||
"refused.face_rounded": "Jedna z jej zaokrąglonych ścian nie nadaje się do modelowania.",
|
||||
"refused.faces_apart": "Jej ściany nie stykają się dość dokładnie, by nadawała się do modelowania.",
|
||||
"refused.intersect": "Nie udało się wyznaczyć przecięcia z {body}.",
|
||||
"refused.intersect": "Nie udało się przyciąć z {body}.",
|
||||
"refused.join": "Nie udało się połączyć z {body}.",
|
||||
"refused.repair": "Napraw część",
|
||||
"refused.show_face": "Pokaż ścianę",
|
||||
@@ -3336,10 +3198,8 @@
|
||||
"render.category_paint": "Farba",
|
||||
"render.category_plastic": "Tworzywo",
|
||||
"render.category_wood": "Drewno",
|
||||
"render.caustics_area_lights": "Kaustyka od świateł powierzchniowych",
|
||||
"render.caustics_depth": "Głębokość ścieżki kaustyki",
|
||||
"render.caustics_enable": "Kaustyka szkła od świateł punktowych i kierunkowych",
|
||||
"render.caustics_environment": "Kaustyka od otoczenia",
|
||||
"render.caustics_help": "Liczba fotonów decyduje o szumie i nakładzie pracy. Promień zbierania zamienia ostre szczegóły na gładsze oszacowanie. CPU wymaga oświetlenia pośredniego.",
|
||||
"render.caustics_path_required": "Kaustyka na CPU wymaga oświetlenia pośredniego.",
|
||||
"render.caustics_photons": "Fotony",
|
||||
@@ -3556,12 +3416,6 @@
|
||||
"render.cpu_subsurface_denoise_reason": "Rozpraszanie podpowierzchniowe na CPU wymaga wyłączonego odszumiania. Twoje ustawienia zostają zachowane.",
|
||||
"render.cpu_subsurface_help": "Rozpraszanie zmienia kierunek światła wewnątrz zamkniętego szkła. Wartości dodatnie wymagają oświetlenia pośredniego i wyłączonego odszumiania. Współczynnik to liczba zderzeń na mm; 0 nie dodaje rozpraszania. Dodatnie przesunięcie fazy faworyzuje kierunki do przodu, ujemne do tyłu.",
|
||||
"render.cpu_summary": "Śledzenie promieni na CPU · odbicia · HDR · światła kierunkowe i punktowe",
|
||||
"render.cpu_threads": "Wątki CPU",
|
||||
"render.cpu_threads_auto": "Automatycznie",
|
||||
"render.cpu_threads_custom": "Własne",
|
||||
"render.cpu_threads_help": "Automatycznie używa dostępnych wątków CPU. Jeden wątek renderuje sekwencyjnie. Więcej wątków może wymagać więcej pamięci.",
|
||||
"render.cpu_threads_single": "Jeden",
|
||||
"render.cpu_threads_web": "Renderowanie w przeglądarce internetowej używa jednego wątku kooperacyjnego. Zapisany wybór wątków dla wersji desktopowej zostaje zachowany.",
|
||||
"render.cpu_transparent_help": "Przezroczysty PNG usuwa tło i zachowuje pokrycie krawędzi. Nieprzezroczystość brył i ścian musi pozostać na 100%.",
|
||||
"render.cpu_volume_enable_path": "Włącz oświetlenie pośrednie",
|
||||
"render.cpu_volume_enable_path_plan": "Śledzenie promieni na CPU · głębokość ścieżki {depth} · próbki na piksel: {spp} · ziarno {seed} · śledzone cienie. Zachowuje przygotowaną atmosferę i budżet obrazu.",
|
||||
@@ -3570,40 +3424,6 @@
|
||||
"render.cpu_volume_method_incompatible": "Tych ustawień świateł, próbkowania lub atmosfery nie można przenieść do tej metody. Twoje ustawienia zostają zachowane.",
|
||||
"render.cpu_volume_path_required": "Włącz oświetlenie pośrednie, aby użyć atmosfery CPU.",
|
||||
"render.cpu_warm_cool": "Ciepłe i chłodne",
|
||||
"render.denoise_class_background": "Pominięte: tło",
|
||||
"render.denoise_class_coverage": "Pominięte: częściowe pokrycie",
|
||||
"render.denoise_class_degenerate": "Pominięte: zdegenerowana ścieżka",
|
||||
"render.denoise_class_depth": "Pominięte: limit głębokości ścieżki",
|
||||
"render.denoise_class_emitter": "Pominięte: źródło światła",
|
||||
"render.denoise_class_exit": "Pominięte: niezgodne wyjście",
|
||||
"render.denoise_class_identity": "Pominięte: limit identyfikatorów bufora pomocniczego",
|
||||
"render.denoise_class_lens": "Pominięte: głębia ostrości",
|
||||
"render.denoise_class_material": "Pominięte: nieobsługiwany materiał",
|
||||
"render.denoise_class_nested": "Pominięte: zagnieżdżony ośrodek",
|
||||
"render.denoise_class_optical": "Kwalifikujące się: powierzchnia po załamaniu",
|
||||
"render.denoise_class_reflection": "Pominięte: odbijający punkt końcowy",
|
||||
"render.denoise_class_surface": "Kwalifikujące się: powierzchnia",
|
||||
"render.denoise_class_tir": "Pominięte: całkowite wewnętrzne odbicie",
|
||||
"render.denoise_class_transmission": "Pominięte: dalsze przepuszczanie",
|
||||
"render.denoise_class_unavailable": "Pominięte: brak bufora pomocniczego",
|
||||
"render.denoise_class_volume": "Pominięte: rozpraszanie objętościowe",
|
||||
"render.denoise_diagnostics_counts": "Kwalifikujące się: {eligible} · Pominięte: {skipped}",
|
||||
"render.denoise_diagnostics_enable": "Zbieraj diagnostykę odszumiania",
|
||||
"render.denoise_diagnostics_grid": "Liczby pikseli przy rozmiarze wewnętrznym {width} × {height}.",
|
||||
"render.denoise_diagnostics_help": "Zbiera liczby kwalifikujących się i pominiętych pikseli, nawet gdy odszumianie jest wyłączone. Liczby dotyczą wewnętrznego rozmiaru renderowania; maska używa rozmiaru wyjściowego.",
|
||||
"render.denoise_diagnostics_title": "Diagnostyka odszumiania",
|
||||
"render.denoise_export_mask": "Eksportuj maskę PNG",
|
||||
"render.denoise_firefly": "Tłum pojedyncze jasne piksele",
|
||||
"render.denoise_format_mask": "Maska PNG",
|
||||
"render.denoise_mask_copy_help": "Wyeksportuj maskę odszumiania jako PNG albo wróć do wyrenderowanego obrazu, aby go skopiować.",
|
||||
"render.denoise_mask_unavailable": "Włącz diagnostykę odszumiania i wyrenderuj obraz, aby obejrzeć jego maskę.",
|
||||
"render.denoise_optical_filter": "Filtruj lustra i szkło",
|
||||
"render.denoise_optical_filter_help": "Filtruje kwalifikujące się światło odbite i przepuszczone. Wyłącz, aby odszumiać tylko nieprzezroczyste powierzchnie.",
|
||||
"render.denoise_passes": "Przebiegi filtra",
|
||||
"render.denoise_settings_help": "Więcej przebiegów wygładza większy obszar. Siła określa, jak mocno filtruje każdy przebieg. Nieobsługiwane ścieżki optyczne pozostają niefiltrowane.",
|
||||
"render.denoise_strength": "Siła odszumiania",
|
||||
"render.denoise_view_image": "Wyrenderowany obraz",
|
||||
"render.denoise_view_mask": "Maska odszumiania",
|
||||
"render.diag_bodies": "Widoczne bryły",
|
||||
"render.diag_job_details": "Uchwycone szczegóły zadania",
|
||||
"render.diag_last_check": "Ostatnia kontrola wstępna",
|
||||
@@ -3736,7 +3556,6 @@
|
||||
"render.error_pbr_opacity": "nieprzezroczysty PBR wymaga nieprzezroczystości 1",
|
||||
"render.error_physical_glass": "wybrany przepis nie jest fizycznym szkłem",
|
||||
"render.error_preview_device": "podgląd produktu wymaga zainstalowanego urządzenia z buforem głębi",
|
||||
"render.error_product_placement": "nieprawidłowe umiejscowienie produktu",
|
||||
"render.error_product_triangle": "nieprawidłowy trójkąt produktu",
|
||||
"render.error_provisional_identity": "nieprawidłowy tymczasowy identyfikator renderowania lub liczba próbek",
|
||||
"render.error_quality_bounds": "odwzorowanie jakości przekracza natywne granice",
|
||||
@@ -3810,7 +3629,6 @@
|
||||
"render.glass_model_help": "Chropowatość szkła rozprasza przepuszczane promienie. Dyspersja i barwa warstwy używają trzech pasm długości fal. Chropowate arkusze wymagają wyłączenia prostego przepuszczania cieni.",
|
||||
"render.glass_roughness": "Chropowatość szkła",
|
||||
"render.gpu_depth": "Maksymalna głębokość ścieżki",
|
||||
"render.gpu_job_shader_wait": "Przygotowanie shaderów może potrwać kilka minut",
|
||||
"render.gpu_options_help": "Próbki, głębokość, ziarno i ekspozycja są zapisywane z tymi ustawieniami. Zapisywana jest tylko aktywna metoda obrazu.",
|
||||
"render.gpu_path_crashed": "SolidCraft zamknął się podczas ostatniego przygotowywania na tej karcie graficznej, najpewniej w sterowniku grafiki, więc nie będzie tego robić ponownie sam. Wybierz zamiast tego śledzenie promieni na CPU albo spróbuj ponownie (aktualizacja sterownika też powoduje ponowną próbę).",
|
||||
"render.gpu_path_failed": "Śledzenie ścieżek na GPU jest niedostępne",
|
||||
@@ -3820,10 +3638,6 @@
|
||||
"render.gpu_path_retry": "Spróbuj ponownie",
|
||||
"render.gpu_seed": "Ziarno losowania",
|
||||
"render.gpu_unavailable": "Śledzenie ścieżek na GPU nie jest gotowe na tym urządzeniu.",
|
||||
"render.graphics_api_restart_vulkan": "Uruchom ponownie z Vulkanem",
|
||||
"render.graphics_api_restart_warning": "Zmiana API grafiki uruchamia ponownie SolidCraft i prosi o zapisanie niezapisanych projektów. Jeśli nowe API nie otworzy okna, przy następnym uruchomieniu przywracany jest poprzedni wybór.",
|
||||
"render.graphics_api_settings": "Ustawienia grafiki",
|
||||
"render.graphics_api_vulkan_required": "Śledzenie ścieżek na GPU wymaga w tej sesji Vulkana. Uruchom ponownie z Vulkanem albo dalej używaj metody obrazu na CPU.",
|
||||
"render.ground_apply": "Dopasuj i zapisz podłoże",
|
||||
"render.ground_gap": "Jawny odstęp",
|
||||
"render.ground_linear_rgb": "Rozproszony współczynnik odbicia · liniowe RGB",
|
||||
@@ -4163,8 +3977,6 @@
|
||||
"render.material_transient": "Tymczasowe nadpisania optyczne dla tej karty dokumentu. Nie są zapisywane w projekcie, ustawieniach renderowania ani bibliotece materiałów.",
|
||||
"render.material_v_up": "Oś V tekstury skierowana w obrazie w górę",
|
||||
"render.material_weight": "Waga lakieru (0–1)",
|
||||
"render.metal_energy_compensation": "Odzyskuj odbicie chropowatego metalu",
|
||||
"render.metal_energy_compensation_help": "Kompensuje światło tracone między mikrościankami chropowatego metalu. Renderowanie na CPU wymaga oświetlenia pośredniego.",
|
||||
"render.method_cpu": "Śledzenie promieni na CPU",
|
||||
"render.method_gpu": "Śledzenie ścieżek na GPU",
|
||||
"render.method_limit_raster": "Rasteryzacja używa stałego, nieprzezroczystego podglądu studyjnego. Aby użyć obsługiwanych ustawień optycznych, wybierz jawną metodę śledzenia promieni.",
|
||||
@@ -4293,8 +4105,6 @@
|
||||
"render.quality_denoise_on": "Redukcja szumu: wł.",
|
||||
"render.quality_draft": "Robocza",
|
||||
"render.quality_high": "Wysoka",
|
||||
"render.quality_legacy_samples": "Zapisana wartość: {samples} spp. Nowo wybierane wartości są ograniczone do {maximum} spp.",
|
||||
"render.quality_preset_limit": "To ustawienie wstępne przekracza limit wyboru 16384 spp. Wybierz liczbę próbek do 16384.",
|
||||
"render.quality_projection_help": "Ten przegląd zmienia tylko rozdzielczość, budżet próbek, głębokość ścieżki, ziarno i redukcję szumu. Pozostałe ustawienia zostają zachowane; budżet nie gwarantuje zbieżności.",
|
||||
"render.quality_review": "Przejrzyj budżet próbkowania",
|
||||
"render.quality_summary": "{width} × {height} · próbki: {samples} · głębokość {depth} · ziarno {seed} · {denoise}",
|
||||
@@ -4442,7 +4252,6 @@
|
||||
"render.show_shots_label": "Pokaż pasek ujęć",
|
||||
"render.size_4k": "4K",
|
||||
"render.size_4k_uhd": "4K UHD",
|
||||
"render.size_8k_uhd": "8K UHD",
|
||||
"render.size_custom": "Niestandardowy…",
|
||||
"render.size_full_hd": "Full HD",
|
||||
"render.size_hd": "HD",
|
||||
@@ -4605,11 +4414,6 @@
|
||||
"render.subsurface_enable": "Rozpraszanie wewnętrzne metodą błądzenia losowego",
|
||||
"render.subsurface_help": "Tylko zamknięty dielektryk. Rozpraszanie i absorpcja RGB to osobne współczynniki na mm. Ścieżki rozpraszają się wewnątrz rzeczywistej geometrii, a potem załamują lub odbijają na granicy. Wyłącz proste przepuszczanie cieni, odszumianie powierzchni i objętość zewnętrzną; użyj jednej warstwy Fresnela. To nie jest shader dyfuzji, siatka gęstości ani zmierzony materiał. Gęste ośrodki mogą wymagać więcej próbek i większej głębokości ścieżki.",
|
||||
"render.subsurface_rate": "Częstość zderzeń przy rozpraszaniu",
|
||||
"render.supersampling": "Supersampling",
|
||||
"render.supersampling_dimensions": "Renderuje {width} × {height} i zmniejsza do {output_width} × {output_height}. Używa czterokrotnie więcej pikseli.",
|
||||
"render.supersampling_help": "Renderuje w dwukrotnej szerokości i wysokości, a potem zmniejsza w świetle liniowym. Rozmiar wyjściowy pozostaje bez zmian.",
|
||||
"render.supersampling_off": "Wył.",
|
||||
"render.supersampling_x2": "2×",
|
||||
"render.surface_import.author": "Autor",
|
||||
"render.surface_import.busy": "Importowanie obrazu…",
|
||||
"render.surface_import.complete": "Wybierz obraz i wpisz jego autora, źródło i licencję.",
|
||||
@@ -4623,8 +4427,6 @@
|
||||
"render.surface_import.saved_cpu": "Zapisz mapy, aby używać ich przy renderowaniu na CPU.",
|
||||
"render.surface_import.source": "Źródło",
|
||||
"render.surface_import.title": "Importuj obraz",
|
||||
"render.texture_filtering": "Filtruj szczegóły tekstur",
|
||||
"render.texture_filtering_help": "Używa poziomów mipmap i filtrowania anizotropowego, aby zmniejszyć aliasing tekstur przy pomniejszeniu. Zużywa więcej pamięci; wyłączenie zachowuje poprzednie filtrowanie.",
|
||||
"render.tone_aces_fitted": "ACES (dopasowane)",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "Szkło GPU obsługuje certyfikowane rozłączne ośrodki. Nieograniczone zagnieżdżanie ośrodków i bezszumowy estymator przepuszczania nie są dostępne.",
|
||||
@@ -4825,8 +4627,6 @@
|
||||
"sketch.hint.spline3d": "Splajn 3D: klikaj punkty dopasowania w 3D; prawy przycisk lub Esc kończy",
|
||||
"sketch.hint.trim": "Przytnij: kliknij fragment krzywej do usunięcia lub przeciągnij przez fragmenty do usunięcia",
|
||||
"sketch.make_selection": "Zamień zaznaczenie na {linetype}",
|
||||
"sketch.move_released": "Przesunięto. Zwolnione wiązania z geometrią, która została na miejscu ({count}): {names}.",
|
||||
"sketch.move_released_one": "Przesunięto. Zwolniono {count} wiązanie z geometrią, która została na miejscu ({names}).",
|
||||
"sketch.open_ends": "Otwarte: {count}",
|
||||
"sketch.status_conflict": "wiązania w konflikcie",
|
||||
"sketch.status_fully_constrained": "w pełni związany",
|
||||
@@ -5061,7 +4861,6 @@
|
||||
"ui.agent_chat_allow_once": "Zezwól raz",
|
||||
"ui.agent_chat_allowed": "Zezwolono",
|
||||
"ui.agent_chat_answered": "Odpowiedź: {option}",
|
||||
"ui.agent_chat_answered_in": "Odpowiedź po {time}",
|
||||
"ui.agent_chat_approval": "Zezwolić agentowi na to działanie?",
|
||||
"ui.agent_chat_attach_selection": "Dołącz zaznaczenie",
|
||||
"ui.agent_chat_attach_selection_tip": "Wyślij z wiadomością to, co zaznaczono w modelu (ściany, krawędzie, operacje, bryły), aby agent wiedział, o co chodzi",
|
||||
@@ -5090,7 +4889,6 @@
|
||||
"ui.agent_chat_ctx_bodies": "Bryły",
|
||||
"ui.agent_chat_ctx_elements": "Elementy",
|
||||
"ui.agent_chat_ctx_features": "Operacje",
|
||||
"ui.agent_chat_ctx_gone": "Nie ma już w projekcie: zostało zmienione lub usunięte.",
|
||||
"ui.agent_chat_ctx_more": "+{n} więcej",
|
||||
"ui.agent_chat_ctx_other": "Inne",
|
||||
"ui.agent_chat_custom_heading": "Własny model",
|
||||
@@ -5104,7 +4902,6 @@
|
||||
"ui.agent_chat_dock_right": "Zadokuj po prawej",
|
||||
"ui.agent_chat_drop_hint": "Upuść, aby dołączyć do następnej wiadomości",
|
||||
"ui.agent_chat_edit": "Edytuj",
|
||||
"ui.agent_chat_ended_after": "Zakończono po {time}",
|
||||
"ui.agent_chat_exited": "Agent się zatrzymał.",
|
||||
"ui.agent_chat_failed_start": "Nie udało się uruchomić agenta: {error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}: plik CAD jest importowany do projektu, a nie odczytywany",
|
||||
@@ -5142,7 +4939,6 @@
|
||||
"ui.agent_chat_model_no_tools": "{model} nie może używać narzędzi SolidCraft, więc może tylko rozmawiać. Wybierz model obsługujący narzędzia.",
|
||||
"ui.agent_chat_model_rejected": "Agent nie przyjął modelu {model}: {error}",
|
||||
"ui.agent_chat_model_search": "Szukaj modeli…",
|
||||
"ui.agent_chat_model_search_or_id": "Szukaj lub wpisz dowolny identyfikator modelu…",
|
||||
"ui.agent_chat_model_tip": "Model używany przez agenta. Twój wybór jest zapamiętywany dla tego agenta.",
|
||||
"ui.agent_chat_models_failed": "Nie udało się pobrać listy modeli: {error}",
|
||||
"ui.agent_chat_models_heading": "Dostępne modele",
|
||||
@@ -5154,7 +4950,6 @@
|
||||
"ui.agent_chat_next_picture": "Następny obraz",
|
||||
"ui.agent_chat_no_cli": "Nie znaleziono solidcraft-cli obok aplikacji, więc agent nie ma dostępu do projektu.",
|
||||
"ui.agent_chat_no_control": "Punkt końcowy czatu z agentem się nie uruchomił, więc agent nie ma dostępu do projektu.",
|
||||
"ui.agent_chat_no_design_hint": "Agent działa bez narzędzi SolidCraft, więc nie może odczytać ani zmienić projektu: nie znaleziono solidcraft-cli obok aplikacji. Zainstaluj SolidCraft razem z solidcraft-cli (w systemie Windows: instalator MSI lub przenośny zip) albo ustaw w SOLIDCRAFT_CLI jego ścieżkę, a potem połącz ponownie.",
|
||||
"ui.agent_chat_no_images": "Ten agent nie przyjmuje obrazów",
|
||||
"ui.agent_chat_no_picture": "W schowku nie ma obrazu",
|
||||
"ui.agent_chat_no_tool_call": "Model nie użył narzędzi SolidCraft, więc projekt nie został zmieniony. Wybierz model obsługujący wywoływanie narzędzi; zobacz Modele lokalne w przewodniku czatu z agentem.",
|
||||
@@ -5202,25 +4997,16 @@
|
||||
"ui.agent_chat_side": "Strona czatu z agentem",
|
||||
"ui.agent_chat_signed_in": "zalogowano",
|
||||
"ui.agent_chat_snap_none_found": "Nie znaleziono agenta programistycznego. Pakiet Snap działa w izolacji, więc nie widzi agentów programistycznych zainstalowanych na tym komputerze i nie może ich uruchomić. Aby z nich korzystać, zainstaluj SolidCraft z pakietu AppImage, .deb lub .rpm. Agent korzystający z API zgodnego z OpenAI działa także w pakiecie Snap: skonfiguruj go przyciskiem Dodaj agenta.",
|
||||
"ui.agent_chat_span_h": "{h} h {min} min",
|
||||
"ui.agent_chat_span_min": "{n} min",
|
||||
"ui.agent_chat_span_s": "{n} s",
|
||||
"ui.agent_chat_state_auth": "Wymagane logowanie",
|
||||
"ui.agent_chat_state_busy": "Pracuje…",
|
||||
"ui.agent_chat_state_exited": "Agent zatrzymany",
|
||||
"ui.agent_chat_state_failed": "Połączenie nieudane",
|
||||
"ui.agent_chat_state_no_design": "Brak dostępu do projektu",
|
||||
"ui.agent_chat_state_off": "Brak połączenia",
|
||||
"ui.agent_chat_state_ready": "Gotowy",
|
||||
"ui.agent_chat_state_starting": "Uruchamianie…",
|
||||
"ui.agent_chat_stop": "Zatrzymaj",
|
||||
"ui.agent_chat_stop_tip": "Zatrzymaj agenta i wszystko, co uruchomił",
|
||||
"ui.agent_chat_stop_turn_tip": "Zatrzymaj agenta (Esc). Wiadomości w kolejce pozostają w kolejce.",
|
||||
"ui.agent_chat_thinking_for": "Myśli od {time}",
|
||||
"ui.agent_chat_tool_running_for": "działa od {time}",
|
||||
"ui.agent_chat_tools_not_started": "Agent działa bez narzędzi SolidCraft, więc nie może odczytać ani zmienić projektu: znaleziono solidcraft-cli, ale jego narzędzia się nie uruchomiły (przyczynę podaje błąd w czacie). Połącz ponownie; jeśli to się powtarza, zainstaluj ponownie SolidCraft.",
|
||||
"ui.agent_chat_transcript_assistant": "Asystent:",
|
||||
"ui.agent_chat_transcript_user": "Użytkownik:",
|
||||
"ui.agent_chat_turn_limit": "Zmiany na wiadomość",
|
||||
"ui.agent_chat_turn_unlimited": "Bez limitu",
|
||||
"ui.agent_chat_undo_changes": "Cofnij te zmiany",
|
||||
@@ -5428,7 +5214,7 @@
|
||||
"ui.comb_length_tip": "Długość zębów: wielokrotność długości automatycznej, przy której najdłuższy ząb ma jedną piątą rozmiaru pokazanych krzywych.",
|
||||
"ui.comb_teeth": "Zęby",
|
||||
"ui.comb_teeth_tip": "Liczba zębów każdego grzebienia, rozmieszczonych równo wzdłuż krzywej. Więcej zębów pokazuje dokładniej, jak zmienia się krzywizna.",
|
||||
"ui.combine": "Operacja logiczna",
|
||||
"ui.combine": "Kombinacja",
|
||||
"ui.coming_later": "wkrótce",
|
||||
"ui.command_done": "Wykonano: {command}",
|
||||
"ui.command_palette_hint": "Wpisz polecenie, np. {example} – lub naciśnij S, aby wyszukać",
|
||||
@@ -5439,24 +5225,14 @@
|
||||
"ui.conic_curve": "Krzywa stożkowa",
|
||||
"ui.constant": "Stały",
|
||||
"ui.constraints_conflict": "Konflikt wiązań",
|
||||
"ui.construct_auto.chosen": "Wybrano na podstawie wskazanych elementów. Wskaż inne odniesienie lub usuń jedno, aby zmienić metodę.",
|
||||
"ui.construct_auto.no_fit": "Wskaż inne odniesienia: z tych nie da się zbudować elementu żadną metodą.",
|
||||
"ui.construct_auto.pick": "Wskaż, z czego budować: metoda wynika z wybranych elementów.",
|
||||
"ui.construct_auto.refs": "Odniesienia",
|
||||
"ui.construct_auto.select": "Najpierw zaznacz odniesienia, z których budować",
|
||||
"ui.construct_axes": "Osie",
|
||||
"ui.construct_planes": "Płaszczyzny",
|
||||
"ui.construct_points": "Punkty",
|
||||
"ui.construction": "Konstrukcyjne",
|
||||
"ui.construction.angle_hint": "Linia konstrukcyjna: kliknij punkt, a potem wpisz kąt lub wskaż kierunek",
|
||||
"ui.construction.axis_hint": "Linia konstrukcyjna: kliknij punkt lub wpisz jego odległość od osi szkicu",
|
||||
"ui.construction.bisector_hint": "Dwusieczna: wybierz dwie przecinające się proste linie, a potem żądane rozwiązanie",
|
||||
"ui.construction.common_hint": "Wspólna styczna: wybierz dwa okręgi/łuki, a potem żądane rozwiązanie",
|
||||
"ui.construction.contact_hint": "Styczna: wybierz okrąg/łuk lub otwarty splajn/krzywą stożkową, a potem punkt styczności (krzywe dowolne: punkty końcowe)",
|
||||
"ui.construction.external_hint": "Styczna: wybierz okrąg lub łuk, punkt zewnętrzny, a potem żądane rozwiązanie",
|
||||
"ui.construction.free_hint": "Linia konstrukcyjna: kliknij dwa punkty; prawy przycisk zaczyna od nowa",
|
||||
"ui.construction.normal_needs_length": "Linia nieskończona jest zawsze konstrukcyjna: wpisz długość, aby narysować zwykłą linię",
|
||||
"ui.construction.parallel_hint": "Linia konstrukcyjna: wybierz prostą linię, a potem kliknij lub wpisz odsunięcie; prawy przycisk zmienia odniesienie",
|
||||
"ui.construction.point_hint": "Linia konstrukcyjna: kliknij punkt, przez który ma przechodzić",
|
||||
"ui.construction.reference_hint": "Linia konstrukcyjna: wybierz prostą linię, a potem punkt; prawy przycisk zmienia odniesienie",
|
||||
"ui.construction.ribbon_label": "Linie pom.",
|
||||
@@ -5599,17 +5375,6 @@
|
||||
"ui.ellipse": "Elipsa",
|
||||
"ui.emboss_wrap_note": "Ortogonalne profile liniowe styczne do jednego obsługiwanego walca owijają się automatycznie. Wybierz ściankę, aby wskazać cel.",
|
||||
"ui.end": "Koniec",
|
||||
"ui.end_cap.chamfer": "Faza naroży",
|
||||
"ui.end_cap.direction": "Kierunek",
|
||||
"ui.end_cap.faces": "Ściany końcowe",
|
||||
"ui.end_cap.inset": "Wpuszczenie",
|
||||
"ui.end_cap.inset_kind": "Wpuszczenie według",
|
||||
"ui.end_cap.inset_ratio": "Współczynnik wpuszczenia",
|
||||
"ui.end_cap.inward": "Do wewnątrz",
|
||||
"ui.end_cap.length": "Długość",
|
||||
"ui.end_cap.outward": "Na zewnątrz",
|
||||
"ui.end_cap.ratio": "Stosunek do grubości",
|
||||
"ui.end_cap_title": "ZAŚLEPKA",
|
||||
"ui.end_radius": "Promień końcowy",
|
||||
"ui.end_scale": "Skala końcowa",
|
||||
"ui.ends": "Końce",
|
||||
@@ -5630,10 +5395,7 @@
|
||||
"ui.explode_needs_two_components": "Widok rozstrzelony odsuwa od siebie komponenty, więc projekt potrzebuje co najmniej dwóch (Nowy komponent, Wstaw projekt lub Utwórz komponenty z brył)",
|
||||
"ui.exportDxf": "Eksportuj DXF…",
|
||||
"ui.export_ellipsis_keymap": "Eksportuj…",
|
||||
"ui.export_glb": "Eksportuj GLB",
|
||||
"ui.export_keymap_tip": "Klawisze, ulubione z okna S i pasek narzędzi, jako plik",
|
||||
"ui.export_selects_only_body": "Zaznaczaj jedyną bryłę przy eksporcie siatki",
|
||||
"ui.export_selects_only_body_tip": "Eksportuj STL, Eksportuj 3MF i Wyślij do slicera zaczynają z zaznaczoną bryłą projektu, gdy jest ona jedyna.",
|
||||
"ui.extend": "Wydłuż",
|
||||
"ui.extent": "Zakres",
|
||||
"ui.extent_type": "Typ zakresu",
|
||||
@@ -5693,8 +5455,6 @@
|
||||
"ui.finest": "Najdokładniejsza",
|
||||
"ui.finish_sketch": "Zakończ szkic",
|
||||
"ui.first_dimension_scales": "Pierwszy wymiar skaluje rysunek",
|
||||
"ui.first_dimension_scales_about_origin": "Skaluj względem początku układu szkicu",
|
||||
"ui.first_dimension_scales_about_origin_tip": "Włączone: rysunek zawsze rośnie lub maleje względem początku układu szkicu, więc kształt narysowany z dala od niego się przesuwa. Wyłączone: rysunek, którego nic nie wiąże z początkiem układu, skaluje się względem własnego środka i zostaje tam, gdzie go narysowano.",
|
||||
"ui.first_dimension_scales_tip": "Włączone: pierwszy wymiar długości w szkicu skaluje mierzony rysunek, więc jego kształt pozostaje ten sam. Wyłączone: wymiar zmienia tylko to, co mierzy.",
|
||||
"ui.fit": "Dopasuj",
|
||||
"ui.fix_unfix": "Ustal/zwolnij",
|
||||
@@ -5725,7 +5485,7 @@
|
||||
"ui.heading_chamfer": "FAZA",
|
||||
"ui.heading_circular_pattern": "SZYK KOŁOWY",
|
||||
"ui.heading_coil": "SPIRALA",
|
||||
"ui.heading_combine": "OPERACJA LOGICZNA",
|
||||
"ui.heading_combine": "KOMBINACJA",
|
||||
"ui.heading_cone": "STOŻEK",
|
||||
"ui.heading_configure": "KONFIGURACJA",
|
||||
"ui.heading_constraints": "WIĄZANIA",
|
||||
@@ -5749,7 +5509,7 @@
|
||||
"ui.heading_home": "WIDOK GŁÓWNY",
|
||||
"ui.heading_insert": "WSTAW",
|
||||
"ui.heading_inspect": "SPRAWDŹ",
|
||||
"ui.heading_interference": "KOLIZJA",
|
||||
"ui.heading_interference": "INTERFERENCJA",
|
||||
"ui.heading_loft": "WYCIĄGNIĘCIE ZŁOŻONE",
|
||||
"ui.heading_make": "WYTWARZANIE",
|
||||
"ui.heading_measure": "POMIAR",
|
||||
@@ -5769,7 +5529,6 @@
|
||||
"ui.heading_plastic": "TWORZYWA",
|
||||
"ui.heading_position": "POŁOŻENIE",
|
||||
"ui.heading_prepare": "PRZYGOTOWANIE",
|
||||
"ui.heading_press_pull": "WCIŚNIJ/WYCIĄGNIJ",
|
||||
"ui.heading_rectangular_pattern": "SZYK PROSTOKĄTNY",
|
||||
"ui.heading_revolve": "OBRÓT",
|
||||
"ui.heading_right": "PRAWY",
|
||||
@@ -5792,7 +5551,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "NARZĘDZIA",
|
||||
"ui.heading_utility": "NARZĘDZIE",
|
||||
"ui.heading_weldments": "KONSTRUKCJE SPAWANE",
|
||||
"ui.heading_weldments": "SPAWANIE",
|
||||
"ui.height": "Wysokość",
|
||||
"ui.help": "Pomoc",
|
||||
"ui.hide": "Ukryj",
|
||||
@@ -5846,7 +5605,7 @@
|
||||
"ui.insert_design.unsaved": "Niezapisane zmiany",
|
||||
"ui.inspect": "Sprawdź",
|
||||
"ui.instances": "Wystąpienia",
|
||||
"ui.intersect": "Przecięcie",
|
||||
"ui.intersect": "Przytnij",
|
||||
"ui.intersection_curve_second_pick": "Krzywa przecięcia: kliknij drugą ścianę lub krzywą (prawy przycisk zaczyna od nowa)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "Izoluj",
|
||||
@@ -5906,8 +5665,6 @@
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF ze znacznikami ścian",
|
||||
"ui.mesh_format_stl": "STL",
|
||||
"ui.mesh_import_origin": "Przenieś do początku układu",
|
||||
"ui.mesh_import_origin_tip": "Wyśrodkowuje siatkę w X i Y, z podstawą na Z = 0. Gdy wyłączone, siatka zachowuje współrzędne zapisane w pliku.",
|
||||
"ui.minor_radius": "Promień mniejszy",
|
||||
"ui.mirror": "Lustro",
|
||||
"ui.mirror_line": "Linia odbicia",
|
||||
@@ -6012,42 +5769,6 @@
|
||||
"ui.panel_host.surface": "Panel",
|
||||
"ui.panel_host.tab": "Karta panelu",
|
||||
"ui.panel_host.tabs": "Wszystkie karty",
|
||||
"ui.panel_layout.applied": "Zastosowano {name} w środowisku {workspace}",
|
||||
"ui.panel_layout.cancel": "Anuluj",
|
||||
"ui.panel_layout.confirm_replace": "Zastąpić „{name}”?",
|
||||
"ui.panel_layout.copy": "Kopiuj",
|
||||
"ui.panel_layout.current": "Bieżący układ",
|
||||
"ui.panel_layout.custom": "Własny",
|
||||
"ui.panel_layout.custom_hint": "Panele przesunięto względem układu „{name}”.",
|
||||
"ui.panel_layout.details": "Szczegóły",
|
||||
"ui.panel_layout.layout_workspace": "Środowisko układu",
|
||||
"ui.panel_layout.page_defaults_hint": "Resetuje ustawienia tej strony. Rozmieszczenie paneli i zapisane układy zostają zachowane.",
|
||||
"ui.panel_layout.panel_layout": "Układ paneli",
|
||||
"ui.panel_layout.panel_style": "Styl paneli",
|
||||
"ui.panel_layout.replace": "Zastąp układ",
|
||||
"ui.panel_layout.replace_current": "Zastąp bieżącym",
|
||||
"ui.panel_layout.replace_short": "Zastąp",
|
||||
"ui.panel_layout.reset": "Resetuj środowisko",
|
||||
"ui.panel_layout.reset_hint": "Przywraca domyślne rozmieszczenie paneli w środowisku {workspace}. Zapisane układy zostają zachowane.",
|
||||
"ui.panel_layout.reset_short": "Resetuj",
|
||||
"ui.panel_layout.restore_failed": "Nie udało się przywrócić układu",
|
||||
"ui.panel_layout.save": "Zapisz układ",
|
||||
"ui.panel_layout.save_as": "Zapisz jako",
|
||||
"ui.panel_layout.save_as_tile": "Zapisz jako…",
|
||||
"ui.panel_layout.selected": "zaznaczony",
|
||||
"ui.panel_layout.suggested_name": "Układ {n}",
|
||||
"ui.panel_layout.try_again": "Spróbuj ponownie",
|
||||
"ui.panel_layout.validation.builtin": "Wbudowanych układów nie można zmieniać",
|
||||
"ui.panel_layout.validation.duplicate": "Ta nazwa jest już używana",
|
||||
"ui.panel_layout.validation.empty": "Wpisz nazwę",
|
||||
"ui.panel_layout.validation.full": "Zapisano 64 z 64. Wybierz jeden do zastąpienia.",
|
||||
"ui.panel_layout.validation.invalid": "Użyj 1–64 znaków, bez znaków sterujących",
|
||||
"ui.panel_layout.workspace.classic": "Klasyczny",
|
||||
"ui.panel_layout.workspace.modelling": "Modelowanie",
|
||||
"ui.panel_layout.workspace.render": "Renderowanie",
|
||||
"ui.panel_layout.workspace.review": "Przegląd",
|
||||
"ui.panel_layout.workspace.sketch": "Szkic",
|
||||
"ui.panel_layout.your_layout": "twój układ",
|
||||
"ui.parallel": "Równoległość",
|
||||
"ui.parameters": "Parametry",
|
||||
"ui.part_size_needs_a_part": "Zaznacz wstawioną część znormalizowaną (śrubę, nakrętkę, podkładkę lub kołek), aby zmienić jej rozmiar. Aby powiększyć lub pomniejszyć bryłę, użyj polecenia Skala.",
|
||||
@@ -6113,11 +5834,8 @@
|
||||
"ui.profiles": "Profile",
|
||||
"ui.project": "Rzutuj",
|
||||
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
|
||||
"ui.project_references_construction": "Rzutuj odniesienia jako geometrię konstrukcyjną",
|
||||
"ui.project_references_construction_note": "Rzutuj, Przecięcie, Dołącz geometrię 3D oraz krawędzie ściany nowego szkicu na ścianie tworzą geometrię konstrukcyjną: odniesienia do rysowania, pomijane w profilach. Pozostają połączone z modelem.",
|
||||
"ui.properties": "Właściwości",
|
||||
"ui.properties.copy_matrix": "Kopiuj macierz",
|
||||
"ui.properties.copy_value": "Kliknij, aby skopiować",
|
||||
"ui.properties.first_moments": "Momenty statyczne",
|
||||
"ui.properties.inertia": "Moment bezwładności",
|
||||
"ui.properties.inertia_note": "Względem środka masy, w osiach globalnych (g·{unit}²)",
|
||||
@@ -6130,7 +5848,6 @@
|
||||
"ui.properties.measuring_failed": "Pomiar zatrzymał się przed zakończeniem.",
|
||||
"ui.properties.principal_axis": "Oś główna {number}",
|
||||
"ui.properties.principal_moments": "Główne momenty bezwładności",
|
||||
"ui.properties.value_copied": "Skopiowano wartość",
|
||||
"ui.pull_direction": "Kierunek wyjmowania",
|
||||
"ui.quality": "Jakość",
|
||||
"ui.quantity": "Liczba",
|
||||
@@ -6151,10 +5868,8 @@
|
||||
"ui.relink_design_changed": "Złożenie zmieniło się podczas wyboru źródła. Uruchom ponownie polecenie Połącz ponownie ze źródłem.",
|
||||
"ui.relink_done": "Ponownie połączono źródło dla wszystkich wystąpień.",
|
||||
"ui.relink_not_linked": "Ten komponent nie jest połączony z projektem źródłowym.",
|
||||
"ui.relink_one_done": "Ponownie połączono źródło dla tego wystąpienia.",
|
||||
"ui.relink_select_one": "Najpierw zaznacz jeden połączony komponent lub jedno z jego wystąpień.",
|
||||
"ui.relink_source_all": "Połącz ponownie ze źródłem (wszystkie wystąpienia)…",
|
||||
"ui.relink_source_one": "Połącz ponownie ze źródłem…",
|
||||
"ui.removeFromQuickAccess": "Usuń z szybkiego dostępu",
|
||||
"ui.removeFromToolbar": "Usuń z paska narzędzi",
|
||||
"ui.removeSection": "Usuń przekrój",
|
||||
@@ -6163,8 +5878,6 @@
|
||||
"ui.renderer": "Renderer",
|
||||
"ui.repeat": "Powtórz",
|
||||
"ui.repeat_nothing": "Nie ma jeszcze czego powtórzyć: najpierw uruchom narzędzie lub okno.",
|
||||
"ui.replace_sketch.from_file": "Z pliku projektu…",
|
||||
"ui.replace_sketch.no_sketches": "Ten projekt nie ma szkicu do pobrania",
|
||||
"ui.replace_with_link_done": "Kopie są teraz umiejscowieniami połączonego projektu.",
|
||||
"ui.report_a_bug": "Zgłoś błąd",
|
||||
"ui.report_characters_left_one": "Pozostało znaków: {count}",
|
||||
@@ -6250,16 +5963,6 @@
|
||||
"ui.sketch_intersect_selection_filter": "Filtr zaznaczania",
|
||||
"ui.sketch_keys_only": "Nie używaj klawiszy modelu w szkicach",
|
||||
"ui.sketch_keys_only_tip": "Podczas edycji szkicu działają tylko klawisze szkicu i klawisze działające wszędzie; klawisz modelu, np. E lub F, nic nie robi, więc przypadkowe naciśnięcie nie zamknie rozbudowanego szkicu.",
|
||||
"ui.sketch_links_added_curve": "Polecenie {command} dodało do szkicu 1 krzywą.",
|
||||
"ui.sketch_links_added_curves": "Polecenie {command} dodało krzywe do szkicu ({count}).",
|
||||
"ui.sketch_links_added_curves_and_points": "Polecenie {command} dodało do szkicu: {curves} i {points}.",
|
||||
"ui.sketch_links_added_point": "Polecenie {command} dodało do szkicu 1 punkt.",
|
||||
"ui.sketch_links_added_points": "Polecenie {command} dodało punkty do szkicu ({count}).",
|
||||
"ui.sketch_links_curve_count": "krzywe ({count})",
|
||||
"ui.sketch_links_curve_count_one": "1 krzywa",
|
||||
"ui.sketch_links_hidden": "Wynik jest ukryty, bo opcja {option} jest wyłączona w palecie szkicu.",
|
||||
"ui.sketch_links_point_count": "punkty ({count})",
|
||||
"ui.sketch_links_point_count_one": "1 punkt",
|
||||
"ui.sketch_modifiers": "Modyfikatory szkicu",
|
||||
"ui.sketch_project_geometry": "Geometria",
|
||||
"ui.sketch_report.all_constrained": "Wszystko jest w pełni związane.",
|
||||
@@ -6299,7 +6002,6 @@
|
||||
"ui.step_values": "Zmieniaj wartości klawiszami strzałek i kółkiem",
|
||||
"ui.step_values_tip": "W polu wartości, w którym piszesz, strzałki w górę i w dół zmieniają zwykłą liczbę o 1 (Shift: 10, Alt: 0,1), podobnie kółko myszy nad polem. Wyrażenia pozostają tak, jak je wpisano.",
|
||||
"ui.stitch_tolerance_hint": "ciasna",
|
||||
"ui.straighten": "Wyprostuj",
|
||||
"ui.stretch": "Rozciągnij",
|
||||
"ui.supersampling": "Supersampling (SSAA)",
|
||||
"ui.supersampling_adaptive": "Tylko gdy widok jest nieruchomy",
|
||||
@@ -6373,8 +6075,6 @@
|
||||
"ui.view_angles_yaw": "Odchylenie",
|
||||
"ui.view_angles_zero": "Ustaw {axis} na 0°",
|
||||
"ui.view_centre": "Środek widoku",
|
||||
"ui.view_compass_tip": "Widok od strony {axis}. Kliknij ponownie, aby odwrócić. Przeciągnij, aby obracać widok.",
|
||||
"ui.view_compass_tip_snap": "Widok od strony {axis}. Kliknij ponownie, aby odwrócić. Przeciągnij, aby obracać widok; przytrzymaj Shift, aby przyciągać.",
|
||||
"ui.viewport_status_clear": "Wyłącz {name}",
|
||||
"ui.viewport_status_clear_hint": "Wyłącz {name} · Esc",
|
||||
"ui.viewport_status_edges": "Krawędzie: {count}",
|
||||
|
||||
@@ -19,14 +19,9 @@ solidcraft-kernel = { workspace = true }
|
||||
solidcraft-profile = { workspace = true }
|
||||
solidcraft-render = { workspace = true }
|
||||
solidcraft-derive = { workspace = true }
|
||||
solidcraft-format = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
web-time = { workspace = true }
|
||||
|
||||
# Lists the format change files (build.rs).
|
||||
[build-dependencies]
|
||||
solidcraft-format = { workspace = true }
|
||||
|
||||
+72
-8
@@ -1,12 +1,18 @@
|
||||
//! Lists the format changes of each family of files versioned by `src/format.rs`: designs in
|
||||
//! `src/format/`, drawings in `src/drawing/format/` (#3890). One file per change, in `v<N>/` once a
|
||||
//! release fixed its number, else in `pending/` (see `src/format/pending/README.md`). The lists are
|
||||
//! generated (`solidcraft_format::build`) so that a change adds a file and never edits a line
|
||||
//! another change also edits.
|
||||
//! generated so that a change adds a file and never edits a line another change also edits.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::fmt::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use solidcraft_format::build::Family;
|
||||
/// A family's change files: their folder, the last format before them, and the prefix of the
|
||||
/// family's lists in `format.rs`.
|
||||
struct Family {
|
||||
folder: &'static str,
|
||||
base: u32,
|
||||
list_prefix: &'static str,
|
||||
}
|
||||
|
||||
/// Every family, each read by `scripts/fold-format-pending.sh` too. A design's upgrade steps are
|
||||
/// written out in `format.rs` up to 22 (`JSON_STEPS`); a drawing's first format is 1.
|
||||
@@ -15,10 +21,68 @@ const FAMILIES: [Family; 2] =
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let lists = solidcraft_format::build::lists(&manifest, &FAMILIES).unwrap_or_else(|e| panic!("{e}"));
|
||||
for folder in &lists.watch {
|
||||
println!("cargo:rerun-if-changed={}", folder.display());
|
||||
let mut out = String::new();
|
||||
let mut mods = String::new();
|
||||
for family in &FAMILIES {
|
||||
lists(family, &manifest, &mut out, &mut mods);
|
||||
}
|
||||
out.push_str(&mods);
|
||||
let dest = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("format_parts.rs");
|
||||
std::fs::write(dest, lists.code).unwrap();
|
||||
std::fs::write(dest, out).unwrap();
|
||||
}
|
||||
|
||||
/// A family's `FOLDED` and `PENDING` lists; its folded formats follow on from its base.
|
||||
fn lists(family: &Family, manifest: &Path, out: &mut String, mods: &mut String) {
|
||||
let Family { folder, base, list_prefix } = family;
|
||||
let root = manifest.join(folder);
|
||||
println!("cargo:rerun-if-changed={}", root.display());
|
||||
let mut folded: Vec<(u32, PathBuf)> = std::fs::read_dir(&root)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", root.display()))
|
||||
.filter_map(|entry| {
|
||||
let path = entry.ok()?.path();
|
||||
let version = path.file_name()?.to_str()?.strip_prefix('v')?.parse().ok()?;
|
||||
path.is_dir().then_some((version, path))
|
||||
})
|
||||
.collect();
|
||||
folded.sort();
|
||||
for (i, (version, _)) in folded.iter().enumerate() {
|
||||
let expected = base + 1 + i as u32;
|
||||
assert_eq!(*version, expected, "{folder}/: expected v{expected}/ next, found v{version}/ (versions are contiguous)");
|
||||
}
|
||||
// Module names start with the family's (`drawing_pending_…`), so two families' changes of one
|
||||
// name never clash.
|
||||
let module = list_prefix.to_ascii_lowercase();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"/// The changes of each format after the base, oldest first (`{folder}/v<N>/`).\nconst {list_prefix}FOLDED: &[(u32, &[Part])] = &["
|
||||
);
|
||||
for (version, dir) in &folded {
|
||||
let _ = writeln!(out, " ({version}, &[");
|
||||
parts(dir, &format!("{module}v{version}"), out, mods);
|
||||
out.push_str(" ]),\n");
|
||||
}
|
||||
let _ = writeln!(out, "];\n\n/// The changes no release has a number for yet (`{folder}/pending/`).\nconst {list_prefix}PENDING: &[Part] = &[");
|
||||
parts(&root.join("pending"), &format!("{module}pending"), out, mods);
|
||||
out.push_str("];\n\n");
|
||||
}
|
||||
|
||||
/// The `Part`s of the change files in `dir`, in file-name order, and a module for each.
|
||||
fn parts(dir: &Path, prefix: &str, out: &mut String, mods: &mut String) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
let mut files: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).filter(|p| p.extension().is_some_and(|x| x == "rs")).collect();
|
||||
files.sort();
|
||||
for path in files {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default().to_string();
|
||||
assert!(
|
||||
!name.is_empty() && name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
|
||||
"{}: name a format change file with lowercase letters, digits and dashes",
|
||||
path.display()
|
||||
);
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(text.trim_start().starts_with("//!"), "{}: start a format change with a `//!` note", path.display());
|
||||
let module = format!("{prefix}_{}", name.replace('-', "_"));
|
||||
let step = if text.contains("fn step(") { format!("Some({module}::step)") } else { "None".into() };
|
||||
let _ = writeln!(out, " Part {{ name: {name:?}, step: {step} }},");
|
||||
let _ = writeln!(mods, "#[path = {:?}]\nmod {module};", path.display().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,7 +721,6 @@ mod tests {
|
||||
width: "5 mm".into(),
|
||||
height: "10 mm".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -866,7 +866,6 @@ pub enum FeatureKind {
|
||||
tangent_chain: bool,
|
||||
},
|
||||
Box {
|
||||
/// The box's lowest corner, or its centre when `centred` is set.
|
||||
#[serde(with = "point_expr")]
|
||||
#[walk(placement)]
|
||||
corner: PointExpr,
|
||||
@@ -878,10 +877,6 @@ pub enum FeatureKind {
|
||||
height: Expr,
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
/// Placed by its centre: `corner` is the centre, so a new length, width or height grows the box
|
||||
/// on both sides and it stays centred (#14487).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
centred: bool,
|
||||
},
|
||||
Cylinder {
|
||||
#[serde(with = "point_expr")]
|
||||
|
||||
@@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcraft_geom::drawing_sheet::{self, DrawingBorderProfile};
|
||||
|
||||
use crate::format::{CheckNumbers as _, DRAWING};
|
||||
use crate::format::DRAWING;
|
||||
use crate::ids::{self, IdSource};
|
||||
use crate::{DocError, Metadata, Result, expr};
|
||||
|
||||
|
||||
+5
-22
@@ -6081,12 +6081,6 @@ fn hole_references(f: &Feature) -> &[crate::HoleReference] {
|
||||
|
||||
/// Where a cylinder stands: its base, or with reference edges (#6380) at their distances in the
|
||||
/// plane of the flat face at its base, square to its axis.
|
||||
/// A box's lowest corner from its stored point and sides: the point itself, or the box's centre
|
||||
/// when it was placed by its centre (#14487).
|
||||
fn box_low_corner(at: Vec3, size: Vec3, centred: bool) -> Vec3 {
|
||||
if centred { at - size * 0.5 } else { at }
|
||||
}
|
||||
|
||||
fn cylinder_base(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, base: &crate::PointExpr, axis: Vec3) -> Result<Vec3> {
|
||||
let at = point(vals, base)?;
|
||||
with_references(vals, st, f, at, -axis, true)
|
||||
@@ -7442,9 +7436,9 @@ fn feature_tools_with_constructor(
|
||||
let plane = Plane::from_normal(start, t).ok_or_else(|| DocError::Invalid("pipe profile plane".into()))?;
|
||||
Ok(unnamed(vec![kernel::sweep(&plane, &pipe_region(vals, r, wall)?, &segs)?]))
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, centred, .. } => {
|
||||
FeatureKind::Box { corner, length, width, height, .. } => {
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let corner = box_low_corner(point(vals, corner)?, s, *centred);
|
||||
let corner = point(vals, corner)?;
|
||||
Ok(vec![crate::lineage::box_tool(kernel::box_solid(corner, corner + s)?).with_recipe(tool_recipe(doc, f, vals))])
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, .. } => Ok(vec![
|
||||
@@ -9993,9 +9987,9 @@ fn eval_feature(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, operation, centred } => {
|
||||
FeatureKind::Box { corner, length, width, height, operation } => {
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let corner = box_low_corner(point(vals, corner)?, s, *centred);
|
||||
let corner = point(vals, corner)?;
|
||||
let b = kernel::box_solid(corner, corner + s)?;
|
||||
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_box_certificate(f.id)? } else { b };
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation, false, warning)?;
|
||||
@@ -11195,7 +11189,6 @@ mod world_placement_cache_tests {
|
||||
width: "3 mm".into(),
|
||||
height: "4 mm".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
Some("AuthoredBox"),
|
||||
)
|
||||
@@ -11232,14 +11225,7 @@ mod world_placement_cache_tests {
|
||||
let mut boxes = |comp: u64, operation: Operation, corner: Vec3| {
|
||||
let id = doc
|
||||
.add_feature(
|
||||
FeatureKind::Box {
|
||||
corner: crate::point_expr(corner),
|
||||
length: "10".into(),
|
||||
width: "10".into(),
|
||||
height: "2".into(),
|
||||
operation,
|
||||
centred: false,
|
||||
},
|
||||
FeatureKind::Box { corner: crate::point_expr(corner), length: "10".into(), width: "10".into(), height: "2".into(), operation },
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -11389,7 +11375,6 @@ mod chain_tests {
|
||||
width: "30".into(),
|
||||
height: "20".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -11436,7 +11421,6 @@ mod chain_tests {
|
||||
width: w.into(),
|
||||
height: h.into(),
|
||||
operation,
|
||||
centred: false,
|
||||
};
|
||||
doc.add_feature(block(Vec3::new(0.0, 0.0, 20.0), ["40", "20", "20"], Operation::NewBody), None).unwrap();
|
||||
doc.add_feature(block(Vec3::new(20.0, 0.0, 0.0), ["20", "20", "20"], Operation::Join), None).unwrap();
|
||||
@@ -11483,7 +11467,6 @@ mod chain_tests {
|
||||
width: "20".into(),
|
||||
height: "20".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
};
|
||||
doc.add_feature(solid, None).unwrap();
|
||||
doc.add_feature(fillet(Vec3::new(20.0, 10.0, 20.0), "2", true), None).unwrap();
|
||||
|
||||
@@ -574,7 +574,6 @@ fn plate_with_holes(doc: &mut Document, comp: u64, tools: u64, holes: usize) ->
|
||||
width: "10".into(),
|
||||
height: "4".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
};
|
||||
let plate = doc.add_feature(kind, None).unwrap();
|
||||
doc.feature_mut(plate).unwrap().component = comp;
|
||||
@@ -583,7 +582,7 @@ fn plate_with_holes(doc: &mut Document, comp: u64, tools: u64, holes: usize) ->
|
||||
let body = model.state().bodies.iter().find(|b| b.feature == plate).unwrap().name.clone();
|
||||
for k in 0..holes {
|
||||
let corner = point_expr(Vec3::new(10.0 * k as f64 + 4.0, 4.0, -1.0));
|
||||
let kind = FeatureKind::Box { corner, length: "2".into(), width: "2".into(), height: "6".into(), operation: Operation::Cut, centred: false };
|
||||
let kind = FeatureKind::Box { corner, length: "2".into(), width: "2".into(), height: "6".into(), operation: Operation::Cut };
|
||||
let id = doc.add_feature(kind, None).unwrap();
|
||||
let f = doc.feature_mut(id).unwrap();
|
||||
f.component = tools;
|
||||
@@ -843,7 +842,6 @@ fn shared_evaluation_requires_its_actual_model_and_unmodified_geometry_inputs()
|
||||
width: "3".into(),
|
||||
height: "4".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -989,8 +987,7 @@ fn saved_states_read_back_their_sketch_keys() {
|
||||
doc.add_feature(line_sketch(PlaneRef::Origin { name: "XY".into() }, Vec2::new(f64::from(i) + 1.0, 1.0)), None).unwrap();
|
||||
for j in 0..3 {
|
||||
let corner = point_expr(Vec3::new(f64::from(i) * 20.0, f64::from(j) * 20.0, 0.0));
|
||||
let kind =
|
||||
FeatureKind::Box { corner, length: "5".into(), width: "5".into(), height: "5".into(), operation: Operation::NewBody, centred: false };
|
||||
let kind = FeatureKind::Box { corner, length: "5".into(), width: "5".into(), height: "5".into(), operation: Operation::NewBody };
|
||||
doc.add_feature(kind, None).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1076,14 +1073,13 @@ fn blocks_with_cuts(doc: &mut Document, comp: u64, blocks: usize, cuts: usize) {
|
||||
let y = 100.0 * comp as f64;
|
||||
for k in 0..blocks {
|
||||
let corner = point_expr(Vec3::new(20.0 * k as f64, y, 0.0));
|
||||
let kind =
|
||||
FeatureKind::Box { corner, length: "10".into(), width: "10".into(), height: "4".into(), operation: Operation::NewBody, centred: false };
|
||||
let kind = FeatureKind::Box { corner, length: "10".into(), width: "10".into(), height: "4".into(), operation: Operation::NewBody };
|
||||
let id = doc.add_feature(kind, None).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = comp;
|
||||
}
|
||||
for k in 0..cuts {
|
||||
let corner = point_expr(Vec3::new(20.0 * k as f64 + 4.0, y + 4.0, -1.0));
|
||||
let kind = FeatureKind::Box { corner, length: "2".into(), width: "2".into(), height: "6".into(), operation: Operation::Cut, centred: false };
|
||||
let kind = FeatureKind::Box { corner, length: "2".into(), width: "2".into(), height: "6".into(), operation: Operation::Cut };
|
||||
let id = doc.add_feature(kind, None).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = comp;
|
||||
}
|
||||
@@ -1285,7 +1281,6 @@ fn block(doc: &mut Document, name: &str, corner: Vec3, size: [f64; 3]) {
|
||||
width: size[1].to_string().into(),
|
||||
height: size[2].to_string().into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1349,7 +1344,6 @@ fn a_cut_still_runs_where_its_tool_touches_grazes_or_meets_a_curved_face() {
|
||||
width: "40".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::Cut,
|
||||
centred: false,
|
||||
};
|
||||
assert_eq!(cut_work(&mut doc, along).cut, 1);
|
||||
// A dome: a sphere's cap rising 10 mm above the circle it stands on. A thin tool drilled into
|
||||
@@ -1363,7 +1357,6 @@ fn a_cut_still_runs_where_its_tool_touches_grazes_or_meets_a_curved_face() {
|
||||
width: "60".into(),
|
||||
height: "40".into(),
|
||||
operation: Operation::Cut,
|
||||
centred: false,
|
||||
};
|
||||
doc.add_feature(below, None).unwrap();
|
||||
let mut model = Model::new();
|
||||
@@ -2033,7 +2026,6 @@ fn an_ambiguous_target_is_refused_before_a_sheet_is_machined() {
|
||||
width: "10".into(),
|
||||
height: "2".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -2048,7 +2040,6 @@ fn an_ambiguous_target_is_refused_before_a_sheet_is_machined() {
|
||||
width: "5".into(),
|
||||
height: "20".into(),
|
||||
operation: Operation::Cut,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
+212
-78
@@ -243,30 +243,6 @@ use serde_json::Value;
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
pub use solidcraft_format::{Alias, Newer};
|
||||
|
||||
/// A kind of file whose format is versioned here: designs, and drawings (#3890). Each family has
|
||||
/// its own format tag, its own numbers and its own folder of change files (`build.rs` lists them);
|
||||
/// reading a file's format, telling a file a newer build saved and the upgrade are the same code
|
||||
/// for every family, the shared `solidcraft_format`.
|
||||
pub type Family<'a> = solidcraft_format::Family<'a, DocError>;
|
||||
|
||||
/// A family's upgrade path: the steps written out by hand (designs' up to [`HAND_WRITTEN`]), the
|
||||
/// folded formats after them (each a list of changes) and the pending changes, which make the
|
||||
/// next format.
|
||||
pub(crate) type Formats<'a> = solidcraft_format::Formats<'a, DocError>;
|
||||
|
||||
/// One change to a family's file format: a file in its `v<N>/` or `pending/` folder.
|
||||
pub(crate) type Part = solidcraft_format::Part<DocError>;
|
||||
|
||||
impl From<solidcraft_format::FormatError> for DocError {
|
||||
fn from(e: solidcraft_format::FormatError) -> Self {
|
||||
match e {
|
||||
solidcraft_format::FormatError::Invalid(why) => DocError::Invalid(why),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The format this build writes: the last folded one, or the next while changes are pending.
|
||||
pub const FORMAT: u32 = FORMATS.current();
|
||||
|
||||
@@ -274,61 +250,223 @@ pub const FORMAT: u32 = FORMATS.current();
|
||||
/// of [`Part`]s (`src/format/`, listed by `build.rs`).
|
||||
const HAND_WRITTEN: u32 = 22;
|
||||
|
||||
/// One change to a family's file format: a file in its `v<N>/` or `pending/` folder.
|
||||
pub(crate) struct Part {
|
||||
/// The file's name, which a file written while the change was pending lists.
|
||||
pub name: &'static str,
|
||||
/// How a file from before the change is rewritten as it opens, if it is.
|
||||
pub step: Option<JsonStep>,
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/format_parts.rs"));
|
||||
|
||||
/// Every format this build knows and how to upgrade a design through them.
|
||||
const FORMATS: Formats<'static> = Formats { hand: &JSON_STEPS, folded: FOLDED, pending: PENDING };
|
||||
|
||||
/// Another spelling a design of format 29 may carry: the personal format-29 renderer's name for
|
||||
/// the engineering material cards. Its card schema is unchanged; recognising the alias must never
|
||||
/// grant support to another reader, so it counts only while this build has the change.
|
||||
const DESIGN_ALIASES: [Alias; 1] = [Alias { format: 29, name: "physical-material-cards", known_as: "material-engineering-cards" }];
|
||||
/// A kind of file whose format is versioned here: designs, and drawings (#3890). Each family has
|
||||
/// its own format tag, its own numbers and its own folder of change files (`build.rs` lists them);
|
||||
/// reading a file's format, telling a file a newer build saved and the upgrade are the same code
|
||||
/// for every family.
|
||||
pub struct Family<'a> {
|
||||
/// What a file of the family is called in messages.
|
||||
noun: &'static str,
|
||||
/// The tag before the number in a file's `format`.
|
||||
prefix: &'static str,
|
||||
/// The tag files saved before the rename carry: read, never written (docs/compatibility.md).
|
||||
legacy_prefix: Option<&'static str>,
|
||||
/// The format of a file that names none: designs saved before format strings are format 1; a
|
||||
/// drawing always names its own.
|
||||
untagged: Option<u32>,
|
||||
formats: Formats<'a>,
|
||||
}
|
||||
|
||||
/// Designs (`solidcraft/N`): the upgrade steps written out up to format 22, then the change files
|
||||
/// in `src/format/`.
|
||||
pub const DESIGN: Family<'static> = Family {
|
||||
app: "SolidCraft",
|
||||
noun: "design",
|
||||
prefix: "solidcraft/",
|
||||
legacy_prefix: Some(LEGACY_FORMAT_PREFIX),
|
||||
untagged: Some(1),
|
||||
aliases: &DESIGN_ALIASES,
|
||||
formats: FORMATS,
|
||||
};
|
||||
pub const DESIGN: Family<'static> =
|
||||
Family { noun: "design", prefix: "solidcraft/", legacy_prefix: Some(LEGACY_FORMAT_PREFIX), untagged: Some(1), formats: FORMATS };
|
||||
|
||||
/// Drawings (`solidcraft-drawing/N`, #3890): format 1 is the first drawing record, and every later
|
||||
/// change is a file in `src/drawing/format/`.
|
||||
pub const DRAWING: Family<'static> = Family {
|
||||
app: "SolidCraft",
|
||||
noun: "drawing",
|
||||
prefix: "solidcraft-drawing/",
|
||||
legacy_prefix: None,
|
||||
untagged: None,
|
||||
aliases: &[],
|
||||
formats: Formats { hand: &[], folded: DRAWING_FOLDED, pending: DRAWING_PENDING },
|
||||
};
|
||||
|
||||
/// Every number a file of a family holds is in range ([`check_numbers`]), but for its top-level
|
||||
/// `kept` field: other tools' data (a design's `metadata`, a drawing's `properties`), kept as it is
|
||||
/// and never read as a number (#1015); parsing already bounds its nesting.
|
||||
pub(crate) trait CheckNumbers {
|
||||
fn check_numbers(&self, v: &Value, kept: &str) -> Result<()>;
|
||||
}
|
||||
impl Family<'_> {
|
||||
/// The format this build writes: the last folded one, or the next while changes are pending.
|
||||
pub const fn current(&self) -> u32 {
|
||||
self.formats.current()
|
||||
}
|
||||
|
||||
impl CheckNumbers for Family<'_> {
|
||||
fn check_numbers(&self, v: &Value, kept: &str) -> Result<()> {
|
||||
/// The names of the pending changes this build writes into a file (`format_pending`).
|
||||
pub fn pending_names(&self) -> Vec<String> {
|
||||
self.formats.pending.iter().map(|p| p.name.to_string()).collect()
|
||||
}
|
||||
|
||||
/// The format tag of format `n`.
|
||||
pub fn format_string(&self, n: u32) -> String {
|
||||
format!("{}{n}", self.prefix)
|
||||
}
|
||||
|
||||
/// The version in one of this family's format tags; `None` for any other tag, another
|
||||
/// family's included.
|
||||
pub fn version_of(&self, s: &str) -> Option<u32> {
|
||||
s.strip_prefix(self.prefix).or_else(|| self.legacy_prefix.and_then(|p| s.strip_prefix(p)))?.parse().ok()
|
||||
}
|
||||
|
||||
/// Read a file's format and take its `format_pending` out of its JSON, before anything else
|
||||
/// in it is read: a file that isn't this family's is refused here. One a newer build saved is
|
||||
/// not: [`Family::newer`] says so.
|
||||
pub fn take_version(&self, v: &mut Value) -> Result<(u32, Option<Vec<String>>)> {
|
||||
let noun = self.noun;
|
||||
let not_ours = |what: String| DocError::Invalid(format!("not a SolidCraft {noun} ({what})"));
|
||||
let Some(obj) = v.as_object() else { return Err(not_ours("expected a JSON object".into())) };
|
||||
let version = match obj.get("format") {
|
||||
None => self.untagged.ok_or_else(|| not_ours("no format".into()))?,
|
||||
Some(Value::String(f)) => self.version_of(f).ok_or_else(|| not_ours(format!("format `{f}`")))?,
|
||||
Some(_) => return Err(not_ours("format is not a string".into())),
|
||||
};
|
||||
let applied = take_names(v, "format_pending", noun)?;
|
||||
if version == 0 {
|
||||
return Err(not_ours("format 0".into()));
|
||||
}
|
||||
Ok((version, applied))
|
||||
}
|
||||
|
||||
/// What says a file of format `version` with the changes `applied` came from a newer build:
|
||||
/// a number past this build's, or a change this build doesn't have anywhere in its history.
|
||||
/// `None` for a file this build fully knows.
|
||||
pub fn newer(&self, version: u32, applied: Option<&[String]>) -> Option<Newer> {
|
||||
self.formats.newer(version, applied)
|
||||
}
|
||||
|
||||
/// Bring a file of format `version` up to this build's; `applied` is its `format_pending`
|
||||
/// (both from [`Family::take_version`]).
|
||||
pub fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
self.formats.upgrade(self.noun, v, version, applied)
|
||||
}
|
||||
|
||||
/// Every number a file of the family holds is in range ([`check_numbers`]), but for its
|
||||
/// top-level `kept` field: other tools' data (a design's `metadata`, a drawing's `properties`),
|
||||
/// kept as it is and never read as a number (#1015); parsing already bounds its nesting.
|
||||
pub(crate) fn check_numbers(&self, v: &Value, kept: &str) -> Result<()> {
|
||||
match v {
|
||||
Value::Object(m) => m.iter().filter(|(k, _)| k.as_str() != kept).try_for_each(|(_, x)| check_numbers(x, self.noun(), 1)),
|
||||
v => check_numbers(v, self.noun(), 0),
|
||||
Value::Object(m) => m.iter().filter(|(k, _)| k.as_str() != kept).try_for_each(|(_, x)| check_numbers(x, self.noun, 1)),
|
||||
v => check_numbers(v, self.noun, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How a file shows a newer build saved it ([`Family::newer`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Newer {
|
||||
/// The number the file says.
|
||||
pub format: u32,
|
||||
/// The number this build reads up to.
|
||||
pub reads_up_to: u32,
|
||||
/// The changes the file names that this build doesn't have.
|
||||
pub unknown_changes: Vec<String>,
|
||||
}
|
||||
|
||||
/// The names of the pending changes this build writes into a design (`format_pending`).
|
||||
pub fn pending_names() -> Vec<String> {
|
||||
DESIGN.pending_names()
|
||||
}
|
||||
|
||||
/// A family's upgrade path: the steps written out by hand (designs' up to [`HAND_WRITTEN`]), the
|
||||
/// folded formats after them (each a list of changes) and the pending changes, which make the
|
||||
/// next format.
|
||||
struct Formats<'a> {
|
||||
hand: &'a [JsonStep],
|
||||
folded: &'a [(u32, &'a [Part])],
|
||||
pending: &'a [Part],
|
||||
}
|
||||
|
||||
impl Formats<'_> {
|
||||
const fn current(&self) -> u32 {
|
||||
self.hand.len() as u32 + 1 + self.folded.len() as u32 + if self.pending.is_empty() { 0 } else { 1 }
|
||||
}
|
||||
|
||||
/// The changes format `version` is made of, if it is made of changes.
|
||||
fn parts(&self, version: u32) -> Option<&[Part]> {
|
||||
let after = version.checked_sub(self.hand.len() as u32 + 2)? as usize;
|
||||
match self.folded.get(after) {
|
||||
Some((_, parts)) => Some(parts),
|
||||
None if after == self.folded.len() && !self.pending.is_empty() => Some(self.pending),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A concept is implemented only when this reader registers its descriptor.
|
||||
fn knows_part_name(&self, name: &str) -> bool {
|
||||
self.folded.iter().flat_map(|(_, parts)| *parts).chain(self.pending).any(|part| part.name == name)
|
||||
}
|
||||
|
||||
/// Read-only spelling from the personal format-29 renderer. Its card schema is
|
||||
/// unchanged; recognizing the alias must never grant support to another reader.
|
||||
fn canonical_applied_name<'name>(&self, version: u32, name: &'name str) -> &'name str {
|
||||
if version == 29 && name == "physical-material-cards" && self.knows_part_name("material-engineering-cards") {
|
||||
"material-engineering-cards"
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Family::newer`]. Changes are recognized across the entire history: a branch's pending
|
||||
/// change may have landed at a later number. A list beside a format that isn't made of changes
|
||||
/// means nothing (a tool or a test set an older number on a newer file).
|
||||
fn newer(&self, version: u32, applied: Option<&[String]>) -> Option<Newer> {
|
||||
let current = self.current();
|
||||
let listed = applied.filter(|_| version > current || self.parts(version).is_some()).unwrap_or_default();
|
||||
let unknown_changes: Vec<String> =
|
||||
listed.iter().filter(|name| !self.knows_part_name(self.canonical_applied_name(version, name))).cloned().collect();
|
||||
(version > current || !unknown_changes.is_empty()).then_some(Newer { format: version, reads_up_to: current, unknown_changes })
|
||||
}
|
||||
|
||||
/// Bring a `noun` of format `version` up to [`Formats::current`]. `applied` is the file's
|
||||
/// `format_pending`: the changes of its format it already has, when it was written while they
|
||||
/// were pending; the others' steps run first, and already-applied changes never run again at
|
||||
/// their later number. A file past this build's number has no steps to run.
|
||||
fn upgrade(&self, noun: &str, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
if version == 0 {
|
||||
return Err(DocError::Invalid(format!("not a SolidCraft {noun} (format 0)")));
|
||||
}
|
||||
// A list beside a format that isn't made of changes is ignored, like any field an upgrade
|
||||
// drops.
|
||||
let applied = applied.filter(|_| self.parts(version).is_some());
|
||||
let already_applied =
|
||||
|part: &Part| applied.is_some_and(|names| names.iter().any(|name| self.canonical_applied_name(version, name) == part.name));
|
||||
if applied.is_some() {
|
||||
for part in self.parts(version).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
|
||||
part.step.map_or(Ok(()), |step| step(v))?;
|
||||
}
|
||||
}
|
||||
for from in version..self.current() {
|
||||
match self.hand.get(from as usize - 1) {
|
||||
Some(step) => step(v)?,
|
||||
None => {
|
||||
for part in self.parts(from + 1).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
|
||||
part.step.map_or(Ok(()), |step| step(v))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a short list of names out of a file's JSON: its `format_pending` (the pending changes it
|
||||
/// has).
|
||||
fn take_names(v: &mut Value, key: &str, noun: &str) -> Result<Option<Vec<String>>> {
|
||||
let Some(list) = v.as_object_mut().and_then(|o| o.remove(key)) else { return Ok(None) };
|
||||
let invalid = || DocError::Invalid(format!("damaged {noun}: `{key}` is not a short list of names"));
|
||||
let list = list.as_array().filter(|l| l.len() <= 1000).ok_or_else(invalid)?;
|
||||
list.iter().map(|n| n.as_str().filter(|n| n.len() <= 128).map(str::to_string).ok_or_else(invalid)).collect::<Result<_>>().map(Some)
|
||||
}
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
DESIGN.format_string(n)
|
||||
}
|
||||
@@ -344,7 +482,7 @@ pub fn version_of(s: &str) -> Option<u32> {
|
||||
|
||||
/// JSON-level upgrades up to [`HAND_WRITTEN`]: entry `i` turns format `i + 1` into `i + 2`.
|
||||
/// Never add to it: a new format is a file in `src/format/pending/`.
|
||||
type JsonStep = solidcraft_format::Step<DocError>;
|
||||
type JsonStep = fn(&mut Value) -> Result<()>;
|
||||
const JSON_STEPS: [JsonStep; HAND_WRITTEN as usize - 1] = [
|
||||
|_| Ok(()),
|
||||
v2_to_v3,
|
||||
@@ -1224,30 +1362,30 @@ mod tests {
|
||||
#[test]
|
||||
fn pending_changes_apply_in_order_after_older_formats() {
|
||||
let pending = [traced("a-first"), traced("b-second")];
|
||||
let formats = Family { formats: Formats { hand: &JSON_STEPS, folded: FOLDED, pending: &pending }, ..DESIGN };
|
||||
let formats = Formats { hand: &JSON_STEPS, folded: FOLDED, pending: &pending };
|
||||
let next = HAND_WRITTEN + FOLDED.len() as u32 + 1;
|
||||
assert_eq!(formats.current(), next);
|
||||
for version in [1, HAND_WRITTEN, next - 1] {
|
||||
let mut v = serde_json::json!({"features": [], "assembly": {"joints": [{"flip": false}]}});
|
||||
formats.upgrade(&mut v, version, None).unwrap();
|
||||
formats.upgrade("design", &mut v, version, None).unwrap();
|
||||
assert_eq!(trace_of(&v), ["a", "b"], "from format {version}");
|
||||
// The hand-written steps ran too: format 3's joints were flipped on the way up.
|
||||
assert_eq!(v["assembly"]["joints"][0]["flip"], Value::Bool(version < 4), "from format {version}");
|
||||
}
|
||||
// A design of the pending number written before `b-second` existed gets just that step.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, next, Some(&["a-first".to_string()])).unwrap();
|
||||
formats.upgrade("design", &mut v, next, Some(&["a-first".to_string()])).unwrap();
|
||||
assert_eq!(trace_of(&v), ["b"]);
|
||||
// One that has both, or that this build wrote, needs nothing.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
|
||||
formats.upgrade("design", &mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
|
||||
assert_eq!(trace_of(&v), Vec::<&str>::new());
|
||||
// A change this build doesn't know is a newer build's: said, and kept.
|
||||
let said = formats.newer(next, Some(&["c-later".into()])).unwrap();
|
||||
assert_eq!(said, Newer { format: next, reads_up_to: next, unknown_changes: vec!["c-later".into()] });
|
||||
// A list on a format that isn't made of changes means nothing.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
|
||||
formats.upgrade("design", &mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
|
||||
assert_eq!(trace_of(&v), ["a", "b"]);
|
||||
}
|
||||
|
||||
@@ -1259,16 +1397,16 @@ mod tests {
|
||||
let mut folded: Vec<(u32, &[Part])> = FOLDED.to_vec();
|
||||
let next = HAND_WRITTEN + folded.len() as u32 + 1;
|
||||
folded.push((next, &parts));
|
||||
let formats = Family { formats: Formats { hand: &JSON_STEPS, folded: &folded, pending: &[] }, ..DESIGN };
|
||||
let formats = Formats { hand: &JSON_STEPS, folded: &folded, pending: &[] };
|
||||
assert_eq!(formats.current(), next);
|
||||
let mut old = serde_json::json!({});
|
||||
formats.upgrade(&mut old, HAND_WRITTEN, None).unwrap();
|
||||
formats.upgrade("design", &mut old, HAND_WRITTEN, None).unwrap();
|
||||
assert_eq!(trace_of(&old), ["a", "b"]);
|
||||
let mut nightly = serde_json::json!({});
|
||||
formats.upgrade(&mut nightly, next, Some(&["a-first".into()])).unwrap();
|
||||
formats.upgrade("design", &mut nightly, next, Some(&["a-first".into()])).unwrap();
|
||||
assert_eq!(trace_of(&nightly), ["b"]);
|
||||
let mut released = serde_json::json!({});
|
||||
formats.upgrade(&mut released, next, None).unwrap();
|
||||
formats.upgrade("design", &mut released, next, None).unwrap();
|
||||
assert_eq!(trace_of(&released), Vec::<&str>::new());
|
||||
}
|
||||
|
||||
@@ -1277,11 +1415,11 @@ mod tests {
|
||||
let hand: [JsonStep; 27] = [|_| Ok(()); 27];
|
||||
let first = [traced("a-first")];
|
||||
let cards = [traced("material-engineering-cards")];
|
||||
let pending = Family { formats: Formats { hand: &hand, folded: &[(29, &first)], pending: &cards }, ..DESIGN };
|
||||
let folded = Family { formats: Formats { hand: &hand, folded: &[(29, &first), (30, &cards)], pending: &[] }, ..DESIGN };
|
||||
let pending = Formats { hand: &hand, folded: &[(29, &first)], pending: &cards };
|
||||
let folded = Formats { hand: &hand, folded: &[(29, &first), (30, &cards)], pending: &[] };
|
||||
for formats in [pending, folded] {
|
||||
let mut file = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
|
||||
assert_eq!(trace_of(&file), ["b", "a"], "never replay existing card semantics");
|
||||
}
|
||||
}
|
||||
@@ -1290,7 +1428,7 @@ mod tests {
|
||||
fn personal_card_alias_without_support_is_a_newer_builds() {
|
||||
let hand: [JsonStep; 27] = [|_| Ok(()); 27];
|
||||
let first = [traced("a-first")];
|
||||
let formats = Family { formats: Formats { hand: &hand, folded: &[(29, &first)], pending: &[] }, ..DESIGN };
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &[] };
|
||||
let said = formats.newer(29, Some(&["physical-material-cards".into()])).unwrap();
|
||||
assert_eq!(said.unknown_changes, ["physical-material-cards"]);
|
||||
}
|
||||
@@ -1301,7 +1439,7 @@ mod tests {
|
||||
let first = [traced("a-first")];
|
||||
let second = [traced("b-second")];
|
||||
let cards = [traced("material-engineering-cards")];
|
||||
let formats = Family { formats: Formats { hand: &hand, folded: &[(28, &first), (29, &second), (30, &cards)], pending: &[] }, ..DESIGN };
|
||||
let formats = Formats { hand: &hand, folded: &[(28, &first), (29, &second), (30, &cards)], pending: &[] };
|
||||
for version in [28, 30] {
|
||||
let said = formats.newer(version, Some(&["physical-material-cards".into()])).unwrap();
|
||||
assert_eq!(said.unknown_changes, ["physical-material-cards"]);
|
||||
@@ -1313,7 +1451,7 @@ mod tests {
|
||||
let hand: [JsonStep; 27] = [|_| Ok(()); 27];
|
||||
let first = [traced("a-first")];
|
||||
let cards = [traced("material-engineering-cards")];
|
||||
let formats = Family { formats: Formats { hand: &hand, folded: &[(29, &first)], pending: &cards }, ..DESIGN };
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &cards };
|
||||
for names in
|
||||
[["physical-material-cards".into(), "unknown-card-codec".into()], ["unknown-card-codec".into(), "physical-material-cards".into()]]
|
||||
{
|
||||
@@ -1325,14 +1463,14 @@ mod tests {
|
||||
fn alias_and_canonical_markers_never_reapply_the_card_migration() {
|
||||
let hand: [JsonStep; 27] = [|_| Ok(()); 27];
|
||||
let first = [traced("a-first"), traced("material-engineering-cards")];
|
||||
let formats = Family { formats: Formats { hand: &hand, folded: &[(29, &first)], pending: &[] }, ..DESIGN };
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &[] };
|
||||
for names in [
|
||||
vec!["physical-material-cards".into()],
|
||||
vec!["physical-material-cards".into(), "material-engineering-cards".into()],
|
||||
vec!["material-engineering-cards".into(), "physical-material-cards".into()],
|
||||
] {
|
||||
let mut file = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut file, 29, Some(&names)).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, Some(&names)).unwrap();
|
||||
assert_eq!(trace_of(&file), ["b", "a"]);
|
||||
}
|
||||
}
|
||||
@@ -1342,10 +1480,10 @@ mod tests {
|
||||
let hand: [JsonStep; 27] = [|_| Ok(()); 27];
|
||||
let first = [traced("a-first")];
|
||||
let cards = [traced("material-engineering-cards")];
|
||||
let formats = Family { formats: Formats { hand: &hand, folded: &[(29, &first)], pending: &cards }, ..DESIGN };
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &cards };
|
||||
let mut file = serde_json::json!({"trace": ["a"], "metadata": {"format_pending": ["physical-material-cards"]}});
|
||||
let metadata = file["metadata"].clone();
|
||||
formats.upgrade(&mut file, 29, None).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, None).unwrap();
|
||||
assert_eq!(trace_of(&file), ["a", "b"]);
|
||||
assert_eq!(file["metadata"], metadata);
|
||||
}
|
||||
@@ -1357,7 +1495,7 @@ mod tests {
|
||||
let parts = [traced("a-first")];
|
||||
let later = [traced("b-second")];
|
||||
for pending in [&[][..], &later[..]] {
|
||||
let formats = Family { formats: Formats { hand: &[], folded: &[(2, &parts)], pending }, ..DESIGN };
|
||||
let formats = Formats { hand: &[], folded: &[(2, &parts)], pending };
|
||||
assert_eq!(formats.newer(2, Some(&["experimental-card".into()])).unwrap().unknown_changes, ["experimental-card"]);
|
||||
}
|
||||
}
|
||||
@@ -1368,14 +1506,14 @@ mod tests {
|
||||
fn pending_concepts_moved_to_later_formats_are_not_reapplied() {
|
||||
let first = [traced("a-first")];
|
||||
let later = [traced("b-second")];
|
||||
let pending = Family { formats: Formats { hand: &[], folded: &[(2, &first)], pending: &later }, ..DESIGN };
|
||||
let folded = Family { formats: Formats { hand: &[], folded: &[(2, &first), (3, &later)], pending: &[] }, ..DESIGN };
|
||||
let pending = Formats { hand: &[], folded: &[(2, &first)], pending: &later };
|
||||
let folded = Formats { hand: &[], folded: &[(2, &first), (3, &later)], pending: &[] };
|
||||
for formats in [pending, folded] {
|
||||
let mut experimental = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut experimental, 2, Some(&["b-second".into()])).unwrap();
|
||||
formats.upgrade("design", &mut experimental, 2, Some(&["b-second".into()])).unwrap();
|
||||
assert_eq!(trace_of(&experimental), ["b", "a"]);
|
||||
let mut released = serde_json::json!({"trace": ["a"]});
|
||||
formats.upgrade(&mut released, 2, None).unwrap();
|
||||
formats.upgrade("design", &mut released, 2, None).unwrap();
|
||||
assert_eq!(trace_of(&released), ["a", "b"], "ordinary released files still get later migrations");
|
||||
}
|
||||
}
|
||||
@@ -1805,7 +1943,6 @@ mod tests {
|
||||
width: "4".into(),
|
||||
height: "6".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -2071,7 +2208,6 @@ mod tests {
|
||||
width: "20".into(),
|
||||
height: "5".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -2197,7 +2333,6 @@ mod tests {
|
||||
width: "10".into(),
|
||||
height: "10".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
Some(&format!("Box{i}")),
|
||||
)
|
||||
@@ -2228,7 +2363,6 @@ mod tests {
|
||||
width: "10".into(),
|
||||
height: "10".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//! Designs save `centred: true` on a box placed by its centre (`solid.box` with `center`); its
|
||||
//! `corner` is then that centre, so a later length, width or height keeps the box centred. Older
|
||||
//! designs have no such flag: their boxes keep their stored corner and need no migration. The last
|
||||
//! release opens such a design with a warning about the newer format, reads the centre as the
|
||||
//! corner and keeps what it can't read (`crates/doc/src/newer_file.rs`).
|
||||
+326
-21
@@ -11,9 +11,7 @@
|
||||
//! with what was read.
|
||||
//!
|
||||
//! All of these are named in [`NewerFile::not_understood`] and go back into the file when it is
|
||||
//! saved ([`restore`]), as they came. What keeps them is the shared `solidcraft_format::newer`; what
|
||||
//! is a design's (its features kept as placeholders, its parameters left out, the sections a design
|
||||
//! can't open without) is here. Saving keeps the file's own format number and change list:
|
||||
//! saved ([`restore`]), as they came. Saving keeps the file's own format number and change list:
|
||||
//! the file still holds what the newer build wrote, and a lower number would make that build
|
||||
//! upgrade its own data as if it were old. What can't be put back because the design changed around
|
||||
//! it ([`not_kept`]) is said in the reply to the save.
|
||||
@@ -23,19 +21,16 @@ use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value, json};
|
||||
use solidcraft_format::newer::{leave_out_unreadable, looks_default, read_around, short};
|
||||
|
||||
use crate::format::{self, DESIGN, Newer};
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
pub use solidcraft_format::newer::NewerFile;
|
||||
|
||||
/// The app, as a newer file's notice names it.
|
||||
const APP: &str = "SolidCraft";
|
||||
/// How a [`RawFeature`] is written while it is inside this build; [`restore`] unwraps it.
|
||||
const FOREIGN_TAG: &str = "newer_file_feature";
|
||||
/// Sections a design can't open without: filled in when the file's can't be read.
|
||||
const CORE: [&str; 3] = ["name", "params", "features"];
|
||||
/// The most sections left out one at a time to find the one a design can't be read with.
|
||||
const MAX_SECTION_TRIES: usize = 64;
|
||||
/// Fields a design container adds beside what a design holds; not unknown to this build.
|
||||
const CONTAINER_ONLY: [&str; 6] = ["step_blob", "mesh_blob", "image_blob", "points_blob", "hdr_blob", "texture_blob"];
|
||||
|
||||
@@ -56,16 +51,132 @@ impl RawFeature {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a design `newer` says a newer build saved holds that this build doesn't read: nothing
|
||||
/// left out of it yet (a design container's reader adds what it leaves out).
|
||||
pub fn newer_file(newer: &Newer) -> NewerFile {
|
||||
NewerFile::new(APP, newer)
|
||||
/// What the file says and holds that this build doesn't read.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct NewerFile {
|
||||
/// The number the file says.
|
||||
pub format: u32,
|
||||
/// The number this build reads up to.
|
||||
pub reads_up_to: u32,
|
||||
/// The changes the file names that this build doesn't have.
|
||||
pub unknown_changes: Vec<String>,
|
||||
/// What this build couldn't read, by name.
|
||||
pub not_understood: Vec<String>,
|
||||
/// Fields known records lost in typed reading.
|
||||
kept: Option<Kept>,
|
||||
/// Top-level sections left out whole.
|
||||
sections: Vec<Section>,
|
||||
/// Elements left out of a top-level list: its key and the element.
|
||||
elements: Vec<(String, Value)>,
|
||||
/// The parts of a design container this build has no use for: name and bytes. A blob only one
|
||||
/// of the kept pieces refers to is one of them.
|
||||
pub parts: Vec<(String, Arc<[u8]>)>,
|
||||
/// The file's `container.json`, when this build doesn't know what it says.
|
||||
pub container: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Section {
|
||||
key: String,
|
||||
value: Value,
|
||||
/// This build's record of the section, without the file's: while it is unchanged, the file's
|
||||
/// goes back.
|
||||
empty: Option<Value>,
|
||||
}
|
||||
|
||||
/// The fields of a record that typed reading dropped.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Kept {
|
||||
Field(Value),
|
||||
Fields(BTreeMap<String, Kept>),
|
||||
Items(Vec<(Anchor, Kept)>),
|
||||
}
|
||||
|
||||
/// Which element of a list some kept fields belong to.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Anchor {
|
||||
Id(u64),
|
||||
Name(String),
|
||||
/// Its place, good only while the list keeps its length.
|
||||
Index {
|
||||
at: usize,
|
||||
of: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl NewerFile {
|
||||
/// What to tell the user, whatever else is said.
|
||||
pub fn notice(&self) -> String {
|
||||
let reads = self.reads_up_to;
|
||||
let format = self.format;
|
||||
match self.unknown_changes.first() {
|
||||
Some(change) if format <= reads => {
|
||||
format!(
|
||||
"Saved by a newer SolidCraft (format {format} with `{change}`, which this version doesn't know). If something's missing, update."
|
||||
)
|
||||
}
|
||||
_ => format!("Saved by a newer SolidCraft (format {format}, this version reads up to {reads}). If something's missing, update."),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every line to report: the notice, then what couldn't be read, by name.
|
||||
pub fn warnings(&self) -> Vec<String> {
|
||||
let unread = self.not_understood.iter().map(|what| format!("Not read: {what}. Saving keeps it."));
|
||||
std::iter::once(self.notice()).chain(unread).collect()
|
||||
}
|
||||
|
||||
/// What a reply says about it.
|
||||
pub fn report(&self) -> Value {
|
||||
json!({
|
||||
"format": self.format,
|
||||
"reads_up_to": self.reads_up_to,
|
||||
"unknown_changes": self.unknown_changes,
|
||||
"not_understood": self.not_understood,
|
||||
"notice": self.notice(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the next save needs a container to keep what the file holds: plain JSON has no room
|
||||
/// for parts beside the design.
|
||||
pub fn needs_container(&self) -> bool {
|
||||
!self.parts.is_empty()
|
||||
}
|
||||
|
||||
/// Whether anything kept back names this text (a blob's name in a container).
|
||||
pub fn mentions(&self, text: &str) -> bool {
|
||||
fn value(v: &Value, text: &str) -> bool {
|
||||
match v {
|
||||
Value::String(s) => s == text,
|
||||
Value::Array(a) => a.iter().any(|x| value(x, text)),
|
||||
Value::Object(o) => o.values().any(|x| value(x, text)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn kept(k: &Kept, text: &str) -> bool {
|
||||
match k {
|
||||
Kept::Field(v) => value(v, text),
|
||||
Kept::Fields(m) => m.values().any(|x| kept(x, text)),
|
||||
Kept::Items(l) => l.iter().any(|(_, x)| kept(x, text)),
|
||||
}
|
||||
}
|
||||
self.kept.as_ref().is_some_and(|k| kept(k, text))
|
||||
|| self.sections.iter().any(|s| value(&s.value, text))
|
||||
|| self.elements.iter().any(|(_, v)| value(v, text))
|
||||
}
|
||||
|
||||
/// Say that a section the container reader couldn't finish reading is left out.
|
||||
/// `empty` is what the design writes for the section while it holds nothing, if anything.
|
||||
pub fn keep_section(&mut self, key: &str, value: Value, empty: Option<Value>, why: &str) {
|
||||
self.not_understood.push(format!("section `{key}` ({why})"));
|
||||
self.sections.push(Section { key: key.into(), value, empty });
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a design a newer build saved, or say why it truly can't be read.
|
||||
pub(crate) fn read(mut raw: Value, version: u32, applied: Option<Vec<String>>, newer: Newer) -> Result<Document> {
|
||||
let applied_names = applied.as_deref();
|
||||
let mut file = newer_file(&newer);
|
||||
let mut file =
|
||||
NewerFile { format: newer.format, reads_up_to: newer.reads_up_to, unknown_changes: newer.unknown_changes.clone(), ..NewerFile::default() };
|
||||
let (mut doc, read_as) = match format::decode(raw.clone(), version, applied_names) {
|
||||
Ok(doc) => (doc, raw),
|
||||
Err(_) => {
|
||||
@@ -84,7 +195,12 @@ pub(crate) fn read(mut raw: Value, version: u32, applied: Option<Vec<String>>, n
|
||||
// Compared with what was read, the file's own shape after the same upgrade.
|
||||
DESIGN.upgrade(&mut upgraded, version, applied_names)?;
|
||||
let known = serde_json::to_value(&doc).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
|
||||
file.compare(&upgraded, &known, FOREIGN_TAG, &CONTAINER_ONLY);
|
||||
let mut paths = Vec::new();
|
||||
file.kept = vanished(&upgraded, &known, "", &mut paths);
|
||||
file.not_understood.extend(paths);
|
||||
for section in &mut file.sections {
|
||||
section.empty = known.get(§ion.key).cloned();
|
||||
}
|
||||
if newer.format > newer.reads_up_to {
|
||||
// The file's own number and list: it still holds what that build wrote.
|
||||
doc.format = DESIGN.format_string(newer.format);
|
||||
@@ -126,27 +242,72 @@ fn salvage(
|
||||
Ok(()) => true,
|
||||
Err(why) => {
|
||||
let name = param.get("name").and_then(Value::as_str).unwrap_or("unnamed");
|
||||
let note = format!("parameter `{name}` ({})", short(&why.to_string(), "design"));
|
||||
file.keep_element("params", param.clone(), note);
|
||||
file.not_understood.push(format!("parameter `{name}` ({})", short(&why.to_string())));
|
||||
file.elements.push(("params".into(), param.clone()));
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
// Any other section, by itself.
|
||||
leave_out_unreadable(top, file, "design", piece);
|
||||
let keys: Vec<String> = top.keys().cloned().collect();
|
||||
for key in &keys {
|
||||
let value = top.get(key).cloned().unwrap_or_default();
|
||||
if let Err(why) = piece(key, &value) {
|
||||
move_out(top, key, &why.to_string(), file);
|
||||
}
|
||||
}
|
||||
if !top.get("name").is_some_and(Value::is_string) {
|
||||
top.insert("name".into(), json!("Untitled"));
|
||||
}
|
||||
for key in ["params", "features"] {
|
||||
top.entry(key).or_insert_with(|| json!([]));
|
||||
}
|
||||
read_around(top, file, &CORE, "design", |top: &Map<String, Value>| format::decode(Value::Object(top.clone()), version, applied))
|
||||
let attempt = |top: &Map<String, Value>| format::decode(Value::Object(top.clone()), version, applied);
|
||||
let first = match attempt(top) {
|
||||
Ok(doc) => return Ok(doc),
|
||||
Err(why) => why,
|
||||
};
|
||||
// Sections that are fine by themselves but not together with the rest.
|
||||
let keys: Vec<String> = top.keys().filter(|k| !CORE.contains(&k.as_str())).cloned().collect();
|
||||
// Each try reads the whole design: a file of many sections goes straight to the last resort.
|
||||
for key in keys.iter().filter(|_| keys.len() <= MAX_SECTION_TRIES) {
|
||||
let mut without = top.clone();
|
||||
without.remove(key);
|
||||
if let Ok(doc) = attempt(&without) {
|
||||
move_out(top, key, &first.to_string(), file);
|
||||
return Ok(doc);
|
||||
}
|
||||
}
|
||||
let mut bare = top.clone();
|
||||
bare.retain(|key, _| CORE.contains(&key.as_str()));
|
||||
match attempt(&bare) {
|
||||
Ok(doc) => {
|
||||
for key in &keys {
|
||||
move_out(top, key, &first.to_string(), file);
|
||||
}
|
||||
Ok(doc)
|
||||
}
|
||||
Err(_) => Err(first.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave a section out and keep it. A section the design needs is left empty.
|
||||
fn move_out(top: &mut Map<String, Value>, key: &str, why: &str, file: &mut NewerFile) {
|
||||
let Some(value) = top.remove(key) else { return };
|
||||
file.not_understood.push(format!("section `{key}` ({})", short(why)));
|
||||
file.sections.push(Section { key: key.into(), value, empty: None });
|
||||
}
|
||||
|
||||
/// The first line of a reader's error, without the list of every kind it knows.
|
||||
fn short(why: &str) -> &str {
|
||||
let why = why.split(", expected").next().unwrap_or(why);
|
||||
why.strip_prefix("design file: ").unwrap_or(why)
|
||||
}
|
||||
|
||||
fn feature_note(feature: &Value, why: &str) -> String {
|
||||
let name = feature.get("name").and_then(Value::as_str).unwrap_or("unnamed");
|
||||
let kind = feature.get("type").and_then(Value::as_str).unwrap_or("unnamed");
|
||||
let why = short(why, "design");
|
||||
let why = short(why);
|
||||
if why.starts_with("unknown variant") {
|
||||
format!("feature `{name}`, a `{kind}` feature this version doesn't know")
|
||||
} else {
|
||||
@@ -201,16 +362,160 @@ fn unwrapped(feature: &mut Value) {
|
||||
*feature = Value::Object(raw);
|
||||
}
|
||||
|
||||
/// A value a writer leaves out without losing anything: not worth telling anyone about.
|
||||
fn looks_default(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Null => true,
|
||||
Value::Bool(b) => !b,
|
||||
Value::Number(n) => n.as_f64() == Some(0.0),
|
||||
Value::String(s) => s.is_empty(),
|
||||
Value::Array(a) => a.is_empty(),
|
||||
Value::Object(o) => o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What `raw` has that `known`, what was read of it, doesn't. `paths` gets those fields' names.
|
||||
fn vanished(raw: &Value, known: &Value, path: &str, paths: &mut Vec<String>) -> Option<Kept> {
|
||||
match (raw, known) {
|
||||
(Value::Object(raw), Value::Object(known)) => {
|
||||
if known.get("type").and_then(Value::as_str) == Some(FOREIGN_TAG) {
|
||||
return None;
|
||||
}
|
||||
let mut kept = BTreeMap::new();
|
||||
for (key, value) in raw {
|
||||
if CONTAINER_ONLY.contains(&key.as_str()) || (path.is_empty() && matches!(key.as_str(), "format" | "format_pending")) {
|
||||
continue;
|
||||
}
|
||||
let inner = if path.is_empty() { key.clone() } else { format!("{path}.{key}") };
|
||||
match known.get(key) {
|
||||
None => {
|
||||
if !looks_default(value) {
|
||||
paths.push(format!("field `{inner}`"));
|
||||
}
|
||||
kept.insert(key.clone(), Kept::Field(value.clone()));
|
||||
}
|
||||
Some(known) => {
|
||||
if let Some(k) = vanished(value, known, &inner, paths) {
|
||||
kept.insert(key.clone(), k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(!kept.is_empty()).then_some(Kept::Fields(kept))
|
||||
}
|
||||
(Value::Array(raw), Value::Array(known)) => {
|
||||
let mut items = Vec::new();
|
||||
for (at, element) in raw.iter().enumerate() {
|
||||
let anchor = anchor_of(element).unwrap_or(Anchor::Index { at, of: raw.len() });
|
||||
let Some(partner) = find(known, &anchor) else { continue };
|
||||
let label = match (element.get("name").and_then(Value::as_str), &anchor) {
|
||||
(Some(name), _) => format!("{path}[{name}]"),
|
||||
(None, Anchor::Id(id)) => format!("{path}[{id}]"),
|
||||
_ => format!("{path}[{at}]"),
|
||||
};
|
||||
if let Some(k) = vanished(element, partner, &label, paths) {
|
||||
items.push((anchor, k));
|
||||
}
|
||||
}
|
||||
(!items.is_empty()).then_some(Kept::Items(items))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor_of(element: &Value) -> Option<Anchor> {
|
||||
if let Some(id) = element.get("id").and_then(Value::as_u64) {
|
||||
return Some(Anchor::Id(id));
|
||||
}
|
||||
element.get("name").and_then(Value::as_str).map(|name| Anchor::Name(name.to_string()))
|
||||
}
|
||||
|
||||
fn find<'a>(list: &'a [Value], anchor: &Anchor) -> Option<&'a Value> {
|
||||
match anchor {
|
||||
Anchor::Id(id) => list.iter().find(|x| x.get("id").and_then(Value::as_u64) == Some(*id)),
|
||||
Anchor::Name(name) => list.iter().find(|x| x.get("name").and_then(Value::as_str) == Some(name)),
|
||||
Anchor::Index { at, of } => (list.len() == *of).then(|| list.get(*at)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_mut<'a>(list: &'a mut [Value], anchor: &Anchor) -> Option<&'a mut Value> {
|
||||
match anchor {
|
||||
Anchor::Id(id) => list.iter_mut().find(|x| x.get("id").and_then(Value::as_u64) == Some(*id)),
|
||||
Anchor::Name(name) => list.iter_mut().find(|x| x.get("name").and_then(Value::as_str) == Some(name)),
|
||||
Anchor::Index { at, of } => (list.len() == *of).then(|| list.get_mut(*at)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put the kept fields back. An element that is gone was deleted here, its fields with it; one
|
||||
/// whose place can't be told any more, and a field whose record is gone, go into `lost`.
|
||||
fn apply(kept: &Kept, value: &mut Value, path: &str, lost: &mut Vec<String>) {
|
||||
match kept {
|
||||
Kept::Field(_) => {}
|
||||
Kept::Fields(fields) => {
|
||||
let Some(object) = value.as_object_mut() else {
|
||||
lost.push(format!("field `{path}`"));
|
||||
return;
|
||||
};
|
||||
for (key, sub) in fields {
|
||||
let inner = if path.is_empty() { key.clone() } else { format!("{path}.{key}") };
|
||||
match (sub, object.get_mut(key)) {
|
||||
(Kept::Field(v), None) => {
|
||||
object.insert(key.clone(), v.clone());
|
||||
}
|
||||
(Kept::Field(_), Some(_)) => {}
|
||||
(sub, Some(inner_value)) => apply(sub, inner_value, &inner, lost),
|
||||
// The list it was in is empty and not written: its elements were deleted.
|
||||
(Kept::Items(_), None) => {}
|
||||
(_, None) => lost.push(format!("field `{inner}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Kept::Items(items) => {
|
||||
let Some(list) = value.as_array_mut() else {
|
||||
lost.push(format!("field `{path}`"));
|
||||
return;
|
||||
};
|
||||
for (anchor, sub) in items {
|
||||
match (find_mut(list, anchor), anchor) {
|
||||
(Some(element), _) => apply(sub, element, path, lost),
|
||||
(None, Anchor::Index { at, .. }) => lost.push(format!("element {at} of `{path}`")),
|
||||
(None, _) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write back what the design holds of the file a newer build saved, into `value`, the design's
|
||||
/// JSON. Returns what couldn't go back ([`not_kept`]).
|
||||
pub fn restore(doc: &Document, value: &mut Value) -> Vec<String> {
|
||||
let Some(file) = &doc.newer_file else { return Vec::new() };
|
||||
let mut lost = Vec::new();
|
||||
let Some(file) = &doc.newer_file else { return lost };
|
||||
if let Some(Value::Array(features)) = value.get_mut("features") {
|
||||
for feature in features.iter_mut().filter(|f| f.get("type").and_then(Value::as_str) == Some(FOREIGN_TAG)) {
|
||||
unwrapped(feature);
|
||||
}
|
||||
}
|
||||
file.restore_into(value)
|
||||
if let Some(kept) = &file.kept {
|
||||
apply(kept, value, "", &mut lost);
|
||||
}
|
||||
if let Some(top) = value.as_object_mut() {
|
||||
for section in &file.sections {
|
||||
if top.get(§ion.key) == section.empty.as_ref() {
|
||||
top.insert(section.key.clone(), section.value.clone());
|
||||
} else {
|
||||
lost.push(format!("section `{}`, replaced by what was edited here", section.key));
|
||||
}
|
||||
}
|
||||
for (key, element) in &file.elements {
|
||||
let name = element.get("name");
|
||||
match top.entry(key.clone()).or_insert_with(|| json!([])) {
|
||||
Value::Array(list) if name.is_none() || !list.iter().any(|x| x.get("name") == name) => list.push(element.clone()),
|
||||
_ => lost.push(format!("`{key}` element {}, replaced by what was made here", name.and_then(Value::as_str).unwrap_or("unnamed"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
lost
|
||||
}
|
||||
|
||||
/// What of the file a newer build saved the next save can't keep, because the design was changed
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(super) fn local_geometry(f: &Feature, components: &std::collections::HashMap
|
||||
match &f.kind {
|
||||
// A cylinder placed by reference edges follows the body it stands on.
|
||||
FeatureKind::Cylinder { references, .. } if !references.is_empty() => false,
|
||||
FeatureKind::Box { corner: _, length: _, width: _, height: _, operation: Operation::NewBody, centred: _ }
|
||||
FeatureKind::Box { corner: _, length: _, width: _, height: _, operation: Operation::NewBody }
|
||||
| FeatureKind::Cylinder { base: _, axis: _, radius: _, height: _, operation: Operation::NewBody, references: _ }
|
||||
| FeatureKind::Sphere { center: _, radius: _, operation: Operation::NewBody }
|
||||
| FeatureKind::Torus { center: _, major: _, minor: _, operation: Operation::NewBody }
|
||||
|
||||
@@ -333,7 +333,6 @@ mod tests {
|
||||
width: "3 mm".into(),
|
||||
height: "4 mm".into(),
|
||||
operation: crate::Operation::NewBody,
|
||||
centred: false,
|
||||
};
|
||||
doc.add_feature(block, Some("Block")).unwrap();
|
||||
let mut model = crate::Model::default();
|
||||
|
||||
@@ -285,7 +285,6 @@ fn set_back_restores_everything_when_states_share_bodies() {
|
||||
width: "10".into(),
|
||||
height: height.into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -394,7 +393,6 @@ fn rolling_a_large_unchanged_timeline_forward_reuses_its_geometry() {
|
||||
width: "10".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -946,7 +944,6 @@ fn errors_are_reported_not_fatal() {
|
||||
width: "1".into(),
|
||||
height: "1".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1029,7 +1026,6 @@ fn primitives_and_combine() {
|
||||
width: "10".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1445,7 +1441,6 @@ fn empty_boolean_targets_without_overlap_keep_plain_message() {
|
||||
width: "1".into(),
|
||||
height: "1".into(),
|
||||
operation,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1460,7 +1455,7 @@ fn empty_boolean_targets_without_overlap_keep_plain_message() {
|
||||
/// A box of `size` at `corner`, made with `operation`.
|
||||
fn cube(doc: &mut Document, corner: Vec3, size: [f64; 3], operation: Operation) -> u64 {
|
||||
let [length, width, height] = size.map(|x| Expr::new(x.to_string()));
|
||||
doc.add_feature(FeatureKind::Box { corner: point_expr(corner), length, width, height, operation, centred: false }, None).unwrap()
|
||||
doc.add_feature(FeatureKind::Box { corner: point_expr(corner), length, width, height, operation }, None).unwrap()
|
||||
}
|
||||
|
||||
/// The bodies of `doc` evaluated, by name with their volumes and pieces, and how feature
|
||||
|
||||
@@ -321,27 +321,6 @@ fn shallow_loft_cut_from_the_top_face() {
|
||||
assert!((got - want).abs() < 1e-9 * want, "{got} vs {want}");
|
||||
}
|
||||
|
||||
/// A carved top lofted through six closed spline sections from its slab's top face and joined
|
||||
/// onto the slab. The loft lies on and above that face and shares only it with the slab, so the
|
||||
/// joined top is exactly the slab plus the loft built as its own body.
|
||||
#[test]
|
||||
fn carved_top_lofted_onto_its_slab_joins() {
|
||||
let script = include_str!("../../../examples/regressions/loft_join_carved_top_on_slab.json");
|
||||
let joined = replay(script);
|
||||
assert_eq!(joined.model.state().bodies.len(), 1);
|
||||
let mut apart: Value = serde_json::from_str(script).unwrap();
|
||||
let loft = apart["commands"].as_array_mut().unwrap().last_mut().unwrap();
|
||||
loft["params"]["operation"] = json!("new_body");
|
||||
loft["params"]["body_name"] = json!("Arch");
|
||||
loft["params"].as_object_mut().unwrap().remove("targets");
|
||||
let mut s = Session::default();
|
||||
s.run_script(&apart).unwrap();
|
||||
let (slab, arch) = (volume(&s, "Top"), volume(&s, "Arch"));
|
||||
assert!(slab > 0.0 && arch > 0.0, "{slab} {arch}");
|
||||
let got = volume(&joined, "Top");
|
||||
assert!((got - (slab + arch)).abs() < 1e-7 * got, "{got} vs {slab} + {arch}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oblique_cut_through_a_small_arc_matches_a_split() {
|
||||
let scripts = [
|
||||
|
||||
@@ -71,7 +71,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("box")
|
||||
.contract(&BOX_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z] (numbers in mm or length expressions; default: the origin; a box placed by its center stays centred there when its size changes); operation?, touching?, targets?: [body], name?, body_name?"),
|
||||
.params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, touching?, targets?: [body], name?, body_name?"),
|
||||
CommandSpec::new("solid.cylinder", "Cylinder", crate::checked!("solid.cylinder", prim_cylinder))
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("cylinder")
|
||||
@@ -1534,14 +1534,27 @@ fn prim_box(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for (e, n) in [(&l, "length"), (&w, "width"), (&h, "height")] {
|
||||
check_expr(s, e, Kind::Length, cmd, n)?;
|
||||
}
|
||||
// A box placed by its centre keeps that centre, so a later length, width or height grows it on
|
||||
// both sides (#14487).
|
||||
let (corner, centred) = match (point_param(s, p, "corner", cmd)?, point_param(s, p, "center", cmd)?) {
|
||||
(Some(c), _) => (c, false),
|
||||
(None, Some(c)) => (c, true),
|
||||
_ => (point_expr(Vec3::ZERO), false),
|
||||
let corner = match (point_param(s, p, "corner", cmd)?, point_param(s, p, "center", cmd)?) {
|
||||
(Some(c), _) => c,
|
||||
// The corner half a size back from the centre (numbers stay numbers, in mm). The centre's
|
||||
// bare numbers are millimetres, the sizes' the design's units.
|
||||
(None, Some([cx, cy, cz])) => {
|
||||
let unit = typed_length_unit(s);
|
||||
let mm_per_unit = solidcraft_doc::expr::unit_info(&unit).map_or(1.0, |(_, k)| k);
|
||||
let back = |c: Expr, size: &Expr| -> Expr {
|
||||
match (c.parse::<f64>(), size.parse::<f64>()) {
|
||||
(Ok(a), Ok(b)) => format!("{}", a - b * mm_per_unit / 2.0).into(),
|
||||
_ => {
|
||||
let [c, size] = length_operands(s, [(&c, "mm"), (size, &unit)]);
|
||||
format!("({c}) - ({size}) / 2").into()
|
||||
}
|
||||
}
|
||||
};
|
||||
[back(cx, &l), back(cy, &w), back(cz, &h)]
|
||||
}
|
||||
_ => point_expr(Vec3::ZERO),
|
||||
};
|
||||
add_feature(s, p, FeatureKind::Box { corner, length: l, width: w, height: h, operation: operation(p, cmd)?, centred })
|
||||
add_feature(s, p, FeatureKind::Box { corner, length: l, width: w, height: h, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
fn prim_cylinder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -654,13 +654,6 @@ fn opened_design(
|
||||
s.replace_design(opened);
|
||||
s.disk = disk;
|
||||
s.read_only = read_only;
|
||||
// A design saved with print faces (by an older version, or with them kept) opens without
|
||||
// them when they are not kept (#11512). The rest of it opens as saved, and dropping them is
|
||||
// neither an unsaved change nor a step to undo.
|
||||
if !s.keep_print_faces && !s.doc.print_faces.is_empty() {
|
||||
s.doc_mut().print_faces.clear();
|
||||
s.mark_saved();
|
||||
}
|
||||
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
|
||||
let now = solidcraft_io::design_lock::now_secs();
|
||||
let open_elsewhere = solidcraft_io::design_lock::held_elsewhere(std::path::Path::new(path), now).map(|lock| OpenElsewhere {
|
||||
@@ -981,10 +974,6 @@ impl Session {
|
||||
if !self.visibility.is_empty() {
|
||||
design.to_mut().visibility = self.visibility.clone();
|
||||
}
|
||||
// Print faces not kept stay with this session: the file is saved without them (#11512).
|
||||
if !self.keep_print_faces && !design.print_faces.is_empty() {
|
||||
design.to_mut().print_faces.clear();
|
||||
}
|
||||
// A sketch edited with the timeline rolled to it: the file holds the position the edit
|
||||
// returns to, not the temporary rollback (#14010).
|
||||
if let Some(marker) = self.sketch_restore_marker() {
|
||||
|
||||
@@ -423,38 +423,6 @@ mod tests {
|
||||
assert!(g.execute("plastic.lip", &json!({"face": [40, 25, 0], "width": 1, "height": 2})).is_err(), "a face without an inner edge");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groove_on_bottom_rim_of_rounded_shell() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": solidcore_record::Plane::XY, "name": "Outline"}));
|
||||
run(
|
||||
&mut s,
|
||||
"sketch.rectangle.rounded",
|
||||
json!({"p0": [-25.263865881072945, -28.541660374585607], "p1": [25.263865881072945, 28.541660374585607], "radius": 3}),
|
||||
);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Outline", "distance": 10}));
|
||||
run(&mut s, "solid.shell", json!({"faces": [[0, 0, 0]], "thickness": 2}));
|
||||
let state = s.model.state();
|
||||
let shell = &state.body("Body1").unwrap().body;
|
||||
assert!(shell.validity().is_empty(), "shell: {:?}", shell.validity());
|
||||
let (plane, band) = solidcraft_kernel::rim_band(shell, Vec3::new(0.0, -27.541660374585607, 0.0), 1.5, 0.0, false).unwrap();
|
||||
let cutter = solidcraft_kernel::extrude(&plane, &[band], -2.0, 0.0).unwrap();
|
||||
for body in &cutter {
|
||||
assert!(body.validity().is_empty(), "cutter: {:?}", body.validity());
|
||||
}
|
||||
let before = volume(&mut s);
|
||||
let result = s.execute("plastic.lip", &json!({"face": [0, -27.541660374585607, 0], "width": 1.5, "height": 2, "type": "groove"}));
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
let area = |w: f64, h: f64, r: f64| w * h - (4.0 - PI) * r * r;
|
||||
let w = 50.52773176214589;
|
||||
let h = 57.083320749171214;
|
||||
let groove_area = area(w - 1.0, h - 1.0, 2.5) - area(w - 4.0, h - 4.0, 1.0);
|
||||
let removed = before - volume(&mut s);
|
||||
assert!((removed - groove_area * 2.0).abs() < 0.05, "removed {removed}, expected {}", groove_area * 2.0);
|
||||
assert!(s.model.state().body("Body1").unwrap().body.validity().is_empty());
|
||||
}
|
||||
|
||||
/// A lip follows its rim face when a parameter moves it (#11209): it was kept by a point
|
||||
/// only, so a taller box left the point below the rim and the lip failed.
|
||||
#[test]
|
||||
|
||||
@@ -296,63 +296,6 @@ mod tests {
|
||||
assert!(opened.print_faces.is_empty());
|
||||
}
|
||||
|
||||
/// A file in a fresh folder of the system's temporary folder, for `case`.
|
||||
fn temp_file(case: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-print-faces-{case}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temporary folder");
|
||||
dir.join("block.solidcraft").to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_keep_print_faces_a_save_leaves_them_out_and_the_session_keeps_them() {
|
||||
let mut s = block();
|
||||
s.keep_print_faces = false;
|
||||
run(&mut s, "body.print_face", json!({"body": "Block", "point": [10, 5, 5]}));
|
||||
let path = temp_file("save");
|
||||
run(&mut s, "file.save", json!({"path": path}));
|
||||
// The design on screen still has it, and is not left with unsaved changes.
|
||||
assert!(s.doc.print_faces.contains_key("Block"));
|
||||
assert!(!s.is_dirty());
|
||||
let mut opened = Session::default();
|
||||
run(&mut opened, "doc.open", json!({"path": path}));
|
||||
assert!(opened.doc.print_faces.is_empty(), "the file has no print face");
|
||||
assert_eq!(opened.doc.features.len(), s.doc.features.len());
|
||||
assert_eq!(opened.doc.params, s.doc.params);
|
||||
// With keep_print_faces on (the engine's default), the file keeps it, as before.
|
||||
s.keep_print_faces = true;
|
||||
run(&mut s, "file.save", json!({"path": path}));
|
||||
let mut opened = Session::default();
|
||||
run(&mut opened, "doc.open", json!({"path": path}));
|
||||
assert_eq!(opened.doc.print_faces, s.doc.print_faces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_design_saved_with_print_faces_opens_without_them_unless_they_are_kept() {
|
||||
let mut s = block();
|
||||
run(&mut s, "material.assign", json!({"bodies": ["Block"], "material": "Brass"}));
|
||||
run(&mut s, "body.print_face", json!({"body": "Block", "point": [10, 5, 5]}));
|
||||
// Saved as an older version saved it: with the print face in the file.
|
||||
let path = temp_file("older");
|
||||
run(&mut s, "file.save", json!({"path": path}));
|
||||
let mut opened = Session { keep_print_faces: false, ..Session::default() };
|
||||
run(&mut opened, "doc.open", json!({"path": path}));
|
||||
assert!(opened.doc.print_faces.is_empty(), "the stored print face is dropped as the design opens");
|
||||
assert!(!opened.is_dirty(), "dropping it is not an unsaved change");
|
||||
assert!(opened.undo.is_empty(), "nor a step to undo");
|
||||
// Everything else the file holds is there.
|
||||
assert_eq!(opened.doc.features.len(), s.doc.features.len());
|
||||
assert_eq!(opened.doc.params, s.doc.params);
|
||||
assert_eq!(opened.doc.materials, s.doc.materials);
|
||||
assert_eq!(opened.model.state().bodies.len(), 1);
|
||||
// The setting is the user's: it survives the open.
|
||||
assert!(!opened.keep_print_faces);
|
||||
// Kept, the design opens with its print face.
|
||||
let mut kept = Session::default();
|
||||
run(&mut kept, "doc.open", json!({"path": path}));
|
||||
assert_eq!(kept.doc.print_faces, s.doc.print_faces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renaming_the_body_keeps_its_print_face() {
|
||||
let mut s = block();
|
||||
|
||||
@@ -175,27 +175,6 @@ fn a_box_placed_by_its_centre_reads_its_sizes_in_the_design_units() {
|
||||
keeps_shape_through(&mut s, &["in", "mm"]);
|
||||
}
|
||||
|
||||
/// A box placed by its centre grows on both sides when its size changes later, also after a save and
|
||||
/// reopen; a box placed by its corner keeps growing from that corner (#14487).
|
||||
#[test]
|
||||
fn a_box_placed_by_its_centre_stays_centred_when_its_size_changes() {
|
||||
let near = |a: [f64; 6], b: [f64; 6]| a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-6);
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "center": [0, 0, 0], "name": "Box", "body_name": "A"}));
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "corner": [50, 0, 0], "name": "Corner", "body_name": "B"}));
|
||||
assert!(near(shape(&mut s, "A").bbox, [-10.0, -5.0, -5.0, 10.0, 5.0, 5.0]), "{:?}", shape(&mut s, "A"));
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Box", "set": {"length": "30"}}));
|
||||
let a = shape(&mut s, "A");
|
||||
assert!(near(a.bbox, [-15.0, -5.0, -5.0, 15.0, 5.0, 5.0]), "{a:?}");
|
||||
assert!(rel(a.volume, 3000.0) < 1e-9, "{a:?}");
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Corner", "set": {"length": "30"}}));
|
||||
assert!(near(shape(&mut s, "B").bbox, [50.0, 0.0, 0.0, 80.0, 10.0, 10.0]), "{:?}", shape(&mut s, "B"));
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
run(&mut reopened, "timeline.edit", json!({"feature": "Box", "set": {"width": "20", "height": "4"}}));
|
||||
assert!(near(shape(&mut reopened, "A").bbox, [-15.0, -10.0, -2.0, 15.0, 10.0, 2.0]), "{:?}", shape(&mut reopened, "A"));
|
||||
keeps_shape_through(&mut reopened, &["in", "mm"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn press_pull_keeps_its_distance_when_the_units_change() {
|
||||
// A face pushed in: the cut's depth is the distance's size.
|
||||
@@ -245,8 +224,7 @@ fn bare_numbers_beside_a_unitless_expression_stay_bare() {
|
||||
assert_eq!(inputs(&s, 0)[2], "(16) / (2 * 4)", "the revolutions");
|
||||
assert!(s.model.result(s.doc.features[0].id).is_some_and(|r| r.error.is_none()));
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "center": ["2 * 5", 0, 0], "body_name": "B"}));
|
||||
// The centre is kept as typed; the box stays centred on it (#14487).
|
||||
assert_eq!(s.doc.features[1].kind.point_expr().map(|p| p[0].to_string()), Some("2 * 5".to_string()));
|
||||
assert_eq!(s.doc.features[1].kind.point_expr().map(|p| p[0].to_string()), Some("(2 * 5) - (20) / 2".to_string()));
|
||||
let b = shape(&mut s, "B");
|
||||
assert!(b.bbox[0].abs() < 1e-6 && (b.bbox[3] - 20.0).abs() < 1e-6, "{b:?}");
|
||||
}
|
||||
|
||||
@@ -794,7 +794,7 @@ fn primitive_placement(kind: &solidcraft_doc::FeatureKind, cmd: &str) -> Vec<(&'
|
||||
use solidcraft_doc::FeatureKind as Feature;
|
||||
let point = placement_point;
|
||||
match (kind, cmd) {
|
||||
(Feature::Box { corner, centred, .. }, "solid.box") => vec![(if *centred { "center" } else { "corner" }, point(corner))],
|
||||
(Feature::Box { corner, .. }, "solid.box") => vec![("corner", point(corner))],
|
||||
(Feature::Sphere { center, .. }, "solid.sphere") | (Feature::Torus { center, .. }, "solid.torus") => vec![("center", point(center))],
|
||||
(Feature::Cylinder { base, axis, .. }, "solid.cylinder") | (Feature::Coil { base, axis, .. }, "solid.coil") => {
|
||||
vec![("base", point(base)), ("axis", pt(axis.0))]
|
||||
|
||||
@@ -29,14 +29,7 @@ fn generated_assembly(parts: usize) -> Document {
|
||||
let mut made = Vec::new();
|
||||
for i in 0..parts {
|
||||
let o = Vec3::new(30.0 * (i % 20) as f64, 30.0 * (i / 20) as f64, 0.0);
|
||||
let block = FeatureKind::Box {
|
||||
corner: point_expr(o),
|
||||
length: mm(20.0),
|
||||
width: mm(12.0),
|
||||
height: mm(6.0),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
};
|
||||
let block = FeatureKind::Box { corner: point_expr(o), length: mm(20.0), width: mm(12.0), height: mm(6.0), operation: Operation::NewBody };
|
||||
made.push((i, doc.add_feature(block, Some(&format!("Plate {i}"))).unwrap()));
|
||||
for (k, x) in [2.0, 9.0, 16.0].into_iter().enumerate() {
|
||||
let pin = FeatureKind::Cylinder {
|
||||
|
||||
@@ -538,12 +538,6 @@ pub struct Session {
|
||||
/// How the host uploads a model to EasyPrint (`file.open_in_easyprint`); `None`: it can't. It
|
||||
/// belongs to the session like `param_check`, so it survives opening and starting designs.
|
||||
pub easyprint_upload: Option<EasyPrintUpload>,
|
||||
/// Whether print faces (`body.print_face`) are saved with the design and kept when one is
|
||||
/// opened (#11512). On by default, so scripts and the command line keep them; the app sets it
|
||||
/// from its "Keep print faces" preference, which is off unless the user turns it on. Off: a
|
||||
/// save leaves them out of the file and an opened design starts without them. The user's,
|
||||
/// like `param_check`, so it survives opening and starting designs.
|
||||
pub keep_print_faces: bool,
|
||||
/// After a live frame: its sketch, the document it left and the revision. Its links were
|
||||
/// brought up to date by that frame, so the gesture's next frame needn't project them again
|
||||
/// while nothing else has run (#3222).
|
||||
@@ -849,7 +843,6 @@ impl Session {
|
||||
project_construction: false,
|
||||
param_check: schema::ParamCheck::Off,
|
||||
easyprint_upload: None,
|
||||
keep_print_faces: true,
|
||||
live_links: None,
|
||||
active_component: 0,
|
||||
active_occurrence_path: None,
|
||||
@@ -916,7 +909,6 @@ impl Session {
|
||||
let single_surface_analysis = self.single_surface_analysis;
|
||||
let param_check = self.param_check;
|
||||
let easyprint_upload = self.easyprint_upload.take();
|
||||
let keep_print_faces = self.keep_print_faces;
|
||||
let render_viewport = self.render_viewport.clone();
|
||||
let render_jobs = self.render_jobs.scratch();
|
||||
let checkpoints = std::mem::take(&mut self.checkpoints);
|
||||
@@ -937,7 +929,6 @@ impl Session {
|
||||
self.set_single_surface_analysis(single_surface_analysis);
|
||||
self.param_check = param_check;
|
||||
self.easyprint_upload = easyprint_upload;
|
||||
self.keep_print_faces = keep_print_faces;
|
||||
self.render_viewport = render_viewport;
|
||||
self.render_jobs = render_jobs;
|
||||
self.checkpoints = checkpoints;
|
||||
@@ -2003,7 +1994,6 @@ impl Session {
|
||||
project_construction: self.project_construction,
|
||||
param_check: self.param_check,
|
||||
easyprint_upload: self.easyprint_upload.clone(),
|
||||
keep_print_faces: self.keep_print_faces,
|
||||
live_links: None,
|
||||
active_component: self.active_component,
|
||||
active_occurrence_path: self.active_occurrence_path.clone(),
|
||||
|
||||
@@ -37,8 +37,7 @@ pub struct BoxParams {
|
||||
#[serde(default, deserialize_with = "present")]
|
||||
#[schemars(with = "PointExpr3")]
|
||||
pub corner: Option<PointExpr3>,
|
||||
/// The box's centre, instead of `corner`. The box stays centred there when its length, width
|
||||
/// or height changes later.
|
||||
/// The box's centre, instead of `corner`.
|
||||
#[serde(default, deserialize_with = "present")]
|
||||
#[schemars(with = "PointExpr3")]
|
||||
pub center: Option<PointExpr3>,
|
||||
|
||||
@@ -143,7 +143,6 @@ fn placement_dependent_cross_component_cuts_still_evaluate_and_undo() {
|
||||
width: "10".into(),
|
||||
height: "4".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -160,7 +159,6 @@ fn placement_dependent_cross_component_cuts_still_evaluate_and_undo() {
|
||||
width: "2".into(),
|
||||
height: "6".into(),
|
||||
operation: Operation::Cut,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "solidcraft-format"
|
||||
description = "Versioned JSON file formats: the format tag, the registry of versions (hand-written steps, then one folder of changes per version, then the pending changes), the upgrade of an older file and the telling of a newer one"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
# The container: content-addressed blobs (SHA-256) in a ZIP package.
|
||||
sha2 = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
@@ -1,66 +0,0 @@
|
||||
# solidcraft-format
|
||||
|
||||
Versioned file formats: what an app needs to read every file it ever wrote, to open a file a
|
||||
newer build of it wrote without losing what it can't read, and to package a file with its binary
|
||||
payloads. It holds no app types; each app registers its own families of files and their versions,
|
||||
and keeps its own payload encodings.
|
||||
|
||||
A file starts with `"format": "<prefix>N"`. A family's versions are, in order: the steps written
|
||||
out by hand for its first formats, then one folder per format (`v<N>/`, one file per change, each
|
||||
with a JSON step when older files are rewritten as they open), then the changes in flight
|
||||
(`pending/`), which make the next number until a release folds them into `v<N>/`. App code only
|
||||
knows the current format: all translation lives in the steps.
|
||||
|
||||
## API
|
||||
|
||||
| Item | What it does |
|
||||
|---|---|
|
||||
| `Family { app, noun, prefix, legacy_prefix, untagged, aliases, formats }` | One kind of file: its tag, its messages, its versions |
|
||||
| `Formats { hand, folded, pending }` | A family's versions: hand-written steps, folded formats and their changes, pending changes |
|
||||
| `Part { name, step }` | One change: its file's name, and its JSON step if it has one |
|
||||
| `Step<E>` | `fn(&mut Value) -> Result<(), E>`: a JSON-level step, returning the app's error |
|
||||
| `Alias { format, name, known_as }` | Another spelling of a change name that files of one format may carry |
|
||||
| `Family::current()` | The format this build writes |
|
||||
| `Family::format_string(n)`, `Family::version_of(tag)` | A format's tag, a tag's format (the legacy prefix read too) |
|
||||
| `Family::pending_names()` | The changes in flight, written into a file's `format_pending` |
|
||||
| `Family::take_version(&mut json)` | A file's format and its `format_pending`, taken out before anything else is read; a file not of the family is refused |
|
||||
| `Family::newer(version, applied)` | `Some(Newer)` when a newer build wrote the file: a number past this build's, or a change it doesn't know |
|
||||
| `Family::upgrade(&mut json, version, applied)` | Run the steps that bring the file to this build's format, each once |
|
||||
| `Newer { format, reads_up_to, unknown_changes }` | How a file shows a newer build wrote it |
|
||||
| `FormatError` | This crate's refusals; the app's error takes it with `From` |
|
||||
| `build::lists(manifest, families)` | For a build script: the generated `FOLDED` and `PENDING` lists and a module per change file |
|
||||
| `newer::NewerFile` | What a file a newer build saved holds that this build doesn't read: `new(app, &newer)`, `notice`, `warnings`, `report`, `keep_section`, `keep_element`, `compare` (the file against what was read of it), `restore_into` (put it all back on saving), `mentions`, `needs_container`, and the container's unknown `parts` and `container` |
|
||||
| `newer::leave_out_unreadable`, `newer::read_around` | Leave out, and keep, the top-level sections a file can't be read with |
|
||||
| `newer::short`, `newer::looks_default` | A reader's error's first line; a value a writer leaves out |
|
||||
| `container::Writer` | A ZIP container being written: `part`, `blob` (content-addressed, `blobs/<sha256>.<ext>`), `room`/`space` for optional caches, `finish` |
|
||||
| `container::Reader` | One being read over a seekable source, its directory checked on opening: `document`, `limited_part`, `blob`, `limited_blob`, `has_part`, `part_names`, `hydrated` |
|
||||
| `container::Limits` | The app's budgets: entries, expanded size, per-part limits, Deflate level, file size |
|
||||
| `container::Compression`, `container::Thumbnail`, `container::digest`, `container::is_container` | Fast or compact payloads; a PNG picture keyed to its file; the SHA-256 name of bytes; the ZIP signature |
|
||||
|
||||
## Registering a family
|
||||
|
||||
The app's build script lists the folders:
|
||||
|
||||
```rust
|
||||
use solidcraft_format::build::Family;
|
||||
// format 1 is the first record, so its folded formats start at 2 (`base` is the last format before them)
|
||||
const FAMILIES: [Family; 1] = [Family { folder: "src/format", base: 1, list_prefix: "" }];
|
||||
let lists = solidcraft_format::build::lists(&manifest, &FAMILIES)?;
|
||||
// write lists.code to OUT_DIR, rerun on each of lists.watch
|
||||
```
|
||||
|
||||
and its format module includes them, with its error type:
|
||||
|
||||
```rust
|
||||
type Part = solidcraft_format::Part<AppError>;
|
||||
include!(concat!(env!("OUT_DIR"), "/format_parts.rs"));
|
||||
pub const DESIGN: solidcraft_format::Family<'static, AppError> = solidcraft_format::Family {
|
||||
app: "App", noun: "design", prefix: "app/", legacy_prefix: None, untagged: None, aliases: &[],
|
||||
formats: solidcraft_format::Formats { hand: &[], folded: FOLDED, pending: PENDING },
|
||||
};
|
||||
```
|
||||
|
||||
A change file is `v<N>/<name>.rs` or `pending/<name>.rs`, named with lowercase letters, digits and
|
||||
dashes, starting with a `//!` note (what is saved, what an older file gets, what the last release
|
||||
does with a file that has it), and with `pub fn step(v: &mut Value) -> Result<(), AppError>` when
|
||||
older files are rewritten as they open.
|
||||
@@ -1,100 +0,0 @@
|
||||
//! For a build script: the generated lists of a family's changes. Each change is one `.rs` file in
|
||||
//! the family's folder, in `v<N>/` once a release fixed its number, else in `pending/`; the lists
|
||||
//! are generated so that a change adds a file and never edits a line another change also edits.
|
||||
//!
|
||||
//! The generated code, for the app to `include!`, is per family a `FOLDED` list (each folded
|
||||
//! format and its parts) and a `PENDING` list, both prefixed by the family's `list_prefix`, then a
|
||||
//! module per change file. It names the app's `Part` type, which the including module has in scope
|
||||
//! (`type Part = solidcraft_format::Part<AppError>;`), and a change file's step as `step` (a
|
||||
//! `pub fn step(v: &mut Value) -> Result<(), AppError>`, when the file has one).
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A family's change files: their folder (relative to the crate's manifest), the last format
|
||||
/// before them, and the prefix of the family's lists.
|
||||
pub struct Family {
|
||||
pub folder: &'static str,
|
||||
pub base: u32,
|
||||
pub list_prefix: &'static str,
|
||||
}
|
||||
|
||||
/// What [`lists`] made: the code to write, and the folders the build script reruns on.
|
||||
pub struct Lists {
|
||||
pub code: String,
|
||||
pub watch: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// The `FOLDED` and `PENDING` lists of every family, and a module for each change file. Refuses
|
||||
/// versions that are not contiguous from a family's base, a change file not named with lowercase
|
||||
/// letters, digits and dashes, and one that doesn't start with a `//!` note.
|
||||
pub fn lists(manifest: &Path, families: &[Family]) -> Result<Lists, String> {
|
||||
let mut code = String::new();
|
||||
let mut mods = String::new();
|
||||
let mut watch = Vec::new();
|
||||
for family in families {
|
||||
let root = manifest.join(family.folder);
|
||||
family_lists(family, &root, &mut code, &mut mods)?;
|
||||
watch.push(root);
|
||||
}
|
||||
code.push_str(&mods);
|
||||
Ok(Lists { code, watch })
|
||||
}
|
||||
|
||||
/// A family's `FOLDED` and `PENDING` lists; its folded formats follow on from its base.
|
||||
fn family_lists(family: &Family, root: &Path, out: &mut String, mods: &mut String) -> Result<(), String> {
|
||||
let Family { folder, base, list_prefix } = family;
|
||||
let mut folded: Vec<(u32, PathBuf)> = std::fs::read_dir(root)
|
||||
.map_err(|e| format!("{}: {e}", root.display()))?
|
||||
.filter_map(|entry| {
|
||||
let path = entry.ok()?.path();
|
||||
let version = path.file_name()?.to_str()?.strip_prefix('v')?.parse().ok()?;
|
||||
path.is_dir().then_some((version, path))
|
||||
})
|
||||
.collect();
|
||||
folded.sort();
|
||||
for (i, (version, _)) in folded.iter().enumerate() {
|
||||
let expected = base + 1 + i as u32;
|
||||
if *version != expected {
|
||||
return Err(format!("{folder}/: expected v{expected}/ next, found v{version}/ (versions are contiguous)"));
|
||||
}
|
||||
}
|
||||
// Module names start with the family's (`drawing_pending_…`), so two families' changes of one
|
||||
// name never clash.
|
||||
let module = list_prefix.to_ascii_lowercase();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"/// The changes of each format after the base, oldest first (`{folder}/v<N>/`).\nconst {list_prefix}FOLDED: &[(u32, &[Part])] = &["
|
||||
);
|
||||
for (version, dir) in &folded {
|
||||
let _ = writeln!(out, " ({version}, &[");
|
||||
parts(dir, &format!("{module}v{version}"), out, mods)?;
|
||||
out.push_str(" ]),\n");
|
||||
}
|
||||
let _ = writeln!(out, "];\n\n/// The changes no release has a number for yet (`{folder}/pending/`).\nconst {list_prefix}PENDING: &[Part] = &[");
|
||||
parts(&root.join("pending"), &format!("{module}pending"), out, mods)?;
|
||||
out.push_str("];\n\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `Part`s of the change files in `dir`, in file-name order, and a module for each.
|
||||
fn parts(dir: &Path, prefix: &str, out: &mut String, mods: &mut String) -> Result<(), String> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return Ok(()) };
|
||||
let mut files: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).filter(|p| p.extension().is_some_and(|x| x == "rs")).collect();
|
||||
files.sort();
|
||||
for path in files {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default().to_string();
|
||||
if name.is_empty() || !name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
|
||||
return Err(format!("{}: name a format change file with lowercase letters, digits and dashes", path.display()));
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if !text.trim_start().starts_with("//!") {
|
||||
return Err(format!("{}: start a format change with a `//!` note", path.display()));
|
||||
}
|
||||
let module = format!("{prefix}_{}", name.replace('-', "_"));
|
||||
let step = if text.contains("fn step(") { format!("Some({module}::step)") } else { "None".into() };
|
||||
let _ = writeln!(out, " Part {{ name: {name:?}, step: {step} }},");
|
||||
let _ = writeln!(mods, "#[path = {:?}]\nmod {module};", path.display().to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
//! The file container: a ZIP package of named parts (the file's JSON, binary payloads, optional
|
||||
//! caches), never extracted to filesystem paths. Payloads are content-addressed blobs,
|
||||
//! `blobs/<sha256>.<extension>`, stored once however often they are referred to. Every size is
|
||||
//! bounded by the app's [`Limits`]: the file, each part, all parts expanded, the payloads resolved,
|
||||
//! and the number of entries; a part is read no further than its declared size (a decompression
|
||||
//! bomb stops there). What a part holds, and how a payload is encoded, is the app's.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::FormatError;
|
||||
|
||||
/// How hard the payloads are compressed: Fast favours interactive file operations; Compact trades
|
||||
/// CPU time for space.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Compression {
|
||||
#[default]
|
||||
Fast,
|
||||
Compact,
|
||||
}
|
||||
|
||||
/// An app's budgets for its containers. The functions are read each time they are needed, so a
|
||||
/// limit the app lets the user raise takes effect at once.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Limits {
|
||||
/// The most entries a container holds.
|
||||
pub max_entries: usize,
|
||||
/// The most bytes all parts may expand to, and all payloads may resolve to.
|
||||
pub max_expanded: fn() -> usize,
|
||||
/// The most bytes a part of this name may expand to.
|
||||
pub part_limit: fn(&str) -> usize,
|
||||
/// The Deflate level of a compressed part of this name and size.
|
||||
pub level: fn(&str, usize) -> i64,
|
||||
/// Whether a container of this many bytes may be read, or why not.
|
||||
pub check_size: fn(u64) -> Result<(), String>,
|
||||
}
|
||||
|
||||
fn invalid(message: impl std::fmt::Display) -> FormatError {
|
||||
FormatError::Invalid(message.to_string())
|
||||
}
|
||||
|
||||
/// Whether the bytes start as a container does, whatever the file is called.
|
||||
pub fn is_container(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"PK\x03\x04")
|
||||
}
|
||||
|
||||
/// The SHA-256 of the bytes, in lowercase hexadecimal: a blob's name, and how a cache is keyed to
|
||||
/// the file it was written for.
|
||||
pub fn digest(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
/// A container being written.
|
||||
pub struct Writer {
|
||||
zip: zip::ZipWriter<Cursor<Vec<u8>>>,
|
||||
names: BTreeSet<String>,
|
||||
expanded: usize,
|
||||
resolved: usize,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn new(limits: Limits) -> Writer {
|
||||
Writer { zip: zip::ZipWriter::new(Cursor::new(Vec::new())), names: BTreeSet::new(), expanded: 0, resolved: 0, limits }
|
||||
}
|
||||
|
||||
/// The container's bytes.
|
||||
pub fn finish(self) -> Result<Vec<u8>, FormatError> {
|
||||
Ok(self.zip.finish().map_err(invalid)?.into_inner())
|
||||
}
|
||||
|
||||
/// Add a part, compressed or stored. A name already written is written once.
|
||||
pub fn part(&mut self, name: &str, bytes: &[u8], compressed: bool) -> Result<(), FormatError> {
|
||||
if bytes.len() > (self.limits.part_limit)(name) {
|
||||
return Err(invalid("part too large"));
|
||||
}
|
||||
if !self.names.insert(name.to_owned()) {
|
||||
return Ok(());
|
||||
}
|
||||
self.expanded = self.expanded.checked_add(bytes.len()).ok_or_else(|| invalid("expanded size overflow"))?;
|
||||
if self.expanded > (self.limits.max_expanded)() || self.names.len() > self.limits.max_entries {
|
||||
return Err(invalid("package too large"));
|
||||
}
|
||||
let method = if compressed { zip::CompressionMethod::Deflated } else { zip::CompressionMethod::Stored };
|
||||
let level = (self.limits.level)(name, bytes.len());
|
||||
// A part of 4 GiB or more (only with a raised file limit) needs a ZIP64 entry.
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(method)
|
||||
.compression_level(compressed.then_some(level))
|
||||
.large_file(bytes.len() as u64 >= u64::from(u32::MAX));
|
||||
self.zip.start_file(name, options).map_err(invalid)?;
|
||||
self.zip.write_all(bytes).map_err(invalid)
|
||||
}
|
||||
|
||||
/// Would `entries` more optional parts of `extra` bytes, stored uncompressed, still keep the
|
||||
/// file within `budget` and its expanded size within the reader's budget?
|
||||
pub fn room(&self, entries: usize, extra: usize, budget: usize) -> bool {
|
||||
self.names.len().saturating_add(entries) <= self.limits.max_entries && extra <= self.space(entries, budget)
|
||||
}
|
||||
|
||||
/// The bytes `entries` more optional parts may hold together ([`Writer::room`]).
|
||||
pub fn space(&self, entries: usize, budget: usize) -> usize {
|
||||
if self.names.len().saturating_add(entries) > self.limits.max_entries {
|
||||
return 0;
|
||||
}
|
||||
// The bytes written so far, plus what the open compressed part may still hold back.
|
||||
let written = self.zip.get_ref().map_or(usize::MAX, |c| c.get_ref().len()).saturating_add(1 << 20);
|
||||
let directory = self.names.len().saturating_add(entries).saturating_mul(256).saturating_add(65_536);
|
||||
budget.saturating_sub(written.saturating_add(directory)).min((self.limits.max_expanded)().saturating_sub(self.expanded))
|
||||
}
|
||||
|
||||
/// Add a payload as a blob, `blobs/<sha256>.<extension>`, and say its name, for the JSON that
|
||||
/// refers to it.
|
||||
pub fn blob(&mut self, bytes: &[u8], extension: &str, compressed: bool) -> Result<Value, FormatError> {
|
||||
self.resolved = self
|
||||
.resolved
|
||||
.checked_add(bytes.len())
|
||||
.filter(|&n| n <= (self.limits.max_expanded)())
|
||||
.ok_or_else(|| invalid("resolved payloads too large"))?;
|
||||
let name = format!("blobs/{}.{extension}", digest(bytes));
|
||||
self.part(&name, bytes, compressed)?;
|
||||
Ok(Value::String(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// A container being read, over a seekable source. Opening checks the directory (the entry count,
|
||||
/// unique and plain names, every part's declared size), not the parts.
|
||||
pub struct Reader<R: Read + Seek> {
|
||||
zip: zip::ZipArchive<R>,
|
||||
blobs: BTreeMap<String, Vec<u8>>,
|
||||
/// The bytes of payloads resolved so far, repeats counted: bounded by the expanded budget.
|
||||
pub hydrated: usize,
|
||||
document: Option<Arc<[u8]>>,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Reader<R> {
|
||||
pub fn new(mut source: R, limits: Limits) -> Result<Self, FormatError> {
|
||||
let length = source.seek(SeekFrom::End(0)).map_err(invalid)?;
|
||||
(limits.check_size)(length).map_err(invalid)?;
|
||||
let tail_length = length.min(65_557) as usize;
|
||||
source.seek(SeekFrom::Start(length - tail_length as u64)).map_err(invalid)?;
|
||||
let mut tail = vec![0; tail_length];
|
||||
source.read_exact(&mut tail).map_err(invalid)?;
|
||||
// zip's name index can coalesce duplicate central-directory entries. The bounded format
|
||||
// does not need ZIP64/multiple disks: verify the declared entry count as well so
|
||||
// duplicate names cannot silently select a payload.
|
||||
let eocd = tail.windows(4).rposition(|v| v == b"PK\x05\x06").ok_or_else(|| invalid("missing ZIP directory"))?;
|
||||
let end = tail.get(eocd..).filter(|v| v.len() >= 22).ok_or_else(|| invalid("truncated ZIP directory"))?;
|
||||
let u16_at = |offset| -> Result<u16, FormatError> {
|
||||
let array: [u8; 2] = end.get(offset..offset + 2).ok_or_else(|| invalid("truncated ZIP directory"))?.try_into().map_err(invalid)?;
|
||||
Ok(u16::from_le_bytes(array))
|
||||
};
|
||||
let declared = u16_at(10)? as usize;
|
||||
if declared > limits.max_entries
|
||||
|| u16_at(4)? != 0
|
||||
|| u16_at(6)? != 0
|
||||
|| u16_at(8)? as usize != declared
|
||||
|| end.len() != 22 + u16_at(20)? as usize
|
||||
{
|
||||
return Err(invalid("too many entries or unsupported ZIP directory"));
|
||||
}
|
||||
// A container is a ZIP with nothing in front of it. Saying so skips zip's search for a
|
||||
// prefix, which reads back through the stored parts and would load a large payload
|
||||
// before a preview can be shown.
|
||||
let config = zip::read::Config { archive_offset: zip::read::ArchiveOffset::Known(0) };
|
||||
let mut zip = zip::ZipArchive::with_config(config, source).map_err(invalid)?;
|
||||
if u16_at(4)? != 0
|
||||
|| u16_at(6)? != 0
|
||||
|| u16_at(8)? as usize != zip.len()
|
||||
|| u16_at(10)? as usize != zip.len()
|
||||
|| end.len() != 22 + u16_at(20)? as usize
|
||||
{
|
||||
return Err(invalid("duplicate entries or unsupported ZIP directory"));
|
||||
}
|
||||
let mut names = BTreeSet::new();
|
||||
let mut total = 0u64;
|
||||
for i in 0..zip.len() {
|
||||
let part = zip.by_index_raw(i).map_err(invalid)?;
|
||||
let name = part.name().map_err(invalid)?.into_owned();
|
||||
if name.is_empty()
|
||||
|| name.contains('\\')
|
||||
|| name.split('/').any(|c| c.is_empty() || c == "." || c == "..")
|
||||
|| !names.insert(name.to_owned())
|
||||
{
|
||||
return Err(invalid("duplicate or invalid archive entry"));
|
||||
}
|
||||
total = total.checked_add(part.size()).ok_or_else(|| invalid("expanded size overflow"))?;
|
||||
let limit = (limits.part_limit)(&name);
|
||||
if part.size() > limit as u64 || total > (limits.max_expanded)() as u64 {
|
||||
return Err(invalid("expanded package too large"));
|
||||
}
|
||||
}
|
||||
Ok(Self { zip, blobs: BTreeMap::new(), hydrated: 0, document: None, limits })
|
||||
}
|
||||
|
||||
/// The file's own part, `document.json`, read once and kept: provisional caches and the
|
||||
/// authoritative reading are bound to the same bytes, even if another process overwrites the
|
||||
/// open file in place. Blobs remain hash-checked.
|
||||
pub fn document(&mut self, limit: usize) -> Result<Arc<[u8]>, FormatError> {
|
||||
if self.document.is_none() {
|
||||
self.document = Some(self.limited_part("document.json", limit)?.into());
|
||||
}
|
||||
self.document.as_ref().filter(|d| d.len() <= limit).cloned().ok_or_else(|| invalid("metadata budget"))
|
||||
}
|
||||
|
||||
/// Whether the container has a part of this name.
|
||||
pub fn has_part(&self, name: &str) -> bool {
|
||||
self.zip.index_for_name(name).is_some()
|
||||
}
|
||||
|
||||
/// The names of every part, in the container's order.
|
||||
pub fn part_names(&self) -> Vec<String> {
|
||||
self.zip.file_names().filter_map(|name| name.ok().map(|name| name.into_owned())).collect()
|
||||
}
|
||||
|
||||
/// The part of this name, at most `limit` bytes.
|
||||
pub fn limited_part(&mut self, name: &str, limit: usize) -> Result<Vec<u8>, FormatError> {
|
||||
let part = self.zip.by_name(name).map_err(|e| invalid(format!("required part `{name}`: {e}")))?;
|
||||
let expected = part.size();
|
||||
if expected > limit as u64 {
|
||||
return Err(invalid("part too large"));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
bytes.try_reserve_exact(expected as usize).map_err(|_| invalid("could not reserve required part"))?;
|
||||
// A compressed part may expand beyond its declared size (a decompression bomb): stop
|
||||
// one byte past it instead of at the limit.
|
||||
part.take(expected + 1).read_to_end(&mut bytes).map_err(invalid)?;
|
||||
if bytes.len() > limit || bytes.len() as u64 != expected {
|
||||
return Err(invalid("part too large or length mismatch"));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// The blob `value`'s `field` names, of this extension, within its part limit.
|
||||
pub fn blob(&mut self, value: &Value, field: &str, extension: &str) -> Result<&[u8], FormatError> {
|
||||
let limit = (self.limits.part_limit)(&format!("blob.{extension}"));
|
||||
self.limited_blob(value, field, extension, limit)
|
||||
}
|
||||
|
||||
/// [`Reader::blob`] within `limit` bytes. Its hash is checked when it is first read, and every
|
||||
/// reference counts against the resolved budget.
|
||||
pub fn limited_blob(&mut self, value: &Value, field: &str, extension: &str, limit: usize) -> Result<&[u8], FormatError> {
|
||||
let name = value.get(field).and_then(Value::as_str).ok_or_else(|| invalid(format!("missing `{field}` reference")))?;
|
||||
let hash = name
|
||||
.strip_prefix("blobs/")
|
||||
.and_then(|n| n.strip_suffix(&format!(".{extension}")))
|
||||
.filter(|h| h.len() == 64 && h.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)))
|
||||
.ok_or_else(|| invalid("invalid blob reference"))?;
|
||||
if !self.blobs.contains_key(name) {
|
||||
let bytes = self.limited_part(name, limit)?;
|
||||
if digest(&bytes) != hash {
|
||||
return Err(invalid(format!("blob hash mismatch: {name}")));
|
||||
}
|
||||
self.blobs.insert(name.to_owned(), bytes);
|
||||
}
|
||||
let bytes = self.blobs.get(name).ok_or_else(|| invalid("missing blob"))?;
|
||||
if bytes.len() > limit {
|
||||
return Err(invalid("blob exceeds its resource budget"));
|
||||
}
|
||||
// Count repeated references too: a tiny archive must not expand into unbounded owned
|
||||
// strings or arrays when the same blob is referenced repeatedly.
|
||||
self.hydrated = self
|
||||
.hydrated
|
||||
.checked_add(bytes.len())
|
||||
.filter(|&n| n <= (self.limits.max_expanded)())
|
||||
.ok_or_else(|| invalid("resolved payloads too large"))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// A small PNG picture of the file, kept in the container as a disposable cache keyed to the
|
||||
/// exact file it shows: `cache/thumbnail.json` (its key) and `cache/thumbnail.png`. Reading it
|
||||
/// never resolves a payload; a picture that isn't one, or is stale, is a miss.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Thumbnail {
|
||||
/// The key's format tag.
|
||||
pub format: &'static str,
|
||||
/// The build that wrote it: only the same build reads it back.
|
||||
pub app: &'static str,
|
||||
/// Its width and height in pixels.
|
||||
pub size: [usize; 2],
|
||||
/// The most bytes it may have.
|
||||
pub max_bytes: usize,
|
||||
}
|
||||
|
||||
impl Thumbnail {
|
||||
/// Whether the bytes start as a PNG of this size within the byte budget (not a decode: a
|
||||
/// consumer decodes with its own dimension and allocation limits).
|
||||
pub fn valid(&self, bytes: &[u8]) -> bool {
|
||||
bytes.len() <= self.max_bytes
|
||||
&& bytes.get(..8) == Some(b"\x89PNG\r\n\x1a\n")
|
||||
&& bytes.get(8..16) == Some(b"\0\0\0\rIHDR")
|
||||
&& bytes.get(16..20) == Some((self.size[0] as u32).to_be_bytes().as_slice())
|
||||
&& bytes.get(20..24) == Some((self.size[1] as u32).to_be_bytes().as_slice())
|
||||
}
|
||||
|
||||
/// Add the picture, keyed to `document` (the file's own part), when it is one and there is
|
||||
/// room for it within `budget`; otherwise leave it out, never failing the save.
|
||||
pub fn write(&self, writer: &mut Writer, png: &[u8], document: &[u8], budget: usize) -> Result<(), FormatError> {
|
||||
if !self.valid(png) {
|
||||
return Ok(());
|
||||
}
|
||||
let key =
|
||||
serde_json::to_vec(&serde_json::json!({"format": self.format, "app": self.app, "document": digest(document), "payload": digest(png)}))
|
||||
.map_err(invalid)?;
|
||||
if !writer.room(2, key.len().saturating_add(png.len()), budget) {
|
||||
return Ok(());
|
||||
}
|
||||
writer.part("cache/thumbnail.json", &key, false)?;
|
||||
writer.part("cache/thumbnail.png", png, false)
|
||||
}
|
||||
|
||||
/// The picture, when this build wrote it for exactly the file's own part (`document.json`,
|
||||
/// read within `document_limit`).
|
||||
pub fn read<R: Read + Seek>(&self, reader: &mut Reader<R>, document_limit: usize) -> Result<Vec<u8>, FormatError> {
|
||||
let key: Value = serde_json::from_slice(&reader.limited_part("cache/thumbnail.json", 4096)?).map_err(invalid)?;
|
||||
let document = reader.document(document_limit)?;
|
||||
if key.get("format").and_then(Value::as_str) != Some(self.format)
|
||||
|| key.get("app").and_then(Value::as_str) != Some(self.app)
|
||||
|| key.get("document").and_then(Value::as_str) != Some(digest(&document).as_str())
|
||||
{
|
||||
return Err(invalid("thumbnail key"));
|
||||
}
|
||||
let png = reader.limited_part("cache/thumbnail.png", self.max_bytes)?;
|
||||
if !self.valid(&png) || key.get("payload").and_then(Value::as_str) != Some(digest(&png).as_str()) {
|
||||
return Err(invalid("thumbnail payload"));
|
||||
}
|
||||
Ok(png)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "container_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,66 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
const LIMITS: Limits = Limits {
|
||||
max_entries: 8,
|
||||
max_expanded: || 1 << 20,
|
||||
part_limit: |name| if name.ends_with(".big") { 1 << 20 } else { 1024 },
|
||||
level: |_, _| 1,
|
||||
check_size: |length| if length <= 1 << 20 { Ok(()) } else { Err("too large".into()) },
|
||||
};
|
||||
|
||||
/// Parts and blobs written are read back as they were; a blob is stored once, named by its hash,
|
||||
/// and checked against it.
|
||||
#[test]
|
||||
fn parts_and_blobs_read_back() {
|
||||
let mut writer = Writer::new(LIMITS);
|
||||
writer.part("document.json", br#"{"a":1}"#, true).unwrap();
|
||||
let first = writer.blob(b"payload", "bin", false).unwrap();
|
||||
let again = writer.blob(b"payload", "bin", true).unwrap();
|
||||
assert_eq!(first, again, "the same payload is one blob");
|
||||
assert_eq!(first, Value::String(format!("blobs/{}.bin", digest(b"payload"))));
|
||||
let bytes = writer.finish().unwrap();
|
||||
assert!(is_container(&bytes));
|
||||
let mut reader = Reader::new(Cursor::new(bytes), LIMITS).unwrap();
|
||||
assert_eq!(reader.part_names().len(), 2);
|
||||
assert!(reader.has_part("document.json") && !reader.has_part("other.json"));
|
||||
assert_eq!(&*reader.document(64).unwrap(), br#"{"a":1}"#);
|
||||
let reference = serde_json::json!({"blob": first});
|
||||
assert_eq!(reader.blob(&reference, "blob", "bin").unwrap(), b"payload");
|
||||
assert!(reader.blob(&reference, "blob", "png").is_err(), "another extension");
|
||||
assert_eq!(reader.hydrated, 7);
|
||||
reader.hydrated = (LIMITS.max_expanded)();
|
||||
assert!(reader.blob(&reference, "blob", "bin").unwrap_err().to_string().contains("resolved payloads too large"));
|
||||
}
|
||||
|
||||
/// A part past its limit, or more entries than the limit, are refused as they are written.
|
||||
#[test]
|
||||
fn budgets_are_kept_when_writing() {
|
||||
let mut writer = Writer::new(LIMITS);
|
||||
assert_eq!(writer.part("small.json", &[0; 1025], false), Err(FormatError::Invalid("part too large".into())));
|
||||
assert!(writer.part("large.big", &[0; 1025], true).is_ok());
|
||||
for i in 0..7 {
|
||||
writer.part(&format!("{i}.json"), b"x", false).unwrap();
|
||||
}
|
||||
assert_eq!(writer.part("one-more.json", b"x", false), Err(FormatError::Invalid("package too large".into())));
|
||||
assert!(!writer.room(1, 0, 1 << 20), "no room for an entry past the limit");
|
||||
}
|
||||
|
||||
/// A thumbnail is kept only for the file it shows.
|
||||
#[test]
|
||||
fn a_thumbnail_is_keyed_to_its_file() {
|
||||
let picture = Thumbnail { format: "app-thumbnail/1", app: "1.0", size: [2, 1], max_bytes: 1024 };
|
||||
let mut png = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
|
||||
png.extend_from_slice(&2u32.to_be_bytes());
|
||||
png.extend_from_slice(&1u32.to_be_bytes());
|
||||
assert!(picture.valid(&png));
|
||||
let mut writer = Writer::new(LIMITS);
|
||||
writer.part("document.json", b"{}", false).unwrap();
|
||||
picture.write(&mut writer, &png, b"{}", 8 << 20).unwrap();
|
||||
let mut reader = Reader::new(Cursor::new(writer.finish().unwrap()), LIMITS).unwrap();
|
||||
assert_eq!(picture.read(&mut reader, 64).unwrap(), png);
|
||||
let mut writer = Writer::new(LIMITS);
|
||||
writer.part("document.json", b"{\"changed\":1}", false).unwrap();
|
||||
picture.write(&mut writer, &png, b"{}", 8 << 20).unwrap();
|
||||
let mut reader = Reader::new(Cursor::new(writer.finish().unwrap()), LIMITS).unwrap();
|
||||
assert!(picture.read(&mut reader, 64).is_err(), "stale");
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
//! Versioned file formats: what an app needs to read every file it ever wrote, and to open a file
|
||||
//! a newer build of it wrote.
|
||||
//!
|
||||
//! A file starts with `"format": "<prefix>N"`. A [`Family`] is one kind of file (an app's designs,
|
||||
//! its drawings): its tag, its own numbers, and how a file of an older number is brought up to
|
||||
//! this build's, step by step (N → N+1 → … → current). The app's code only knows the current
|
||||
//! format; all translation lives in the steps:
|
||||
//! - the steps written out by hand for the first formats, one per format ([`Formats::hand`]);
|
||||
//! - then one folder per format, `v<N>/`, one file per change in it ([`Part`]), each with a step
|
||||
//! when files from before the change are rewritten as they open ([`Formats::folded`]);
|
||||
//! - then the changes no release has a number for yet, in `pending/` ([`Formats::pending`]):
|
||||
//! while there are any, the build writes the next number, with their names in the file's
|
||||
//! `format_pending`, so a later build runs just the steps a file is missing. A release moves
|
||||
//! them into the next `v<N>/`. [`build`] lists the folders for a build script.
|
||||
//!
|
||||
//! A file a newer build wrote says so by a number past this build's or a change name this build
|
||||
//! doesn't have ([`Family::newer`]); the app then reads what it can and keeps the rest
|
||||
//! ([`newer`]). A file is saved as JSON, or in a [`container`] with its binary payloads.
|
||||
//!
|
||||
//! Steps are JSON-level (the file's shape) and return the app's own error type; every error this
|
||||
//! crate makes is a [`FormatError`], which the app's error type takes `From`.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
pub mod build;
|
||||
pub mod container;
|
||||
pub mod newer;
|
||||
|
||||
/// Why a file is refused: its text says what is wrong, as the user reads it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FormatError {
|
||||
/// The file isn't one of the family's, or it is damaged.
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FormatError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FormatError::Invalid(why) => f.write_str(why),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FormatError {}
|
||||
|
||||
/// One JSON-level upgrade step: rewrites a file in place, or says why it can't.
|
||||
pub type Step<E> = fn(&mut Value) -> Result<(), E>;
|
||||
|
||||
/// One change to a family's file format: a file in its `v<N>/` or `pending/` folder.
|
||||
pub struct Part<E> {
|
||||
/// The file's name, which a file written while the change was pending lists.
|
||||
pub name: &'static str,
|
||||
/// How a file from before the change is rewritten as it opens, if it is.
|
||||
pub step: Option<Step<E>>,
|
||||
}
|
||||
|
||||
/// Another spelling of a change name that a file of one format may carry (a build that named the
|
||||
/// change otherwise): read as the change it stands for, never written.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Alias {
|
||||
/// The format whose files may say it.
|
||||
pub format: u32,
|
||||
/// The spelling in the file.
|
||||
pub name: &'static str,
|
||||
/// The change it stands for.
|
||||
pub known_as: &'static str,
|
||||
}
|
||||
|
||||
/// A family's upgrade path: the steps written out by hand, the folded formats after them (each a
|
||||
/// list of changes) and the pending changes, which make the next format.
|
||||
pub struct Formats<'a, E> {
|
||||
/// Entry `i` turns format `i + 1` into `i + 2`.
|
||||
pub hand: &'a [Step<E>],
|
||||
/// Each format after the hand-written ones, oldest first, and its changes.
|
||||
pub folded: &'a [(u32, &'a [Part<E>])],
|
||||
/// The changes no release has a number for yet.
|
||||
pub pending: &'a [Part<E>],
|
||||
}
|
||||
|
||||
/// A kind of file whose format is versioned: its own format tag, its own numbers and its own
|
||||
/// changes. Reading a file's format, telling a file a newer build saved and the upgrade are the
|
||||
/// same for every family.
|
||||
pub struct Family<'a, E> {
|
||||
/// The app, as messages name it.
|
||||
pub app: &'static str,
|
||||
/// What a file of the family is called in messages.
|
||||
pub noun: &'static str,
|
||||
/// The tag before the number in a file's `format`.
|
||||
pub prefix: &'static str,
|
||||
/// The tag files saved under an older name carry: read, never written.
|
||||
pub legacy_prefix: Option<&'static str>,
|
||||
/// The format of a file that names none, if such files exist.
|
||||
pub untagged: Option<u32>,
|
||||
/// Other spellings of change names that files may carry.
|
||||
pub aliases: &'a [Alias],
|
||||
/// The upgrade path.
|
||||
pub formats: Formats<'a, E>,
|
||||
}
|
||||
|
||||
/// How a file shows a newer build saved it ([`Family::newer`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Newer {
|
||||
/// The number the file says.
|
||||
pub format: u32,
|
||||
/// The number this build reads up to.
|
||||
pub reads_up_to: u32,
|
||||
/// The changes the file names that this build doesn't have.
|
||||
pub unknown_changes: Vec<String>,
|
||||
}
|
||||
|
||||
impl<E> Formats<'_, E> {
|
||||
/// The format this build writes: the last folded one, or the next while changes are pending.
|
||||
pub const fn current(&self) -> u32 {
|
||||
self.hand.len() as u32 + 1 + self.folded.len() as u32 + if self.pending.is_empty() { 0 } else { 1 }
|
||||
}
|
||||
|
||||
/// The changes format `version` is made of, if it is made of changes.
|
||||
fn parts(&self, version: u32) -> Option<&[Part<E>]> {
|
||||
let after = version.checked_sub(self.hand.len() as u32 + 2)? as usize;
|
||||
match self.folded.get(after) {
|
||||
Some((_, parts)) => Some(parts),
|
||||
None if after == self.folded.len() && !self.pending.is_empty() => Some(self.pending),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a change of this name is anywhere in this build's history.
|
||||
fn knows_part_name(&self, name: &str) -> bool {
|
||||
self.folded.iter().flat_map(|(_, parts)| *parts).chain(self.pending).any(|part| part.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Family<'_, E> {
|
||||
/// The format this build writes: the last folded one, or the next while changes are pending.
|
||||
pub const fn current(&self) -> u32 {
|
||||
self.formats.current()
|
||||
}
|
||||
|
||||
/// What a file of the family is called in messages.
|
||||
pub const fn noun(&self) -> &'static str {
|
||||
self.noun
|
||||
}
|
||||
|
||||
/// The names of the pending changes this build writes into a file (`format_pending`).
|
||||
pub fn pending_names(&self) -> Vec<String> {
|
||||
self.formats.pending.iter().map(|p| p.name.to_string()).collect()
|
||||
}
|
||||
|
||||
/// The format tag of format `n`.
|
||||
pub fn format_string(&self, n: u32) -> String {
|
||||
format!("{}{n}", self.prefix)
|
||||
}
|
||||
|
||||
/// The version in one of this family's format tags; `None` for any other tag, another
|
||||
/// family's included.
|
||||
pub fn version_of(&self, s: &str) -> Option<u32> {
|
||||
s.strip_prefix(self.prefix).or_else(|| self.legacy_prefix.and_then(|p| s.strip_prefix(p)))?.parse().ok()
|
||||
}
|
||||
|
||||
/// The change a name a file of format `version` lists stands for: its alias's, or itself.
|
||||
/// An alias counts only while this build has the change it stands for, so recognising it
|
||||
/// never grants support to another reader.
|
||||
fn canonical_applied_name<'name>(&self, version: u32, name: &'name str) -> &'name str {
|
||||
match self.aliases.iter().find(|a| a.format == version && a.name == name) {
|
||||
Some(alias) if self.formats.knows_part_name(alias.known_as) => alias.known_as,
|
||||
_ => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// What says a file of format `version` with the changes `applied` came from a newer build:
|
||||
/// a number past this build's, or a change this build doesn't have anywhere in its history
|
||||
/// (a branch's pending change may have landed at a later number). A list beside a format that
|
||||
/// isn't made of changes means nothing (a tool or a test set an older number on a newer file).
|
||||
/// `None` for a file this build fully knows.
|
||||
pub fn newer(&self, version: u32, applied: Option<&[String]>) -> Option<Newer> {
|
||||
let current = self.current();
|
||||
let listed = applied.filter(|_| version > current || self.formats.parts(version).is_some()).unwrap_or_default();
|
||||
let unknown_changes: Vec<String> =
|
||||
listed.iter().filter(|name| !self.formats.knows_part_name(self.canonical_applied_name(version, name))).cloned().collect();
|
||||
(version > current || !unknown_changes.is_empty()).then_some(Newer { format: version, reads_up_to: current, unknown_changes })
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: From<FormatError>> Family<'_, E> {
|
||||
/// Read a file's format and take its `format_pending` out of its JSON, before anything else
|
||||
/// in it is read: a file that isn't this family's is refused here. One a newer build saved is
|
||||
/// not: [`Family::newer`] says so.
|
||||
pub fn take_version(&self, v: &mut Value) -> Result<(u32, Option<Vec<String>>), E> {
|
||||
let (app, noun) = (self.app, self.noun);
|
||||
let not_ours = |what: String| E::from(FormatError::Invalid(format!("not a {app} {noun} ({what})")));
|
||||
let Some(obj) = v.as_object() else { return Err(not_ours("expected a JSON object".into())) };
|
||||
let version = match obj.get("format") {
|
||||
None => self.untagged.ok_or_else(|| not_ours("no format".into()))?,
|
||||
Some(Value::String(f)) => self.version_of(f).ok_or_else(|| not_ours(format!("format `{f}`")))?,
|
||||
Some(_) => return Err(not_ours("format is not a string".into())),
|
||||
};
|
||||
let applied = take_names(v, "format_pending", noun).map_err(E::from)?;
|
||||
if version == 0 {
|
||||
return Err(not_ours("format 0".into()));
|
||||
}
|
||||
Ok((version, applied))
|
||||
}
|
||||
|
||||
/// Bring a file of format `version` up to this build's; `applied` is its `format_pending`
|
||||
/// (both from [`Family::take_version`]): the changes of its format it already has, when it
|
||||
/// was written while they were pending. The others' steps run first, and already-applied
|
||||
/// changes never run again at their later number. A file past this build's number has no
|
||||
/// steps to run.
|
||||
pub fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<(), E> {
|
||||
let formats = &self.formats;
|
||||
if version == 0 {
|
||||
return Err(E::from(FormatError::Invalid(format!("not a {} {} (format 0)", self.app, self.noun))));
|
||||
}
|
||||
// A list beside a format that isn't made of changes is ignored, like any field an upgrade
|
||||
// drops.
|
||||
let applied = applied.filter(|_| formats.parts(version).is_some());
|
||||
let already_applied =
|
||||
|part: &Part<E>| applied.is_some_and(|names| names.iter().any(|name| self.canonical_applied_name(version, name) == part.name));
|
||||
if applied.is_some() {
|
||||
for part in formats.parts(version).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
|
||||
part.step.map_or(Ok(()), |step| step(v))?;
|
||||
}
|
||||
}
|
||||
for from in version..self.current() {
|
||||
match formats.hand.get(from as usize - 1) {
|
||||
Some(step) => step(v)?,
|
||||
None => {
|
||||
for part in formats.parts(from + 1).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
|
||||
part.step.map_or(Ok(()), |step| step(v))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a short list of names out of a file's JSON: its `format_pending` (the pending changes it
|
||||
/// has).
|
||||
fn take_names(v: &mut Value, key: &str, noun: &str) -> Result<Option<Vec<String>>, FormatError> {
|
||||
let Some(list) = v.as_object_mut().and_then(|o| o.remove(key)) else { return Ok(None) };
|
||||
let invalid = || FormatError::Invalid(format!("damaged {noun}: `{key}` is not a short list of names"));
|
||||
let list = list.as_array().filter(|l| l.len() <= 1000).ok_or_else(invalid)?;
|
||||
list.iter().map(|n| n.as_str().filter(|n| n.len() <= 128).map(str::to_string).ok_or_else(invalid)).collect::<Result<_, _>>().map(Some)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lib_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,134 +0,0 @@
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// The app's error, as an app has one.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct AppError(String);
|
||||
|
||||
impl From<FormatError> for AppError {
|
||||
fn from(e: FormatError) -> Self {
|
||||
AppError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Each step appends its name to the file's `log`, so the order steps ran in can be read back.
|
||||
fn log(v: &mut Value, step: &str) {
|
||||
if let Some(list) = v.get_mut("log").and_then(Value::as_array_mut) {
|
||||
list.push(json!(step));
|
||||
}
|
||||
}
|
||||
|
||||
const HAND: [Step<AppError>; 2] = [
|
||||
|v| {
|
||||
log(v, "1to2");
|
||||
Ok(())
|
||||
},
|
||||
|v| {
|
||||
log(v, "2to3");
|
||||
Ok(())
|
||||
},
|
||||
];
|
||||
const V4: [Part<AppError>; 2] = [
|
||||
Part {
|
||||
name: "a",
|
||||
step: Some(|v| {
|
||||
log(v, "a");
|
||||
Ok(())
|
||||
}),
|
||||
},
|
||||
Part { name: "b", step: None },
|
||||
];
|
||||
const V5: [Part<AppError>; 1] = [Part {
|
||||
name: "c",
|
||||
step: Some(|v| {
|
||||
log(v, "c");
|
||||
Ok(())
|
||||
}),
|
||||
}];
|
||||
const FOLDED: [(u32, &[Part<AppError>]); 2] = [(4, &V4), (5, &V5)];
|
||||
const PENDING: [Part<AppError>; 2] = [
|
||||
Part {
|
||||
name: "d",
|
||||
step: Some(|v| {
|
||||
log(v, "d");
|
||||
Ok(())
|
||||
}),
|
||||
},
|
||||
Part {
|
||||
name: "e",
|
||||
step: Some(|v| {
|
||||
log(v, "e");
|
||||
Ok(())
|
||||
}),
|
||||
},
|
||||
];
|
||||
const ALIASES: [Alias; 1] = [Alias { format: 5, name: "old-c", known_as: "c" }];
|
||||
|
||||
/// Format 1 untagged; hand steps to 3; folded 4 and 5; pending changes make 6.
|
||||
const FAMILY: Family<'static, AppError> = Family {
|
||||
app: "App",
|
||||
noun: "design",
|
||||
prefix: "app/",
|
||||
legacy_prefix: Some("old/"),
|
||||
untagged: Some(1),
|
||||
aliases: &ALIASES,
|
||||
formats: Formats { hand: &HAND, folded: &FOLDED, pending: &PENDING },
|
||||
};
|
||||
|
||||
fn steps_run(version: u32, applied: Option<&[String]>) -> Vec<String> {
|
||||
let mut v = json!({"log": []});
|
||||
FAMILY.upgrade(&mut v, version, applied).unwrap();
|
||||
v["log"].as_array().unwrap().iter().map(|s| s.as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tags_and_numbers() {
|
||||
assert_eq!(FAMILY.current(), 6);
|
||||
assert_eq!(FAMILY.format_string(6), "app/6");
|
||||
assert_eq!(FAMILY.version_of("app/4"), Some(4));
|
||||
assert_eq!(FAMILY.version_of("old/3"), Some(3), "the legacy tag is read");
|
||||
assert_eq!(FAMILY.version_of("other/3"), None);
|
||||
assert_eq!(FAMILY.pending_names(), vec!["d".to_string(), "e".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_taken_apart_or_refused_with_the_apps_words() {
|
||||
let mut v = json!({"format": "app/6", "format_pending": ["d"], "x": 1});
|
||||
assert_eq!(FAMILY.take_version(&mut v), Ok((6, Some(vec!["d".to_string()]))));
|
||||
assert!(v.get("format_pending").is_none(), "taken out");
|
||||
assert_eq!(FAMILY.take_version(&mut json!({})), Ok((1, None)), "untagged");
|
||||
let refused = |mut v: Value| FAMILY.take_version(&mut v).unwrap_err().0;
|
||||
assert_eq!(refused(json!([])), "not a App design (expected a JSON object)");
|
||||
assert_eq!(refused(json!({"format": "other/2"})), "not a App design (format `other/2`)");
|
||||
assert_eq!(refused(json!({"format": 2})), "not a App design (format is not a string)");
|
||||
assert_eq!(refused(json!({"format": "app/0"})), "not a App design (format 0)");
|
||||
assert_eq!(refused(json!({"format": "app/2", "format_pending": [1]})), "damaged design: `format_pending` is not a short list of names");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_step_runs_once_in_order() {
|
||||
assert_eq!(steps_run(1, None), ["1to2", "2to3", "a", "c", "d", "e"]);
|
||||
assert_eq!(steps_run(4, None), ["c", "d", "e"]);
|
||||
assert_eq!(steps_run(6, None), Vec::<String>::new());
|
||||
// A file written while `d` was pending has it: only `e` runs.
|
||||
assert_eq!(steps_run(6, Some(&["d".to_string()])), ["e"]);
|
||||
// A file of format 5 written while `c` was still pending, without it: `c` runs first.
|
||||
assert_eq!(steps_run(5, Some(&[])), ["c", "d", "e"]);
|
||||
// Under its other spelling `c` counts as had.
|
||||
assert_eq!(steps_run(5, Some(&["old-c".to_string()])), ["d", "e"]);
|
||||
// A list beside a hand-written format means nothing.
|
||||
assert_eq!(steps_run(2, Some(&["d".to_string()])), ["2to3", "a", "c", "d", "e"]);
|
||||
let mut v = json!({});
|
||||
assert_eq!(FAMILY.upgrade(&mut v, 0, None), Err(AppError("not a App design (format 0)".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_newer_file_is_told_by_its_number_or_an_unknown_change() {
|
||||
assert_eq!(FAMILY.newer(6, None), None);
|
||||
assert_eq!(FAMILY.newer(6, Some(&["d".to_string()])), None);
|
||||
assert_eq!(FAMILY.newer(7, None), Some(Newer { format: 7, reads_up_to: 6, unknown_changes: Vec::new() }));
|
||||
assert_eq!(FAMILY.newer(6, Some(&["z".to_string()])), Some(Newer { format: 6, reads_up_to: 6, unknown_changes: vec!["z".to_string()] }));
|
||||
assert_eq!(FAMILY.newer(5, Some(&["old-c".to_string()])), None, "an alias of a known change");
|
||||
assert_eq!(FAMILY.newer(4, Some(&["old-c".to_string()])), Some(Newer { format: 4, reads_up_to: 6, unknown_changes: vec!["old-c".to_string()] }));
|
||||
assert_eq!(FAMILY.newer(2, Some(&["z".to_string()])), None, "a list beside a hand-written format means nothing");
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
//! A file a newer build saved: it opens, with a warning, and saving it loses nothing.
|
||||
//!
|
||||
//! A newer format number, or a change the file names that this build doesn't have, says a newer
|
||||
//! build saved the file ([`crate::Family::newer`]). It may use nothing this build lacks, so the app
|
||||
//! reads it as usual. When part of it can't be read, that part stays out of the way and the rest
|
||||
//! opens: the app reads the elements of its lists it can ([`NewerFile::keep_element`]), then any
|
||||
//! other top-level section it can't read is left out whole ([`leave_out_unreadable`],
|
||||
//! [`read_around`]), and a field it doesn't know, which typed reading would drop, is noticed by
|
||||
//! comparing the file with what was read ([`NewerFile::compare`]).
|
||||
//!
|
||||
//! All of these are named in [`NewerFile::not_understood`] and go back into the file when it is
|
||||
//! saved ([`NewerFile::restore_into`]), as they came. What can't be put back because the file was
|
||||
//! changed around it is what `restore_into` returns.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::Newer;
|
||||
|
||||
/// The most sections left out one at a time to find the one a file can't be read with.
|
||||
const MAX_SECTION_TRIES: usize = 64;
|
||||
|
||||
/// What the file says and holds that this build doesn't read.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct NewerFile {
|
||||
/// The app, as the notice names it.
|
||||
pub app: &'static str,
|
||||
/// The number the file says.
|
||||
pub format: u32,
|
||||
/// The number this build reads up to.
|
||||
pub reads_up_to: u32,
|
||||
/// The changes the file names that this build doesn't have.
|
||||
pub unknown_changes: Vec<String>,
|
||||
/// What this build couldn't read, by name.
|
||||
pub not_understood: Vec<String>,
|
||||
/// Fields known records lost in typed reading.
|
||||
kept: Option<Kept>,
|
||||
/// Top-level sections left out whole.
|
||||
sections: Vec<Section>,
|
||||
/// Elements left out of a top-level list: its key and the element.
|
||||
elements: Vec<(String, Value)>,
|
||||
/// The parts of a container this build has no use for: name and bytes. A blob only one of the
|
||||
/// kept pieces refers to is one of them.
|
||||
pub parts: Vec<(String, Arc<[u8]>)>,
|
||||
/// The file's `container.json`, when this build doesn't know what it says.
|
||||
pub container: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Section {
|
||||
key: String,
|
||||
value: Value,
|
||||
/// This build's record of the section, without the file's: while it is unchanged, the file's
|
||||
/// goes back.
|
||||
empty: Option<Value>,
|
||||
}
|
||||
|
||||
/// The fields of a record that typed reading dropped.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Kept {
|
||||
Field(Value),
|
||||
Fields(BTreeMap<String, Kept>),
|
||||
Items(Vec<(Anchor, Kept)>),
|
||||
}
|
||||
|
||||
/// Which element of a list some kept fields belong to.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Anchor {
|
||||
Id(u64),
|
||||
Name(String),
|
||||
/// Its place, good only while the list keeps its length.
|
||||
Index {
|
||||
at: usize,
|
||||
of: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl NewerFile {
|
||||
/// A file of `app` that `newer` says a newer build saved, nothing left out of it yet.
|
||||
pub fn new(app: &'static str, newer: &Newer) -> NewerFile {
|
||||
NewerFile {
|
||||
app,
|
||||
format: newer.format,
|
||||
reads_up_to: newer.reads_up_to,
|
||||
unknown_changes: newer.unknown_changes.clone(),
|
||||
..NewerFile::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// What to tell the user, whatever else is said.
|
||||
pub fn notice(&self) -> String {
|
||||
let (app, reads, format) = (self.app, self.reads_up_to, self.format);
|
||||
match self.unknown_changes.first() {
|
||||
Some(change) if format <= reads => {
|
||||
format!("Saved by a newer {app} (format {format} with `{change}`, which this version doesn't know). If something's missing, update.")
|
||||
}
|
||||
_ => format!("Saved by a newer {app} (format {format}, this version reads up to {reads}). If something's missing, update."),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every line to report: the notice, then what couldn't be read, by name.
|
||||
pub fn warnings(&self) -> Vec<String> {
|
||||
let unread = self.not_understood.iter().map(|what| format!("Not read: {what}. Saving keeps it."));
|
||||
std::iter::once(self.notice()).chain(unread).collect()
|
||||
}
|
||||
|
||||
/// What a reply says about it.
|
||||
pub fn report(&self) -> Value {
|
||||
json!({
|
||||
"format": self.format,
|
||||
"reads_up_to": self.reads_up_to,
|
||||
"unknown_changes": self.unknown_changes,
|
||||
"not_understood": self.not_understood,
|
||||
"notice": self.notice(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the next save needs a container to keep what the file holds: plain JSON has no room
|
||||
/// for parts beside the file's own.
|
||||
pub fn needs_container(&self) -> bool {
|
||||
!self.parts.is_empty()
|
||||
}
|
||||
|
||||
/// Whether anything kept back names this text (a blob's name in a container).
|
||||
pub fn mentions(&self, text: &str) -> bool {
|
||||
fn kept(k: &Kept, text: &str) -> bool {
|
||||
match k {
|
||||
Kept::Field(v) => value_mentions(v, text),
|
||||
Kept::Fields(m) => m.values().any(|x| kept(x, text)),
|
||||
Kept::Items(l) => l.iter().any(|(_, x)| kept(x, text)),
|
||||
}
|
||||
}
|
||||
self.kept.as_ref().is_some_and(|k| kept(k, text))
|
||||
|| self.sections.iter().any(|s| value_mentions(&s.value, text))
|
||||
|| self.elements.iter().any(|(_, v)| value_mentions(v, text))
|
||||
}
|
||||
|
||||
/// Say that a section the reader couldn't finish reading is left out. `empty` is what the
|
||||
/// app writes for the section while it holds nothing, if anything.
|
||||
pub fn keep_section(&mut self, key: &str, value: Value, empty: Option<Value>, why: &str) {
|
||||
self.not_understood.push(format!("section `{key}` ({why})"));
|
||||
self.sections.push(Section { key: key.into(), value, empty });
|
||||
}
|
||||
|
||||
/// Say that an element of the top-level list `key` is left out, as `note` names it; it goes
|
||||
/// back at the end of the list when the file is saved.
|
||||
pub fn keep_element(&mut self, key: &str, element: Value, note: String) {
|
||||
self.not_understood.push(note);
|
||||
self.elements.push((key.into(), element));
|
||||
}
|
||||
|
||||
/// Compare the file as it reads (`file`, after the same upgrade as what was read) with what was
|
||||
/// read of it (`known`, its typed record written out): what `known` lacks is kept and named.
|
||||
/// An object whose `type` is `placeholder_tag` (an element the app kept whole) and the fields
|
||||
/// in `container_only` (what a container adds beside the file's own) are not compared; nor
|
||||
/// are the top level's `format` and `format_pending`.
|
||||
pub fn compare(&mut self, file: &Value, known: &Value, placeholder_tag: &str, container_only: &[&str]) {
|
||||
let mut paths = Vec::new();
|
||||
self.kept = vanished(file, known, "", &mut paths, placeholder_tag, container_only);
|
||||
self.not_understood.extend(paths);
|
||||
for section in &mut self.sections {
|
||||
section.empty = known.get(§ion.key).cloned();
|
||||
}
|
||||
}
|
||||
|
||||
/// Write back what was kept of the file into `value`, the file as this build writes it.
|
||||
/// Returns what couldn't go back, because the file was changed around it.
|
||||
pub fn restore_into(&self, value: &mut Value) -> Vec<String> {
|
||||
let mut lost = Vec::new();
|
||||
if let Some(kept) = &self.kept {
|
||||
apply(kept, value, "", &mut lost);
|
||||
}
|
||||
if let Some(top) = value.as_object_mut() {
|
||||
for section in &self.sections {
|
||||
if top.get(§ion.key) == section.empty.as_ref() {
|
||||
top.insert(section.key.clone(), section.value.clone());
|
||||
} else {
|
||||
lost.push(format!("section `{}`, replaced by what was edited here", section.key));
|
||||
}
|
||||
}
|
||||
for (key, element) in &self.elements {
|
||||
let name = element.get("name");
|
||||
match top.entry(key.clone()).or_insert_with(|| json!([])) {
|
||||
Value::Array(list) if name.is_none() || !list.iter().any(|x| x.get("name") == name) => list.push(element.clone()),
|
||||
_ => lost.push(format!("`{key}` element {}, replaced by what was made here", name.and_then(Value::as_str).unwrap_or("unnamed"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
lost
|
||||
}
|
||||
}
|
||||
|
||||
fn value_mentions(v: &Value, text: &str) -> bool {
|
||||
match v {
|
||||
Value::String(s) => s == text,
|
||||
Value::Array(a) => a.iter().any(|x| value_mentions(x, text)),
|
||||
Value::Object(o) => o.values().any(|x| value_mentions(x, text)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The first line of a reader's error, without the list of every kind it knows, and without the
|
||||
/// `"<noun> file: "` its reader starts it with.
|
||||
pub fn short<'a>(why: &'a str, noun: &str) -> &'a str {
|
||||
let why = why.split(", expected").next().unwrap_or(why);
|
||||
why.strip_prefix(&format!("{noun} file: ")).unwrap_or(why)
|
||||
}
|
||||
|
||||
/// Leave a section out and keep it. A section the file needs is left empty.
|
||||
fn move_out(top: &mut Map<String, Value>, key: &str, why: &str, noun: &str, file: &mut NewerFile) {
|
||||
let Some(value) = top.remove(key) else { return };
|
||||
file.not_understood.push(format!("section `{key}` ({})", short(why, noun)));
|
||||
file.sections.push(Section { key: key.into(), value, empty: None });
|
||||
}
|
||||
|
||||
/// Every top-level section of `top` that doesn't read by itself (`piece`, the app's reading of a
|
||||
/// file holding just that section) is left out and kept in `file`. `noun` is what a file of the
|
||||
/// app's is called, which its reader's errors start with.
|
||||
pub fn leave_out_unreadable<E: std::fmt::Display>(
|
||||
top: &mut Map<String, Value>,
|
||||
file: &mut NewerFile,
|
||||
noun: &str,
|
||||
piece: impl Fn(&str, &Value) -> Result<(), E>,
|
||||
) {
|
||||
let keys: Vec<String> = top.keys().cloned().collect();
|
||||
for key in &keys {
|
||||
let value = top.get(key).cloned().unwrap_or_default();
|
||||
if let Err(why) = piece(key, &value) {
|
||||
move_out(top, key, &why.to_string(), noun, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `top` with `attempt`, leaving out the sections it can't be read with: if it doesn't read,
|
||||
/// each section but the `core` ones (which the file can't be read without) is tried left out alone,
|
||||
/// and as a last resort every one of them is. What is left out is kept in `file`. The error is the
|
||||
/// first attempt's, when even the core alone doesn't read.
|
||||
pub fn read_around<D, E: std::fmt::Display>(
|
||||
top: &mut Map<String, Value>,
|
||||
file: &mut NewerFile,
|
||||
core: &[&str],
|
||||
noun: &str,
|
||||
attempt: impl Fn(&Map<String, Value>) -> Result<D, E>,
|
||||
) -> Result<D, String> {
|
||||
let first = match attempt(top) {
|
||||
Ok(doc) => return Ok(doc),
|
||||
Err(why) => why.to_string(),
|
||||
};
|
||||
// Sections that are fine by themselves but not together with the rest.
|
||||
let keys: Vec<String> = top.keys().filter(|k| !core.contains(&k.as_str())).cloned().collect();
|
||||
// Each try reads the whole file: a file of many sections goes straight to the last resort.
|
||||
for key in keys.iter().filter(|_| keys.len() <= MAX_SECTION_TRIES) {
|
||||
let mut without = top.clone();
|
||||
without.remove(key);
|
||||
if let Ok(doc) = attempt(&without) {
|
||||
move_out(top, key, &first, noun, file);
|
||||
return Ok(doc);
|
||||
}
|
||||
}
|
||||
let mut bare = top.clone();
|
||||
bare.retain(|key, _| core.contains(&key.as_str()));
|
||||
match attempt(&bare) {
|
||||
Ok(doc) => {
|
||||
for key in &keys {
|
||||
move_out(top, key, &first, noun, file);
|
||||
}
|
||||
Ok(doc)
|
||||
}
|
||||
Err(_) => Err(first),
|
||||
}
|
||||
}
|
||||
|
||||
/// A value a writer leaves out without losing anything: not worth telling anyone about.
|
||||
pub fn looks_default(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Null => true,
|
||||
Value::Bool(b) => !b,
|
||||
Value::Number(n) => n.as_f64() == Some(0.0),
|
||||
Value::String(s) => s.is_empty(),
|
||||
Value::Array(a) => a.is_empty(),
|
||||
Value::Object(o) => o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What `raw` has that `known`, what was read of it, doesn't. `paths` gets those fields' names.
|
||||
fn vanished(raw: &Value, known: &Value, path: &str, paths: &mut Vec<String>, placeholder_tag: &str, container_only: &[&str]) -> Option<Kept> {
|
||||
match (raw, known) {
|
||||
(Value::Object(raw), Value::Object(known)) => {
|
||||
if known.get("type").and_then(Value::as_str) == Some(placeholder_tag) {
|
||||
return None;
|
||||
}
|
||||
let mut kept = BTreeMap::new();
|
||||
for (key, value) in raw {
|
||||
if container_only.contains(&key.as_str()) || (path.is_empty() && matches!(key.as_str(), "format" | "format_pending")) {
|
||||
continue;
|
||||
}
|
||||
let inner = if path.is_empty() { key.clone() } else { format!("{path}.{key}") };
|
||||
match known.get(key) {
|
||||
None => {
|
||||
if !looks_default(value) {
|
||||
paths.push(format!("field `{inner}`"));
|
||||
}
|
||||
kept.insert(key.clone(), Kept::Field(value.clone()));
|
||||
}
|
||||
Some(known) => {
|
||||
if let Some(k) = vanished(value, known, &inner, paths, placeholder_tag, container_only) {
|
||||
kept.insert(key.clone(), k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(!kept.is_empty()).then_some(Kept::Fields(kept))
|
||||
}
|
||||
(Value::Array(raw), Value::Array(known)) => {
|
||||
let mut items = Vec::new();
|
||||
for (at, element) in raw.iter().enumerate() {
|
||||
let anchor = anchor_of(element).unwrap_or(Anchor::Index { at, of: raw.len() });
|
||||
let Some(partner) = find(known, &anchor) else { continue };
|
||||
let label = match (element.get("name").and_then(Value::as_str), &anchor) {
|
||||
(Some(name), _) => format!("{path}[{name}]"),
|
||||
(None, Anchor::Id(id)) => format!("{path}[{id}]"),
|
||||
_ => format!("{path}[{at}]"),
|
||||
};
|
||||
if let Some(k) = vanished(element, partner, &label, paths, placeholder_tag, container_only) {
|
||||
items.push((anchor, k));
|
||||
}
|
||||
}
|
||||
(!items.is_empty()).then_some(Kept::Items(items))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor_of(element: &Value) -> Option<Anchor> {
|
||||
if let Some(id) = element.get("id").and_then(Value::as_u64) {
|
||||
return Some(Anchor::Id(id));
|
||||
}
|
||||
element.get("name").and_then(Value::as_str).map(|name| Anchor::Name(name.to_string()))
|
||||
}
|
||||
|
||||
fn find<'a>(list: &'a [Value], anchor: &Anchor) -> Option<&'a Value> {
|
||||
match anchor {
|
||||
Anchor::Id(id) => list.iter().find(|x| x.get("id").and_then(Value::as_u64) == Some(*id)),
|
||||
Anchor::Name(name) => list.iter().find(|x| x.get("name").and_then(Value::as_str) == Some(name)),
|
||||
Anchor::Index { at, of } => (list.len() == *of).then(|| list.get(*at)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_mut<'a>(list: &'a mut [Value], anchor: &Anchor) -> Option<&'a mut Value> {
|
||||
match anchor {
|
||||
Anchor::Id(id) => list.iter_mut().find(|x| x.get("id").and_then(Value::as_u64) == Some(*id)),
|
||||
Anchor::Name(name) => list.iter_mut().find(|x| x.get("name").and_then(Value::as_str) == Some(name)),
|
||||
Anchor::Index { at, of } => (list.len() == *of).then(|| list.get_mut(*at)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put the kept fields back. An element that is gone was deleted here, its fields with it; one
|
||||
/// whose place can't be told any more, and a field whose record is gone, go into `lost`.
|
||||
fn apply(kept: &Kept, value: &mut Value, path: &str, lost: &mut Vec<String>) {
|
||||
match kept {
|
||||
Kept::Field(_) => {}
|
||||
Kept::Fields(fields) => {
|
||||
let Some(object) = value.as_object_mut() else {
|
||||
lost.push(format!("field `{path}`"));
|
||||
return;
|
||||
};
|
||||
for (key, sub) in fields {
|
||||
let inner = if path.is_empty() { key.clone() } else { format!("{path}.{key}") };
|
||||
match (sub, object.get_mut(key)) {
|
||||
(Kept::Field(v), None) => {
|
||||
object.insert(key.clone(), v.clone());
|
||||
}
|
||||
(Kept::Field(_), Some(_)) => {}
|
||||
(sub, Some(inner_value)) => apply(sub, inner_value, &inner, lost),
|
||||
// The list it was in is empty and not written: its elements were deleted.
|
||||
(Kept::Items(_), None) => {}
|
||||
(_, None) => lost.push(format!("field `{inner}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Kept::Items(items) => {
|
||||
let Some(list) = value.as_array_mut() else {
|
||||
lost.push(format!("field `{path}`"));
|
||||
return;
|
||||
};
|
||||
for (anchor, sub) in items {
|
||||
match (find_mut(list, anchor), anchor) {
|
||||
(Some(element), _) => apply(sub, element, path, lost),
|
||||
(None, Anchor::Index { at, .. }) => lost.push(format!("element {at} of `{path}`")),
|
||||
(None, _) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "newer_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,48 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
fn newer() -> Newer {
|
||||
Newer { format: 9, reads_up_to: 8, unknown_changes: Vec::new() }
|
||||
}
|
||||
|
||||
/// What typed reading dropped is named and goes back on saving, where it was.
|
||||
#[test]
|
||||
fn dropped_fields_are_named_and_put_back() {
|
||||
let file = json!({"name": "A", "format": "app/9", "items": [{"id": 1, "size": 2, "colour": "red"}], "extra": {"x": 1}});
|
||||
let known = json!({"name": "A", "items": [{"id": 1, "size": 2}]});
|
||||
let mut said = NewerFile::new("App", &newer());
|
||||
said.compare(&file, &known, "placeholder", &[]);
|
||||
assert_eq!(said.not_understood, ["field `extra`", "field `items[1].colour`"]);
|
||||
let mut saved = json!({"name": "A", "items": [{"id": 1, "size": 3}]});
|
||||
assert!(said.restore_into(&mut saved).is_empty());
|
||||
assert_eq!(saved, json!({"name": "A", "items": [{"id": 1, "size": 3, "colour": "red"}], "extra": {"x": 1}}));
|
||||
assert_eq!(said.notice(), "Saved by a newer App (format 9, this version reads up to 8). If something's missing, update.");
|
||||
}
|
||||
|
||||
/// A section that can't be read is left out, kept, and goes back while it is unchanged here.
|
||||
#[test]
|
||||
fn unreadable_sections_are_left_out_and_kept() {
|
||||
let mut top = match json!({"name": "A", "good": 1, "bad": 2}) {
|
||||
Value::Object(m) => m,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut said = NewerFile::new("App", &newer());
|
||||
leave_out_unreadable(&mut top, &mut said, "design", |key: &str, _: &Value| if key == "bad" { Err("design file: no") } else { Ok(()) });
|
||||
assert!(!top.contains_key("bad"));
|
||||
assert_eq!(said.not_understood, ["section `bad` (no)"]);
|
||||
let read =
|
||||
read_around(
|
||||
&mut top,
|
||||
&mut said,
|
||||
&["name"],
|
||||
"design",
|
||||
|top: &Map<String, Value>| {
|
||||
if top.contains_key("good") { Err("design file: clash") } else { Ok(top.len()) }
|
||||
},
|
||||
);
|
||||
assert_eq!(read, Ok(1));
|
||||
assert_eq!(said.not_understood, ["section `bad` (no)", "section `good` (clash)"]);
|
||||
let mut saved = json!({"name": "A"});
|
||||
said.compare(&saved.clone(), &saved.clone(), "placeholder", &[]);
|
||||
assert!(said.restore_into(&mut saved).is_empty());
|
||||
assert_eq!(saved, json!({"name": "A", "good": 1, "bad": 2}));
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Optional output filtering; never writes the Monte Carlo accumulation buffer.
|
||||
use super::{Context, Result, SHADER, TIMEOUT, Task, checked, one_compile_at_a_time, placeholder_bind, scopes, warm_up};
|
||||
use solidcraft_render::job::{CancelToken, DenoiseDiagnostics, DenoiseGuideClass, resolve_denoise_class};
|
||||
use super::{Context, Result, SHADER, TIMEOUT, Task, checked, placeholder_bind, scopes, warm_up};
|
||||
use solidcraft_render::job::{DenoiseDiagnostics, DenoiseGuideClass, resolve_denoise_class};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
pub(super) const GUIDE_BYTES_PER_PIXEL: u64 = 96; // two complete surface guides
|
||||
pub(super) const BYTES_PER_PIXEL: u64 = GUIDE_BYTES_PER_PIXEL + 32; // two colour vec4s
|
||||
const FILTER: &str = include_str!("denoise.wgsl");
|
||||
pub(super) const GUIDES: &str = include_str!("denoise_guides.wgsl");
|
||||
const GUIDES: &str = include_str!("denoise_guides.wgsl");
|
||||
#[cfg(test)]
|
||||
#[path = "denoise_tests.rs"]
|
||||
mod tests;
|
||||
@@ -85,21 +85,10 @@ impl Pipelines {
|
||||
Ok(Self { guide, filter, classify, filter_layout })
|
||||
}
|
||||
|
||||
// The guide program inlines the path tracer's BVH walks, so it compiles under the
|
||||
// process-wide lock of the path programs (#6706), never beside one of them. As for a scene
|
||||
// program, the lock is held until the pipelines are published: a job that waited for another
|
||||
// job's compile then uses its pipelines instead of compiling them again, and a job cancelled
|
||||
// while it waited returns before compiling. A compile that ran is kept for the next job.
|
||||
fn shared<'a>(context: &'a Context, cancel: &CancelToken) -> Result<&'a Self> {
|
||||
fn shared(context: &Context) -> Result<&Self> {
|
||||
if let Some(pipelines) = context.denoise.get() {
|
||||
return Ok(pipelines);
|
||||
}
|
||||
cancel.check()?;
|
||||
let _compiling = one_compile_at_a_time();
|
||||
if let Some(pipelines) = context.denoise.get() {
|
||||
return Ok(pipelines);
|
||||
}
|
||||
cancel.check()?;
|
||||
let built = Self::new(context)?;
|
||||
Ok(context.denoise.get_or_init(|| built))
|
||||
}
|
||||
@@ -139,7 +128,7 @@ impl Resources {
|
||||
size: u64,
|
||||
adaptive_preview: bool,
|
||||
) -> Result<Self> {
|
||||
Self::build(context, path_uniform, scene, accumulation, size, adaptive_preview, None, &CancelToken::default())
|
||||
Self::build(context, path_uniform, scene, accumulation, size, adaptive_preview, None)
|
||||
}
|
||||
|
||||
pub(super) fn new_for_layout(
|
||||
@@ -149,9 +138,8 @@ impl Resources {
|
||||
accumulation: &wgpu::Buffer,
|
||||
adaptive_preview: bool,
|
||||
layout: super::image_pages::Layout,
|
||||
cancel: &CancelToken,
|
||||
) -> Result<Self> {
|
||||
Self::build(context, path_uniform, scene, accumulation, u64::from(layout.max_window_pixels()) * 16, adaptive_preview, Some(layout), cancel)
|
||||
Self::build(context, path_uniform, scene, accumulation, u64::from(layout.max_window_pixels()) * 16, adaptive_preview, Some(layout))
|
||||
}
|
||||
|
||||
fn build(
|
||||
@@ -162,9 +150,8 @@ impl Resources {
|
||||
size: u64,
|
||||
adaptive_preview: bool,
|
||||
layout: Option<super::image_pages::Layout>,
|
||||
cancel: &CancelToken,
|
||||
) -> Result<Self> {
|
||||
let pipelines = Pipelines::shared(context, cancel)?;
|
||||
let pipelines = Pipelines::shared(context)?;
|
||||
let device = &context.device;
|
||||
let error_scopes = scopes(device);
|
||||
let buffer = |label, bytes| {
|
||||
|
||||
@@ -10,7 +10,6 @@ struct GuideInterface {
|
||||
reflected: vec3<f32>, transmitted: vec3<f32>, fresnel: vec3<f32>,
|
||||
};
|
||||
const GUIDE_MISS: u32 = 0xffffffffu;
|
||||
const GUIDE_CAMERA: u32 = 3u;
|
||||
// Zero body tags bypass filtering. Store the exact early-return reason in the
|
||||
// otherwise-unused albedo lane; positive/negative tags retain their old layout.
|
||||
fn guide_bypass(base:u32,reason:u32) {
|
||||
@@ -109,39 +108,77 @@ fn denoise_guides(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let xy=vec2<f32>(f32(pixel%u.image.x)+0.5,f32(pixel/u.image.x)+0.5);
|
||||
let uv=vec2<f32>(xy.x/f32(u.image.x)*2.0-1.0,1.0-xy.y/f32(u.image.y)*2.0);
|
||||
let camera=primary_ray(uv,vec2<f32>(0.0));
|
||||
// Native compilers inline every call site, so the camera ray and its optical
|
||||
// continuations share one walk, one emitter test and one surface look. The
|
||||
// camera ray has no low origin: the walk then takes its ordinary candidate
|
||||
// branch, the arithmetic of trace(), and the emitter test is that of
|
||||
// emitter_hit_bounded(), both on the same values as a separate call.
|
||||
var ray=GuideRay(PrecisePoint(camera.origin,vec3<f32>(0.0)),camera.direction,GUIDE_MISS,u.lighting.x);
|
||||
// Stage GUIDE_CAMERA is the camera ray. Stage 0 is the reflection endpoint,
|
||||
// 1 is the same closed body's exit, and 2 is the forward endpoint.
|
||||
var stage=GUIDE_CAMERA;
|
||||
var t:Triangle;
|
||||
var primary:Hit;
|
||||
var geometric=vec3<f32>(0.0);
|
||||
let primary=trace(camera.origin,camera.direction,false);
|
||||
if u.environment.w>0.0 && emitter_hit_bounded(camera.origin,camera.direction,u.lighting.x).distance<primary.distance {guide_bypass(base,4u);return;}
|
||||
if primary.triangle==GUIDE_MISS {guide_bypass(base,3u);return;}
|
||||
if u.volume_min.w>0.0 {guide_bypass(base,8u);return;}
|
||||
let t=triangle(primary.triangle);
|
||||
let position=surface_point(t,primary.bary);
|
||||
let outward=guide_outward(t);
|
||||
let entering=dot(outward,-camera.direction)>0.0;
|
||||
let geometric=select(-outward,outward,entering);
|
||||
let primary_footprint=guide_footprint(guide_depth(position));
|
||||
let primary_travel=length(position-camera.origin);
|
||||
let closed=t.base.w>0.0 && t.parameters.w<0.0;
|
||||
var reflected=vec3<f32>(0.0);
|
||||
var transmitted=vec3<f32>(0.0);
|
||||
var has_reflection=false;
|
||||
var has_transmission=false;
|
||||
var forward_stage=2u;
|
||||
var primary_footprint=0.0;
|
||||
var primary_travel=0.0;
|
||||
if t.base.w<=0.0 {
|
||||
let look=guide_look(t,primary,camera.direction,position,primary_travel,0u);
|
||||
if !guide_sharp(t,look) {guide_write(base,t.optics.w,look,position,primary_footprint);return;}
|
||||
if look.metallic<0.99 || look.roughness>0.05 || t.coat.x>0.0 {guide_bypass(base,5u);return;}
|
||||
reflected=reflect(camera.direction,look.normal);
|
||||
has_reflection=true;
|
||||
} else {
|
||||
if any(t.glass!=vec4<f32>(0.0)) {guide_bypass(base,5u);return;}
|
||||
if t.interior.x!=0.0 {guide_bypass(base,8u);return;}
|
||||
if t.interior.w!=0.0 {guide_bypass(base,6u);return;}
|
||||
if closed && !entering {guide_bypass(base,6u);return;}
|
||||
let bend=guide_interface(t,primary.bary,camera.direction,geometric,1.0,t.parameters.z);
|
||||
reflected=bend.reflected;
|
||||
let f=bend.fresnel;
|
||||
has_reflection=max(f.x,f.y)>0.0;
|
||||
has_transmission=f.z>=0.0;
|
||||
transmitted=bend.transmitted;
|
||||
if !closed && has_transmission {
|
||||
transmitted=camera.direction;
|
||||
// Use the same sheet weights without calling the transport helper
|
||||
// that can write a failure counter on a tangent absorbing sheet.
|
||||
if t.parameters.w>0.0 && !(f.z>0.0) {guide_bypass(base,10u);return;}
|
||||
var attenuation=vec3<f32>(1.0);
|
||||
if t.parameters.w>0.0 {attenuation=exp(-t.optics.xyz*(t.parameters.w/f.z));}
|
||||
has_transmission=any(sheet_polarization(f.x,attenuation).transmission+sheet_polarization(f.y,attenuation).transmission>vec3<f32>(0.0));
|
||||
}
|
||||
}
|
||||
if t.optics.w<1.0 || t.optics.w>511.0 {guide_bypass(base,15u);return;}
|
||||
if u.eye.w>0.5 && u.right.w>0.0 {guide_bypass(base,7u);return;}
|
||||
if u.sampling.y<2u || (closed && has_transmission && u.sampling.y<3u) {guide_bypass(base,9u);return;}
|
||||
if !has_reflection && !has_transmission {guide_bypass(base,10u);return;}
|
||||
if (has_reflection && dot(reflected,geometric)<=0.000001) || (has_transmission && dot(transmitted,geometric)>=-0.000001) {guide_bypass(base,10u);return;}
|
||||
// A certified root closed entry proves vacuum along this primary segment.
|
||||
// A mirror or sheet can be hit first even when a nested camera starts inside glass.
|
||||
if !closed && !guide_vacuum_origin(camera) {guide_bypass(base,6u);return;}
|
||||
|
||||
var reflected_body=0u;
|
||||
var transmitted_body=0u;
|
||||
var reflected_look:GuideLook;
|
||||
var reflected_position=vec3<f32>(0.0);
|
||||
var travel=0.0;
|
||||
var bounce=0u;
|
||||
for (var event=0u;event<4u;event+=1u) {
|
||||
let first=stage==GUIDE_CAMERA;
|
||||
let hit=trace_precise_within(ray.point.hi,ray.point.lo,ray.direction,false,ray.excluded,ray.minimum,stage==1u,FAR,-1.0);
|
||||
// Stage 0 is the reflection endpoint, 1 is the same closed body's exit,
|
||||
// and 2 is the forward endpoint. One call site bounds the extra BVH work.
|
||||
let forward_stage=select(2u,1u,closed);
|
||||
var stage=select(forward_stage,0u,has_reflection);
|
||||
var ray=guide_launch(t,primary,geometric,select(transmitted,reflected,has_reflection));
|
||||
var travel=primary_travel;
|
||||
var bounce=1u;
|
||||
for (var event=0u;event<3u;event+=1u) {
|
||||
let hit=trace_precise(ray.point.hi,ray.point.lo,ray.direction,false,ray.excluded,ray.minimum,stage==1u);
|
||||
// A secondary optical branch has no surface correspondence. The primary
|
||||
// pixel is still on glass/mirror, rather than direct background/emission.
|
||||
if u.environment.w>0.0 && emitter_hit_precise(ray.point.hi,ray.point.lo,ray.direction,ray.minimum).distance<hit.distance {guide_bypass(base,select(0u,4u,first));return;}
|
||||
if hit.triangle==GUIDE_MISS {guide_bypass(base,select(0u,3u,first));return;}
|
||||
if first && u.volume_min.w>0.0 {guide_bypass(base,8u);return;}
|
||||
if u.environment.w>0.0 && emitter_hit_precise(ray.point.hi,ray.point.lo,ray.direction,ray.minimum).distance<hit.distance {guide_bypass(base,0u);return;}
|
||||
if hit.triangle==GUIDE_MISS {guide_bypass(base,0u);return;}
|
||||
let next=triangle(hit.triangle);
|
||||
if !first {travel+=hit.distance;}
|
||||
travel+=hit.distance;
|
||||
if stage==1u {
|
||||
let normal=-guide_outward(next);
|
||||
if next.optics.w!=t.optics.w || !guide_smooth_glass(next) || next.parameters.w>=0.0 || dot(normal,ray.direction)>=-0.000001 {guide_bypass(base,11u);return;}
|
||||
@@ -153,72 +190,16 @@ fn denoise_guides(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
bounce=2u;
|
||||
continue;
|
||||
}
|
||||
if !first {
|
||||
if next.base.w>0.0 {guide_bypass(base,13u);return;}
|
||||
if next.optics.w<1.0 || next.optics.w>511.0 {guide_bypass(base,15u);return;}
|
||||
}
|
||||
let position=surface_point(next,hit.bary);
|
||||
if first {travel=length(position-camera.origin);}
|
||||
// Every endpoint is opaque here; only the camera hit can be glass.
|
||||
var look:GuideLook;
|
||||
if next.base.w<=0.0 {look=guide_look(next,hit,ray.direction,position,travel,bounce);}
|
||||
if first {
|
||||
t=next;
|
||||
primary=hit;
|
||||
primary_travel=travel;
|
||||
let outward=guide_outward(t);
|
||||
let entering=dot(outward,-camera.direction)>0.0;
|
||||
geometric=select(-outward,outward,entering);
|
||||
primary_footprint=guide_footprint(guide_depth(position));
|
||||
let closed=t.base.w>0.0 && t.parameters.w<0.0;
|
||||
var reflected=vec3<f32>(0.0);
|
||||
var has_reflection=false;
|
||||
if t.base.w<=0.0 {
|
||||
if !guide_sharp(t,look) {guide_write(base,t.optics.w,look,position,primary_footprint);return;}
|
||||
if look.metallic<0.99 || look.roughness>0.05 || t.coat.x>0.0 {guide_bypass(base,5u);return;}
|
||||
reflected=reflect(camera.direction,look.normal);
|
||||
has_reflection=true;
|
||||
} else {
|
||||
if any(t.glass!=vec4<f32>(0.0)) {guide_bypass(base,5u);return;}
|
||||
if t.interior.x!=0.0 {guide_bypass(base,8u);return;}
|
||||
if t.interior.w!=0.0 {guide_bypass(base,6u);return;}
|
||||
if closed && !entering {guide_bypass(base,6u);return;}
|
||||
let bend=guide_interface(t,primary.bary,camera.direction,geometric,1.0,t.parameters.z);
|
||||
reflected=bend.reflected;
|
||||
let f=bend.fresnel;
|
||||
has_reflection=max(f.x,f.y)>0.0;
|
||||
has_transmission=f.z>=0.0;
|
||||
transmitted=bend.transmitted;
|
||||
if !closed && has_transmission {
|
||||
transmitted=camera.direction;
|
||||
// Use the same sheet weights without calling the transport helper
|
||||
// that can write a failure counter on a tangent absorbing sheet.
|
||||
if t.parameters.w>0.0 && !(f.z>0.0) {guide_bypass(base,10u);return;}
|
||||
var attenuation=vec3<f32>(1.0);
|
||||
if t.parameters.w>0.0 {attenuation=exp(-t.optics.xyz*(t.parameters.w/f.z));}
|
||||
has_transmission=any(sheet_polarization(f.x,attenuation).transmission+sheet_polarization(f.y,attenuation).transmission>vec3<f32>(0.0));
|
||||
}
|
||||
}
|
||||
if t.optics.w<1.0 || t.optics.w>511.0 {guide_bypass(base,15u);return;}
|
||||
if u.eye.w>0.5 && u.right.w>0.0 {guide_bypass(base,7u);return;}
|
||||
if u.sampling.y<2u || (closed && has_transmission && u.sampling.y<3u) {guide_bypass(base,9u);return;}
|
||||
if !has_reflection && !has_transmission {guide_bypass(base,10u);return;}
|
||||
if (has_reflection && dot(reflected,geometric)<=0.000001) || (has_transmission && dot(transmitted,geometric)>=-0.000001) {guide_bypass(base,10u);return;}
|
||||
// A certified root closed entry proves vacuum along this primary segment.
|
||||
// A mirror or sheet can be hit first even when a nested camera starts inside glass.
|
||||
if !closed && !guide_vacuum_origin(camera) {guide_bypass(base,6u);return;}
|
||||
forward_stage=select(2u,1u,closed);
|
||||
stage=select(forward_stage,0u,has_reflection);
|
||||
ray=guide_launch(t,primary,geometric,select(transmitted,reflected,has_reflection));
|
||||
bounce=1u;
|
||||
continue;
|
||||
}
|
||||
if guide_sharp(next,look) {guide_bypass(base,14u);return;}
|
||||
if next.base.w>0.0 {guide_bypass(base,13u);return;}
|
||||
if next.optics.w<1.0 || next.optics.w>511.0 {guide_bypass(base,15u);return;}
|
||||
let endpoint_position=surface_point(next,hit.bary);
|
||||
let endpoint_look=guide_look(next,hit,ray.direction,endpoint_position,travel,bounce);
|
||||
if guide_sharp(next,endpoint_look) {guide_bypass(base,14u);return;}
|
||||
if stage==0u {
|
||||
reflected_body=u32(next.optics.w);
|
||||
if has_transmission {
|
||||
reflected_look=look;
|
||||
reflected_position=position;
|
||||
reflected_look=endpoint_look;
|
||||
reflected_position=endpoint_position;
|
||||
ray=guide_launch(t,primary,geometric,transmitted);
|
||||
stage=forward_stage;
|
||||
travel=primary_travel;
|
||||
@@ -226,7 +207,7 @@ fn denoise_guides(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
}
|
||||
} else {transmitted_body=u32(next.optics.w);}
|
||||
let identity=0xa0000000u|(u32(t.optics.w)<<18u)|(reflected_body<<9u)|transmitted_body;
|
||||
guide_write(base,bitcast<f32>(identity),look,position,primary_footprint);
|
||||
guide_write(base,bitcast<f32>(identity),endpoint_look,endpoint_position,primary_footprint);
|
||||
if reflected_body!=0u && transmitted_body!=0u {
|
||||
guide_write(base+3u,f32(reflected_body),reflected_look,reflected_position,primary_footprint);
|
||||
}
|
||||
|
||||
@@ -186,35 +186,6 @@ fn f64_atrous_preserves_body_normal_depth_and_albedo_edges() {
|
||||
assert!(filtered.iter().zip(&pixels).all(|(a, b)| (a[0] - b[0]).abs() < 1e-12), "edge {edge}");
|
||||
}
|
||||
}
|
||||
// Native compilers inline every call site, and their cost grows faster than the inlined size
|
||||
// (#14105). The camera ray and its optical continuations share one walk; the camera origin
|
||||
// query is the other.
|
||||
#[test]
|
||||
fn guides_inline_two_bvh_walks() {
|
||||
fn walks(module: &naga::Module, block: &naga::Block) -> usize {
|
||||
block
|
||||
.iter()
|
||||
.map(|statement| match statement {
|
||||
naga::Statement::Call { function, .. } => {
|
||||
let callee = &module.functions[*function];
|
||||
let walk = matches!(callee.name.as_deref(), Some("trace_precise_within" | "trace_ordered_within" | "origin_boundary"));
|
||||
usize::from(walk) + walks(module, &callee.body)
|
||||
}
|
||||
naga::Statement::Block(inner) => walks(module, inner),
|
||||
naga::Statement::If { accept, reject, .. } => walks(module, accept) + walks(module, reject),
|
||||
naga::Statement::Switch { cases, .. } => cases.iter().map(|case| walks(module, &case.body)).sum(),
|
||||
naga::Statement::Loop { body, continuing, .. } => walks(module, body) + walks(module, continuing),
|
||||
_ => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
let module = naga::front::wgsl::parse_str(&format!("{SHADER}\n{GUIDES}")).unwrap();
|
||||
let guides = module.entry_points.iter().find(|entry| entry.name == "denoise_guides").unwrap();
|
||||
let inlined = walks(&module, &guides.function.body);
|
||||
// Exactly two, not at most two: if a walk function is renamed, the names above match nothing
|
||||
// and an upper bound would pass on zero walks.
|
||||
assert_eq!(inlined, 2, "BVH walks inlined into `denoise_guides`");
|
||||
}
|
||||
fn context() -> Option<Arc<Context>> {
|
||||
let (_, device, queue) = match open_headless() {
|
||||
Ok(v) => v,
|
||||
|
||||
+23
-29
@@ -65,7 +65,7 @@ const SHADER: &str = path_shader!(16);
|
||||
const SINGLE_MEDIUM_SHADER: &str = path_shader!(1);
|
||||
const MAX_TRIANGLES: u64 = MAX_GPU_PATH_TRIANGLES;
|
||||
const PATH_WORKGROUP_SIZE: u32 = 32;
|
||||
const SCENE_PIPELINE_CACHE_SLOTS: usize = 2;
|
||||
const SCENE_PIPELINE_CACHE_SLOTS: usize = 5;
|
||||
const CLAMPED_PIPELINE_CACHE_SLOTS: usize = SCENE_PIPELINE_CACHE_SLOTS + 1;
|
||||
const PARAMETER_BYTES: usize = 2096;
|
||||
// Five vec4s per source; exact device+upload growth is additional to v2's budget.
|
||||
@@ -206,10 +206,10 @@ struct Context {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
// Plain opaque and unmapped glass are ready before the host is published.
|
||||
// The mapped program is prepared lazily in the remaining slot.
|
||||
// Plain opaque and non-nested glass are ready before the host is published.
|
||||
// The four map/nested variants are prepared lazily in the remaining slots.
|
||||
scene_pipelines: [std::sync::OnceLock<wgpu::ComputePipeline>; SCENE_PIPELINE_CACHE_SLOTS],
|
||||
// The same three programs with the indirect clamp compiled in, each built and warmed by the
|
||||
// The same six programs with the indirect clamp compiled in, each built and warmed by the
|
||||
// first job that asks for it. Jobs without a clamp never compile or use them.
|
||||
clamped_pipelines: [std::sync::OnceLock<wgpu::ComputePipeline>; CLAMPED_PIPELINE_CACHE_SLOTS],
|
||||
// Built and warmed by the first denoised job, then shared by later ones.
|
||||
@@ -240,27 +240,27 @@ impl Context {
|
||||
}
|
||||
// A job with an indirect clamp (JobOptions::clamp_indirect) uses its scene's program with
|
||||
// the clamp compiled in; every other job keeps the programs without it.
|
||||
fn scene_pipeline_slot(&self, opaque: bool, maps: bool, clamp: bool) -> Option<&std::sync::OnceLock<wgpu::ComputePipeline>> {
|
||||
fn scene_pipeline_slot(&self, opaque: bool, maps: bool, nested: bool, clamp: bool) -> Option<&std::sync::OnceLock<wgpu::ComputePipeline>> {
|
||||
if clamp {
|
||||
return self.clamped_pipelines.get(clamped_pipeline_variant(opaque, maps));
|
||||
return self.clamped_pipelines.get(clamped_pipeline_variant(opaque, maps, nested));
|
||||
}
|
||||
scene_pipeline_variant(opaque, maps).and_then(|index| self.scene_pipelines.get(index))
|
||||
scene_pipeline_variant(opaque, maps, nested).and_then(|index| self.scene_pipelines.get(index))
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn prepared_scene_pipeline(&self, opaque: bool, maps: bool, clamp: bool) -> Result<&wgpu::ComputePipeline> {
|
||||
if !clamp && scene_pipeline_variant(opaque, maps).is_none() {
|
||||
fn prepared_scene_pipeline(&self, opaque: bool, maps: bool, nested: bool, clamp: bool) -> Result<&wgpu::ComputePipeline> {
|
||||
if !clamp && scene_pipeline_variant(opaque, maps, nested).is_none() {
|
||||
return Ok(&self.pipeline);
|
||||
}
|
||||
self.scene_pipeline_slot(opaque, maps, clamp).and_then(std::sync::OnceLock::get).ok_or_else(|| gpu("captured scene has no prepared pipeline"))
|
||||
self.scene_pipeline_slot(opaque, maps, nested, clamp)
|
||||
.and_then(std::sync::OnceLock::get)
|
||||
.ok_or_else(|| gpu("captured scene has no prepared pipeline"))
|
||||
}
|
||||
// A job cancelled while it waited for another compile returns before starting its own; a
|
||||
// compile that ran is warmed up and kept for the next job.
|
||||
fn prepare_scene_pipeline(&self, opaque: bool, maps: bool, clamp: bool, cancel: &CancelToken) -> Result<&wgpu::ComputePipeline> {
|
||||
fn prepare_scene_pipeline(&self, opaque: bool, maps: bool, nested: bool, clamp: bool) -> Result<&wgpu::ComputePipeline> {
|
||||
self.ensure_available()?;
|
||||
if !clamp && scene_pipeline_variant(opaque, maps).is_none() {
|
||||
if !clamp && scene_pipeline_variant(opaque, maps, nested).is_none() {
|
||||
return Ok(&self.pipeline);
|
||||
}
|
||||
let slot = self.scene_pipeline_slot(opaque, maps, clamp).ok_or_else(|| gpu("scene pipeline variant exceeds its finite cache"))?;
|
||||
let slot = self.scene_pipeline_slot(opaque, maps, nested, clamp).ok_or_else(|| gpu("scene pipeline variant exceeds its finite cache"))?;
|
||||
if let Some(pipeline) = slot.get() {
|
||||
return Ok(pipeline);
|
||||
}
|
||||
@@ -268,7 +268,6 @@ impl Context {
|
||||
if let Some(pipeline) = slot.get() {
|
||||
return Ok(pipeline);
|
||||
}
|
||||
cancel.check()?;
|
||||
let scopes = scopes(&self.device);
|
||||
// RADV's one-medium opaque specialization corrupts shading normals
|
||||
// after texture sampling (the mapped Lambertian plane loses its rim
|
||||
@@ -388,8 +387,8 @@ impl Executor {
|
||||
clamped_pipelines: std::array::from_fn(|_| std::sync::OnceLock::new()),
|
||||
});
|
||||
// The original host prepared glass before advertising readiness. Keep the
|
||||
// common unmapped program out of the first image job's work.
|
||||
context.prepare_scene_pipeline(false, false, false, &CancelToken::default())?;
|
||||
// common no-map/non-nested program out of the first image job's work.
|
||||
context.prepare_scene_pipeline(false, false, false, false)?;
|
||||
Ok(Self { context })
|
||||
}
|
||||
}
|
||||
@@ -547,17 +546,13 @@ fn scene_shader(opaque: bool) -> &'static str {
|
||||
if opaque { SINGLE_MEDIUM_SHADER } else { SHADER }
|
||||
}
|
||||
|
||||
/// A captured scene's program slot, or None for the eager opaque/no-map program. A slot is one
|
||||
/// compiled program: prepare_scene_pipeline builds scene_shader(opaque && !maps) with only
|
||||
/// OPAQUE_SCENE, HAS_TEXTURES and the clamp, so an opaque scene with maps shares the mapped glass
|
||||
/// program, and nested media, a uniform, share the program of disjoint media.
|
||||
fn scene_pipeline_variant(opaque: bool, maps: bool) -> Option<usize> {
|
||||
if opaque && !maps { None } else { Some(usize::from(maps)) }
|
||||
fn scene_pipeline_variant(opaque: bool, maps: bool, nested: bool) -> Option<usize> {
|
||||
if opaque { maps.then_some(0) } else { Some(1 + 2 * usize::from(maps) + usize::from(nested)) }
|
||||
}
|
||||
|
||||
/// The clamped program's slot: all three scene programs, the eager opaque/no-map one included.
|
||||
fn clamped_pipeline_variant(opaque: bool, maps: bool) -> usize {
|
||||
scene_pipeline_variant(opaque, maps).map_or(0, |index| index + 1)
|
||||
/// The clamped program's slot: all six scene programs, the eager opaque/no-map one included.
|
||||
fn clamped_pipeline_variant(opaque: bool, maps: bool, nested: bool) -> usize {
|
||||
scene_pipeline_variant(opaque, maps, nested).map_or(0, |index| index + 1)
|
||||
}
|
||||
|
||||
fn use_opaque_pipeline(scene: &ProductScene, options: JobOptions) -> bool {
|
||||
@@ -1120,7 +1115,7 @@ impl Task {
|
||||
} else if let Some(resources) = &photons {
|
||||
resources.render.clone()
|
||||
} else {
|
||||
context.prepare_scene_pipeline(opaque_scene, has_textures, clamp, &cancel)?.clone()
|
||||
context.prepare_scene_pipeline(opaque_scene, has_textures, options.nested_media, clamp)?.clone()
|
||||
};
|
||||
let optical_resolve = optical_pipelines.map(|pipelines| pipelines.resolve);
|
||||
cancel.check()?;
|
||||
@@ -1189,7 +1184,6 @@ impl Task {
|
||||
&accumulation,
|
||||
options.adaptive.is_some(),
|
||||
page_layout,
|
||||
&cancel,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
|
||||
+125
-205
@@ -276,18 +276,16 @@ fn fp_sub_bits(left: u32, right: u32) -> u32 {
|
||||
// RTE for a general floating subtraction, so approximate differences must retain
|
||||
// the integer implementation. The fast case has an exactly representable normal
|
||||
// result, independent of rounding direction and permitted subnormal flushing.
|
||||
// `exact` is false where fp_sub_barrier_bits takes the integer subtraction.
|
||||
struct ExactDifference { bits: u32, exact: bool };
|
||||
fn fp_sub_exact(left: u32, right: u32) -> ExactDifference {
|
||||
fn fp_sub_barrier_bits(left: u32, right: u32) -> u32 {
|
||||
let a=left&0x7fffffffu;
|
||||
let b=right&0x7fffffffu;
|
||||
if a>=0x7f800000u || b>=0x7f800000u { return ExactDifference(0u,false); }
|
||||
if a>=0x7f800000u || b>=0x7f800000u { return fp_sub_bits(left,right); }
|
||||
if a==0u {
|
||||
if b==0u { return ExactDifference(left&(right^0x80000000u)&0x80000000u,true); }
|
||||
return ExactDifference(right^0x80000000u,true);
|
||||
if b==0u { return left&(right^0x80000000u)&0x80000000u; }
|
||||
return right^0x80000000u;
|
||||
}
|
||||
if b==0u { return ExactDifference(left,true); }
|
||||
if left==right { return ExactDifference(0u,true); }
|
||||
if b==0u { return left; }
|
||||
if left==right { return 0u; }
|
||||
let small=min(a,b);
|
||||
let large=max(a,b);
|
||||
let small_exponent=small>>23u;
|
||||
@@ -298,50 +296,25 @@ fn fp_sub_exact(left: u32, right: u32) -> ExactDifference {
|
||||
// Encoded exponent 24 has spacing 2^-126. Any distinct pair admitted here
|
||||
// therefore has a normal difference, including the closest adjacent pair.
|
||||
if same_sign && small_exponent>=24u && within_two {
|
||||
return ExactDifference(bitcast<u32>(bitcast<f32>(left)-bitcast<f32>(right)),true);
|
||||
return bitcast<u32>(bitcast<f32>(left)-bitcast<f32>(right));
|
||||
}
|
||||
return ExactDifference(0u,false);
|
||||
}
|
||||
fn fp_sub_barrier_bits(left: u32, right: u32) -> u32 {
|
||||
let difference=fp_sub_exact(left,right);
|
||||
if difference.exact { return difference.bits; }
|
||||
return fp_sub_bits(left,right);
|
||||
}
|
||||
fn anchored_delta_bits(a: u32, a_low: u32, b: u32, b_low: u32) -> u32 {
|
||||
// Step 0 is offset=a_low-b_low and step 1 anchor=a-b, each exactly as
|
||||
// fp_sub_barrier_bits; step 2 is anchor+offset. Native compilers inline
|
||||
// every call, so the three share one integer addition.
|
||||
var offset=0u;
|
||||
var anchor=0u;
|
||||
var bits=0u;
|
||||
for (var step=0u; step<3u; step+=1u) {
|
||||
var left=anchor;
|
||||
var right=offset;
|
||||
var integer=true;
|
||||
if step<2u {
|
||||
left=select(a_low,a,step==1u);
|
||||
right=select(b_low,b,step==1u);
|
||||
let difference=fp_sub_exact(left,right);
|
||||
bits=difference.bits;
|
||||
integer=!difference.exact;
|
||||
// fp_sub_bits(left,right) is fp_add_bits(left,right^sign).
|
||||
right^=0x80000000u;
|
||||
}
|
||||
// At step 2 this nonlinear integer operation is the reassociation barrier.
|
||||
// Keep it even when both component differences used exact hardware subtraction.
|
||||
if integer { bits=fp_add_bits(left,right); }
|
||||
if step==0u {
|
||||
offset=bits;
|
||||
// Identical finite anchors have +0 difference. Adding it preserves a
|
||||
// nonzero offset; either sign of a zero offset becomes +0.
|
||||
if a==b && (a&0x7fffffffu)<0x7f800000u { return select(offset,0u,(offset&0x7fffffffu)==0u); }
|
||||
} else if step==1u {
|
||||
anchor=bits;
|
||||
// IEEE RTE addition of two zeros retains -0 only if both are -0.
|
||||
if (offset&0x7fffffffu)==0u { return select(anchor,anchor&offset,(anchor&0x7fffffffu)==0u); }
|
||||
}
|
||||
let offset=fp_sub_barrier_bits(a_low,b_low);
|
||||
// Identical finite anchors have +0 difference. Adding it preserves a
|
||||
// nonzero offset; either sign of a zero offset becomes +0.
|
||||
if a==b && (a&0x7fffffffu)<0x7f800000u {
|
||||
return select(offset,0u,(offset&0x7fffffffu)==0u);
|
||||
}
|
||||
return bits;
|
||||
let anchor=fp_sub_barrier_bits(a,b);
|
||||
if (offset&0x7fffffffu)==0u {
|
||||
// IEEE RTE addition of two zeros retains -0 only if both are -0.
|
||||
return select(anchor,anchor&offset,(anchor&0x7fffffffu)==0u);
|
||||
}
|
||||
// This nonlinear integer operation is the reassociation barrier. Keep it
|
||||
// even when both component differences used exact hardware subtraction.
|
||||
return fp_add_bits(anchor,offset);
|
||||
}
|
||||
fn anchored_delta_component(a: f32, a_low: f32, b: f32, b_low: f32) -> f32 {
|
||||
return bitcast<f32>(anchored_delta_bits(bitcast<u32>(a),bitcast<u32>(a_low),bitcast<u32>(b),bitcast<u32>(b_low)));
|
||||
@@ -350,10 +323,7 @@ fn anchored_delta_component(a: f32, a_low: f32, b: f32, b_low: f32) -> f32 {
|
||||
struct PrecisePoint { hi: vec3<f32>, lo: vec3<f32> };
|
||||
fn precise_point_delta(a: vec3<f32>, a_low: vec3<f32>, b: vec3<f32>, b_low: vec3<f32>) -> vec3<f32> {
|
||||
if all(a_low==vec3<f32>(0.0)) && all(b_low==vec3<f32>(0.0)) { return a-b; }
|
||||
// The three components share one inlined integer delta.
|
||||
var delta=vec3<f32>(0.0);
|
||||
for (var k=0u; k<3u; k+=1u) { delta[k]=anchored_delta_component(a[k],a_low[k],b[k],b_low[k]); }
|
||||
return delta;
|
||||
return vec3<f32>(anchored_delta_component(a.x,a_low.x,b.x,b_low.x),anchored_delta_component(a.y,a_low.y,b.y,b_low.y),anchored_delta_component(a.z,a_low.z,b.z,b_low.z));
|
||||
}
|
||||
fn ray_segment_precise(o: vec3<f32>, o_low: vec3<f32>, previous: vec3<f32>, previous_low: vec3<f32>, d: vec3<f32>, distance: f32) -> vec3<f32> {
|
||||
// Subtract the physical origins before adding a possibly tiny ray segment.
|
||||
@@ -386,8 +356,10 @@ fn triangle_ahead(t: Triangle, o: vec3<f32>, d: vec3<f32>) -> bool {
|
||||
if any((d==vec3<f32>(0.0))&((o<lo)|(o>hi))) { return false; }
|
||||
return true;
|
||||
}
|
||||
// The same test from the bounds' integer-defined deltas to an anchored origin.
|
||||
fn deltas_ahead(lo: vec3<f32>, hi: vec3<f32>, d: vec3<f32>) -> bool {
|
||||
fn triangle_ahead_precise(t: Triangle, o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>) -> bool {
|
||||
if all(o_low==vec3<f32>(0.0)) { return triangle_ahead(t,o,d); }
|
||||
let lo=precise_point_delta(min(t.p0.xyz,min(t.p1.xyz,t.p2.xyz)),vec3<f32>(0.0),o,o_low);
|
||||
let hi=precise_point_delta(max(t.p0.xyz,max(t.p1.xyz,t.p2.xyz)),vec3<f32>(0.0),o,o_low);
|
||||
if any((lo>=vec3<f32>(0.0))&(d<vec3<f32>(0.0))) || any((hi<=vec3<f32>(0.0))&(d>vec3<f32>(0.0))) { return false; }
|
||||
if any((d==vec3<f32>(0.0))&((lo>vec3<f32>(0.0))|(hi<vec3<f32>(0.0)))) { return false; }
|
||||
return true;
|
||||
@@ -420,6 +392,12 @@ fn split_product(a: f32, b: f32) -> vec2<f32> {
|
||||
let e3=split_materialize(e2-ah*bl);
|
||||
return vec2<f32>(p,split_materialize(al*bl-e3));
|
||||
}
|
||||
fn split_two_sum(a: f32, b: f32) -> vec2<f32> {
|
||||
let swap=abs(a)<abs(b);let large=select(a,b,swap);let small=select(b,a,swap);
|
||||
let sum=split_add(large,small);
|
||||
let delta=split_materialize(sum-large);
|
||||
return vec2<f32>(sum,split_materialize(small-delta));
|
||||
}
|
||||
fn split_rescale(value: f32, exponent: i32) -> f32 {
|
||||
if value==0.0 { return value; }
|
||||
let scaled=split_scaled(value);let combined=scaled.exponent+exponent+1;
|
||||
@@ -430,43 +408,16 @@ fn split_difference_products(a: f32, b: f32, c: f32, d: f32) -> f32 {
|
||||
let aa=split_scaled(a);let bb=split_scaled(b);let cc=split_scaled(c);let dd=split_scaled(d);
|
||||
let p=split_product(aa.value,bb.value);let q=split_product(cc.value,dd.value);
|
||||
let ep=aa.exponent+bb.exponent;let eq=cc.exponent+dd.exponent;
|
||||
// Native compilers inline every call, so the possible sums run as steps that
|
||||
// share one split_add and one split_rescale. Each step is one split_add(x,y):
|
||||
// 0 the sum of the product that is not zero (negated for q) when the other
|
||||
// is; 1 the two-sum tail of (pp.y,-qq.y); 2 the two-sum total of (high,tail.x);
|
||||
// 3 total.y+tail.y; 4 total.x plus that.
|
||||
var step=1u;
|
||||
var x=0.0;var y=0.0;
|
||||
var exponent=0;
|
||||
var high=0.0;
|
||||
var tail=vec2<f32>(0.0);var total=vec2<f32>(0.0);
|
||||
var value=0.0;
|
||||
if p.x==0.0 || q.x==0.0 {
|
||||
step=0u;
|
||||
x=select(p.x,q.x,p.x==0.0);y=select(p.y,q.y,p.x==0.0);exponent=select(ep,eq,p.x==0.0);
|
||||
} else {
|
||||
exponent=max(ep,eq);
|
||||
let pp=ldexp(p,vec2<i32>(ep-exponent));let qq=ldexp(q,vec2<i32>(eq-exponent));
|
||||
high=split_materialize(pp.x-qq.x);
|
||||
x=pp.y;y=-qq.y;
|
||||
}
|
||||
loop {
|
||||
// A two-sum adds its operands larger first and keeps the rounding error.
|
||||
let swap=(step==1u || step==2u) && abs(x)<abs(y);
|
||||
let large=select(x,y,swap);let small=select(y,x,swap);
|
||||
let sum=split_add(large,small);
|
||||
if step==0u { value=select(sum,-sum,p.x==0.0); break; }
|
||||
if step==4u { value=sum; break; }
|
||||
if step==3u { x=total.x;y=sum;step=4u;continue; }
|
||||
let delta=split_materialize(sum-large);
|
||||
let pair=vec2<f32>(sum,split_materialize(small-delta));
|
||||
if step==2u { total=pair;x=total.y;y=tail.y;step=3u;continue; }
|
||||
tail=pair;
|
||||
if high==0.0 { value=tail.x; break; }
|
||||
if tail.x==0.0 { value=high; break; }
|
||||
x=high;y=tail.x;step=2u;
|
||||
}
|
||||
return split_rescale(value,exponent);
|
||||
if p.x==0.0 { return split_rescale(-split_add(q.x,q.y),eq); }
|
||||
if q.x==0.0 { return split_rescale(split_add(p.x,p.y),ep); }
|
||||
let exponent=max(ep,eq);
|
||||
let pp=ldexp(p,vec2<i32>(ep-exponent));let qq=ldexp(q,vec2<i32>(eq-exponent));
|
||||
let high=split_materialize(pp.x-qq.x);
|
||||
let tail=split_two_sum(pp.y,-qq.y);
|
||||
if high==0.0 { return split_rescale(tail.x,exponent); }
|
||||
if tail.x==0.0 { return split_rescale(high,exponent); }
|
||||
let total=split_two_sum(high,tail.x);
|
||||
return split_rescale(split_add(total.x,split_add(total.y,tail.y)),exponent);
|
||||
}
|
||||
fn difference_products(a: f32, b: f32, c: f32, d: f32) -> f32 {
|
||||
let ab=a*b;let cd=c*d;
|
||||
@@ -478,19 +429,6 @@ fn difference_products(a: f32, b: f32, c: f32, d: f32) -> f32 {
|
||||
}
|
||||
return split_difference_products(a,b,c,d);
|
||||
}
|
||||
// Signed edge functions of a projected triangle: weight k pairs the two vertices
|
||||
// after vertex k. Native compilers inline difference_products at every call
|
||||
// site, so all three weights share one.
|
||||
fn edge_weights(a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> vec3<f32> {
|
||||
var weights=vec3<f32>(0.0);
|
||||
for (var k=0u; k<3u; k+=1u) {
|
||||
// Selected, not indexed: the pairs (b,c), (c,a), (a,b) stay in registers.
|
||||
let e=select(select(a,c,k==1u),b,k==0u);
|
||||
let f=select(select(b,a,k==1u),c,k==0u);
|
||||
weights[k]=difference_products(e.x,f.y,e.y,f.x);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
struct Hit { distance: f32, triangle: u32, bary: vec3<f32> };
|
||||
// A closed continuation that leaves its producing triangle at a shared edge or
|
||||
// vertex can meet an adjacent face of the same body within the origin's rounding
|
||||
@@ -515,8 +453,7 @@ fn tie_preferred(t: Triangle, d: vec3<f32>, inside: bool) -> bool {
|
||||
return front!=inside;
|
||||
}
|
||||
fn trace_precise(o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>, any_hit: bool, excluded: u32, minimum: f32, inside: bool) -> Hit {
|
||||
// Without a low part, trace_precise_within is the ordered walk: the same
|
||||
// origin, pad, error bound and candidate test. One walk is inlined here.
|
||||
if all(o_low==vec3<f32>(0.0)) { return trace_ordered_within(o,d,any_hit,excluded,minimum,inside,FAR,-1.0); }
|
||||
return trace_precise_within(o,o_low,d,any_hit,excluded,minimum,inside,FAR,-1.0);
|
||||
}
|
||||
// Only hits closer than `limit` are reported, and nodes beyond it are culled.
|
||||
@@ -589,72 +526,85 @@ fn trace_precise_within(o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>, any_hit: b
|
||||
// IDs join only connected, exactly coplanar uploaded f32 triangles
|
||||
// of one body; curved facets and other bodies remain candidates.
|
||||
if excluded!=0xffffffffu && geometry[geometry[3]+i]==geometry[geometry[3]+excluded] { continue; }
|
||||
let t = triangle(i);
|
||||
let lo=min(t.p0.xyz,min(t.p1.xyz,t.p2.xyz));
|
||||
let hi=max(t.p0.xyz,max(t.p1.xyz,t.p2.xyz));
|
||||
// Native compilers inline the candidate arithmetic once per call site,
|
||||
// so one candidate test serves three policies, each evaluated exactly
|
||||
// as on its own:
|
||||
// 0: an origin without residual takes the former ordered test;
|
||||
// 1: a residual origin's candidate beyond near_radius uses ordinary
|
||||
// arithmetic from the rounded origin while each decision clears
|
||||
// its error bound, and otherwise takes
|
||||
// 2: integer-defined deltas from the anchored origin.
|
||||
var policy=0u;
|
||||
if has_origin_residual { policy=select(2u,1u,any(max(lo-rounded,rounded-hi)>vec3<f32>(near_radius))); }
|
||||
// Each pass ends with break (next candidate) or, once, continue (policy 2).
|
||||
loop {
|
||||
var p=array<vec3<f32>,3>(t.p0.xyz-rounded,t.p1.xyz-rounded,t.p2.xyz-rounded);
|
||||
if policy==0u {
|
||||
if !triangle_ahead(t,o,d) { break; }
|
||||
} else if policy==1u {
|
||||
// The rounded origin is within origin_error of the true one on each
|
||||
// axis. Bound and edge decisions that close to their threshold could
|
||||
// flip, while a nearby neighbour across the edge is tested exactly:
|
||||
// a crossing would fall between them. Such candidates take policy 2.
|
||||
if any((rounded<=lo-vec3<f32>(origin_error))&(d<vec3<f32>(0.0))) || any((rounded>=hi+vec3<f32>(origin_error))&(d>vec3<f32>(0.0))) { break; }
|
||||
if any((d==vec3<f32>(0.0))&((rounded<lo-vec3<f32>(origin_error))|(rounded>hi+vec3<f32>(origin_error)))) { break; }
|
||||
} else {
|
||||
// Bound deltas first: a candidate that is not ahead is rejected
|
||||
// before its vertex deltas are formed. The five points lo, hi,
|
||||
// p0, p1, p2 share one delta call site.
|
||||
var low=vec3<f32>(0.0);
|
||||
var ahead=true;
|
||||
for (var k=0u; k<5u; k+=1u) {
|
||||
let point=select(select(select(t.p2.xyz,t.p1.xyz,k==3u),t.p0.xyz,k==2u),select(hi,lo,k==0u),k<2u);
|
||||
let delta=precise_point_delta(point,vec3<f32>(0.0),o,o_low);
|
||||
if k==0u { low=delta; }
|
||||
else if k==1u { if !deltas_ahead(low,delta,d) { ahead=false; break; } }
|
||||
else if k==2u { p[0]=delta; }
|
||||
else if k==3u { p[1]=delta; }
|
||||
else { p[2]=delta; }
|
||||
}
|
||||
if !ahead { break; }
|
||||
}
|
||||
let a=vec2<f32>(p[0][kx]-shear.x*p[0][kz],p[0][ky]-shear.y*p[0][kz]);
|
||||
let b=vec2<f32>(p[1][kx]-shear.x*p[1][kz],p[1][ky]-shear.y*p[1][kz]);
|
||||
let c=vec2<f32>(p[2][kx]-shear.x*p[2][kz],p[2][ky]-shear.y*p[2][kz]);
|
||||
let weights=edge_weights(a,b,c);
|
||||
if policy==1u && !all(abs(weights)>origin_error*vec3<f32>(length(b-c),length(c-a),length(a-b))) { policy=2u; continue; }
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { break; }
|
||||
// The ordinary source uses exactly the former ordered candidate
|
||||
// branch. Sharing only the node walk avoids another inlined BVH.
|
||||
if !has_origin_residual {
|
||||
let t = triangle(i);
|
||||
if !triangle_ahead(t,o,d) { continue; }
|
||||
let pa=t.p0.xyz-o; let pb=t.p1.xyz-o; let pc=t.p2.xyz-o;
|
||||
let a=vec2<f32>(pa[kx]-shear.x*pa[kz],pa[ky]-shear.y*pa[kz]);
|
||||
let b=vec2<f32>(pb[kx]-shear.x*pb[kz],pb[ky]-shear.y*pb[kz]);
|
||||
let c=vec2<f32>(pc[kx]-shear.x*pc[kz],pc[ky]-shear.y*pc[kz]);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { continue; }
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
// As before, policy 1 has no det test: each weight cleared a
|
||||
// nonnegative bound. Nor does it test a repeated crossing.
|
||||
if det==0.0 && policy!=1u { break; }
|
||||
if det==0.0 { continue; }
|
||||
// Retain all three directly evaluated weights: 1-u-v can erase a
|
||||
// small corner clearance when the other two weights sum to one.
|
||||
let bary=weights/det;
|
||||
var depth=0.0;
|
||||
if policy==2u { depth=surface_ray_depth_precise(t,bary,o,o_low,kz); }
|
||||
else { depth=surface_ray_depth(t,bary,rounded,kz); }
|
||||
let depth=surface_ray_depth(t,bary,o,kz);
|
||||
let distance=depth*shear.z;
|
||||
let key=depth*order;
|
||||
if policy!=1u && excluded!=0xffffffffu && repeated_crossing(t,source,d,inside,distance,origin_error) { break; }
|
||||
if excluded!=0xffffffffu && repeated_crossing(t,source,d,inside,distance,origin_error) { continue; }
|
||||
if distance > minimum && distance < limit && (key < best || (key == best && tie_preferred(t,d,inside))) {
|
||||
hit = Hit(distance,i,bary); best=key;
|
||||
if any_hit || (distance < blocker && (t.base.w<=0.0 || t.coat.w<=0.0)) { return hit; }
|
||||
}
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
let t = triangle(i);
|
||||
let lo=min(t.p0.xyz,min(t.p1.xyz,t.p2.xyz));
|
||||
let hi=max(t.p0.xyz,max(t.p1.xyz,t.p2.xyz));
|
||||
let gap=max(lo-rounded,rounded-hi);
|
||||
if any(gap>vec3<f32>(near_radius)) {
|
||||
// The rounded origin is within origin_error of the true one on each
|
||||
// axis. Bound and edge decisions that close to their threshold could
|
||||
// flip, while a nearby neighbour across the edge is tested exactly:
|
||||
// a crossing would fall between them. Such candidates take the exact
|
||||
// path below.
|
||||
if any((rounded<=lo-vec3<f32>(origin_error))&(d<vec3<f32>(0.0))) || any((rounded>=hi+vec3<f32>(origin_error))&(d>vec3<f32>(0.0))) { continue; }
|
||||
if any((d==vec3<f32>(0.0))&((rounded<lo-vec3<f32>(origin_error))|(rounded>hi+vec3<f32>(origin_error)))) { continue; }
|
||||
let qa=t.p0.xyz-rounded; let qb=t.p1.xyz-rounded; let qc=t.p2.xyz-rounded;
|
||||
let fa=vec2<f32>(qa[kx]-shear.x*qa[kz],qa[ky]-shear.y*qa[kz]);
|
||||
let fb=vec2<f32>(qb[kx]-shear.x*qb[kz],qb[ky]-shear.y*qb[kz]);
|
||||
let fc=vec2<f32>(qc[kx]-shear.x*qc[kz],qc[ky]-shear.y*qc[kz]);
|
||||
let far_weights=vec3<f32>(difference_products(fb.x,fc.y,fb.y,fc.x),difference_products(fc.x,fa.y,fc.y,fa.x),difference_products(fa.x,fb.y,fa.y,fb.x));
|
||||
let edge_error=origin_error*vec3<f32>(length(fb-fc),length(fc-fa),length(fa-fb));
|
||||
if all(abs(far_weights)>edge_error) {
|
||||
if any(far_weights<vec3<f32>(0.0)) && any(far_weights>vec3<f32>(0.0)) { continue; }
|
||||
let far_det=far_weights.x+far_weights.y+far_weights.z;
|
||||
let far_bary=far_weights/far_det;
|
||||
let far_depth=surface_ray_depth(t,far_bary,rounded,kz);
|
||||
let far_distance=far_depth*shear.z;
|
||||
let far_key=far_depth*order;
|
||||
if far_distance > minimum && far_distance < limit && (far_key < best || (far_key == best && tie_preferred(t,d,inside))) {
|
||||
hit = Hit(far_distance,i,far_bary); best=far_key;
|
||||
if any_hit || (far_distance < blocker && (t.base.w<=0.0 || t.coat.w<=0.0)) { return hit; }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !triangle_ahead_precise(t,o,o_low,d) { continue; }
|
||||
let pa=precise_point_delta(t.p0.xyz,vec3<f32>(0.0),o,o_low);
|
||||
let pb=precise_point_delta(t.p1.xyz,vec3<f32>(0.0),o,o_low);
|
||||
let pc=precise_point_delta(t.p2.xyz,vec3<f32>(0.0),o,o_low);
|
||||
let a=vec2<f32>(pa[kx]-shear.x*pa[kz],pa[ky]-shear.y*pa[kz]);
|
||||
let b=vec2<f32>(pb[kx]-shear.x*pb[kz],pb[ky]-shear.y*pb[kz]);
|
||||
let c=vec2<f32>(pc[kx]-shear.x*pc[kz],pc[ky]-shear.y*pc[kz]);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { continue; }
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
if det==0.0 { continue; }
|
||||
// Retain all three directly evaluated weights: 1-u-v can erase a
|
||||
// small corner clearance when the other two weights sum to one.
|
||||
let bary=weights/det;
|
||||
let depth=surface_ray_depth_precise(t,bary,o,o_low,kz);
|
||||
let distance=depth*shear.z;
|
||||
let key=depth*order;
|
||||
if excluded!=0xffffffffu && repeated_crossing(t,source,d,inside,distance,origin_error) { continue; }
|
||||
if distance > minimum && distance < limit && (key < best || (key == best && tie_preferred(t,d,inside))) {
|
||||
hit = Hit(distance,i,bary); best=key;
|
||||
if any_hit || (distance < blocker && (t.base.w<=0.0 || t.coat.w<=0.0)) { return hit; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,7 +669,7 @@ fn trace_ordered_within(o: vec3<f32>, d: vec3<f32>, any_hit: bool, excluded: u32
|
||||
let a=vec2<f32>(pa[kx]-shear.x*pa[kz],pa[ky]-shear.y*pa[kz]);
|
||||
let b=vec2<f32>(pb[kx]-shear.x*pb[kz],pb[ky]-shear.y*pb[kz]);
|
||||
let c=vec2<f32>(pc[kx]-shear.x*pc[kz],pc[ky]-shear.y*pc[kz]);
|
||||
let weights=edge_weights(a,b,c);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { continue; }
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
if det==0.0 { continue; }
|
||||
@@ -777,7 +727,7 @@ fn origin_on_boundary(t: Triangle, o: vec3<f32>) -> bool {
|
||||
let x=(axis+1u)%3u;let y=(x+1u)%3u;
|
||||
let pa=t.p0.xyz-o;let pb=t.p1.xyz-o;let pc=t.p2.xyz-o;
|
||||
let a=vec2<f32>(pa[x],pa[y]);let b=vec2<f32>(pb[x],pb[y]);let c=vec2<f32>(pc[x],pc[y]);
|
||||
let weights=edge_weights(a,b,c);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
return !(any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)));
|
||||
}
|
||||
fn origin_boundary(o: vec3<f32>, d: vec3<f32>) -> OriginBoundary {
|
||||
@@ -834,25 +784,10 @@ fn origin_boundary(o: vec3<f32>, d: vec3<f32>) -> OriginBoundary {
|
||||
// Keep the ray projection homogeneous: a rounded d[kx]/d[kz]
|
||||
// must not turn an exact uploaded-f32 edge tangent into a crossing.
|
||||
// All projected coordinates share d[kz], so barycentrics are invariant.
|
||||
// Steps 0-5 project vertex k/2 on axis kx (even k) or ky (odd k);
|
||||
// steps 6-8 then form the edge weights as edge_weights does. All
|
||||
// nine share one inlined difference_products.
|
||||
var a=vec2<f32>(0.0);var b=vec2<f32>(0.0);var c=vec2<f32>(0.0);
|
||||
var weights=vec3<f32>(0.0);
|
||||
for (var k=0u; k<9u; k+=1u) {
|
||||
var products=vec4<f32>(0.0);
|
||||
if k<6u {
|
||||
let point=select(select(pc,pb,k<4u),pa,k<2u);
|
||||
let axis=select(kx,ky,(k&1u)!=0u);
|
||||
products=vec4<f32>(point[axis],d[kz],d[axis],point[kz]);
|
||||
} else {
|
||||
let e=select(select(a,c,k==7u),b,k==6u);
|
||||
let f=select(select(b,a,k==7u),c,k==6u);
|
||||
products=vec4<f32>(e.x,f.y,e.y,f.x);
|
||||
}
|
||||
let value=difference_products(products.x,products.y,products.z,products.w);
|
||||
if k<2u { a[k]=value; } else if k<4u { b[k-2u]=value; } else if k<6u { c[k-4u]=value; } else { weights[k-6u]=value; }
|
||||
}
|
||||
let a=vec2<f32>(difference_products(pa[kx],d[kz],d[kx],pa[kz]),difference_products(pa[ky],d[kz],d[ky],pa[kz]));
|
||||
let b=vec2<f32>(difference_products(pb[kx],d[kz],d[kx],pb[kz]),difference_products(pb[ky],d[kz],d[ky],pb[kz]));
|
||||
let c=vec2<f32>(difference_products(pc[kx],d[kz],d[kx],pc[kz]),difference_products(pc[ky],d[kz],d[ky],pc[kz]));
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { continue; }
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
if det==0.0 { continue; }
|
||||
@@ -1149,9 +1084,8 @@ fn direct_light(t: Triangle, position: vec3<f32>, physical: PrecisePoint, o: vec
|
||||
if !studio || emitter_hit_precise(shadow_origin,shadow_origin_low,shadow_direction,shadow_minimum).distance>=emitter_limit {
|
||||
visibility = shadow_transmittance(shadow_origin,shadow_origin_low,shadow_direction,shadow_distance,shadow_minimum);
|
||||
if !OPAQUE_SCENE && studio && u.volume_min.w>0.0 {
|
||||
// A punctual connection is attenuated along its physical segment.
|
||||
let attenuation=volume_transmittance_precise(select(shadow_origin,physical.hi,punctual),select(shadow_origin_low,physical.lo,punctual),
|
||||
select(shadow_direction,sample.direction,punctual),select(shadow_distance,physical_distance,punctual));
|
||||
var attenuation=volume_transmittance_precise(shadow_origin,shadow_origin_low,shadow_direction,shadow_distance);
|
||||
if punctual { attenuation=volume_transmittance_precise(physical.hi,physical.lo,sample.direction,physical_distance); }
|
||||
visibility.transmittance*=attenuation;
|
||||
visibility.branch_pdf*=attenuation;
|
||||
}
|
||||
@@ -1374,18 +1308,11 @@ fn sample_path_lane(pixel: u32, lane: u32) -> vec4<f32> {
|
||||
if u.environment.w > 0.0 {
|
||||
if emitter.distance < hit.distance {
|
||||
let light = u.area_lights[emitter.index];
|
||||
// The physical and scatter segments to the emitter share one inlined
|
||||
// precise subtraction.
|
||||
var segment=vec3<f32>(0.0);
|
||||
var scatter_segment=vec3<f32>(0.0);
|
||||
for (var k=0u; k<2u; k+=1u) {
|
||||
let delta=ray_segment_precise(o,o_low,select(previous_position,previous_scatter_position,k==1u),
|
||||
select(previous_position_low,previous_scatter_position_low,k==1u),d,emitter.distance);
|
||||
if k==0u { segment=delta; } else { scatter_segment=delta; }
|
||||
}
|
||||
let segment = ray_segment_precise(o,o_low,previous_position,previous_position_low,d,emitter.distance);
|
||||
if medium_count>0u {
|
||||
throughput *= exp(-media[medium_count-1u].sigma*length(segment));
|
||||
}
|
||||
let scatter_segment = ray_segment_precise(o,o_low,previous_scatter_position,previous_scatter_position_low,d,emitter.distance);
|
||||
let pdf = area_pdf(light,scatter_segment);
|
||||
if pdf>0.0 && !suppress_direct && !photon_owns_smooth_tail(smooth_tail,false) {
|
||||
let weight = select(power_weight(previous_pdf,pdf),1.0,previous_delta);
|
||||
@@ -1426,17 +1353,10 @@ fn sample_path_lane(pixel: u32, lane: u32) -> vec4<f32> {
|
||||
position=point.hi;position_low=point.lo;
|
||||
}
|
||||
let shading_position=position+position_low;
|
||||
// The travelled ray segment and, inside a medium, its absorbing segment
|
||||
// share one inlined precise subtraction.
|
||||
var travelled=vec3<f32>(0.0);
|
||||
var absorbing=vec3<f32>(0.0);
|
||||
for (var k=0u; k<select(1u,2u,medium_count>0u); k+=1u) {
|
||||
let delta=precise_point_delta(position,position_low,select(o,previous_position,k==1u),select(o_low,previous_position_low,k==1u));
|
||||
if k==0u { travelled=delta; } else { absorbing=delta; }
|
||||
}
|
||||
texture_path_length+=length(travelled);
|
||||
texture_path_length+=length(precise_point_delta(position,position_low,o,o_low));
|
||||
if medium_count > 0u {
|
||||
throughput *= exp(-media[medium_count-1u].sigma*length(absorbing));
|
||||
let segment=precise_point_delta(position,position_low,previous_position,previous_position_low);
|
||||
throughput *= exp(-media[medium_count-1u].sigma*length(segment));
|
||||
}
|
||||
previous_position = position; previous_position_low=position_low;
|
||||
o = position + geometric*(u.lighting.x*2.0); o_low=vec3<f32>(0.0);
|
||||
@@ -1857,7 +1777,7 @@ fn ordering_candidate(t: Triangle, o: vec3<f32>, d: vec3<f32>) -> vec2<f32> {
|
||||
let a=vec2<f32>(pa[kx]-shear.x*pa[kz],pa[ky]-shear.y*pa[kz]);
|
||||
let b=vec2<f32>(pb[kx]-shear.x*pb[kz],pb[ky]-shear.y*pb[kz]);
|
||||
let c=vec2<f32>(pc[kx]-shear.x*pc[kz],pc[ky]-shear.y*pc[kz]);
|
||||
let weights=edge_weights(a,b,c);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
let bary=weights/det;
|
||||
let distance=surface_ray_depth(t,bary,o,kz)*shear.z;
|
||||
|
||||
+65
-166
@@ -2361,71 +2361,48 @@ fn shader_validates_without_a_device() {
|
||||
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty()).validate(&caustic).unwrap();
|
||||
}
|
||||
|
||||
/// Visits every statement a native compiler inlines into `block`: nested blocks, and at each call
|
||||
/// site the callee's body again, as inlining copies it.
|
||||
fn each_inlined(module: &naga::Module, block: &naga::Block, visit: &mut impl FnMut(&naga::Statement)) {
|
||||
for statement in block.iter() {
|
||||
visit(statement);
|
||||
match statement {
|
||||
naga::Statement::Call { function, .. } => each_inlined(module, &module.functions[*function].body, visit),
|
||||
naga::Statement::Block(inner) => each_inlined(module, inner, visit),
|
||||
naga::Statement::If { accept, reject, .. } => {
|
||||
each_inlined(module, accept, visit);
|
||||
each_inlined(module, reject, visit);
|
||||
}
|
||||
naga::Statement::Switch { cases, .. } => cases.iter().for_each(|case| each_inlined(module, &case.body, visit)),
|
||||
naga::Statement::Loop { body, continuing, .. } => {
|
||||
each_inlined(module, body, visit);
|
||||
each_inlined(module, continuing, visit);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #6705: native compilers inline every call. With 33 inlined BVH walks in `main` (three path
|
||||
/// samples, a shadow walk per light source family) NVIDIA's Vulkan compiler crashed or ran out
|
||||
/// of memory and never built the pipeline. Count the walks `main` inlines, so another call site
|
||||
/// is a deliberate choice, checked on such a driver.
|
||||
#[test]
|
||||
fn main_inlines_few_bvh_walks() {
|
||||
fn callees(block: &naga::Block, out: &mut Vec<naga::Handle<naga::Function>>) {
|
||||
for statement in block.iter() {
|
||||
match statement {
|
||||
naga::Statement::Call { function, .. } => out.push(*function),
|
||||
naga::Statement::Block(inner) => callees(inner, out),
|
||||
naga::Statement::If { accept, reject, .. } => {
|
||||
callees(accept, out);
|
||||
callees(reject, out);
|
||||
}
|
||||
naga::Statement::Switch { cases, .. } => cases.iter().for_each(|case| callees(&case.body, out)),
|
||||
naga::Statement::Loop { body, continuing, .. } => {
|
||||
callees(body, out);
|
||||
callees(continuing, out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn walks(module: &naga::Module, function: &naga::Function) -> usize {
|
||||
let mut called = Vec::new();
|
||||
callees(&function.body, &mut called);
|
||||
called
|
||||
.into_iter()
|
||||
.map(|handle| {
|
||||
let callee = &module.functions[handle];
|
||||
let walk = matches!(callee.name.as_deref(), Some("trace_precise_within" | "trace_ordered_within" | "origin_boundary"));
|
||||
usize::from(walk) + walks(module, callee)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
let module = naga::front::wgsl::parse_str(SHADER).unwrap();
|
||||
let main = module.entry_points.iter().find(|entry| entry.name == "main").unwrap();
|
||||
let mut inlined = 0;
|
||||
each_inlined(&module, &main.function.body, &mut |statement| {
|
||||
if let naga::Statement::Call { function, .. } = statement {
|
||||
let callee = module.functions[*function].name.as_deref();
|
||||
inlined += usize::from(matches!(callee, Some("trace_precise_within" | "trace_ordered_within" | "origin_boundary")));
|
||||
}
|
||||
});
|
||||
let inlined = walks(&module, &main.function);
|
||||
assert!(inlined <= 5, "`main` inlines {inlined} BVH walks");
|
||||
}
|
||||
|
||||
/// #14105: the walks' size matters too. A native compiler's time and memory grow faster than the
|
||||
/// statements it inlines: on one Vulkan driver the path programs took up to 17 minutes and 61 GB
|
||||
/// to compile. Each budget is about 1.5 times its entry point's count when #14105 was fixed, far
|
||||
/// below the count before (before -> fixed): path `main` 196,450 -> 28,414, `optical_resolve`
|
||||
/// 54,888 -> 7,777, `denoise_guides` 70,517 -> 7,906, caustic `main` 211,786 -> 33,386 and
|
||||
/// `photon_emit` 107,085 -> 14,782. A larger program is a deliberate choice, checked on such a driver.
|
||||
#[test]
|
||||
fn heavy_entry_points_inline_few_statements() {
|
||||
let guides = format!("{SHADER}\n{}", denoise::GUIDES);
|
||||
let caustics = photons::shader_source().unwrap();
|
||||
for (program, source, entry, budget) in [
|
||||
("path", SHADER, "main", 45_000),
|
||||
("path", SHADER, "optical_resolve", 12_000),
|
||||
("denoise", guides.as_str(), "denoise_guides", 12_000),
|
||||
("caustic", caustics.as_str(), "main", 50_000),
|
||||
("caustic", caustics.as_str(), "photon_emit", 22_000),
|
||||
] {
|
||||
let module = naga::front::wgsl::parse_str(source).unwrap();
|
||||
let function = &module.entry_points.iter().find(|point| point.name == entry).unwrap().function;
|
||||
let mut statements = 0usize;
|
||||
each_inlined(&module, &function.body, &mut |_| statements += 1);
|
||||
assert!(statements <= budget, "the {program} program's `{entry}` inlines {statements} statements, over its budget of {budget}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_materialization_lane_keeps_positive_zero_at_the_shader_offset() {
|
||||
let Some(executor) = executor() else { return };
|
||||
@@ -3975,27 +3952,23 @@ fn texture_admission_rejects_glass_and_stale_identity_and_honors_cancellation()
|
||||
assert!(admission(&scene, &input, JobOptions::default()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn host_readiness_includes_unmapped_glass() {
|
||||
fn host_readiness_includes_unmapped_non_nested_glass() {
|
||||
let Some(executor) = executor() else { return };
|
||||
let context = &executor.context;
|
||||
assert!(context.prepared_scene_pipeline(true, false, false).is_ok());
|
||||
assert!(context.prepared_scene_pipeline(false, false, false).is_ok(), "a ready host must already prepare ordinary unmapped glass");
|
||||
assert!(context.prepared_scene_pipeline(true, false, false, false).is_ok());
|
||||
assert!(context.prepared_scene_pipeline(false, false, false, false).is_ok(), "a ready host must already prepare ordinary unmapped glass");
|
||||
assert!(context.clamped_pipelines.iter().all(|slot| slot.get().is_none()), "indirect-clamp programs stay lazy");
|
||||
let plain = scene_pipeline_variant(false, false).unwrap();
|
||||
let plain = scene_pipeline_variant(false, false, false).unwrap();
|
||||
assert!(context.scene_pipelines[plain].get().is_some());
|
||||
for (opaque, maps) in [(true, true), (false, true)] {
|
||||
let variant = scene_pipeline_variant(opaque, maps).unwrap();
|
||||
assert!(context.scene_pipelines[variant].get().is_none(), "the mapped program stays lazy");
|
||||
for (opaque, maps, nested) in [(true, true, false), (false, false, true), (false, true, false), (false, true, true)] {
|
||||
let variant = scene_pipeline_variant(opaque, maps, nested).unwrap();
|
||||
assert!(context.scene_pipelines[variant].get().is_none(), "uncommon map/nesting programs stay lazy");
|
||||
}
|
||||
let mut glass = sphere(white());
|
||||
glass.meshes[0].transmission = Some(clear_glass());
|
||||
let options = JobOptions { samples: 1, max_depth: 3, ..Default::default() };
|
||||
// Nested media are a uniform: such a job runs the ready unmapped program, no second compile.
|
||||
let nested = Task::new(context.clone(), glass.clone(), request(2), JobOptions { nested_media: true, ..options }, CancelToken::default()).unwrap();
|
||||
assert_eq!(&nested.render_pipeline, context.prepared_scene_pipeline(false, false, false).unwrap());
|
||||
let mut task = Task::new(context.clone(), glass, request(2), options, CancelToken::default()).unwrap();
|
||||
assert!(!task.opaque_scene && !task.has_textures);
|
||||
assert_eq!(task.render_pipeline, nested.render_pipeline);
|
||||
task.complete_sample().unwrap();
|
||||
let pixels = task.read_linear().unwrap();
|
||||
assert_eq!(pixels.len(), 4);
|
||||
@@ -4004,7 +3977,7 @@ fn host_readiness_includes_unmapped_glass() {
|
||||
let options = JobOptions { clamp_indirect: Some(2.0), ..options };
|
||||
let mut clamped = Task::new(context.clone(), sphere(white()), request(2), options, CancelToken::default()).unwrap();
|
||||
clamped.complete_sample().unwrap();
|
||||
let opaque = clamped_pipeline_variant(true, false);
|
||||
let opaque = clamped_pipeline_variant(true, false, false);
|
||||
for (index, slot) in context.clamped_pipelines.iter().enumerate() {
|
||||
assert_eq!(slot.get().is_some(), index == opaque, "clamped program {index}");
|
||||
}
|
||||
@@ -4057,22 +4030,33 @@ fn opaque_scene_specialization_preserves_body_face_and_volume_media() {
|
||||
let mut variants = std::collections::BTreeSet::new();
|
||||
for opaque in [false, true] {
|
||||
for maps in [false, true] {
|
||||
let slot = scene_pipeline_variant(opaque, maps);
|
||||
assert_eq!(slot.is_none(), opaque && !maps, "only opaque/no-map uses the eager base");
|
||||
assert!(slot.is_none_or(|index| index < SCENE_PIPELINE_CACHE_SLOTS));
|
||||
variants.insert(slot);
|
||||
for nested in [false, true] {
|
||||
let slot = scene_pipeline_variant(opaque, maps, nested);
|
||||
assert_eq!(slot.is_none(), opaque && !maps, "only opaque/no-map uses the eager base");
|
||||
assert!(slot.is_none_or(|index| index < SCENE_PIPELINE_CACHE_SLOTS));
|
||||
variants.insert(slot);
|
||||
}
|
||||
if opaque {
|
||||
assert_eq!(scene_pipeline_variant(opaque, maps, false), scene_pipeline_variant(opaque, maps, true));
|
||||
} else {
|
||||
assert_ne!(
|
||||
scene_pipeline_variant(opaque, maps, false),
|
||||
scene_pipeline_variant(opaque, maps, true),
|
||||
"disjoint and nested options retain separate scene cache entries"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(variants.len(), SCENE_PIPELINE_CACHE_SLOTS + 1);
|
||||
// The slot is the program: an opaque scene with maps runs the full program with maps.
|
||||
assert_eq!(scene_pipeline_variant(true, true), scene_pipeline_variant(false, true), "mapped opaque and glass compile one program");
|
||||
// Each of the three programs has its own clamped slot.
|
||||
// Each of the six programs has its own clamped slot.
|
||||
let mut clamped = std::collections::BTreeMap::new();
|
||||
for opaque in [false, true] {
|
||||
for maps in [false, true] {
|
||||
let slot = clamped_pipeline_variant(opaque, maps);
|
||||
assert!(slot < CLAMPED_PIPELINE_CACHE_SLOTS);
|
||||
clamped.entry(slot).or_insert_with(std::collections::BTreeSet::new).insert(scene_pipeline_variant(opaque, maps));
|
||||
for nested in [false, true] {
|
||||
let slot = clamped_pipeline_variant(opaque, maps, nested);
|
||||
assert!(slot < CLAMPED_PIPELINE_CACHE_SLOTS);
|
||||
clamped.entry(slot).or_insert_with(std::collections::BTreeSet::new).insert(scene_pipeline_variant(opaque, maps, nested));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(clamped.len(), CLAMPED_PIPELINE_CACHE_SLOTS);
|
||||
@@ -7515,99 +7499,14 @@ fn gpu_punctual_offset_preserves_small_shift_after_large_local_barycentric_displ
|
||||
|
||||
#[test]
|
||||
fn shared_bvh_node_walk_preserves_original_ordinary_and_compensated_hit_bits() {
|
||||
// Frozen f53 policy, not re-derived from the new implementation: the old
|
||||
// precise and ordered walkers under test-local names, and the old
|
||||
// triangle_ahead_precise the shader no longer needs. legacy_precise_policy
|
||||
// sends zero low parts to legacy_ordered_within, as the old policy did; both
|
||||
// keep three direct difference_products calls per candidate, which the shader
|
||||
// now makes at one edge_weights call site. This reference entry inlines three
|
||||
// Frozen f53 policy, not re-derived from the new implementation. Only its
|
||||
// test-local function name changes; it still calls the byte-identical old
|
||||
// ordered walker for zero low parts. This reference entry inlines three
|
||||
// real walks, rather than compiling the historical seven-walk main.
|
||||
let legacy = r#"fn triangle_ahead_precise(t: Triangle, o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>) -> bool {
|
||||
if all(o_low==vec3<f32>(0.0)) { return triangle_ahead(t,o,d); }
|
||||
let lo=precise_point_delta(min(t.p0.xyz,min(t.p1.xyz,t.p2.xyz)),vec3<f32>(0.0),o,o_low);
|
||||
let hi=precise_point_delta(max(t.p0.xyz,max(t.p1.xyz,t.p2.xyz)),vec3<f32>(0.0),o,o_low);
|
||||
if any((lo>=vec3<f32>(0.0))&(d<vec3<f32>(0.0))) || any((hi<=vec3<f32>(0.0))&(d>vec3<f32>(0.0))) { return false; }
|
||||
if any((d==vec3<f32>(0.0))&((lo>vec3<f32>(0.0))|(hi<vec3<f32>(0.0)))) { return false; }
|
||||
return true;
|
||||
}
|
||||
fn legacy_ordered_within(o: vec3<f32>, d: vec3<f32>, any_hit: bool, excluded: u32, minimum: f32, inside: bool, limit: f32, blocker: f32) -> Hit {
|
||||
var hit = Hit(limit, 0xffffffffu, vec3<f32>(0.0));
|
||||
let origin_error=max(max(abs(o.x),max(abs(o.y),abs(o.z))),1.0)*(1.0/1048576.0);
|
||||
var source: Triangle;
|
||||
if excluded!=0xffffffffu { source=triangle(excluded); }
|
||||
// Project every candidate into the same ray-aligned 2D coordinate system.
|
||||
// Shared edges then use identical products with opposite signs, avoiding the
|
||||
// independently rounded barycentric comparisons of Moller-Trumbore.
|
||||
var kz = 0u;
|
||||
if abs(d.y)>abs(d.x) { kz=1u; }
|
||||
if abs(d.z)>abs(d[kz]) { kz=2u; }
|
||||
let kx=(kz+1u)%3u; let ky=(kx+1u)%3u;
|
||||
let shear=vec3<f32>(d[kx]/d[kz],d[ky]/d[kz],1.0/d[kz]);
|
||||
let order=select(-1.0,1.0,d[kz]>0.0);
|
||||
var best=FAR;
|
||||
// Children are tested at their parent and visited near-first. The build
|
||||
// bounds tree depth to 30, so at most 31 nodes are pending.
|
||||
var stack: array<u32, 32>;
|
||||
var entry: array<f32, 32>;
|
||||
if nodes[0].first == 0xffffffffu { return hit; }
|
||||
entry[0] = box_entry_padded(nodes[0], o, d, hit.distance, u.lighting.x);
|
||||
if entry[0] < 0.0 { return hit; }
|
||||
stack[0] = 0u;
|
||||
var size = 1u;
|
||||
loop {
|
||||
if size == 0u { break; }
|
||||
size -= 1u;
|
||||
if entry[size] > hit.distance { continue; }
|
||||
let node = nodes[stack[size]];
|
||||
if node.count == 0u {
|
||||
if size + 2u > 32u { break; }
|
||||
let left = box_entry_padded(nodes[node.first], o, d, hit.distance, u.lighting.x);
|
||||
let right = box_entry_padded(nodes[node.first+1u], o, d, hit.distance, u.lighting.x);
|
||||
var near_child = node.first; var near_entry = left;
|
||||
var far_child = node.first+1u; var far_entry = right;
|
||||
if right >= 0.0 && (left < 0.0 || right < left) {
|
||||
near_child = node.first+1u; near_entry = right;
|
||||
far_child = node.first; far_entry = left;
|
||||
}
|
||||
if far_entry >= 0.0 { stack[size] = far_child; entry[size] = far_entry; size += 1u; }
|
||||
if near_entry >= 0.0 { stack[size] = near_child; entry[size] = near_entry; size += 1u; }
|
||||
continue;
|
||||
}
|
||||
for (var i = node.first; i < node.first + node.count; i += 1u) {
|
||||
if i == excluded { continue; }
|
||||
// A straight continuation cannot revisit its producing exact plane.
|
||||
// IDs join only connected, exactly coplanar uploaded f32 triangles
|
||||
// of one body; curved facets and other bodies remain candidates.
|
||||
if excluded!=0xffffffffu && geometry[geometry[3]+i]==geometry[geometry[3]+excluded] { continue; }
|
||||
let t = triangle(i);
|
||||
if !triangle_ahead(t,o,d) { continue; }
|
||||
let pa=t.p0.xyz-o; let pb=t.p1.xyz-o; let pc=t.p2.xyz-o;
|
||||
let a=vec2<f32>(pa[kx]-shear.x*pa[kz],pa[ky]-shear.y*pa[kz]);
|
||||
let b=vec2<f32>(pb[kx]-shear.x*pb[kz],pb[ky]-shear.y*pb[kz]);
|
||||
let c=vec2<f32>(pc[kx]-shear.x*pc[kz],pc[ky]-shear.y*pc[kz]);
|
||||
let weights=vec3<f32>(difference_products(b.x,c.y,b.y,c.x),difference_products(c.x,a.y,c.y,a.x),difference_products(a.x,b.y,a.y,b.x));
|
||||
if any(weights<vec3<f32>(0.0)) && any(weights>vec3<f32>(0.0)) { continue; }
|
||||
let det=weights.x+weights.y+weights.z;
|
||||
if det==0.0 { continue; }
|
||||
// Retain all three directly evaluated weights: 1-u-v can erase a
|
||||
// small corner clearance when the other two weights sum to one.
|
||||
let bary=weights/det;
|
||||
let depth=surface_ray_depth(t,bary,o,kz);
|
||||
let distance=depth*shear.z;
|
||||
let key=depth*order;
|
||||
if excluded!=0xffffffffu && repeated_crossing(t,source,d,inside,distance,origin_error) { continue; }
|
||||
if distance > minimum && distance < limit && (key < best || (key == best && tie_preferred(t,d,inside))) {
|
||||
hit = Hit(distance,i,bary); best=key;
|
||||
if any_hit || (distance < blocker && (t.base.w<=0.0 || t.coat.w<=0.0)) { return hit; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
fn legacy_precise_policy(o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>, any_hit: bool, excluded: u32, minimum: f32, inside: bool, limit: f32, blocker: f32) -> Hit {
|
||||
let legacy = r#"fn legacy_precise_policy(o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>, any_hit: bool, excluded: u32, minimum: f32, inside: bool, limit: f32, blocker: f32) -> Hit {
|
||||
// Dispatch once before traversal. Ordinary rays keep the original lean BVH
|
||||
// loop, without precision-helper branches inside every candidate/node test.
|
||||
if all(o_low==vec3<f32>(0.0)) { return legacy_ordered_within(o,d,any_hit,excluded,minimum,inside,limit,blocker); }
|
||||
if all(o_low==vec3<f32>(0.0)) { return trace_ordered_within(o,d,any_hit,excluded,minimum,inside,limit,blocker); }
|
||||
var hit = Hit(limit, 0xffffffffu, vec3<f32>(0.0));
|
||||
// Project every candidate into the same ray-aligned 2D coordinate system.
|
||||
// Shared edges then use identical products with opposite signs, avoiding the
|
||||
|
||||
@@ -20,13 +20,8 @@ fn volume_segment(o: vec3<f32>, d: vec3<f32>, limit: f32) -> vec2<f32> {
|
||||
fn volume_transmittance_precise(o: vec3<f32>, o_low: vec3<f32>, d: vec3<f32>, distance: f32) -> f32 {
|
||||
if all(o_low==vec3<f32>(0.0)) { return volume_transmittance(o,d,distance); }
|
||||
if u.volume_min.w<=0.0 { return 1.0; }
|
||||
// Both box bounds share one inlined precise subtraction.
|
||||
var lo=vec3<f32>(0.0);
|
||||
var hi=vec3<f32>(0.0);
|
||||
for (var k=0u; k<2u; k+=1u) {
|
||||
let delta=precise_point_delta(select(u.volume_min.xyz,u.volume_max.xyz,k==1u),vec3<f32>(0.0),o,o_low);
|
||||
if k==0u { lo=delta; } else { hi=delta; }
|
||||
}
|
||||
let lo=precise_point_delta(u.volume_min.xyz,vec3<f32>(0.0),o,o_low);
|
||||
let hi=precise_point_delta(u.volume_max.xyz,vec3<f32>(0.0),o,o_low);
|
||||
var enter=0.0; var leave=distance;
|
||||
for(var axis=0u;axis<3u;axis+=1u) {
|
||||
if d[axis]==0.0 { if lo[axis]>0.0 || hi[axis]<0.0 { return 1.0; } }
|
||||
|
||||
@@ -18,7 +18,6 @@ solidcraft-color = { workspace = true }
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-kernel = { workspace = true }
|
||||
solidcraft-doc = { workspace = true }
|
||||
solidcraft-format = { workspace = true }
|
||||
solidcraft-profile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ use std::process::Command;
|
||||
|
||||
/// The workspace crates a design's model is computed from: `doc` and every workspace crate it
|
||||
/// depends on (a test checks the list against their manifests).
|
||||
const EVALUATION_CRATES: [&str; 11] = ["color", "geom", "profile", "smlib", "sketch", "kernel", "render", "derive", "doc", "format", "viewport"];
|
||||
const EVALUATION_CRATES: [&str; 10] = ["color", "geom", "profile", "smlib", "sketch", "kernel", "render", "derive", "doc", "viewport"];
|
||||
/// The other files the saved model depends on, relative to the workspace root.
|
||||
const EVALUATION_FILES: [&str; 3] = ["crates/io/build.rs", "crates/io/src/design/native_cache.rs", ".cargo/config.toml"];
|
||||
/// The committed trees whose every file computes models, hashed as one tree id each: the vendored
|
||||
|
||||
+180
-69
@@ -1,12 +1,8 @@
|
||||
//! Versioned design packages. Entries are never extracted to filesystem paths. The optional
|
||||
//! model cache is only read back by the build that wrote it, for the exact document it was
|
||||
//! written with.
|
||||
#[cfg(test)]
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
#[cfg(test)]
|
||||
use std::io::Write;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod drawing;
|
||||
@@ -17,8 +13,8 @@ pub use native_cache::read_design_geometry;
|
||||
pub use thumbnail::{THUMBNAIL_SIZE, read_design_thumbnail, read_design_thumbnail_file};
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use solidcraft_doc::{Document, FeatureKind, MeshData};
|
||||
use solidcraft_format::container::{Limits, Reader, Writer, digest};
|
||||
|
||||
use crate::{IoError, Result, design_metadata::replace};
|
||||
|
||||
@@ -64,58 +60,35 @@ pub struct DesignPreview {
|
||||
}
|
||||
|
||||
/// Fast favors interactive file operations; Compact trades CPU time for space.
|
||||
pub use solidcraft_format::container::Compression as DesignCompression;
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DesignCompression {
|
||||
#[default]
|
||||
Fast,
|
||||
Compact,
|
||||
}
|
||||
|
||||
fn invalid(message: impl std::fmt::Display) -> IoError {
|
||||
IoError::Invalid(format!("design container: {message}"))
|
||||
}
|
||||
|
||||
/// The shared container's refusals are a design container's.
|
||||
impl From<solidcraft_format::FormatError> for IoError {
|
||||
fn from(e: solidcraft_format::FormatError) -> Self {
|
||||
invalid(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the package signature independently of the file name.
|
||||
pub fn is_design_container(bytes: &[u8]) -> bool {
|
||||
solidcraft_format::container::is_container(bytes)
|
||||
bytes.starts_with(b"PK\x03\x04")
|
||||
}
|
||||
|
||||
fn fields(pairs: impl IntoIterator<Item = (&'static str, Value)>) -> Map<String, Value> {
|
||||
pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect()
|
||||
}
|
||||
|
||||
/// A design container's budgets ([`crate::limits`]).
|
||||
const LIMITS: Limits = Limits {
|
||||
max_entries: MAX_ENTRIES,
|
||||
max_expanded,
|
||||
part_limit,
|
||||
level: part_level,
|
||||
check_size: |length| crate::limits::check(crate::limits::Kind::Design, length),
|
||||
};
|
||||
|
||||
/// The Deflate level of a part: STEP sources take [`STEP_LEVEL`], and so does a mesh past the file
|
||||
/// limit: level 1 of a large payload leaves more on disk, and that part exists only because
|
||||
/// compression is what makes it fit. Everything else is level 1.
|
||||
fn part_level(name: &str, length: usize) -> i64 {
|
||||
if name.ends_with(".step") || (name.ends_with(".mesh") && length > max_part()) { STEP_LEVEL } else { 1 }
|
||||
fn digest(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
/// A design container being written, within [`LIMITS`].
|
||||
struct PackageWriter(Writer);
|
||||
|
||||
impl std::ops::Deref for PackageWriter {
|
||||
type Target = Writer;
|
||||
fn deref(&self) -> &Writer {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for PackageWriter {
|
||||
fn deref_mut(&mut self) -> &mut Writer {
|
||||
&mut self.0
|
||||
}
|
||||
struct PackageWriter {
|
||||
zip: zip::ZipWriter<Cursor<Vec<u8>>>,
|
||||
names: BTreeSet<String>,
|
||||
expanded: usize,
|
||||
resolved: usize,
|
||||
}
|
||||
|
||||
/// The uncompressed size a part may have. STEP and mesh blobs may be larger than the
|
||||
@@ -132,10 +105,55 @@ fn part_limit(name: &str) -> usize {
|
||||
|
||||
impl PackageWriter {
|
||||
fn new() -> PackageWriter {
|
||||
PackageWriter(Writer::new(LIMITS))
|
||||
PackageWriter { zip: zip::ZipWriter::new(Cursor::new(Vec::new())), names: BTreeSet::new(), expanded: 0, resolved: 0 }
|
||||
}
|
||||
fn finish(self) -> Result<Vec<u8>> {
|
||||
Ok(self.0.finish()?)
|
||||
Ok(self.zip.finish().map_err(invalid)?.into_inner())
|
||||
}
|
||||
fn part(&mut self, name: &str, bytes: &[u8], compressed: bool) -> Result<()> {
|
||||
if bytes.len() > part_limit(name) {
|
||||
return Err(invalid("part too large"));
|
||||
}
|
||||
if !self.names.insert(name.to_owned()) {
|
||||
return Ok(());
|
||||
}
|
||||
self.expanded = self.expanded.checked_add(bytes.len()).ok_or_else(|| invalid("expanded size overflow"))?;
|
||||
if self.expanded > max_expanded() || self.names.len() > MAX_ENTRIES {
|
||||
return Err(invalid("package too large"));
|
||||
}
|
||||
let method = if compressed { zip::CompressionMethod::Deflated } else { zip::CompressionMethod::Stored };
|
||||
// A mesh past the file limit uses the STEP level: level 1 of a large payload leaves
|
||||
// more on disk, and this part exists only because compression is what makes it fit.
|
||||
let level = if name.ends_with(".step") || (name.ends_with(".mesh") && bytes.len() > max_part()) { STEP_LEVEL } else { 1 };
|
||||
// A part of 4 GiB or more (only with a raised file limit, #4086) needs a ZIP64 entry.
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(method)
|
||||
.compression_level(compressed.then_some(level))
|
||||
.large_file(bytes.len() as u64 >= u64::from(u32::MAX));
|
||||
self.zip.start_file(name, options).map_err(invalid)?;
|
||||
self.zip.write_all(bytes).map_err(invalid)
|
||||
}
|
||||
/// Would `entries` more optional parts of `extra` bytes, stored uncompressed, still keep
|
||||
/// the file within `budget` and its expanded size within the reader's budget?
|
||||
fn room(&self, entries: usize, extra: usize, budget: usize) -> bool {
|
||||
self.names.len().saturating_add(entries) <= MAX_ENTRIES && extra <= self.space(entries, budget)
|
||||
}
|
||||
/// The bytes `entries` more optional parts may hold together ([`PackageWriter::room`]).
|
||||
fn space(&self, entries: usize, budget: usize) -> usize {
|
||||
if self.names.len().saturating_add(entries) > MAX_ENTRIES {
|
||||
return 0;
|
||||
}
|
||||
// The bytes written so far, plus what the open compressed part may still hold back.
|
||||
let written = self.zip.get_ref().map_or(usize::MAX, |c| c.get_ref().len()).saturating_add(1 << 20);
|
||||
let directory = self.names.len().saturating_add(entries).saturating_mul(256).saturating_add(65_536);
|
||||
budget.saturating_sub(written.saturating_add(directory)).min(max_expanded().saturating_sub(self.expanded))
|
||||
}
|
||||
fn blob(&mut self, bytes: &[u8], extension: &str, compressed: bool) -> Result<Value> {
|
||||
self.resolved =
|
||||
self.resolved.checked_add(bytes.len()).filter(|&n| n <= max_expanded()).ok_or_else(|| invalid("resolved payloads too large"))?;
|
||||
let name = format!("blobs/{}.{extension}", digest(bytes));
|
||||
self.part(&name, bytes, compressed)?;
|
||||
Ok(Value::String(name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,25 +498,120 @@ fn decode_cloud(bytes: &[u8], cloud: &mut solidcraft_doc::point_cloud::PointClou
|
||||
cloud.check().map_err(invalid)
|
||||
}
|
||||
|
||||
/// A design container being read, within [`LIMITS`].
|
||||
struct PackageReader<R: Read + Seek>(Reader<R>);
|
||||
|
||||
impl<R: Read + Seek> std::ops::Deref for PackageReader<R> {
|
||||
type Target = Reader<R>;
|
||||
fn deref(&self) -> &Reader<R> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> std::ops::DerefMut for PackageReader<R> {
|
||||
fn deref_mut(&mut self) -> &mut Reader<R> {
|
||||
&mut self.0
|
||||
}
|
||||
struct PackageReader<R: Read + Seek> {
|
||||
zip: zip::ZipArchive<R>,
|
||||
blobs: BTreeMap<String, Vec<u8>>,
|
||||
hydrated: usize,
|
||||
document: Option<Arc<[u8]>>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> PackageReader<R> {
|
||||
fn new(source: R) -> Result<Self> {
|
||||
Ok(Self(Reader::new(source, LIMITS)?))
|
||||
fn new(mut source: R) -> Result<Self> {
|
||||
let length = source.seek(SeekFrom::End(0)).map_err(invalid)?;
|
||||
crate::limits::check(crate::limits::Kind::Design, length).map_err(invalid)?;
|
||||
let tail_length = length.min(65_557) as usize;
|
||||
source.seek(SeekFrom::Start(length - tail_length as u64)).map_err(invalid)?;
|
||||
let mut tail = vec![0; tail_length];
|
||||
source.read_exact(&mut tail).map_err(invalid)?;
|
||||
// zip's name index can coalesce duplicate central-directory entries. Our
|
||||
// bounded format does not need ZIP64/multiple disks: verify the declared
|
||||
// entry count as well so duplicate names cannot silently select a payload.
|
||||
let eocd = tail.windows(4).rposition(|v| v == b"PK\x05\x06").ok_or_else(|| invalid("missing ZIP directory"))?;
|
||||
let end = tail.get(eocd..).filter(|v| v.len() >= 22).ok_or_else(|| invalid("truncated ZIP directory"))?;
|
||||
let u16_at = |offset| -> Result<u16> {
|
||||
let array: [u8; 2] = end.get(offset..offset + 2).ok_or_else(|| invalid("truncated ZIP directory"))?.try_into().map_err(invalid)?;
|
||||
Ok(u16::from_le_bytes(array))
|
||||
};
|
||||
let declared = u16_at(10)? as usize;
|
||||
if declared > MAX_ENTRIES || u16_at(4)? != 0 || u16_at(6)? != 0 || u16_at(8)? as usize != declared || end.len() != 22 + u16_at(20)? as usize {
|
||||
return Err(invalid("too many entries or unsupported ZIP directory"));
|
||||
}
|
||||
// A design file is a ZIP with nothing in front of it. Saying so skips zip's search for a
|
||||
// prefix, which reads back through the stored parts and would load a large embedded
|
||||
// source before the preview can be shown.
|
||||
let config = zip::read::Config { archive_offset: zip::read::ArchiveOffset::Known(0) };
|
||||
let mut zip = zip::ZipArchive::with_config(config, source).map_err(invalid)?;
|
||||
if u16_at(4)? != 0
|
||||
|| u16_at(6)? != 0
|
||||
|| u16_at(8)? as usize != zip.len()
|
||||
|| u16_at(10)? as usize != zip.len()
|
||||
|| end.len() != 22 + u16_at(20)? as usize
|
||||
{
|
||||
return Err(invalid("duplicate entries or unsupported ZIP directory"));
|
||||
}
|
||||
let mut names = BTreeSet::new();
|
||||
let mut total = 0u64;
|
||||
for i in 0..zip.len() {
|
||||
let part = zip.by_index_raw(i).map_err(invalid)?;
|
||||
let name = part.name().map_err(invalid)?.into_owned();
|
||||
if name.is_empty()
|
||||
|| name.contains('\\')
|
||||
|| name.split('/').any(|c| c.is_empty() || c == "." || c == "..")
|
||||
|| !names.insert(name.to_owned())
|
||||
{
|
||||
return Err(invalid("duplicate or invalid archive entry"));
|
||||
}
|
||||
total = total.checked_add(part.size()).ok_or_else(|| invalid("expanded size overflow"))?;
|
||||
let limit = part_limit(&name);
|
||||
if part.size() > limit as u64 || total > max_expanded() as u64 {
|
||||
return Err(invalid("expanded package too large"));
|
||||
}
|
||||
}
|
||||
Ok(Self { zip, blobs: BTreeMap::new(), hydrated: 0, document: None })
|
||||
}
|
||||
// Bind provisional caches and authoritative reconstruction to identical metadata,
|
||||
// even if another process overwrites the open file in place. Blobs remain hash-checked.
|
||||
fn document(&mut self, limit: usize) -> Result<Arc<[u8]>> {
|
||||
if self.document.is_none() {
|
||||
self.document = Some(self.limited_part("document.json", limit)?.into());
|
||||
}
|
||||
self.document.as_ref().filter(|d| d.len() <= limit).cloned().ok_or_else(|| invalid("metadata budget"))
|
||||
}
|
||||
fn has_part(&self, name: &str) -> bool {
|
||||
self.zip.index_for_name(name).is_some()
|
||||
}
|
||||
fn limited_part(&mut self, name: &str, limit: usize) -> Result<Vec<u8>> {
|
||||
let part = self.zip.by_name(name).map_err(|e| invalid(format!("required part `{name}`: {e}")))?;
|
||||
let expected = part.size();
|
||||
if expected > limit as u64 {
|
||||
return Err(invalid("part too large"));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
bytes.try_reserve_exact(expected as usize).map_err(|_| invalid("could not reserve required part"))?;
|
||||
// A compressed part may expand beyond its declared size (a decompression bomb): stop
|
||||
// one byte past it instead of at the limit.
|
||||
part.take(expected + 1).read_to_end(&mut bytes).map_err(invalid)?;
|
||||
if bytes.len() > limit || bytes.len() as u64 != expected {
|
||||
return Err(invalid("part too large or length mismatch"));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
fn blob(&mut self, value: &Value, field: &str, extension: &str) -> Result<&[u8]> {
|
||||
self.limited_blob(value, field, extension, part_limit(&format!("blob.{extension}")))
|
||||
}
|
||||
fn limited_blob(&mut self, value: &Value, field: &str, extension: &str, limit: usize) -> Result<&[u8]> {
|
||||
let name = value.get(field).and_then(Value::as_str).ok_or_else(|| invalid(format!("missing `{field}` reference")))?;
|
||||
let hash = name
|
||||
.strip_prefix("blobs/")
|
||||
.and_then(|n| n.strip_suffix(&format!(".{extension}")))
|
||||
.filter(|h| h.len() == 64 && h.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)))
|
||||
.ok_or_else(|| invalid("invalid blob reference"))?;
|
||||
if !self.blobs.contains_key(name) {
|
||||
let bytes = self.limited_part(name, limit)?;
|
||||
if digest(&bytes) != hash {
|
||||
return Err(invalid(format!("blob hash mismatch: {name}")));
|
||||
}
|
||||
self.blobs.insert(name.to_owned(), bytes);
|
||||
}
|
||||
let bytes = self.blobs.get(name).ok_or_else(|| invalid("missing blob"))?;
|
||||
if bytes.len() > limit {
|
||||
return Err(invalid("blob exceeds its resource budget"));
|
||||
}
|
||||
// Count repeated references too: a tiny archive must not expand into unbounded
|
||||
// owned document strings/arrays when the same blob is referenced repeatedly.
|
||||
self.hydrated =
|
||||
self.hydrated.checked_add(bytes.len()).filter(|&n| n <= max_expanded()).ok_or_else(|| invalid("resolved payloads too large"))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,7 +845,7 @@ fn hydrate_newer<R: Read + Seek>(doc: &mut Document, metadata: &Value, container
|
||||
for (index, feature) in doc.features.iter_mut().enumerate() {
|
||||
let Some(value) = metadata.get("features").and_then(|v| v.get(index)) else { continue };
|
||||
let readable = match &mut feature.kind {
|
||||
FeatureKind::Import { .. } => reader.blob(value, "step_blob", "step").map(|_| ()).map_err(IoError::from),
|
||||
FeatureKind::Import { .. } => reader.blob(value, "step_blob", "step").map(|_| ()),
|
||||
FeatureKind::MeshImport { meshes, .. } => {
|
||||
let mut check = meshes.clone();
|
||||
check.iter_mut().enumerate().try_for_each(|(i, mesh)| {
|
||||
@@ -772,7 +885,8 @@ fn hydrate_newer<R: Read + Seek>(doc: &mut Document, metadata: &Value, container
|
||||
file.container = Some(container);
|
||||
}
|
||||
// A blob a typed record uses comes back from that record; others only for what is kept.
|
||||
for name in reader.part_names() {
|
||||
let names: Vec<String> = reader.zip.file_names().filter_map(|name| name.ok().map(|name| name.into_owned())).collect();
|
||||
for name in names {
|
||||
let ours = matches!(name.as_str(), "document.json" | "container.json") || name.starts_with("cache/");
|
||||
let unused_blob = name.starts_with("blobs/") && !file.mentions(&name) && !foreign_mentions(&doc.features, &name);
|
||||
if !ours && !unused_blob {
|
||||
@@ -864,11 +978,8 @@ fn read_document<R: Read + Seek>(reader: &mut PackageReader<R>) -> Result<Docume
|
||||
if newer_container && doc.newer_file.is_none() {
|
||||
let current = solidcraft_doc::format::FORMAT;
|
||||
let says = container.get("format").and_then(Value::as_str).unwrap_or("a later container").to_string();
|
||||
let file = solidcraft_doc::newer_file::newer_file(&solidcraft_doc::format::Newer {
|
||||
format: current,
|
||||
reads_up_to: current,
|
||||
unknown_changes: vec![says],
|
||||
});
|
||||
let mut file = solidcraft_doc::newer_file::NewerFile::default();
|
||||
(file.format, file.reads_up_to, file.unknown_changes) = (current, current, vec![says]);
|
||||
doc.newer_file = Some(Arc::new(file));
|
||||
}
|
||||
if doc.newer_file.is_some() {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! cache to the exact document, and to the source, platform and geometry kernel that computed it
|
||||
//! (docs/design-container.md).
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
use solidcraft_doc::{ModelState, SavedModel};
|
||||
|
||||
const FORMAT: &str = "solidcraft-model-cache/10";
|
||||
|
||||
@@ -4,20 +4,44 @@ pub const THUMBNAIL_SIZE: [usize; 2] = [240, 150];
|
||||
const MAX_BYTES: usize = 256 << 10;
|
||||
const FORMAT: &str = "solidcraft-thumbnail/1";
|
||||
|
||||
/// The design's picture, as the shared container keeps it.
|
||||
const PICTURE: solidcraft_format::container::Thumbnail =
|
||||
solidcraft_format::container::Thumbnail { format: FORMAT, app: env!("CARGO_PKG_VERSION"), size: THUMBNAIL_SIZE, max_bytes: MAX_BYTES };
|
||||
|
||||
fn valid_header(bytes: &[u8]) -> bool {
|
||||
bytes.len() <= MAX_BYTES
|
||||
&& bytes.get(..8) == Some(b"\x89PNG\r\n\x1a\n")
|
||||
&& bytes.get(8..16) == Some(b"\0\0\0\rIHDR")
|
||||
&& bytes.get(16..20) == Some((THUMBNAIL_SIZE[0] as u32).to_be_bytes().as_slice())
|
||||
&& bytes.get(20..24) == Some((THUMBNAIL_SIZE[1] as u32).to_be_bytes().as_slice())
|
||||
}
|
||||
pub(super) fn write(writer: &mut PackageWriter, png: &[u8], document: &[u8]) -> Result<()> {
|
||||
Ok(PICTURE.write(writer, png, document, max_design())?)
|
||||
if !valid_header(png) {
|
||||
return Ok(());
|
||||
}
|
||||
let key = serde_json::to_vec(&json!({"format": FORMAT, "app": env!("CARGO_PKG_VERSION"), "document": digest(document), "payload": digest(png)}))
|
||||
.map_err(invalid)?;
|
||||
if !writer.room(2, key.len().saturating_add(png.len()), max_design()) {
|
||||
return Ok(());
|
||||
}
|
||||
writer.part("cache/thumbnail.json", &key, false)?;
|
||||
writer.part("cache/thumbnail.png", png, false)
|
||||
}
|
||||
pub(super) fn read<R: Read + Seek>(reader: &mut PackageReader<R>) -> Result<Vec<u8>> {
|
||||
let container: Value = serde_json::from_slice(&reader.limited_part("container.json", 4096)?).map_err(invalid)?;
|
||||
if container.get("format").and_then(Value::as_str) != Some(VERSION) {
|
||||
return Err(invalid("thumbnail container version"));
|
||||
}
|
||||
let key: Value = serde_json::from_slice(&reader.limited_part("cache/thumbnail.json", 4096)?).map_err(invalid)?;
|
||||
let document = reader.document(16 << 20)?;
|
||||
if key.get("format").and_then(Value::as_str) != Some(FORMAT)
|
||||
|| key.get("app").and_then(Value::as_str) != Some(env!("CARGO_PKG_VERSION"))
|
||||
|| key.get("document").and_then(Value::as_str) != Some(digest(&document).as_str())
|
||||
{
|
||||
return Err(invalid("thumbnail key"));
|
||||
}
|
||||
let png = reader.limited_part("cache/thumbnail.png", MAX_BYTES)?;
|
||||
if !valid_header(&png) || key.get("payload").and_then(Value::as_str) != Some(digest(&png).as_str()) {
|
||||
return Err(invalid("thumbnail payload"));
|
||||
}
|
||||
// Consumers must also use a bounded PNG decoder; the header is not a decode.
|
||||
Ok(PICTURE.read(reader, 16 << 20)?)
|
||||
Ok(png)
|
||||
}
|
||||
/// Optional PNG bytes, bounded and keyed to this exact document. Consumers must
|
||||
/// decode with dimension/allocation limits; invalid pictures are cache misses.
|
||||
|
||||
@@ -18,7 +18,6 @@ fn model(body_count: usize) -> (Document, Model) {
|
||||
width: "20".into(),
|
||||
height: "30".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1045,7 +1045,6 @@ mod tests {
|
||||
width: "20".into(),
|
||||
height: "30".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1434,7 +1433,6 @@ mod tests {
|
||||
width: "30".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1558,7 +1556,6 @@ mod tests {
|
||||
width: "30".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
centred: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//! Issue #12126: a sweep whose profile reaches past the centre of a bend in its path. SMLib
|
||||
//! refuses it (the arc's piece is a turn about an axis the profile crosses), and the message
|
||||
//! said only "SMLib failed (status 1025)".
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
use solidcore_record::{Loop2, Plane, Region2, Vec2, Vec3};
|
||||
|
||||
use crate::{PathSeg, sweep};
|
||||
|
||||
/// A square of side `side` on the plane across the path's start (the YZ plane at the origin).
|
||||
fn square(side: f64) -> Region2 {
|
||||
let h = side / 2.0;
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(-h, -h), Vec2::new(h, -h), Vec2::new(h, h), Vec2::new(-h, h)]), holes: vec![] }
|
||||
}
|
||||
|
||||
fn across_start() -> Plane {
|
||||
Plane { origin: Vec3::ZERO, x: Vec3::Y, y: Vec3::Z }
|
||||
}
|
||||
|
||||
/// A straight run along X, then a quarter turn of `radius` towards +Y.
|
||||
fn line_then_bend(radius: f64) -> [PathSeg; 2] {
|
||||
[
|
||||
PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(20.0, 0.0, 0.0) },
|
||||
PathSeg::Arc { a: Vec3::new(20.0, 0.0, 0.0), center: Vec3::new(20.0, radius, 0.0), axis: Vec3::Z, angle: FRAC_PI_2 },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_profile_past_the_centre_of_a_bend_names_the_bend() {
|
||||
// The square reaches 2 mm towards the bend's centre, which is 1.5 mm away.
|
||||
let e = sweep(&across_start(), &square(4.0), &line_then_bend(1.5)).expect_err("the profile crosses the bend's axis");
|
||||
let msg = e.to_string();
|
||||
assert!(!msg.contains("status 1025"), "{msg}");
|
||||
assert!(msg.contains("bend") && msg.contains("1.5"), "{msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_profile_inside_the_bend_sweeps() {
|
||||
// The same square round a 3 mm bend stays clear of its centre.
|
||||
sweep(&across_start(), &square(4.0), &line_then_bend(3.0)).unwrap();
|
||||
}
|
||||
@@ -59,8 +59,6 @@ mod issue_1038_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_1080_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_12126_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_1510_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_2005_tests;
|
||||
|
||||
@@ -188,25 +188,6 @@ fn rim_loops(asm: &Assembly, fi: usize, n: Vec3) -> Result<(Vec<AUse>, Vec<AUse>
|
||||
}
|
||||
|
||||
fn profile(pieces: &[Piece], plane: &Plane) -> Result<Loop2> {
|
||||
let mut pieces = pieces.to_vec();
|
||||
// A source edge's vertex may be within its own tolerance of the circle rather than on it.
|
||||
// An extruded tool has fresh, tighter tolerances, so join the following piece to the
|
||||
// circle's evaluated end instead of carrying that source vertex into the tool profile.
|
||||
for i in 0..pieces.len() {
|
||||
let j = (i + 1) % pieces.len();
|
||||
let Some(piece) = pieces.get(i).copied() else { return Err(fail("piece")) };
|
||||
if piece.arc.is_some() {
|
||||
let end = piece.at(plane.normal(), 1.0);
|
||||
if let Some(current) = pieces.get_mut(i) {
|
||||
current.b = end;
|
||||
}
|
||||
if i != j
|
||||
&& let Some(next) = pieces.get_mut(j)
|
||||
{
|
||||
next.a = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
let segs = pieces
|
||||
.iter()
|
||||
.map(|p| {
|
||||
|
||||
+30
-241
@@ -10,14 +10,10 @@
|
||||
//! through the sections and the stations together.
|
||||
//!
|
||||
//! Sections are exact NURBS curves ([`Section`], from the sketch segments' record curves); a
|
||||
//! station is the plain loft's control points moved by an affine map, so each is exact too. Before
|
||||
//! the stations are made, every section's k-th curve is brought to one form ([`unified`]: the same
|
||||
//! degree and knots, the curve itself unchanged) so that the control points of a line pair with
|
||||
//! those of an arc or a spline against it (#12512); a station's control points then go straight
|
||||
//! between the sections' and its weights with them, so the weights need not agree either.
|
||||
//! station is the plain loft's control points moved by an affine map, so each is exact too.
|
||||
|
||||
use solidcore_record::{Plane, Seg2, Vec3};
|
||||
use solidcraft_geom::bspline::{CurveN, HPoint};
|
||||
use solidcraft_geom::bspline::CurveN;
|
||||
use solidcraft_geom::prelude::*;
|
||||
|
||||
use crate::sweep_path::RailFit;
|
||||
@@ -82,136 +78,15 @@ fn point_at(c: &CurveN<4>, f: f64) -> Option<Vec3> {
|
||||
(w.abs() > 1e-300).then(|| Vec3::new(x / w, y / w, z / w))
|
||||
}
|
||||
|
||||
/// Whether two knots are the same (curves run on [0, 1]).
|
||||
fn near(x: f64, y: f64) -> bool {
|
||||
(x - y).abs() <= 1e-9
|
||||
}
|
||||
|
||||
fn hpoint(h: [f64; 4]) -> HPoint {
|
||||
HPoint { x: h[0], y: h[1], z: h[2], w: h[3] }
|
||||
}
|
||||
|
||||
fn array(h: HPoint) -> [f64; 4] {
|
||||
[h.x, h.y, h.z, h.w]
|
||||
}
|
||||
|
||||
/// `c` on t in [0, 1] (its knots mapped affinely; the curve is unchanged).
|
||||
fn normalized(c: &CurveN<4>) -> Result<CurveN<4>> {
|
||||
let (p, n) = (c.degree, c.ctrl.len());
|
||||
let bad = || KernelError::Invalid("a loft section curve".into());
|
||||
let (Some(&t0), Some(&t1)) = (c.knots.get(p), c.knots.get(n)) else { return Err(bad()) };
|
||||
if n <= p || c.knots.len() != n + p + 1 || !(t1 > t0) || !t0.is_finite() || !t1.is_finite() {
|
||||
return Err(bad());
|
||||
}
|
||||
let knots = c.knots.iter().map(|u| ((u - t0) / (t1 - t0)).clamp(0.0, 1.0)).collect();
|
||||
Ok(CurveN { degree: p, knots, ctrl: c.ctrl.clone() })
|
||||
}
|
||||
|
||||
/// A Bézier piece's homogeneous control points one degree higher, the same curve (NURBS Book
|
||||
/// eq. 5.36).
|
||||
fn elevated(pts: &[HPoint]) -> Vec<HPoint> {
|
||||
let p = pts.len().saturating_sub(1);
|
||||
let at = |i: usize| pts.get(i).copied().unwrap_or(HPoint::ZERO);
|
||||
(0..=p + 1)
|
||||
.map(|i| {
|
||||
let a = i as f64 / (p + 1) as f64;
|
||||
let prev = if i == 0 { HPoint::ZERO } else { at(i - 1) * a };
|
||||
prev + at(i) * (1.0 - a)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The distinct interior knots of `c` (on [0, 1]) with their multiplicities, in order.
|
||||
fn interior(c: &CurveN<4>) -> Vec<(f64, usize)> {
|
||||
let mut out: Vec<(f64, usize)> = Vec::new();
|
||||
for &u in c.knots.iter().filter(|u| **u > 0.0 && **u < 1.0) {
|
||||
match out.last_mut() {
|
||||
Some(last) if near(last.0, u) => last.1 += 1,
|
||||
_ => out.push((u, 1)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `c` (on [0, 1]) as a curve of `degree`, the same curve: split into its Bézier pieces, each
|
||||
/// raised (exactly) and joined again, at each break a knot of multiplicity `degree`.
|
||||
fn raised(c: &CurveN<4>, degree: usize) -> Result<CurveN<4>> {
|
||||
if c.degree == degree {
|
||||
return Ok(c.clone());
|
||||
}
|
||||
let fail = |m: &str| KernelError::Failed(format!("loft rails: {m}"));
|
||||
if c.degree > degree {
|
||||
return Err(fail("degree"));
|
||||
}
|
||||
let h: Vec<HPoint> = c.ctrl.iter().map(|x| hpoint(*x)).collect();
|
||||
let mut breaks = vec![0.0];
|
||||
breaks.extend(interior(c).iter().map(|(u, _)| *u));
|
||||
breaks.push(1.0);
|
||||
let mut ctrl: Vec<[f64; 4]> = Vec::new();
|
||||
let mut knots = vec![0.0; degree + 1];
|
||||
for (i, w) in breaks.windows(2).enumerate() {
|
||||
let (_, mut piece) = solidcraft_geom::bspline::segment(c.degree, &c.knots, &h, w[0], w[1]).map_err(|e| fail(&e.to_string()))?;
|
||||
while piece.len() < degree + 1 {
|
||||
piece = elevated(&piece);
|
||||
}
|
||||
ctrl.extend(piece.iter().skip(usize::from(i > 0)).map(|q| array(*q)));
|
||||
if i > 0 {
|
||||
knots.extend(std::iter::repeat_n(w[0], degree));
|
||||
}
|
||||
}
|
||||
knots.extend(std::iter::repeat_n(1.0, degree + 1));
|
||||
if knots.len() != ctrl.len() + degree + 1 {
|
||||
return Err(fail("degree"));
|
||||
}
|
||||
Ok(CurveN { degree, knots, ctrl })
|
||||
}
|
||||
|
||||
/// Every section's k-th curve brought to one form without changing any curve: each on t in
|
||||
/// [0, 1], raised to the highest degree in its column, then given every knot the others have
|
||||
/// (Boehm insertion), so that their control points correspond one to one. The weights stay each
|
||||
/// curve's own (a line's are all 1, an arc's are not); a station between takes its control
|
||||
/// points straight between the sections' and its weights between theirs.
|
||||
fn unified(sections: &[Section]) -> Result<Vec<Section>> {
|
||||
let n = sections.first().map_or(0, |s| s.curves.len());
|
||||
let fail = |m: &str| KernelError::Failed(format!("loft rails: {m}"));
|
||||
let mut columns: Vec<Vec<CurveN<4>>> = Vec::with_capacity(n);
|
||||
for k in 0..n {
|
||||
let column: Vec<&CurveN<4>> = sections.iter().map(|s| s.curves.get(k).ok_or_else(|| fail("sections"))).collect::<Result<_>>()?;
|
||||
let degree = column.iter().map(|c| c.degree).max().unwrap_or(1);
|
||||
let mut curves: Vec<CurveN<4>> = column.iter().map(|c| raised(&normalized(c)?, degree)).collect::<Result<_>>()?;
|
||||
// The union of the interior knots, each with its greatest multiplicity.
|
||||
let mut union: Vec<(f64, usize)> = Vec::new();
|
||||
for (u, m) in curves.iter().flat_map(interior) {
|
||||
match union.iter_mut().find(|(x, _)| near(*x, u)) {
|
||||
Some(e) => e.1 = e.1.max(m),
|
||||
None => union.push((u, m)),
|
||||
}
|
||||
}
|
||||
union.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut knots = vec![0.0; degree + 1];
|
||||
knots.extend(union.iter().flat_map(|(u, m)| std::iter::repeat_n(*u, *m)));
|
||||
knots.extend(std::iter::repeat_n(1.0, degree + 1));
|
||||
for c in &mut curves {
|
||||
let mut h: Vec<HPoint> = c.ctrl.iter().map(|x| hpoint(*x)).collect();
|
||||
let mut k = c.knots.clone();
|
||||
for (u, m) in &union {
|
||||
let have = k.iter().filter(|x| near(**x, *u)).count();
|
||||
for _ in have..*m {
|
||||
(k, h) = solidcraft_geom::bspline::insert_knot(degree, &k, &h, *u).map_err(|e| fail(&e.to_string()))?;
|
||||
}
|
||||
}
|
||||
if k.len() != knots.len() {
|
||||
return Err(fail("knots"));
|
||||
}
|
||||
*c = CurveN { degree, knots: knots.clone(), ctrl: h.iter().map(|q| array(*q)).collect() };
|
||||
}
|
||||
columns.push(curves);
|
||||
}
|
||||
Ok(sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| Section { plane: s.plane, curves: columns.iter().filter_map(|col| col.get(i).cloned()).collect(), numbers: s.numbers.clone() })
|
||||
.collect())
|
||||
/// Whether two curves pair up: the same degree, knots and weights (their control points
|
||||
/// correspond).
|
||||
fn pairs(a: &CurveN<4>, b: &CurveN<4>) -> bool {
|
||||
let near = |x: f64, y: f64| (x - y).abs() <= 1e-9 * (1.0 + x.abs());
|
||||
a.degree == b.degree
|
||||
&& a.ctrl.len() == b.ctrl.len()
|
||||
&& a.knots.len() == b.knots.len()
|
||||
&& a.knots.iter().zip(&b.knots).all(|(x, y)| near(*x, *y))
|
||||
&& a.ctrl.iter().zip(&b.ctrl).all(|(x, y)| near(x[3], y[3]))
|
||||
}
|
||||
|
||||
/// Where a rail meets a section: the point of the section it guides (as a wire parameter: the
|
||||
@@ -247,8 +122,8 @@ impl Affine {
|
||||
/// The loft's sections with stations added between them so that the loft follows `rails`
|
||||
/// (polylines; see the module notes), in loft order: each entry one section's (or station's)
|
||||
/// curves. `closed` says whether the sections close. Without rails the sections' own curves come
|
||||
/// back. With rails, the sections' k-th curves are first brought to one form ([`unified`]) so
|
||||
/// that their control points correspond.
|
||||
/// back. With rails, every section's k-th curve has to pair with the others' (the same kind,
|
||||
/// so their control points correspond).
|
||||
pub(crate) fn railed(sections: &[Section], closed: bool, rails: &[Vec<Vec3>], fit: RailFit) -> Result<Vec<Vec<CurveN<4>>>> {
|
||||
if rails.is_empty() {
|
||||
return Ok(sections.iter().map(|s| s.curves.clone()).collect());
|
||||
@@ -260,25 +135,11 @@ pub(crate) fn railed(sections: &[Section], closed: bool, rails: &[Vec<Vec3>], fi
|
||||
let count = sections.len();
|
||||
let lead = sections.first().map(|s| &s.curves).ok_or_else(|| fail("sections"))?;
|
||||
let n = lead.len();
|
||||
if count < 2 || n == 0 || sections.iter().any(|s| s.curves.len() != n) {
|
||||
return Err(KernelError::Invalid("the loft sections have different numbers of curves".into()));
|
||||
if count < 2 || n == 0 || sections.iter().any(|s| s.curves.len() != n || s.curves.iter().zip(lead).any(|(a, b)| !pairs(a, b))) {
|
||||
return Err(KernelError::Invalid(
|
||||
"not supported yet: a loft along guide rails between sections whose curves differ in kind (a line against an arc)".into(),
|
||||
));
|
||||
}
|
||||
let sections = &unified(sections)?;
|
||||
let lead = sections.first().map(|s| &s.curves).ok_or_else(|| fail("sections"))?;
|
||||
// Each section's control points in space with their weights (`pctrl`: x, y, z, w) and alone
|
||||
// (`ctrl`).
|
||||
let pctrl: Vec<Vec<Vec<[f64; 4]>>> = sections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.curves
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let (pts, ws) = points(c);
|
||||
pts.iter().zip(ws).map(|(p, w)| [p.x, p.y, p.z, w]).collect()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let ctrl: Vec<Vec<Vec<Vec3>>> = sections.iter().map(|s| s.curves.iter().map(|c| points(c).0).collect()).collect();
|
||||
let mut bounds = solidcraft_geom::Aabb3::EMPTY;
|
||||
for p in ctrl.iter().flatten().flatten() {
|
||||
@@ -301,46 +162,36 @@ pub(crate) fn railed(sections: &[Section], closed: bool, rails: &[Vec<Vec3>], fi
|
||||
return Err(KernelError::Invalid("two loft sections sit on top of each other".into()));
|
||||
}
|
||||
along.iter_mut().for_each(|u| *u /= total);
|
||||
// The plain loft between the sections: each control point (and its weight) straight between
|
||||
// two sections, the curve (quadratic, or cubic from four) through them across more.
|
||||
// The plain loft between the sections: each control point straight between two sections, the
|
||||
// curve (quadratic, or cubic from four) through them across more.
|
||||
let degree = (count - 1).min(3);
|
||||
let across: Vec<Vec<CurveN<4>>> = if count == 2 {
|
||||
let across: Vec<Vec<CurveN<3>>> = if count == 2 {
|
||||
Vec::new()
|
||||
} else {
|
||||
(0..n)
|
||||
.map(|k| {
|
||||
(0..lead.get(k).map_or(0, |c| c.ctrl.len()))
|
||||
.map(|j| {
|
||||
let pts: Vec<[f64; 4]> = pctrl.iter().filter_map(|s| s.get(k)?.get(j).copied()).collect();
|
||||
let pts: Vec<[f64; 3]> = ctrl.iter().filter_map(|s| s.get(k)?.get(j).map(|q| [q.x, q.y, q.z])).collect();
|
||||
CurveN::interpolate(&along, &pts, degree).map_err(|e| fail(&e.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.collect::<Result<_>>()?
|
||||
};
|
||||
let plain = |v: f64| -> Result<Vec<Vec<[f64; 4]>>> {
|
||||
let (Some(a), Some(b)) = (pctrl.first(), pctrl.last()) else { return Err(fail("sections")) };
|
||||
let plain = |v: f64| -> Result<Vec<Vec<Vec3>>> {
|
||||
let (Some(a), Some(b)) = (ctrl.first(), ctrl.last()) else { return Err(fail("sections")) };
|
||||
if count == 2 {
|
||||
let lerp = |p: &[f64; 4], q: &[f64; 4]| [0, 1, 2, 3].map(|i| p[i] + (q[i] - p[i]) * v);
|
||||
return Ok(a.iter().zip(b).map(|(ea, eb)| ea.iter().zip(eb).map(|(p, q)| lerp(p, q)).collect()).collect());
|
||||
return Ok(a.iter().zip(b).map(|(ea, eb)| ea.iter().zip(eb).map(|(p, q)| p.lerp(*q, v)).collect()).collect());
|
||||
}
|
||||
across.iter().map(|cs| cs.iter().map(|c| c.eval(v).map(|(h, _)| h).ok_or_else(|| fail("the plain loft"))).collect()).collect()
|
||||
};
|
||||
// The station curves: the lead's degree and knots, the plain loft's points and weights.
|
||||
let curves_of = |h: &[Vec<[f64; 4]>]| -> Result<Vec<CurveN<4>>> {
|
||||
lead.iter()
|
||||
.zip(h)
|
||||
.map(|(c, pts)| {
|
||||
if pts.iter().any(|q| !(q[3] > 0.0) || !q.iter().all(|x| x.is_finite())) {
|
||||
return Err(fail("the plain loft"));
|
||||
}
|
||||
let ctrl = pts.iter().map(|q| [q[0] * q[3], q[1] * q[3], q[2] * q[3], q[3]]).collect();
|
||||
Ok(CurveN { degree: c.degree, knots: c.knots.clone(), ctrl })
|
||||
})
|
||||
across
|
||||
.iter()
|
||||
.map(|cs| cs.iter().map(|c| c.eval(v).map(|([x, y, z], _)| Vec3::new(x, y, z)).ok_or_else(|| fail("the plain loft"))).collect())
|
||||
.collect()
|
||||
};
|
||||
let curves_of = |pts: &[Vec<Vec3>]| -> Vec<CurveN<4>> { lead.iter().zip(pts).map(|(c, p)| with(c, p)).collect() };
|
||||
// The plain loft's middle and its direction there.
|
||||
let middle_at = |v: f64| -> Result<Vec3> { centroid(&curves_of(&plain(v)?)?).ok_or_else(|| fail("the plain loft")) };
|
||||
let middle_at = |v: f64| -> Result<Vec3> { centroid(&curves_of(&plain(v)?)).ok_or_else(|| fail("the plain loft")) };
|
||||
let middle = |v: f64| -> Result<(Vec3, Vec3)> {
|
||||
let h = 1e-4;
|
||||
let d = middle_at((v + h).min(1.0))? - middle_at((v - h).max(0.0))?;
|
||||
@@ -367,7 +218,7 @@ pub(crate) fn railed(sections: &[Section], closed: bool, rails: &[Vec<Vec3>], fi
|
||||
for q in 1..=per {
|
||||
let f = q as f64 / (per + 1) as f64;
|
||||
let v = v0 + f * (v1 - v0);
|
||||
let station = curves_of(&plain(v)?)?;
|
||||
let station = curves_of(&plain(v)?);
|
||||
let (mid, dir) = middle(v)?;
|
||||
// The station's plane turns from one section's to the next's.
|
||||
let normal = n0.lerp(n1, f).normalized().ok_or_else(|| fail("section planes"))?;
|
||||
@@ -592,65 +443,3 @@ pub(crate) fn station_map(pairs: &[(Vec3, Vec3)], middle: Vec3, dir: Vec3, fit:
|
||||
let shift = if two { Vec3::ZERO } else { dir * lead };
|
||||
Ok(Affine { from, to: to + shift, m })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod unify_tests {
|
||||
use super::*;
|
||||
use solidcore_record::Vec2;
|
||||
|
||||
fn plane(z: f64) -> Plane {
|
||||
Plane { origin: Vec3::new(0.0, 0.0, z), x: Vec3::X, y: Vec3::Y }
|
||||
}
|
||||
|
||||
/// Each unified curve is the curve it was, point for point along its parameter.
|
||||
#[test]
|
||||
fn unified_curves_are_the_curves_they_were() {
|
||||
let half = [
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 10.0, start: 0.0, sweep: std::f64::consts::PI },
|
||||
Seg2::Line { a: Vec2::new(-10.0, 0.0), b: Vec2::new(10.0, 0.0) },
|
||||
];
|
||||
let rect = [
|
||||
Seg2::Line { a: Vec2::new(-20.0, -10.0), b: Vec2::new(20.0, -10.0) },
|
||||
Seg2::Line { a: Vec2::new(20.0, -10.0), b: Vec2::new(20.0, 10.0) },
|
||||
Seg2::Line { a: Vec2::new(20.0, 10.0), b: Vec2::new(-20.0, 10.0) },
|
||||
Seg2::Line { a: Vec2::new(-20.0, 10.0), b: Vec2::new(-20.0, -10.0) },
|
||||
];
|
||||
// Arc against line, line against line, quarter arc against third-of-a-turn arc.
|
||||
let pie = |sweep: f64| {
|
||||
let end = Vec2::new(10.0 * sweep.cos(), 10.0 * sweep.sin());
|
||||
[
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 10.0, start: 0.0, sweep },
|
||||
Seg2::Line { a: end, b: Vec2::ZERO },
|
||||
Seg2::Line { a: Vec2::ZERO, b: Vec2::new(10.0, 0.0) },
|
||||
]
|
||||
};
|
||||
let cases: Vec<Vec<Section>> = vec![
|
||||
vec![
|
||||
Section::of(&plane(0.0), &half, vec![vec![0], vec![1]]).unwrap(),
|
||||
Section::of(&plane(40.0), &rect[..2], vec![vec![0], vec![1]]).unwrap(),
|
||||
],
|
||||
vec![
|
||||
Section::of(&plane(0.0), &pie(std::f64::consts::FRAC_PI_2), vec![vec![0], vec![1], vec![2]]).unwrap(),
|
||||
Section::of(&plane(40.0), &pie(2.0 * std::f64::consts::PI / 3.0), vec![vec![0], vec![1], vec![2]]).unwrap(),
|
||||
],
|
||||
];
|
||||
for sections in &cases {
|
||||
let out = unified(sections).unwrap();
|
||||
for (s, u) in sections.iter().zip(&out) {
|
||||
for (c, d) in s.curves.iter().zip(&u.curves) {
|
||||
assert_eq!(d.knots.len(), d.ctrl.len() + d.degree + 1, "{d:?}");
|
||||
for k in 0..=100 {
|
||||
let f = k as f64 / 100.0;
|
||||
let (p, q) = (point_at(c, f).unwrap(), point_at(d, f).unwrap());
|
||||
assert!(p.dist(q) < 1e-9, "at {f}: {p:?} vs {q:?}\n{c:?}\n{d:?}");
|
||||
}
|
||||
}
|
||||
let lead = &out[0].curves;
|
||||
for (c, d) in lead.iter().zip(&u.curves) {
|
||||
assert_eq!((c.degree, &c.knots, c.ctrl.len()), (d.degree, &d.knots, d.ctrl.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,76 +169,6 @@ fn rails_the_loft_cannot_follow_are_refused() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A pie of `sweep` radians and radius 10 about the origin: the arc, then two lines through the
|
||||
/// centre.
|
||||
fn pie(sweep: f64) -> Loop2 {
|
||||
let end = Vec2::new(10.0 * sweep.cos(), 10.0 * sweep.sin());
|
||||
Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 10.0, start: 0.0, sweep },
|
||||
Seg2::Line { a: end, b: Vec2::ZERO },
|
||||
Seg2::Line { a: Vec2::ZERO, b: Vec2::new(10.0, 0.0) },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// A rail straight up from `p` (x, y) from z = 0 to z = `top`.
|
||||
fn straight(p: Vec2, top: f64) -> Vec<Vec3> {
|
||||
(0..=80).map(|i| f64::from(i) * top / 80.0).map(|z| Vec3::new(p.x, p.y, z)).collect()
|
||||
}
|
||||
|
||||
/// Sections whose curves differ in kind take rails (#12512): a half-disc (an arc and a line)
|
||||
/// lofted to itself along a straight rail up the arc is the plain loft, the half-cylinder, exactly
|
||||
/// (its round wall is the circle at every height); and the half-disc lofts to a rectangle along a
|
||||
/// bulging rail, which it follows (the rail's point on the sections drifts from the arc's top
|
||||
/// to the middle of the rectangle's edge, so the scaled stations reach past the rail).
|
||||
#[test]
|
||||
fn sections_of_an_arc_and_a_line_take_rails() {
|
||||
let half = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 10.0, start: 0.0, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(-10.0, 0.0), b: Vec2::new(10.0, 0.0) },
|
||||
],
|
||||
};
|
||||
let sections = [(at(0.0), half.clone()), (at(40.0), half.clone())];
|
||||
let b = loft_with_rails(§ions, &[straight(Vec2::new(0.0, 10.0), 40.0)], RailFit::Scale).unwrap();
|
||||
let plain = volume(&loft(§ions).unwrap());
|
||||
assert!(close(volume(&b), 2000.0 * PI, 1e-3), "volume {} vs {}", volume(&b), 2000.0 * PI);
|
||||
assert!(close(volume(&b), plain, 1e-6), "{} vs the plain loft's {}", volume(&b), plain);
|
||||
for z in [5.0, 13.0, 20.0, 31.0] {
|
||||
for a in [0.3, 1.1, PI / 2.0, 2.4, 2.9] {
|
||||
let p = Vec3::new(10.0 * a.cos(), 10.0 * a.sin(), z);
|
||||
assert!(off(&b, p) < 1e-6, "{p:?} is {} mm off the half-cylinder", off(&b, p));
|
||||
}
|
||||
}
|
||||
// The reporter's case: the half-disc to a rectangle, with a rail bulging out from the arc.
|
||||
let rect = Loop2::polygon(&[Vec2::new(-20.0, -10.0), Vec2::new(20.0, -10.0), Vec2::new(20.0, 10.0), Vec2::new(-20.0, 10.0)]);
|
||||
let sections = [(at(0.0), half), (at(40.0), rect)];
|
||||
let r: Vec<Vec3> = (0..=80).map(|i| f64::from(i) * 0.5).map(|z| Vec3::new(0.0, 10.0 + 5.0 * bulge(z), z)).collect();
|
||||
let b = loft_with_rails(§ions, std::slice::from_ref(&r), RailFit::Scale).unwrap();
|
||||
let m = measure(&b).unwrap();
|
||||
let worst = r.iter().map(|p| off(&b, *p)).fold(0.0, f64::max);
|
||||
assert!(worst < 1e-3, "the rail is {worst} mm off the loft");
|
||||
assert!(m.volume > 40.0 * 50.0 * PI && m.volume < 40.0 * 800.0, "volume {}", m.volume);
|
||||
assert!(m.bbox.max.y > 15.0 - 1e-2 && m.bbox.min.z.abs() < 1e-9 && (m.bbox.max.z - 40.0).abs() < 1e-9, "{:?}", m.bbox);
|
||||
}
|
||||
|
||||
/// Arcs of different turns (one piece against two, different weights) pair up too: a quarter pie
|
||||
/// to a third-of-a-turn pie along a straight rail up their shared centre corner is the plain
|
||||
/// loft to a tenth of a percent (the stations between two arcs of different weights are not
|
||||
/// the ruled surface's sections exactly).
|
||||
#[test]
|
||||
fn arcs_of_different_turns_take_rails() {
|
||||
let sections = [(at(0.0), pie(PI / 2.0)), (at(40.0), pie(2.0 * PI / 3.0))];
|
||||
let r = straight(Vec2::ZERO, 40.0);
|
||||
let b = loft_with_rails(§ions, std::slice::from_ref(&r), RailFit::Scale).unwrap();
|
||||
let plain = volume(&loft(§ions).unwrap());
|
||||
assert!(close(volume(&b), plain, 1e-3), "{} vs the plain loft's {}", volume(&b), plain);
|
||||
assert!(volume(&b) > 40.0 * 25.0 * PI && volume(&b) < 40.0 * 100.0 * PI / 3.0, "volume {}", volume(&b));
|
||||
let worst = r.iter().map(|p| off(&b, *p)).fold(0.0, f64::max);
|
||||
assert!(worst < 1e-6, "the rail is {worst} mm off the loft");
|
||||
}
|
||||
|
||||
/// A section on the plane through the origin with axes `x` and `y` (normal x × y), with no curves:
|
||||
/// enough for the normals.
|
||||
fn facing(x: Vec3, y: Vec3) -> crate::loft_rails::Section {
|
||||
|
||||
@@ -82,34 +82,6 @@ pub(crate) use imp::{
|
||||
revolve, screw, screw_radial, sphere, sweep, sweep_controlled,
|
||||
};
|
||||
|
||||
/// `parts` joined in order in a balanced tree: each part with its neighbour, then each pair with
|
||||
/// the next pair, and so on, a part left over at the end of a level carried up as it is. Every
|
||||
/// join is of two neighbouring stretches, the left one first, and every part takes part in about
|
||||
/// log2(n) joins; joining each part onto all those before it instead hands the growing whole to
|
||||
/// every join, n²/2 parts' worth of work in all. None for no parts.
|
||||
fn union_tree<T>(parts: Vec<T>, mut join: impl FnMut(T, T) -> Result<T>) -> Result<Option<T>> {
|
||||
let mut level = parts;
|
||||
while level.len() > 1 {
|
||||
let mut next = Vec::with_capacity(level.len().div_ceil(2));
|
||||
let mut it = level.into_iter();
|
||||
while let Some(a) = it.next() {
|
||||
next.push(match it.next() {
|
||||
Some(b) => join(a, b)?,
|
||||
None => a,
|
||||
});
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
Ok(level.pop())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// The runs' pieces the unions joining a cornered sweep's runs have taken in on this thread,
|
||||
/// both operands counted: how the joining's work grows with the number of corners.
|
||||
pub(crate) static UNION_LOAD: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Refuses what no SMLib sweep does, before any work.
|
||||
pub(crate) fn refuse(what: &str) -> KernelError {
|
||||
not_supported(&format!("SMLib {what}"))
|
||||
@@ -906,27 +878,7 @@ mod imp {
|
||||
let c = curves(plane, pieces)?;
|
||||
let route = path.iter().map(path_curve).collect::<Result<Vec<_>>>()?;
|
||||
let frame = if translate { SweepFrame::Translate } else { SweepFrame::Follow };
|
||||
Brep::sweep_placed(&c, &route, frame, true).map_err(|e| sweep_failed(path, e))
|
||||
}
|
||||
|
||||
/// SMLib's refusal of a sweep along `path`, in words where its status says why: an arc of the
|
||||
/// path is swept as a turn about the arc's axis, and `SM_ERR_AXIS_INSIDE_FACE` (1025) is the
|
||||
/// profile reaching across that axis, past the centre of the bend (#12126).
|
||||
fn sweep_failed(path: &[crate::PathSeg], e: solidcraft_smlib::Error) -> KernelError {
|
||||
let tightest = path
|
||||
.iter()
|
||||
.filter_map(|s| match *s {
|
||||
crate::PathSeg::Arc { a, center, .. } => Some(a.dist(center)),
|
||||
_ => None,
|
||||
})
|
||||
.min_by(f64::total_cmp);
|
||||
match (&e, tightest) {
|
||||
(solidcraft_smlib::Error::Status(1025), Some(radius)) => KernelError::Invalid(format!(
|
||||
"the profile reaches past the centre of a bend in the path: it must stay inside the radius of every bend it turns towards (the tightest bend here is {} mm)",
|
||||
crate::blend::trimmed_mm(radius)
|
||||
)),
|
||||
_ => failed("sweep", e),
|
||||
}
|
||||
Brep::sweep_placed(&c, &route, frame, true).map_err(|e| failed("sweep", e))
|
||||
}
|
||||
|
||||
/// `q` moved by the motion `a`.
|
||||
@@ -999,17 +951,6 @@ mod imp {
|
||||
a.boolean(b, op).map_err(|e| failed(what, e))?.ok_or_else(|| failed(what, "nothing is left"))
|
||||
}
|
||||
|
||||
/// The union of the runs' pieces, in a balanced tree ([`union_tree`]) of the plain union.
|
||||
fn joined_runs(parts: Vec<Brep>) -> Result<Brep> {
|
||||
let held: Vec<(Brep, usize)> = parts.into_iter().map(|b| (b, 1)).collect();
|
||||
let joined = union_tree(held, |(a, m), (b, n)| {
|
||||
#[cfg(test)]
|
||||
UNION_LOAD.with(|l| l.set(l.get() + m + n));
|
||||
Ok((combined(&a, &b, solidcraft_smlib::BooleanOp::Union, "sweep corner")?, m + n))
|
||||
})?;
|
||||
joined.map(|(b, _)| b).ok_or_else(|| KernelError::Invalid("empty path".into()))
|
||||
}
|
||||
|
||||
/// A sweep along a path with corners: each smooth run of it swept on its own, and the runs
|
||||
/// joined at each corner. Turning with the path, the profile turns at a corner about the
|
||||
/// line square to both directions, and the runs meet in a mitre: each run is swept on
|
||||
@@ -1051,7 +992,7 @@ mod imp {
|
||||
let closed = end.dist(start) <= 1e-6;
|
||||
let closing = (closed && last_t.dot(first_t) < (1e-3f64).cos()).then_some(first_t);
|
||||
let mut pose = *plane;
|
||||
let mut parts: Vec<Brep> = Vec::with_capacity(runs.len());
|
||||
let mut body: Option<Brep> = None;
|
||||
let mut inside = Vec::new();
|
||||
// The mitre the next run starts from: its point, its normal (along the run) and the turn.
|
||||
let mut entry: Option<(Vec3, Vec3, f64)> = match closing {
|
||||
@@ -1116,7 +1057,10 @@ mod imp {
|
||||
}
|
||||
(piece, moved_plane(&placed(&from, &last), t1 * -post))
|
||||
};
|
||||
parts.push(piece);
|
||||
body = Some(match body {
|
||||
None => piece,
|
||||
Some(b) => combined(&b, &piece, solidcraft_smlib::BooleanOp::Union, "sweep corner")?,
|
||||
});
|
||||
// The profile at the run's end, then turned through the corner onto the next run.
|
||||
pose = at_end;
|
||||
entry = None;
|
||||
@@ -1126,7 +1070,7 @@ mod imp {
|
||||
entry = Some((b1, m, turn));
|
||||
}
|
||||
}
|
||||
let brep = joined_runs(parts)?;
|
||||
let brep = body.ok_or_else(|| KernelError::Invalid("empty path".into()))?;
|
||||
let body = body_of(brep, "sweep")?;
|
||||
let roles = roles(&body, plane, (!closed).then_some(&pose), &seeds(plane, pieces, |q| q), &inside, false)?;
|
||||
with_history(body, &roles, pieces)
|
||||
@@ -1671,7 +1615,7 @@ mod imp {
|
||||
_ => None,
|
||||
};
|
||||
let mut before = 0.0f64;
|
||||
let mut parts: Vec<Brep> = Vec::with_capacity(runs.len());
|
||||
let mut body: Option<Brep> = None;
|
||||
let mut inside = Vec::new();
|
||||
for (ri, run) in runs.iter().enumerate() {
|
||||
let segs = path.get(run.clone()).ok_or_else(|| KernelError::Invalid("path run".into()))?;
|
||||
@@ -1745,7 +1689,10 @@ mod imp {
|
||||
piece = combined(&piece, &half_space(b1, m, size)?, solidcraft_smlib::BooleanOp::Subtract, "sweep mitre")?;
|
||||
}
|
||||
}
|
||||
parts.push(piece);
|
||||
body = Some(match body {
|
||||
None => piece,
|
||||
Some(b) => combined(&b, &piece, solidcraft_smlib::BooleanOp::Union, "sweep corner")?,
|
||||
});
|
||||
// The section at the run's end, turned through the corner onto the next run.
|
||||
at_start = end_map;
|
||||
entry = None;
|
||||
@@ -1756,7 +1703,7 @@ mod imp {
|
||||
}
|
||||
before += length;
|
||||
}
|
||||
let brep = joined_runs(parts)?;
|
||||
let brep = body.ok_or_else(|| KernelError::Invalid("empty path".into()))?;
|
||||
if closed {
|
||||
// Back at the start, the section has to be the profile again (a whole number of its
|
||||
// symmetries of twist).
|
||||
@@ -1947,8 +1894,9 @@ mod imp {
|
||||
/// A loft through `sections` (as [`loft`] takes them) guided by one or two `rails` (polylines
|
||||
/// from the first section to the last, through every section's curve), by the law
|
||||
/// [`crate::loft_rails::railed`] gives a loft's rails: between each two sections stations are
|
||||
/// the plain loft there moved so that the points the rails guide land on the rails. The
|
||||
/// sections' curves are brought to one form there, so a line pairs with an arc (#12512).
|
||||
/// the plain loft there moved so that the points the rails guide land on the rails. Each
|
||||
/// section curve has to pair with the others' (the same kind, so their control points pair
|
||||
/// up).
|
||||
pub(crate) fn loft_rails(sections: &[(Plane, Vec<Piece>)], first: usize, rails: &[Vec<Vec3>], fit: crate::RailFit) -> Result<Body> {
|
||||
if rails.is_empty() || rails.len() > 2 {
|
||||
return Err(KernelError::Invalid("a loft takes one or two guide rails".into()));
|
||||
@@ -2020,29 +1968,3 @@ mod imp {
|
||||
with_history(body, &roles, pieces)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod tests {
|
||||
/// The corner pieces of a sweep along a path are joined in a balanced tree (#14451): every join
|
||||
/// is of two neighbouring stretches, left first, and for 180 runs (a 181-point polyline) the
|
||||
/// joins take in 180 · 8 = 1440 pieces' worth of operands at most, where joining each piece onto
|
||||
/// the body so far takes in 2 + 3 + … + 180 = 16,289.
|
||||
#[test]
|
||||
fn corner_pieces_are_joined_neighbours_first_in_a_balanced_tree() {
|
||||
let mut load = 0;
|
||||
let mut joins = 0;
|
||||
let whole = super::union_tree((0..180).map(|i| i..i + 1).collect(), |a: std::ops::Range<usize>, b: std::ops::Range<usize>| {
|
||||
assert_eq!(a.end, b.start, "joined {a:?} with {b:?}, not its neighbour");
|
||||
load += a.len() + b.len();
|
||||
joins += 1;
|
||||
Ok(a.start..b.end)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(whole, Some(0..180));
|
||||
assert_eq!(joins, 179);
|
||||
assert!(load <= 180 * 8, "{load}");
|
||||
assert_eq!(super::union_tree(Vec::<usize>::new(), |a, _| Ok(a)).unwrap(), None);
|
||||
assert_eq!(super::union_tree(vec![7], |a, _| Ok(a)).unwrap(), Some(7));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,33 +144,6 @@ fn a_parallel_rail_along_a_polyline_turns_with_its_corners() {
|
||||
assert!(close(m.volume, volume, 1e-6), "volume {} vs {volume}", m.volume);
|
||||
}
|
||||
|
||||
/// A densely sampled polyline (#14451): 33 points, 32 runs, 31 mitres. The runs' pieces are
|
||||
/// unioned in a balanced tree, so each takes part in at most ⌈log2 32⌉ = 5 unions (160 pieces'
|
||||
/// worth of union operands in all, where unioning each onto the body so far takes in
|
||||
/// 2 + 3 + … + 32 = 527 and grows with the square of the points), and the solid is the same
|
||||
/// oblique cylinders: volume πr² · L · cos(d/2).
|
||||
#[test]
|
||||
fn a_rail_along_a_dense_polyline_joins_its_runs_in_a_balanced_union() {
|
||||
let n = 32;
|
||||
let arc = |radius: f64| -> Vec<Vec3> {
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let a = PI / 2.0 * f64::from(i) / f64::from(n);
|
||||
Vec3::new(30.0 - radius * a.cos(), 0.0, radius * a.sin())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
crate::smlib_sweeps::UNION_LOAD.with(|l| l.set(0));
|
||||
let b = sweep_path_with(&Plane::XY, &circle(5.0), &arc(30.0), &railed(vec![arc(35.0)], RailFit::Scale)).unwrap();
|
||||
let load = crate::smlib_sweeps::UNION_LOAD.with(std::cell::Cell::get);
|
||||
assert_eq!(load, 32 * 5, "union operands, in runs' pieces");
|
||||
let m = measure(&b).unwrap();
|
||||
let d = PI / 2.0 / f64::from(n);
|
||||
let length = f64::from(n) * 60.0 * (d / 2.0).sin();
|
||||
let volume = PI * 25.0 * length * (d / 2.0).cos();
|
||||
assert!(close(m.volume, volume, 1e-6), "volume {} vs {volume}", m.volume);
|
||||
}
|
||||
|
||||
/// Two rails bulging apart along arcs (radius 25 about (∓5, 0, 20), through x = ±10 at z = 0
|
||||
/// and z = 40) carry a line between them: the sheet lies in the XZ plane between the arcs, area
|
||||
/// 2(a√(R² − a²) + R² asin(a/R)) − 400 with a = 20.
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"bend_up": "#ffffff",
|
||||
"body": "#000000",
|
||||
"body_edge": "#ffffff",
|
||||
"isoparm": "#ffffff",
|
||||
"body_selected": "#ffffff",
|
||||
"border": "#ffffff",
|
||||
"box_crossing": "#ffffff",
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"bend_up": "#000000",
|
||||
"body": "#ffffff",
|
||||
"body_edge": "#000000",
|
||||
"isoparm": "#000000",
|
||||
"body_selected": "#000000",
|
||||
"border": "#000000",
|
||||
"box_crossing": "#000000",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#257231",
|
||||
"body": "#c2beb8",
|
||||
"body_edge": "#403d38",
|
||||
"isoparm": "#817e78",
|
||||
"body_selected": "#de8d6b",
|
||||
"border": "#d9d5d0",
|
||||
"box_crossing": "#257231",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#d4902f",
|
||||
"body": "#382309",
|
||||
"body_edge": "#ffbc48",
|
||||
"isoparm": "#9c7029",
|
||||
"body_selected": "#543615",
|
||||
"border": "#533812",
|
||||
"box_crossing": "#d4902f",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#6ee0bb",
|
||||
"body": "#8baccb",
|
||||
"body_edge": "#eef8ff",
|
||||
"isoparm": "#bdd2e5",
|
||||
"body_selected": "#58bbdf",
|
||||
"border": "#315777",
|
||||
"box_crossing": "#6ee0bb",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#a6e3a1",
|
||||
"body": "#586382",
|
||||
"body_edge": "#477dc8",
|
||||
"isoparm": "#5070a5",
|
||||
"body_selected": "#365280",
|
||||
"border": "#45475a",
|
||||
"box_crossing": "#a6e3a1",
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"bend_up": "#55ff55",
|
||||
"body": "#aa00aa",
|
||||
"body_edge": "#55ffff",
|
||||
"isoparm": "#55ffff",
|
||||
"body_selected": "#ffff55",
|
||||
"border": "#55ffff",
|
||||
"box_crossing": "#ff55ff",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#89cf8e",
|
||||
"body": "#8b8378",
|
||||
"body_edge": "#1b1814",
|
||||
"isoparm": "#534e46",
|
||||
"body_selected": "#b8603f",
|
||||
"border": "#403930",
|
||||
"box_crossing": "#89cf8e",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#257231",
|
||||
"body": "#c8c0b3",
|
||||
"body_edge": "#423e36",
|
||||
"isoparm": "#857f75",
|
||||
"body_selected": "#e2967a",
|
||||
"border": "#e0d8c9",
|
||||
"box_crossing": "#257231",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#36934d",
|
||||
"body": "#b6bbc3",
|
||||
"body_edge": "#393d44",
|
||||
"isoparm": "#787c84",
|
||||
"body_selected": "#79afea",
|
||||
"border": "#c6c6c8",
|
||||
"box_crossing": "#39a75b",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#83c69b",
|
||||
"body": "#68717d",
|
||||
"body_edge": "#2878a9",
|
||||
"isoparm": "#487593",
|
||||
"body_selected": "#245674",
|
||||
"border": "#484d55",
|
||||
"box_crossing": "#83c69b",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#39d45c",
|
||||
"body": "#0b3818",
|
||||
"body_edge": "#4dff79",
|
||||
"isoparm": "#2c9c49",
|
||||
"body_selected": "#18542b",
|
||||
"border": "#285333",
|
||||
"box_crossing": "#39d45c",
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"construction_geom": "#e97926",
|
||||
"body": "#a89984",
|
||||
"body_edge": "#1d2021",
|
||||
"isoparm": "#635d53",
|
||||
"manipulator": "#83a598",
|
||||
"preview_add": "#83a598",
|
||||
"preview_new": "#fabd2f",
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"construction_geom": "#9b3407",
|
||||
"body": "#bdae93",
|
||||
"body_edge": "#3c3836",
|
||||
"isoparm": "#7d7365",
|
||||
"manipulator": "#076678",
|
||||
"preview_add": "#458588",
|
||||
"preview_new": "#d79921",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#40e080",
|
||||
"body": "#555555",
|
||||
"body_edge": "#0070d0",
|
||||
"isoparm": "#2b6393",
|
||||
"body_selected": "#004c80",
|
||||
"border": "#a0a0a0",
|
||||
"box_crossing": "#40e080",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#a3be8c",
|
||||
"body": "#657991",
|
||||
"body_edge": "#5e81ac",
|
||||
"isoparm": "#627d9f",
|
||||
"body_selected": "#34607a",
|
||||
"border": "#4c566a",
|
||||
"box_crossing": "#a3be8c",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#50fa7b",
|
||||
"body": "#656581",
|
||||
"body_edge": "#8456b8",
|
||||
"isoparm": "#755e9d",
|
||||
"body_selected": "#563c79",
|
||||
"border": "#50546d",
|
||||
"box_crossing": "#50fa7b",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#89cf8e",
|
||||
"body": "#74777b",
|
||||
"body_edge": "#0f1113",
|
||||
"isoparm": "#424447",
|
||||
"body_selected": "#c56241",
|
||||
"border": "#2f3235",
|
||||
"box_crossing": "#89cf8e",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#98c379",
|
||||
"body": "#abb2bf",
|
||||
"body_edge": "#181a1f",
|
||||
"isoparm": "#62666f",
|
||||
"body_selected": "#61afef",
|
||||
"border": "#3e4451",
|
||||
"box_crossing": "#98c379",
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"bend_up": "#a5b9a5",
|
||||
"body": "#93989c",
|
||||
"body_edge": "#60666b",
|
||||
"isoparm": "#7a7f84",
|
||||
"body_selected": "#3b4852",
|
||||
"border": "#3d4145",
|
||||
"box_window": "#9eafbd",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#1e8c32",
|
||||
"body": "#adadb6",
|
||||
"body_edge": "#292933",
|
||||
"isoparm": "#6b6b75",
|
||||
"body_selected": "#9999cc",
|
||||
"border": "#888888",
|
||||
"box_crossing": "#28a050",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"bend_up": "#859900",
|
||||
"body": "#586e75",
|
||||
"body_edge": "#346e88",
|
||||
"isoparm": "#466e7f",
|
||||
"body_selected": "#073642",
|
||||
"border": "#586e75",
|
||||
"box_crossing": "#859900",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#859900",
|
||||
"body": "#b5b6a0",
|
||||
"body_edge": "#346e88",
|
||||
"isoparm": "#759294",
|
||||
"body_selected": "#bdd9da",
|
||||
"border": "#b6b3a1",
|
||||
"box_crossing": "#859900",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#008000",
|
||||
"body": "#c0c0c0",
|
||||
"body_edge": "#000000",
|
||||
"isoparm": "#606060",
|
||||
"body_selected": "#8080ff",
|
||||
"border": "#808080",
|
||||
"box_crossing": "#008000",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"bend_up": "#9ece6a",
|
||||
"body": "#4f6089",
|
||||
"body_edge": "#4775ce",
|
||||
"isoparm": "#4b6bac",
|
||||
"body_selected": "#304e7e",
|
||||
"border": "#414868",
|
||||
"box_crossing": "#9ece6a",
|
||||
|
||||
@@ -198,6 +198,7 @@ pub(crate) fn fixes() -> UpstreamFixes {
|
||||
f.delete_vertex_joins_seam_iso_curve = true;
|
||||
f.box_face_failures_propagate = true;
|
||||
f.ellipse_adjust_step_interval_keeps_turns = true;
|
||||
f.fillet_cleanup_removes_input_slits = true;
|
||||
f.fillet_rail_end_on_continued_surface = true;
|
||||
f.glue_vertices_joins_every_region = true;
|
||||
f.fillet_removal_takes_ring_fillets = true;
|
||||
|
||||
@@ -43,7 +43,7 @@ use solidcore::topology::brep::p1 as brep_p1;
|
||||
use solidcore::topology::brep::p2::{is_manifold_solid, is_sheet};
|
||||
use solidcore::topology::brep::p3::{new_brep, with_kernel_callbacks};
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
use solidcore::topology::brep::p9::{OrientSheetNote, orient_sheet_surfaces};
|
||||
use solidcore::topology::brep::p9::orient_sheet_surfaces;
|
||||
use solidcore::topology::brep::p10::merge_curves_on_surface_through_surface;
|
||||
use solidcore::topology::brep::planar_section::face_planar_section_curves;
|
||||
use solidcore::topology::brep_cache::CacheObject;
|
||||
@@ -497,8 +497,7 @@ pub(crate) fn split_body_by_sheet(body: &Body, sheet: &Body, face: usize) -> Res
|
||||
// the sheet's own attributes, so its faces' tags see each swap.
|
||||
let turned = with_kernel_callbacks(k, &mut copy, |k, b, env| orient_sheet_surfaces(k, b, Some(f), !flip, env));
|
||||
copy.refresh_cached_topology();
|
||||
// the orientation's note fails the side as its status 1 did
|
||||
Ok(match OrientSheetNote::stop_on_note(turned) {
|
||||
Ok(match turned {
|
||||
Ok(()) => Ok(copy),
|
||||
Err(e) => Err(status(e)),
|
||||
})
|
||||
|
||||
@@ -1086,9 +1086,6 @@ pub struct Tokens {
|
||||
// ---- bodies and live previews ----
|
||||
pub body: Color32,
|
||||
pub body_edge: Color32,
|
||||
/// Isoparm lines drawn on a surface: lighter than its edges, so they read as the surface's
|
||||
/// flow, not its boundary.
|
||||
pub isoparm: Color32,
|
||||
/// Command manipulators (direction arrows, handles).
|
||||
pub manipulator: Color32,
|
||||
/// Allowed assembly motion, separate from the selected face's blue.
|
||||
@@ -1455,7 +1452,6 @@ impl Tokens {
|
||||
construction_geom: view.construction_geom,
|
||||
body: view.body,
|
||||
body_edge: view.body_edge,
|
||||
isoparm: view.isoparm,
|
||||
manipulator: view.manipulator,
|
||||
joint_manipulator: view.joint_manipulator,
|
||||
joint_axis: view.joint_axis,
|
||||
@@ -1636,20 +1632,6 @@ pub fn panel_motion_ease(progress: f32) -> f32 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A surface's isoparms show on its faces less than its edges do, and stay visible against the
|
||||
/// view's background (seen at the sheet's silhouette), on both bases.
|
||||
#[test]
|
||||
fn isoparms_are_lighter_than_edges_and_visible_on_the_background() {
|
||||
for t in [Tokens::light(), Tokens::dark()] {
|
||||
let (iso, edge) = (contrast_ratio(t.isoparm, t.body), contrast_ratio(t.body_edge, t.body));
|
||||
assert!(iso >= 1.5 && iso < edge, "isoparm {iso:.2} against edge {edge:.2} on the body");
|
||||
for background in [t.viewport_top, t.viewport_bottom] {
|
||||
let ratio = contrast_ratio(t.isoparm, background);
|
||||
assert!(ratio >= 1.5, "isoparm {ratio:.2} on the viewport background {background:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_motion_is_bounded_and_reduced_or_invalid_inputs_finish_immediately() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -189,8 +189,6 @@ impl Tokens {
|
||||
construction_geom: icon_primary(strokes::CONSTRUCTION_PRIMARY[0]),
|
||||
body: Color32::from_rgb(176, 186, 198),
|
||||
body_edge: Color32::from_rgb(40, 44, 52),
|
||||
// Halfway from the edge colour to the body's: about half the edges' contrast.
|
||||
isoparm: Color32::from_rgb(108, 115, 125),
|
||||
manipulator: Color32::from_rgb(40, 110, 220),
|
||||
joint_manipulator: Color32::from_rgb(235, 157, 0),
|
||||
joint_axis: Color32::from_rgb(0, 95, 110),
|
||||
@@ -431,7 +429,6 @@ impl Tokens {
|
||||
t.grid_major = Color32::from_rgb(73, 79, 90);
|
||||
t.body = Color32::from_rgb(152, 156, 162);
|
||||
t.body_edge = Color32::from_rgb(16, 16, 18);
|
||||
t.isoparm = Color32::from_rgb(84, 86, 90);
|
||||
t.sketch = Color32::from_rgb(92, 150, 255);
|
||||
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_halo = Color32::from_rgb(28, 32, 40);
|
||||
|
||||
@@ -1395,11 +1395,8 @@ fn top_controls(chat: &mut AgentChat, view: &PanelView, ui: &mut egui::Ui, state
|
||||
new_chat_confirmation(chat, ui);
|
||||
}
|
||||
// Shared top controls must yield scroll space to the pinned composer, including
|
||||
// when discovery lists several agents or a long message grows the editor. The
|
||||
// sign-in box scrolls in the same place in either layout (#15087), and may use
|
||||
// everything above the composer: there is no conversation to show until it is done.
|
||||
let needs_auth = state == State::NeedsAuth;
|
||||
let picker_max = if shared || needs_auth {
|
||||
// when discovery lists several agents or a long message grows the editor.
|
||||
let picker_max = if shared {
|
||||
let t = Tokens::get();
|
||||
let composer = view.composer_height.unwrap_or(48.0 + t.control_height * 3.0 + t.space_8 * 2.0 + t.space_6 * 3.0);
|
||||
(ui.available_height() - composer - ui.spacing().item_spacing.y).max(0.0)
|
||||
@@ -1419,10 +1416,13 @@ fn top_controls(chat: &mut AgentChat, view: &PanelView, ui: &mut egui::Ui, state
|
||||
ui_kit::assistant_controls(ui, |ui| switcher(chat, view, ui));
|
||||
}
|
||||
picker(chat, view, ui);
|
||||
if needs_auth {
|
||||
if shared && state == State::NeedsAuth {
|
||||
inset(ui, |ui| auth_box(chat, ui));
|
||||
}
|
||||
});
|
||||
if !shared && state == State::NeedsAuth {
|
||||
inset(ui, |ui| auth_box(chat, ui));
|
||||
}
|
||||
ui.cursor().top() - top - picker.inner_rect.height().min(picker.content_size.y) + picker.content_size.y
|
||||
}
|
||||
|
||||
@@ -2202,9 +2202,7 @@ fn auth_box(chat: &mut AgentChat, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
ui.add_space(solidcraft_theme::Tokens::get().space_4);
|
||||
let retry = ui_kit::button(ui, tr("ui.agent_chat_auth_retry"), ui_kit::Button::Normal);
|
||||
solidcraft_ui_registry::publish_handle("agent_chat:auth_retry", retry.rect.center());
|
||||
if retry.clicked() {
|
||||
if ui_kit::button(ui, tr("ui.agent_chat_auth_retry"), ui_kit::Button::Normal).clicked() {
|
||||
chat.actions.push(PanelAction::Connect);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5571,59 +5571,6 @@ fn shared_assistant_body(h: &crate::scenario::Harness) -> egui::Rect {
|
||||
egui::Rect::from_min_size(egui::pos2(body.origin.x, body.origin.y), egui::vec2(body.size.width, body.size.height))
|
||||
}
|
||||
|
||||
/// #15087: an agent that asks to sign in two ways, in the docked Assistant of a small window, with
|
||||
/// the default panel layout and the shared one. The sign-in box scrolls inside the panel, and the
|
||||
/// message box stays in view below it with nothing drawn over it.
|
||||
#[test]
|
||||
fn a_long_sign_in_box_scrolls_inside_a_short_docked_assistant() {
|
||||
use solidcraft_ui_registry::{handle_at, rect_of};
|
||||
for shared in [false, true] {
|
||||
let (mut h, _script) = shared_assistant_harness();
|
||||
h.app.preferences.shared_panel_layout = shared;
|
||||
h.app.ui.show_browser = true;
|
||||
h.app.ui.show_timeline = true;
|
||||
h.call("ui.resize", json!({"width":960,"height":600}));
|
||||
let terminal = |id: &str, name: &str, flag: &str| acp::AuthMethod {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
description: format!("Sign in with {name}"),
|
||||
kind: acp::AuthKind::Terminal { args: vec!["--cli".into(), "auth".into(), "login".into(), flag.into()], command: None },
|
||||
};
|
||||
h.app.agent_chat.argv = vec!["standin-agent".into()];
|
||||
h.app.agent_chat.auth = vec![terminal("console", "Console", "--console"), terminal("subscription", "Subscription", "--claudeai")];
|
||||
// A new chat: its tip is drawn below the sign-in box, where the conversation goes.
|
||||
h.app.agent_chat.items.clear();
|
||||
h.app.agent_chat.input.clear();
|
||||
h.app.agent_chat.state = Some(State::NeedsAuth);
|
||||
h.frames(8);
|
||||
assert_eq!(h.app.panel_runtime().is_some(), shared);
|
||||
let window = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(960.0, 600.0));
|
||||
let body = if shared { shared_assistant_body(&h) } else { rect_of("agent_chat:panel").expect("the docked Assistant") };
|
||||
let composer = rect_of("agent_chat:composer_footer").expect("the message box");
|
||||
let providers = rect_of("agent_chat:providers").expect("the sign-in box's scroll area");
|
||||
let conversation = rect_of("agent_chat:conversation").expect("the conversation");
|
||||
assert!(window.expand(1.0).contains_rect(body), "shared {shared}: the Assistant {body:?} fits the window");
|
||||
assert!(body.expand(1.0).contains_rect(composer), "shared {shared}: the message box {composer:?} stays inside {body:?}");
|
||||
assert!(providers.bottom() <= composer.top() + 1.0, "shared {shared}: the sign-in box {providers:?} ends above the message box {composer:?}");
|
||||
assert!(
|
||||
conversation.bottom() <= composer.top() + 1.0,
|
||||
"shared {shared}: the conversation {conversation:?} ends above the message box {composer:?}"
|
||||
);
|
||||
for _ in 0..10 {
|
||||
if handle_at("agent_chat:auth_retry").is_some_and(|at| providers.contains(at)) {
|
||||
break;
|
||||
}
|
||||
h.call("ui.scroll", json!({"x":providers.center().x,"y":providers.center().y,"delta":-300}));
|
||||
h.frames(4);
|
||||
}
|
||||
let at = handle_at("agent_chat:auth_retry").expect("the I've signed in button is drawn");
|
||||
assert!(
|
||||
providers.contains(at),
|
||||
"shared {shared}: scrolling the sign-in box brings I've signed in: connect again {at:?} into view {providers:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_assistant_wide_and_compact_present_real_regions_without_losing_the_composer_selection() {
|
||||
use solidcraft_engine::panels::{Destination, InstanceId, LayoutAction, Mutation, SectionId, SectionLocation, Size, Zone};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user