mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 11:35:55 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed7206cc0d | |||
| 16f3b939b3 | |||
| ad230eb4a4 | |||
| c1760d4857 | |||
| 4d69d8c87a | |||
| fa2faf99e2 | |||
| 20467e39e2 | |||
| 020e3bebe3 | |||
| 6029a38ea0 | |||
| 9c93a6c340 | |||
| 9fdb7fdefc | |||
| 718d5a4383 | |||
| 1c10f82b34 | |||
| ca4f3ed1a0 | |||
| a1d66c238e | |||
| 70603de654 | |||
| db20c8be23 | |||
| 8f4fe740c7 | |||
| aae4282f1e | |||
| 18b698ffa8 | |||
| 051e6236c3 | |||
| 8691cefbbf | |||
| 6d2465b52d | |||
| c517ca0393 | |||
| ee1bdea97b | |||
| dc54d2f229 | |||
| b916f9bf33 | |||
| 733f0b2254 | |||
| ee02080a25 | |||
| 99d96482af | |||
| a5be592b7d | |||
| d92923a964 | |||
| 71598c0354 | |||
| 06a8a9b7d2 | |||
| 4d2691e636 | |||
| 6e17874cb9 | |||
| bf22dad8e8 | |||
| 731029cd87 | |||
| 6b5c5dc6e8 | |||
| e7ce6c282a | |||
| 67c58cbd28 | |||
| 7c89f7445f | |||
| 26adcfa997 | |||
| 1e41dddae2 | |||
| a9b219bc9f | |||
| 32dc8750df | |||
| ceeff58a7f | |||
| a55286953a | |||
| 9a0846ef68 | |||
| 06749ac464 | |||
| a6e7126bf1 | |||
| 4a906699ee | |||
| a2a7102859 |
@@ -3,13 +3,13 @@
|
||||
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
|
||||
|
||||
## Start here
|
||||
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
|
||||
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Local `plan/` notes are optional.
|
||||
|
||||
## Hard rules (what enforces each)
|
||||
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
|
||||
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
|
||||
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code, errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
|
||||
- New kernel cases go to solidcore: `cargo xtask lists`.
|
||||
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
|
||||
@@ -21,12 +21,14 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
|
||||
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
|
||||
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line: `pr-checks.yml`.
|
||||
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Code, comments, commit messages and docs are in English, always: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
|
||||
- Independent work runs in parallel, dependent work stays sequential: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
|
||||
- Show observable changes with real screenshots or a recording, captured yourself.
|
||||
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
- Run `cargo xtask ci` before every commit. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
|
||||
## Working on X: read the skill first
|
||||
| Working on | Read |
|
||||
|
||||
+30
-2
@@ -799,6 +799,35 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| docs/book/src/media/ui-tour/navigate.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (`--sample`, default dark theme, software renderer) orbiting, panning and zooming, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/sketch-tool-hints.json | carlymx | Original sketch tool hints and engine error translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/primitive-pick-field-tips.json | SolidCraft contributors | Original primitive pick-field tooltip translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/ribbon-panel-hover.json | SolidCraft contributors | Original ribbon panel menu preference labels and translations | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/box-insert-designer.gif | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (2.5× playback, 800 px wide) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/box-insert-designer.mp4 | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (1280×720) | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/agent-http-consent.json | SolidCraft contributors | Original endpoint-specific HTTP consent messages and translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/ground-shadow-placement.json | SolidCraft contributors | Original ground-shadow placement preference messages and translations | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/assemblies/components.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) making components from two original hinge-leaf bodies, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/assemblies/joint.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) creating a revolute joint between two hinge leaves, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/assemblies/drag.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) dragging a jointed hinge leaf, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/assemblies/drive.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) driving a revolute joint between two hinge leaves, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/extrude.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) extruding a sketch's profile, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/shell.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) shelling a block, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/fillet.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) filleting four edges, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/edit.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) editing a feature from the timeline, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/hole.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) placing a hole, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/features/pattern.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) patterning a hole around an axis, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/parameters/dialog.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer) showing the Parameters dialog, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/files/menu.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (`--sample`, default dark theme, software renderer) showing the File menu, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/getting-started/start.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer, 1280x800) showing the start page with its built-in sample thumbnails, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/getting-started/sample.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) opening the built-in Sample Plate from the start page, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sheet-metal/base.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) making a sheet metal base flange, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sheet-metal/flange.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) adding sheet metal edge flanges, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sheet-metal/flat.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) showing a sheet metal flat pattern, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/create.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) starting a sketch on the Top plane, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/draw.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) drawing a rectangle and a circle with typed values, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/dimension.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) dimensioning a sketch until it is fully constrained, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/parts.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer, 1280x800) showing a fully constrained sketch, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/offset.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) offsetting a sketch outline, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/sketching/trim.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) trimming a notch in a sketch, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| assets/locales/ca.json | carlymx | Catalan translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -868,11 +897,10 @@ copied into SolidCraft.
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
|
||||
|
||||
## Vendored renderer compatibility fix
|
||||
|
||||
`vendor/egui-wgpu` is egui-wgpu 0.36.2 from https://github.com/emilk/egui
|
||||
`vendor/egui-wgpu` is egui-wgpu 0.37.0 from https://github.com/emilk/egui
|
||||
(MIT OR Apache-2.0; both original licence texts are included). Rust sources and
|
||||
its original `egui.wgsl` shader are retained; no images or other artwork are copied.
|
||||
SolidCraft's narrow patch selects a safe 1x MSAA path on the chosen OpenGL adapter
|
||||
|
||||
Generated
+168
-163
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "accesskit"
|
||||
version = "0.24.1"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
|
||||
checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be"
|
||||
dependencies = [
|
||||
"enumn",
|
||||
"serde",
|
||||
@@ -15,12 +15,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_atspi_common"
|
||||
version = "0.18.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954"
|
||||
checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.36.0",
|
||||
"accesskit_consumer",
|
||||
"atspi-common",
|
||||
"phf",
|
||||
"serde",
|
||||
@@ -29,41 +29,37 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_consumer"
|
||||
version = "0.35.0"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
|
||||
checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_consumer"
|
||||
version = "0.36.0"
|
||||
name = "accesskit_ios"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60"
|
||||
checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_consumer"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"hashbrown 0.16.1",
|
||||
"accesskit_consumer",
|
||||
"hashbrown 0.17.1",
|
||||
"objc2 0.5.3",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-ui-kit 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_macos"
|
||||
version = "0.26.3"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.38.0",
|
||||
"hashbrown 0.16.1",
|
||||
"accesskit_consumer",
|
||||
"hashbrown 0.17.1",
|
||||
"objc2 0.5.3",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -71,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_unix"
|
||||
version = "0.21.1"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c"
|
||||
checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_atspi_common",
|
||||
@@ -89,13 +85,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_windows"
|
||||
version = "0.32.1"
|
||||
version = "0.35.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
|
||||
checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.35.0",
|
||||
"hashbrown 0.16.1",
|
||||
"accesskit_consumer",
|
||||
"hashbrown 0.17.1",
|
||||
"static_assertions",
|
||||
"windows",
|
||||
"windows-core",
|
||||
@@ -103,11 +99,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_winit"
|
||||
version = "0.32.2"
|
||||
version = "0.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
|
||||
checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_ios",
|
||||
"accesskit_macos",
|
||||
"accesskit_unix",
|
||||
"accesskit_windows",
|
||||
@@ -203,6 +200,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@@ -687,42 +685,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.5",
|
||||
"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.5",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -1123,9 +1097,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
|
||||
checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"emath",
|
||||
@@ -1134,9 +1108,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "eframe"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2"
|
||||
checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1159,6 +1133,7 @@ dependencies = [
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"ron",
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"wasm-bindgen",
|
||||
@@ -1171,9 +1146,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
|
||||
checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
@@ -1193,7 +1168,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1211,32 +1186,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-winit"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc"
|
||||
checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de"
|
||||
dependencies = [
|
||||
"accesskit_winit",
|
||||
"arboard",
|
||||
"block2 0.6.2",
|
||||
"bytemuck",
|
||||
"egui",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit 0.3.2",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"smithay-clipboard",
|
||||
"web-time",
|
||||
"webbrowser",
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b"
|
||||
checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"egui",
|
||||
@@ -1254,9 +1231,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
|
||||
checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
@@ -1330,9 +1307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3"
|
||||
checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1350,15 +1327,16 @@ dependencies = [
|
||||
"skrifa",
|
||||
"smallvec",
|
||||
"unicode-general-category",
|
||||
"unicode-properties",
|
||||
"unicode-segmentation",
|
||||
"vello_cpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
|
||||
checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
@@ -1444,6 +1422,12 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
@@ -1455,6 +1439,12 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -1651,12 +1641,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glifo"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a"
|
||||
checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"hashbrown 0.17.1",
|
||||
"log",
|
||||
"peniko",
|
||||
@@ -1826,6 +1816,15 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
@@ -1834,7 +1833,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1843,7 +1842,7 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2500,6 +2499,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
@@ -2956,6 +2964,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -3005,10 +3023,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
@@ -3017,9 +3046,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
@@ -3027,9 +3056,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
@@ -3040,9 +3069,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
@@ -3762,6 +3791,12 @@ version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "send_wrapper"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -3986,8 +4021,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",
|
||||
@@ -4004,44 +4039,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.5",
|
||||
"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"
|
||||
@@ -4663,7 +4660,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",
|
||||
@@ -4689,6 +4685,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
version = "3.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom",
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -4763,6 +4770,12 @@ version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
@@ -4846,9 +4859,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vello_common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
|
||||
checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fearless_simd",
|
||||
@@ -4861,9 +4874,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vello_cpu"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea"
|
||||
checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"glifo",
|
||||
@@ -5038,32 +5051,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"
|
||||
@@ -5641,7 +5628,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",
|
||||
@@ -5662,7 +5649,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",
|
||||
@@ -5705,6 +5692,24 @@ version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wl-clipboard-rs"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"os_pipe",
|
||||
"rustix 1.1.5",
|
||||
"thiserror 2.0.21",
|
||||
"tree_magic_mini",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.4"
|
||||
|
||||
+5
-7
@@ -103,11 +103,12 @@ tiny-skia = { version = "0.12", default-features = false, features = ["std", "si
|
||||
toml = "1"
|
||||
|
||||
# ui
|
||||
egui = "0.36"
|
||||
eframe = { version = "0.36", default-features = false, features = ["persistence"] }
|
||||
# default_fonts: the built-in Hack is the UI's symbol fallback (#2955); 0.37 no longer turns it on.
|
||||
egui = { version = "0.37", features = ["default_fonts"] }
|
||||
eframe = { version = "0.37", default-features = false, features = ["persistence"] }
|
||||
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
|
||||
web-time = "1"
|
||||
egui-wgpu = "0.36"
|
||||
egui-wgpu = "0.37"
|
||||
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
|
||||
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
|
||||
naga = { version = "30", default-features = false }
|
||||
@@ -162,12 +163,9 @@ debug = "line-tables-only"
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
|
||||
# Deliver trackpad pinch and rotation gestures on Wayland (zwp_pointer_gestures_v1), which
|
||||
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
|
||||
# winit 0.30 never binds and eframe 0.37 still requires winit 0.30.
|
||||
winit = { path = "vendor/winit" }
|
||||
|
||||
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
|
||||
accesskit_macos = { path = "vendor/accesskit_macos" }
|
||||
|
||||
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
|
||||
# builds, since every mutant rebuilds the crates above the one it changes.
|
||||
[profile.mutants]
|
||||
|
||||
@@ -225,6 +225,12 @@ completed drawings publish atomically. The browser still exports synchronously.
|
||||
|
||||
## Render foundation
|
||||
|
||||
- Wood surface images (#1345): native CPU and GPU opaque rendering share physical
|
||||
repeat frames and filtered mip chains. Imported maps retain exact bytes and
|
||||
declared provenance in the design, reopen offline, and expose grain-angle
|
||||
controls. Scalar face appearances suppress body maps. Planar projection and
|
||||
CPU encoding/optical limits remain explicit; see [portable surfaces](docs/render-portable-surfaces-and-stages.md).
|
||||
|
||||
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
|
||||
|
||||
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
|
||||
|
||||
+274
-115
@@ -1,18 +1,17 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 537 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 540 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
accesskit 0.24.1 (MIT OR Apache-2.0)
|
||||
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
|
||||
accesskit_consumer 0.35.0 (MIT OR Apache-2.0)
|
||||
accesskit_consumer 0.36.0 (MIT OR Apache-2.0)
|
||||
accesskit_consumer 0.38.0 (MIT OR Apache-2.0)
|
||||
accesskit_macos 0.26.3 (MIT OR Apache-2.0)
|
||||
accesskit_unix 0.21.1 (MIT OR Apache-2.0)
|
||||
accesskit_windows 0.32.1 (MIT OR Apache-2.0)
|
||||
accesskit_winit 0.32.2 (Apache-2.0)
|
||||
accesskit 0.25.2 (MIT OR Apache-2.0)
|
||||
accesskit_atspi_common 0.21.1 (MIT OR Apache-2.0)
|
||||
accesskit_consumer 0.39.2 (MIT OR Apache-2.0)
|
||||
accesskit_ios 0.2.2 (MIT OR Apache-2.0)
|
||||
accesskit_macos 0.27.2 (MIT OR Apache-2.0)
|
||||
accesskit_unix 0.24.1 (MIT OR Apache-2.0)
|
||||
accesskit_windows 0.35.2 (MIT OR Apache-2.0)
|
||||
accesskit_winit 0.34.2 (Apache-2.0)
|
||||
adler2 2.0.1 (0BSD OR MIT OR Apache-2.0)
|
||||
aes 0.8.4 (MIT OR Apache-2.0)
|
||||
ahash 0.8.12 (MIT OR Apache-2.0)
|
||||
@@ -65,9 +64,7 @@ cadmpeg-core 0.6.7 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.7 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.7 (Apache-2.0)
|
||||
calloop 0.13.0 (MIT)
|
||||
calloop 0.14.5 (MIT)
|
||||
calloop-wayland-source 0.3.0 (MIT)
|
||||
calloop-wayland-source 0.4.1 (MIT)
|
||||
cbc 0.1.2 (MIT OR Apache-2.0)
|
||||
cfg-if 1.0.5 (MIT OR Apache-2.0)
|
||||
cgl 0.3.2 (MIT / Apache-2.0)
|
||||
@@ -109,21 +106,21 @@ document-features 0.2.12 (MIT OR Apache-2.0)
|
||||
downcast-rs 1.2.1 (MIT/Apache-2.0)
|
||||
dpi 0.1.2 (Apache-2.0 AND MIT)
|
||||
dyn-clone 1.0.20 (MIT OR Apache-2.0)
|
||||
ecolor 0.36.2 (MIT OR Apache-2.0)
|
||||
eframe 0.36.2 (MIT OR Apache-2.0)
|
||||
egui 0.36.2 (MIT OR Apache-2.0)
|
||||
egui-wgpu 0.36.2 (MIT OR Apache-2.0)
|
||||
egui-winit 0.36.2 (MIT OR Apache-2.0)
|
||||
egui_glow 0.36.2 (MIT OR Apache-2.0)
|
||||
ecolor 0.37.0 (MIT OR Apache-2.0)
|
||||
eframe 0.37.0 (MIT OR Apache-2.0)
|
||||
egui 0.37.0 (MIT OR Apache-2.0)
|
||||
egui-wgpu 0.37.0 (MIT OR Apache-2.0)
|
||||
egui-winit 0.37.0 (MIT OR Apache-2.0)
|
||||
egui_glow 0.37.0 (MIT OR Apache-2.0)
|
||||
either 1.19.0 (MIT OR Apache-2.0)
|
||||
emath 0.36.2 (MIT OR Apache-2.0)
|
||||
emath 0.37.0 (MIT OR Apache-2.0)
|
||||
encoding_rs 0.8.42 ((Apache-2.0 OR MIT) AND BSD-3-Clause)
|
||||
endi 1.1.1 (MIT)
|
||||
enumflags2 0.7.12 (MIT OR Apache-2.0)
|
||||
enumflags2_derive 0.7.12 (MIT OR Apache-2.0)
|
||||
enumn 0.1.14 (MIT OR Apache-2.0)
|
||||
epaint 0.36.2 (MIT OR Apache-2.0)
|
||||
epaint_default_fonts 0.36.2 ((MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0)
|
||||
epaint 0.37.0 (MIT OR Apache-2.0)
|
||||
epaint_default_fonts 0.37.0 ((MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0)
|
||||
equivalent 1.0.2 (Apache-2.0 OR MIT)
|
||||
errno 0.3.14 (MIT OR Apache-2.0)
|
||||
error-code 3.4.0 (BSL-1.0)
|
||||
@@ -134,7 +131,9 @@ fastrand 2.5.0 (Apache-2.0 OR MIT)
|
||||
fax 0.2.7 (MIT)
|
||||
fdeflate 0.3.7 (MIT OR Apache-2.0)
|
||||
fearless_simd 0.4.1 (Apache-2.0 OR MIT)
|
||||
fixedbitset 0.5.7 (MIT OR Apache-2.0)
|
||||
flate2 1.1.10 (MIT OR Apache-2.0)
|
||||
foldhash 0.1.5 (Zlib)
|
||||
foldhash 0.2.0 (Zlib)
|
||||
font-types 0.12.6 (MIT OR Apache-2.0)
|
||||
foreign-types 0.5.0 (MIT/Apache-2.0)
|
||||
@@ -154,7 +153,7 @@ getrandom 0.2.17 (MIT OR Apache-2.0)
|
||||
getrandom 0.3.4 (MIT OR Apache-2.0)
|
||||
getrandom 0.4.3 (MIT OR Apache-2.0)
|
||||
gif 0.14.2 (MIT OR Apache-2.0)
|
||||
glifo 0.2.0 (Apache-2.0 OR MIT)
|
||||
glifo 0.3.0 (Apache-2.0 OR MIT)
|
||||
glow 0.17.0 (MIT OR Apache-2.0 OR Zlib)
|
||||
gltf 1.4.1 (MIT OR Apache-2.0)
|
||||
gltf-derive 1.4.1 (MIT OR Apache-2.0)
|
||||
@@ -168,6 +167,7 @@ gpu-allocator 0.28.0 (MIT OR Apache-2.0)
|
||||
guillotiere 0.7.0 (MIT/Apache-2.0)
|
||||
half 2.7.1 (MIT OR Apache-2.0)
|
||||
harfrust 0.12.0 (MIT)
|
||||
hashbrown 0.15.5 (MIT OR Apache-2.0)
|
||||
hashbrown 0.16.1 (MIT OR Apache-2.0)
|
||||
hashbrown 0.17.1 (MIT OR Apache-2.0)
|
||||
hermit-abi 0.5.3 (MIT OR Apache-2.0)
|
||||
@@ -234,6 +234,7 @@ ndk-sys 0.6.0+11769913 (MIT OR Apache-2.0)
|
||||
nix 0.29.0 (MIT)
|
||||
nix 0.31.3 (MIT)
|
||||
nohash-hasher 0.2.0 (Apache-2.0 OR MIT)
|
||||
nom 8.0.0 (MIT)
|
||||
num 0.4.3 (MIT OR Apache-2.0)
|
||||
num-bigint 0.4.8 (MIT OR Apache-2.0)
|
||||
num-complex 0.4.6 (MIT OR Apache-2.0)
|
||||
@@ -275,15 +276,17 @@ orbclient 0.3.55 (MIT)
|
||||
ordered-float 2.10.1 (MIT)
|
||||
ordered-float 5.5.0 (MIT)
|
||||
ordered-stream 0.2.0 (MIT OR Apache-2.0)
|
||||
os_pipe 1.2.3 (MIT)
|
||||
parking 2.2.1 (Apache-2.0 OR MIT)
|
||||
parking_lot 0.12.5 (MIT OR Apache-2.0)
|
||||
parking_lot_core 0.9.12 (MIT OR Apache-2.0)
|
||||
peniko 0.6.1 (Apache-2.0 OR MIT)
|
||||
percent-encoding 2.3.2 (MIT OR Apache-2.0)
|
||||
phf 0.13.1 (MIT)
|
||||
phf_generator 0.13.1 (MIT)
|
||||
phf_macros 0.13.1 (MIT)
|
||||
phf_shared 0.13.1 (MIT)
|
||||
petgraph 0.8.3 (MIT OR Apache-2.0)
|
||||
phf 0.14.0 (MIT)
|
||||
phf_generator 0.14.0 (MIT)
|
||||
phf_macros 0.14.0 (MIT)
|
||||
phf_shared 0.14.0 (MIT)
|
||||
pin-project 1.1.13 (Apache-2.0 OR MIT)
|
||||
pin-project-internal 1.1.13 (Apache-2.0 OR MIT)
|
||||
pin-project-lite 0.2.17 (Apache-2.0 OR MIT)
|
||||
@@ -352,6 +355,7 @@ security-framework 3.7.0 (MIT OR Apache-2.0)
|
||||
security-framework-sys 2.17.0 (MIT OR Apache-2.0)
|
||||
self_cell 1.3.0 (Apache-2.0 OR GPL-2.0-only)
|
||||
semver 1.0.28 (MIT OR Apache-2.0)
|
||||
send_wrapper 0.6.0 (MIT/Apache-2.0)
|
||||
serde 1.0.229 (MIT OR Apache-2.0)
|
||||
serde-value 0.7.0 (MIT)
|
||||
serde_core 1.0.229 (MIT OR Apache-2.0)
|
||||
@@ -375,8 +379,6 @@ slab 0.4.12 (MIT)
|
||||
slotmap 1.1.1 (Zlib)
|
||||
smallvec 1.16.2 (MIT OR Apache-2.0)
|
||||
smithay-client-toolkit 0.19.2 (MIT)
|
||||
smithay-client-toolkit 0.20.0 (MIT)
|
||||
smithay-clipboard 0.7.3 (MIT)
|
||||
smol_str 0.2.2 (MIT OR Apache-2.0)
|
||||
solidcore 1.0.1 (Apache-2.0)
|
||||
solidcore-record 0.1.0 (Apache-2.0)
|
||||
@@ -409,6 +411,7 @@ toml_writer 1.1.3+spec-1.1.0 (MIT OR Apache-2.0)
|
||||
tracing 0.1.44 (MIT)
|
||||
tracing-attributes 0.1.31 (MIT)
|
||||
tracing-core 0.1.36 (MIT)
|
||||
tree_magic_mini 3.2.2 (MIT)
|
||||
ttf-parser 0.25.1 (MIT OR Apache-2.0)
|
||||
twox-hash 2.1.5 (MIT)
|
||||
type-map 0.5.1 (MIT/Apache-2.0)
|
||||
@@ -419,6 +422,7 @@ uds_windows 1.2.1 (MIT)
|
||||
unicode-bidi 0.3.18 (MIT OR Apache-2.0)
|
||||
unicode-general-category 1.1.0 (Apache-2.0)
|
||||
unicode-ident 1.0.26 ((MIT OR Apache-2.0) AND Unicode-3.0)
|
||||
unicode-properties 0.1.5 ((MIT OR Apache-2.0) AND Unicode-3.0)
|
||||
unicode-segmentation 1.13.3 (MIT OR Apache-2.0)
|
||||
unicode-width 0.2.2 (MIT OR Apache-2.0)
|
||||
untrusted 0.9.0 (ISC)
|
||||
@@ -428,8 +432,8 @@ url 2.5.8 (MIT OR Apache-2.0)
|
||||
utf8-zero 0.8.1 (MIT OR Apache-2.0)
|
||||
utf8_iter 1.0.4 (Apache-2.0 OR MIT)
|
||||
uuid 1.27.0 (Apache-2.0 OR MIT)
|
||||
vello_common 0.1.0 (Apache-2.0 OR MIT)
|
||||
vello_cpu 0.1.0 (Apache-2.0 OR MIT)
|
||||
vello_common 0.2.0 (Apache-2.0 OR MIT)
|
||||
vello_cpu 0.2.0 (Apache-2.0 OR MIT)
|
||||
wasi 0.11.1+wasi-snapshot-preview1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
|
||||
wasip2 1.0.4+wasi-0.2.12 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
|
||||
wasm-bindgen 0.2.129 (MIT OR Apache-2.0)
|
||||
@@ -442,8 +446,6 @@ wayland-client 0.31.15 (MIT)
|
||||
wayland-csd-frame 0.3.0 (MIT)
|
||||
wayland-cursor 0.31.14 (MIT)
|
||||
wayland-protocols 0.32.13 (MIT)
|
||||
wayland-protocols-experimental 20250721.0.1 (MIT)
|
||||
wayland-protocols-misc 0.3.12 (MIT)
|
||||
wayland-protocols-plasma 0.3.12 (MIT)
|
||||
wayland-protocols-wlr 0.3.12 (MIT)
|
||||
wayland-scanner 0.31.11 (MIT)
|
||||
@@ -500,6 +502,7 @@ winit 0.30.13 (Apache-2.0)
|
||||
winnow 1.0.4 (MIT)
|
||||
winreg 0.56.0 (MIT)
|
||||
wit-bindgen 0.57.1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
|
||||
wl-clipboard-rs 0.9.5 (MIT OR Apache-2.0)
|
||||
writeable 0.6.4 (Unicode-3.0)
|
||||
x11-dl 2.21.0 (MIT)
|
||||
x11rb 0.13.2 (MIT OR Apache-2.0)
|
||||
@@ -570,13 +573,14 @@ num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, p
|
||||
pin-project 1.1.13, pin-project-internal 1.1.13, pin-project-lite 0.2.17, piper 0.2.5, polling
|
||||
3.11.0, portable-atomic 1.15.0, portable-atomic-util 0.2.8, proc-macro-crate 3.5.0, proc-macro2
|
||||
1.0.107, quote 1.0.47, ref-cast 1.0.27, ref-cast-impl 1.0.27, rustc-hash 1.1.0, rustc-hash 2.1.3,
|
||||
rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, serde 1.0.229, serde_core 1.0.229,
|
||||
serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json 1.0.151, serde_path_to_error 0.1.20,
|
||||
serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 2.0.119, syn 3.0.6, thiserror 1.0.69,
|
||||
thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.13.0, typeid 1.0.3,
|
||||
unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
|
||||
wit-bindgen 0.57.1, x11-dl 2.21.0, xdg-home 1.3.0, zbus-lockstep 0.5.2, zbus-lockstep-macros 0.5.2,
|
||||
zbus_names 3.0.0, zmij 1.0.23, zvariant_utils 2.1.0, zvariant_utils 4.2.0
|
||||
rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, send_wrapper 0.6.0, serde 1.0.229,
|
||||
serde_core 1.0.229, serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json 1.0.151,
|
||||
serde_path_to_error 0.1.20, serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 2.0.119, syn
|
||||
3.0.6, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path
|
||||
0.13.0, typeid 1.0.3, unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1,
|
||||
wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, xdg-home 1.3.0, zbus-lockstep 0.5.2,
|
||||
zbus-lockstep-macros 0.5.2, zbus_names 3.0.0, zmij 1.0.23, zvariant_utils 2.1.0, zvariant_utils
|
||||
4.2.0
|
||||
================================================================================
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
@@ -1086,25 +1090,26 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
|
||||
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
|
||||
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
|
||||
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
|
||||
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
|
||||
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
|
||||
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
|
||||
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.15.0, js-sys 0.3.106,
|
||||
keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys
|
||||
0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6,
|
||||
num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4,
|
||||
openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core
|
||||
0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling 3.11.0, pollster
|
||||
0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron
|
||||
0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
|
||||
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
|
||||
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
|
||||
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
|
||||
1.2.1, tempfile 3.27.0, ttf-parser 0.25.1, unicode-bidi 0.3.18, unicode-segmentation 1.13.3,
|
||||
unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2
|
||||
1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
|
||||
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
|
||||
wit-bindgen 0.57.1
|
||||
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, fixedbitset
|
||||
0.5.7, flate2 1.1.10, form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow
|
||||
0.17.0, gltf 1.4.1, gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.15.5, hashbrown 0.16.1,
|
||||
hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap
|
||||
2.14.2, itertools 0.15.0, js-sys 0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static
|
||||
1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num
|
||||
0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2,
|
||||
num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1,
|
||||
parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, petgraph 0.8.3, piper 0.2.5,
|
||||
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
|
||||
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
|
||||
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
|
||||
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
|
||||
security-framework-sys 2.17.0, send_wrapper 0.6.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0,
|
||||
smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait 1.2.1, tempfile 3.27.0, ttf-parser 0.25.1,
|
||||
unicode-bidi 0.3.18, unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2, url
|
||||
2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen
|
||||
0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support
|
||||
0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1,
|
||||
wl-clipboard-rs 0.9.5
|
||||
================================================================================
|
||||
|
||||
Apache License
|
||||
@@ -1310,7 +1315,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
allocator-api2 0.2.21, async-trait 0.1.92, dyn-clone 1.0.20, enumn 0.1.14, glifo 0.2.0, itoa 1.0.18,
|
||||
allocator-api2 0.2.21, async-trait 0.1.92, dyn-clone 1.0.20, enumn 0.1.14, glifo 0.3.0, itoa 1.0.18,
|
||||
libc 0.2.190, litrs 1.0.0, num_enum 0.7.6, num_enum_derive 0.7.6, proc-macro2 1.0.107, quote 1.0.47,
|
||||
ref-cast 1.0.27, ref-cast-impl 1.0.27, rustc-hash 2.1.3, rustversion 1.0.23, semver 1.0.28, serde
|
||||
1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json
|
||||
@@ -1530,7 +1535,7 @@ android-activity 0.6.1, arboard 3.6.1, color 0.3.3, fdeflate 0.3.7, fearless_sim
|
||||
image 0.25.10, image-webp 0.2.4, linebender_resource_handle 0.1.1, miniz_oxide 0.8.9, miniz_oxide
|
||||
0.9.1, naga 30.0.1, naga-types 30.0.1, pin-project 1.1.13, pin-project-internal 1.1.13,
|
||||
pin-project-lite 0.2.17, portable-atomic 1.15.0, portable-atomic-util 0.2.8, range-alloc 0.1.5,
|
||||
raw-window-handle 0.6.2, simdutf8 0.1.5, vello_common 0.1.0, vello_cpu 0.1.0, wgpu 30.0.1, wgpu-core
|
||||
raw-window-handle 0.6.2, simdutf8 0.1.5, vello_common 0.2.0, vello_cpu 0.2.0, wgpu 30.0.1, wgpu-core
|
||||
30.0.1, wgpu-core-deps-apple 30.0.1, wgpu-core-deps-emscripten 30.0.1, wgpu-core-deps-wasm 30.0.1,
|
||||
wgpu-core-deps-windows-linux-android 30.0.1, wgpu-hal 30.0.1, wgpu-naga-bridge 30.0.1, wgpu-types
|
||||
30.0.1
|
||||
@@ -2588,7 +2593,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
bit-set 0.10.0, bit-vec 0.9.1, downcast-rs 1.2.1, egui-wgpu 0.36.2, presser 0.3.1
|
||||
bit-set 0.10.0, bit-vec 0.9.1, downcast-rs 1.2.1, egui-wgpu 0.37.0, presser 0.3.1
|
||||
================================================================================
|
||||
|
||||
Apache License
|
||||
@@ -3134,7 +3139,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
calloop 0.13.0, calloop 0.14.5, smithay-client-toolkit 0.19.2, smithay-client-toolkit 0.20.0
|
||||
calloop 0.13.0, smithay-client-toolkit 0.19.2
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2018 Victor Berger
|
||||
@@ -3158,7 +3163,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
calloop-wayland-source 0.3.0, calloop-wayland-source 0.4.1
|
||||
calloop-wayland-source 0.3.0
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2023 Kirill Chibisov
|
||||
@@ -5231,7 +5236,7 @@ have been licensed under extremely permissive terms.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
================================================================================
|
||||
egui-wgpu 0.36.2
|
||||
egui-wgpu 0.37.0
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
|
||||
@@ -5261,7 +5266,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
either 1.19.0, itertools 0.15.0
|
||||
either 1.19.0, itertools 0.15.0, petgraph 0.8.3
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015
|
||||
@@ -5709,6 +5714,36 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
fixedbitset 0.5.7
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015-2017
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
flate2 1.1.10
|
||||
================================================================================
|
||||
@@ -5740,7 +5775,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
foldhash 0.2.0
|
||||
foldhash 0.1.5, foldhash 0.2.0
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2024 Orson Peters
|
||||
@@ -6511,7 +6546,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
glifo 0.2.0
|
||||
glifo 0.3.0
|
||||
================================================================================
|
||||
|
||||
Copyright 2025 the Vello Authors
|
||||
@@ -7138,7 +7173,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
hashbrown 0.16.1, hashbrown 0.17.1
|
||||
hashbrown 0.15.5, hashbrown 0.16.1, hashbrown 0.17.1
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2016 Amanieu d'Antras
|
||||
@@ -9459,6 +9494,31 @@ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
nom 8.0.0
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2014-2019 Geoffroy Couprie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
num_enum 0.7.6, num_enum_derive 0.7.6
|
||||
================================================================================
|
||||
@@ -9547,6 +9607,30 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
os_pipe 1.2.3
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
parking 2.2.1
|
||||
================================================================================
|
||||
@@ -9562,7 +9646,7 @@ option. All files in the project carrying such notice may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
|
||||
================================================================================
|
||||
phf 0.13.1, phf_generator 0.13.1, phf_macros 0.13.1, phf_shared 0.13.1
|
||||
phf 0.14.0, phf_generator 0.14.0, phf_macros 0.14.0, phf_shared 0.14.0
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -12377,30 +12461,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
smithay-clipboard 0.7.3
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2018 Lucas Timmins & Victor Berger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
solidcore 1.0.1
|
||||
================================================================================
|
||||
@@ -12973,6 +13033,32 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
tree_magic_mini 3.2.2
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Aaron Hancock
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
ttf-parser 0.25.1
|
||||
================================================================================
|
||||
@@ -13795,7 +13881,7 @@ carrying such notice may not be copied, modified, or distributed except
|
||||
according to those terms.
|
||||
|
||||
================================================================================
|
||||
unicode-bidi 0.3.18, unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
unicode-bidi 0.3.18, unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015 The Rust Project Developers
|
||||
@@ -13869,7 +13955,51 @@ dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
|
||||
================================================================================
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
unicode-properties 0.1.5
|
||||
================================================================================
|
||||
|
||||
UNICODE LICENSE V3
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 1991-2026 Unicode, Inc.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of data files and any associated documentation (the "Data Files") or
|
||||
software and any associated documentation (the "Software") to deal in the
|
||||
Data Files or Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||
copies of the Data Files or Software, and to permit persons to whom the
|
||||
Data Files or Software are furnished to do so, provided that either (a)
|
||||
this copyright and permission notice appear with all copies of the Data
|
||||
Files or Software, or (b) this copyright and permission notice appear in
|
||||
associated Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
THIRD PARTY RIGHTS.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||
FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
|
||||
================================================================================
|
||||
unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
================================================================================
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
@@ -14015,7 +14145,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
vello_common 0.1.0, vello_cpu 0.1.0
|
||||
vello_common 0.2.0, vello_cpu 0.2.0
|
||||
================================================================================
|
||||
|
||||
Copyright 2020 the Vello Authors
|
||||
@@ -14046,8 +14176,8 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
wayland-backend 0.3.17, wayland-client 0.31.15, wayland-cursor 0.31.14, wayland-protocols 0.32.13,
|
||||
wayland-protocols-experimental 20250721.0.1, wayland-protocols-misc 0.3.12, wayland-protocols-plasma
|
||||
0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.11, wayland-sys 0.31.11
|
||||
wayland-protocols-plasma 0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.11, wayland-sys
|
||||
0.31.11
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015 Elinor Berger
|
||||
@@ -14942,6 +15072,36 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
wl-clipboard-rs 0.9.5
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2019 Ivan Molodetskikh
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
x11rb 0.13.2, x11rb-protocol 0.13.2
|
||||
================================================================================
|
||||
@@ -15967,15 +16127,14 @@ Crates whose package carries no licence file (their licence is named in Cargo.to
|
||||
texts are as in SolidCraft's own LICENSE-MIT and LICENSE-APACHE)
|
||||
================================================================================
|
||||
|
||||
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_atspi_common 0.18.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_consumer 0.35.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_consumer 0.36.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_consumer 0.38.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_macos 0.26.3: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_unix 0.21.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_windows 0.32.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit 0.25.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_atspi_common 0.21.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_consumer 0.39.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_ios 0.2.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_macos 0.27.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_unix 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_windows 0.35.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
accesskit_winit 0.34.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
|
||||
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
|
||||
cadmpeg-codec-freecad 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
@@ -15988,14 +16147,14 @@ cadmpeg-registry 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
|
||||
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
|
||||
dispatch2 0.3.1: Zlib OR Apache-2.0 OR MIT (https://github.com/madsmtm/objc2)
|
||||
ecolor 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui)
|
||||
eframe 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/eframe)
|
||||
egui 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui)
|
||||
egui-winit 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui-winit)
|
||||
egui_glow 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui_glow)
|
||||
emath 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/emath)
|
||||
epaint 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/epaint)
|
||||
epaint_default_fonts 0.36.2: (MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0 (https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts)
|
||||
ecolor 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui)
|
||||
eframe 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/eframe)
|
||||
egui 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui)
|
||||
egui-winit 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui-winit)
|
||||
egui_glow 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui_glow)
|
||||
emath 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/emath)
|
||||
epaint 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/epaint)
|
||||
epaint_default_fonts 0.37.0: (MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0 (https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts)
|
||||
jni 0.22.4: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-rs)
|
||||
jni-macros 0.22.4: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-rs)
|
||||
jni-sys-macros 0.4.1: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-sys)
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::process::{Command, ExitCode};
|
||||
const TARGET: &str = "solidcraft-cli";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
eprintln!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
|
||||
solidcraft_engine::note!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
|
||||
let sibling = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join(format!("{TARGET}{}", std::env::consts::EXE_SUFFIX))))
|
||||
@@ -23,7 +23,7 @@ fn main() -> ExitCode {
|
||||
None => ExitCode::FAILURE,
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
|
||||
solidcraft_engine::note!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
std::panic::set_hook(Box::new(|_| {
|
||||
PANICS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}));
|
||||
eprintln!("design {seed}");
|
||||
let run = fuzz::run_with(seed, steps, &mut |st| eprintln!("step {}", serde_json::to_string(st).unwrap_or_default()));
|
||||
solidcraft_engine::note!("design {seed}");
|
||||
let run = fuzz::run_with(seed, steps, &mut |st| solidcraft_engine::note!("step {}", serde_json::to_string(st).unwrap_or_default()));
|
||||
let mut design = json!({
|
||||
"outcome": run.outcome,
|
||||
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
|
||||
@@ -45,13 +45,13 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
if run.outcome.is_bug() {
|
||||
design["script"] = run.script();
|
||||
if minimise {
|
||||
eprintln!("minimise");
|
||||
solidcraft_engine::note!("minimise");
|
||||
let m = fuzz::minimise(&run);
|
||||
design["minimal"] = json!({"outcome": m.outcome, "steps": m.steps.iter().map(|s| s.kind).collect::<Vec<_>>(), "script": m.script()});
|
||||
}
|
||||
}
|
||||
let (a, b) = solidcraft_engine::fuzz::boolean_shapes(seed);
|
||||
eprintln!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
|
||||
solidcraft_engine::note!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
|
||||
let b = boolean_case(seed);
|
||||
let bp = PANICS.swap(0, std::sync::atomic::Ordering::SeqCst);
|
||||
println!("{}", json!({"seed": seed, "design": design, "boolean": b, "boolean_panics": bp}));
|
||||
@@ -162,11 +162,14 @@ fn record(v: &Value, out_dir: Option<&str>) {
|
||||
let d = &v["design"];
|
||||
let bug = matches!(d["outcome"]["kind"].as_str(), Some("failed" | "panic" | "invalid" | "volume"));
|
||||
if bug || v.get("timeout").is_some() || v.get("crash").is_some() {
|
||||
eprintln!("seed {seed}: {}", if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") });
|
||||
solidcraft_engine::note!(
|
||||
"seed {seed}: {}",
|
||||
if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") }
|
||||
);
|
||||
}
|
||||
let bk = v["boolean"]["outcome"].as_str().unwrap_or("ok");
|
||||
if bk != "ok" {
|
||||
eprintln!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
|
||||
solidcraft_engine::note!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
|
||||
}
|
||||
if let Some(dir) = out_dir {
|
||||
use std::io::Write;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
|
||||
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! [--param-check off|warn|strict] [--compact]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
|
||||
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
|
||||
@@ -91,7 +91,7 @@ const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [-
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
/// `mcp`'s lines in the usage.
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu] [--compact]\n (--compact, SOLIDCRAFT_MCP_COMPACT=1 or the app's Preferences › AI (with --connect): compact tool output for\n agents: inspect_design summarises sketches and takes `sketches` by name and a `bodies` filter, and tool JSON\n is not pretty-printed; off by default)\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
/// `crashes`'s lines in the usage.
|
||||
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
|
||||
/// `licences`'s lines in the usage.
|
||||
@@ -154,7 +154,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
};
|
||||
if let Err(e) = max_file_size(&mut args) {
|
||||
eprintln!("error: {e}");
|
||||
solidcraft_engine::note!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let r = match args.first().map(String::as_str) {
|
||||
@@ -235,7 +235,7 @@ fn main() -> ExitCode {
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
solidcraft_engine::note!("error: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@ fn run_script_warning(s: &mut Session, path: &str, script: &Value) -> Result<Vec
|
||||
/// Step `step`'s `param_warnings` on stderr, one per line, named after the script at `path`.
|
||||
fn print_param_warnings(path: &str, step: usize, r: &Value) {
|
||||
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
|
||||
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
solidcraft_engine::note!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,10 +598,10 @@ fn cmd_script(args: &[String]) -> Result<(), String> {
|
||||
let out = flag(args, "--out");
|
||||
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
for what in r["omitted"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
eprintln!("note: the script leaves out {what}");
|
||||
solidcraft_engine::note!("note: the script leaves out {what}");
|
||||
}
|
||||
match out {
|
||||
Some(out) => eprintln!("{out}: {} steps", r["steps"]),
|
||||
Some(out) => solidcraft_engine::note!("{out}: {} steps", r["steps"]),
|
||||
None => println!("{}", pretty(&r["script"])),
|
||||
}
|
||||
Ok(())
|
||||
@@ -632,7 +632,7 @@ fn cmd_gpu(args: &[String]) -> Result<(), String> {
|
||||
);
|
||||
match s.lands_on {
|
||||
Some(solidcraft_gpu::select::Landing::Cpu) => {
|
||||
eprintln!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
|
||||
solidcraft_engine::note!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
|
||||
Ok(())
|
||||
}
|
||||
Some(_) => Ok(()),
|
||||
@@ -709,7 +709,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
|
||||
if let Some(w) = solidcraft_engine::capture::warning(png.renderer) {
|
||||
eprintln!("warning: {w}");
|
||||
solidcraft_engine::note!("warning: {w}");
|
||||
reply["warning"] = json!(w);
|
||||
}
|
||||
println!("{}", pretty(&reply));
|
||||
@@ -792,11 +792,15 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
|
||||
}
|
||||
install_renderer();
|
||||
let options = solidcraft_mcp::Options::from_args_and_env(args).map_err(|e| format!("mcp: {e}"))?;
|
||||
if options.compact {
|
||||
eprintln!("solidcraft mcp: compact output (inspect_design summarises sketches; tool JSON is not pretty-printed)");
|
||||
}
|
||||
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
|
||||
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
|
||||
if let Some(s) = &scope {
|
||||
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft mcp: files limited to {} and the folders of designs opened or saved here (--allow-path DIR adds one, --allow-any-path lifts the limit)",
|
||||
roots.join(", ")
|
||||
);
|
||||
@@ -804,7 +808,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
.and_then(|p| std::path::Path::new(p).parent())
|
||||
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
|
||||
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
|
||||
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
solidcraft_engine::note!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
}
|
||||
}
|
||||
// How calls that break a command's parameter contract are treated, headless or bridged.
|
||||
@@ -818,7 +822,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
// Serve without the app too: the tools connect once it runs, and say it isn't until then.
|
||||
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token")).with_param_check(param_check);
|
||||
if let Err(e) = remote.try_connect() {
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft mcp: no SolidCraft app at {addr} yet ({e}); start it with `solidcraft --control {port}` \
|
||||
(or SOLIDCRAFT_CONTROL_PORT={port}), tool calls connect once it runs"
|
||||
);
|
||||
@@ -835,9 +839,9 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
(None, None) => {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
solidcraft_engine::note!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
let remote = solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token")).with_param_check(param_check);
|
||||
return serve_mcp(Box::new(remote), scope);
|
||||
return serve_mcp(Box::new(remote), scope, options);
|
||||
}
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
@@ -852,13 +856,17 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
};
|
||||
serve_mcp(backend, scope)
|
||||
serve_mcp(backend, scope, options)
|
||||
}
|
||||
|
||||
fn serve_mcp(backend: Box<dyn solidcraft_mcp::Backend>, scope: Option<solidcraft_engine::io::vfs::PathScope>) -> Result<(), String> {
|
||||
fn serve_mcp(
|
||||
backend: Box<dyn solidcraft_mcp::Backend>,
|
||||
scope: Option<solidcraft_engine::io::vfs::PathScope>,
|
||||
options: solidcraft_mcp::Options,
|
||||
) -> Result<(), String> {
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).with_options(options).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The port of the running desktop app: the one its discovery file names, if something listens
|
||||
|
||||
@@ -188,3 +188,26 @@ fn max_file_size_raises_the_limit_and_refuses_bad_sizes() {
|
||||
assert!(!output.status.success() && !stderr.contains("too large"), "{stderr}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #10111: a line for a stderr whose reader has gone (the pipe closed before it, as an agent may
|
||||
/// leave the MCP server's, and the app that one starts) is dropped, and the command ends as it
|
||||
/// would with stderr open: here a script that isn't there fails with 1, where `eprintln!`
|
||||
/// panicked with "failed printing to stderr" (101).
|
||||
#[test]
|
||||
fn a_closed_stderr_does_not_stop_the_cli() {
|
||||
for _ in 0..5 {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["run", "no-such-script.json"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
drop(child.stderr.take());
|
||||
assert_eq!(child.wait().unwrap().code(), Some(1));
|
||||
}
|
||||
// The old name's alias writes its deprecation notice first: an agent still configured with
|
||||
// `solvecraft-cli mcp` must get the server, not that panic.
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("--help").stdout(Stdio::null()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
drop(child.stderr.take());
|
||||
assert_eq!(child.wait().unwrap().code(), Some(0));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! `mcp --compact` and `SOLIDCRAFT_MCP_COMPACT` turn on compact tool output; without them the
|
||||
//! server's tools are as they always were.
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// `inspect_design`'s input schema as `solidcraft-cli mcp` with `args` and `env` lists it, or
|
||||
/// the server's stderr when it does not start.
|
||||
fn inspect_schema(args: &[&str], env: Option<&str>) -> Result<Value, String> {
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
// --headless: never bridge to (and read the preference of) an app the developer has running.
|
||||
cmd.args(["mcp", "--headless", "--allow-any-path"]).args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
match env {
|
||||
Some(v) => cmd.env("SOLIDCRAFT_MCP_COMPACT", v),
|
||||
None => cmd.env_remove("SOLIDCRAFT_MCP_COMPACT"),
|
||||
};
|
||||
let mut child = cmd.spawn().unwrap();
|
||||
{
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
// The server may refuse to start and close stdin first.
|
||||
let _ = writeln!(stdin, "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}));
|
||||
}
|
||||
let out = child.wait_with_output().unwrap();
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).into_owned());
|
||||
}
|
||||
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||
let tools = reply["result"]["tools"].as_array().cloned().unwrap_or_default();
|
||||
Ok(tools.into_iter().find(|t| t["name"] == "inspect_design").map(|t| t["inputSchema"].clone()).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_output_is_off_unless_asked() {
|
||||
let plain = inspect_schema(&[], None).unwrap();
|
||||
assert_eq!(plain["properties"]["sketches"]["type"], "boolean", "{plain}");
|
||||
assert!(plain["properties"].get("bodies").is_none(), "{plain}");
|
||||
assert_eq!(inspect_schema(&[], Some("0")).unwrap(), plain);
|
||||
|
||||
for (args, env) in [(&["--compact"][..], None), (&[][..], Some("1")), (&[][..], Some("true"))] {
|
||||
let compact = inspect_schema(args, env).unwrap();
|
||||
assert!(compact["properties"]["bodies"].is_object(), "{args:?} {env:?}: {compact}");
|
||||
}
|
||||
|
||||
let e = inspect_schema(&[], Some("maybe")).unwrap_err();
|
||||
assert!(e.contains("SOLIDCRAFT_MCP_COMPACT"), "{e}");
|
||||
}
|
||||
@@ -60,15 +60,25 @@ fn mcp_connect_serves_before_the_app_runs_and_reaches_it_once_it_does() {
|
||||
let listener = TcpListener::bind(&addr).unwrap();
|
||||
let app = std::thread::spawn(move || {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
let mut line = String::new();
|
||||
BufReader::new(stream.try_clone().unwrap()).read_line(&mut line).unwrap();
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
let reply = if req["token"] == json!(token) {
|
||||
json!({"id": req["id"], "ok": true, "result": {"bodies": [], "method": req["method"]}})
|
||||
} else {
|
||||
json!({"id": req["id"], "ok": false, "error": format!("unauthorized: token {}", req["token"])})
|
||||
};
|
||||
writeln!(&stream, "{reply}").unwrap();
|
||||
let mut lines = BufReader::new(stream.try_clone().unwrap());
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
lines.read_line(&mut line).unwrap();
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
// The server first asks for the agents' output options (the compact preference: off).
|
||||
let options = req["method"] == "mcp.options";
|
||||
let reply = if req["token"] != json!(token) {
|
||||
json!({"id": req["id"], "ok": false, "error": format!("unauthorized: token {}", req["token"])})
|
||||
} else if options {
|
||||
json!({"id": req["id"], "ok": true, "result": {"compact": false}})
|
||||
} else {
|
||||
json!({"id": req["id"], "ok": true, "result": {"bodies": [], "method": req["method"]}})
|
||||
};
|
||||
writeln!(&stream, "{reply}").unwrap();
|
||||
if !options {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let r = ask(4, "tools/call", measure);
|
||||
assert_ne!(r["result"]["isError"], true, "{r}");
|
||||
@@ -111,10 +121,21 @@ fn mcp_without_arguments_finds_the_running_app() {
|
||||
break (stream, line);
|
||||
}
|
||||
};
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
let ok = req["token"] == json!(token);
|
||||
let reply = json!({"id": req["id"], "ok": ok, "result": {"bodies": [], "method": req["method"]}, "error": "unauthorized"});
|
||||
writeln!(&stream, "{reply}").unwrap();
|
||||
let mut lines = BufReader::new(stream.try_clone().unwrap());
|
||||
let mut line = line;
|
||||
// The server first asks for the agents' output options (the compact preference: off).
|
||||
loop {
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
let ok = req["token"] == json!(token);
|
||||
let options = req["method"] == "mcp.options";
|
||||
let result = if options { json!({"compact": false}) } else { json!({"bodies": [], "method": req["method"]}) };
|
||||
writeln!(&stream, "{}", json!({"id": req["id"], "ok": ok, "result": result, "error": "unauthorized"})).unwrap();
|
||||
if !options {
|
||||
break;
|
||||
}
|
||||
line.clear();
|
||||
lines.read_line(&mut line).unwrap();
|
||||
}
|
||||
});
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "--allow-any-path"])
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Download {
|
||||
}
|
||||
if solidcraft_ui_egui::documents::any_dirty(app) {
|
||||
let mut answer = None;
|
||||
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
|
||||
let closed = ui_kit::Window::new(egui::Id::unique("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
|
||||
.required()
|
||||
.show_body(ctx, |ui| {
|
||||
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
|
||||
|
||||
@@ -726,7 +726,7 @@ mod web {
|
||||
return;
|
||||
}
|
||||
let mut act: Option<(&'static str, String)> = None;
|
||||
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
|
||||
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_files"), "folder", "Files in this browser")
|
||||
// Room for long names; the kit still caps the window at the browser's width.
|
||||
.cards(4.0)
|
||||
.show_body(ctx, |ui| {
|
||||
@@ -890,7 +890,7 @@ mod web {
|
||||
fn recovery_prompt(&mut self, ctx: &egui::Context) {
|
||||
let Some((name, path)) = self.files.recovery.borrow().clone() else { return };
|
||||
let mut answer = None;
|
||||
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_recovery"), "restore", "Recover unsaved work?")
|
||||
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_recovery"), "restore", "Recover unsaved work?")
|
||||
.required()
|
||||
.show_body(ctx, |ui| {
|
||||
ui.label(format!("“{name}” had unsaved changes when this page last closed."));
|
||||
|
||||
@@ -68,7 +68,9 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
Ok(l) => {
|
||||
if let Some(port) = listen_on_listener(l, token.clone(), tx, ctx, false) {
|
||||
if matches!(listen_on, control_auth::Listen::Default) {
|
||||
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)"
|
||||
);
|
||||
}
|
||||
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
|
||||
match control_auth::write_current_port(port) {
|
||||
@@ -77,14 +79,14 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
*p = Some((port, token));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
|
||||
Err(e) => solidcraft_engine::note!("solidcraft: could not write the control discovery file ({e})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
Err(e) => solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
}
|
||||
}
|
||||
None => eprintln!("solidcraft: control server not started: no random token available"),
|
||||
None => solidcraft_engine::note!("solidcraft: control server not started: no random token available"),
|
||||
}
|
||||
rx
|
||||
}
|
||||
@@ -95,13 +97,13 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
/// requests go to `tx`, where the chat's gate reads them; one may wait long for the user's Allow.
|
||||
pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u16> {
|
||||
let Some(token) = new_token() else {
|
||||
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
solidcraft_engine::note!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
return None;
|
||||
};
|
||||
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -115,19 +117,21 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
solidcraft_engine::note!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
if let Ok(mut files) = TOKEN_FILES.lock() {
|
||||
files.push((path, token.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
Err(e) => solidcraft_engine::note!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
|
||||
clients need SOLIDCRAFT_CONTROL_TOKEN",
|
||||
path.display()
|
||||
),
|
||||
},
|
||||
None => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN"
|
||||
)
|
||||
}
|
||||
}
|
||||
let token = std::sync::Arc::new(token);
|
||||
|
||||
@@ -112,7 +112,7 @@ pub fn restart(software: bool, lost_at: u64, reason: &str) {
|
||||
pub fn end() -> ! {
|
||||
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
|
||||
std::thread::sleep(END_GRACE);
|
||||
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
solidcraft_engine::note!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
std::process::abort();
|
||||
});
|
||||
if let Err(e) = watchdog {
|
||||
|
||||
+14
-13
@@ -79,7 +79,7 @@ fn mark(step: &str) {
|
||||
static START: std::sync::OnceLock<(std::time::Instant, bool)> = std::sync::OnceLock::new();
|
||||
let (start, on) = START.get_or_init(|| (std::time::Instant::now(), solidcraft_engine::geom::env::var_os("PROFILE_STARTUP").is_some()));
|
||||
if *on {
|
||||
eprintln!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
|
||||
solidcraft_engine::note!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ impl App {
|
||||
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
|
||||
let reason = gpu.lost.reason().unwrap_or_default();
|
||||
log::error!("graphics: the device was lost ({reason}); closing to start again");
|
||||
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::note!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
|
||||
gpu.lost_at = Some(device_lost::now_secs());
|
||||
gpu.reason = reason;
|
||||
@@ -993,7 +993,7 @@ fn main() -> eframe::Result {
|
||||
if let Some(value) = max_file_size {
|
||||
let applied = startup::max_file_size(value.as_deref()).and_then(|bytes| solidcraft_engine::io::limits::set_override(Some(bytes)));
|
||||
if let Err(error) = applied {
|
||||
eprintln!("SolidCraft: {error}");
|
||||
solidcraft_engine::note!("SolidCraft: {error}");
|
||||
return Err(eframe::Error::AppCreation(std::io::Error::other(error).into()));
|
||||
}
|
||||
}
|
||||
@@ -1025,7 +1025,7 @@ fn main() -> eframe::Result {
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
solidcraft_engine::note!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
@@ -1138,13 +1138,13 @@ fn main() -> eframe::Result {
|
||||
// Windows: the taskbar button shows the mark even when slow first frames keep the
|
||||
// taskbar waiting for the window's icon.
|
||||
if !native_frame::set_class_icon(cc) {
|
||||
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
solidcraft_engine::note!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
}
|
||||
// The OS hit test offers the resize border, at the window's DPI, from the first
|
||||
// message (#1381).
|
||||
app.native_resize_border = custom_titlebar && native_frame::install(cc);
|
||||
if custom_titlebar && !app.native_resize_border {
|
||||
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
solidcraft_engine::note!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
}
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
@@ -1175,9 +1175,10 @@ fn main() -> eframe::Result {
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
|
||||
app.preferences.graphics_adapter, info.name
|
||||
app.preferences.graphics_adapter,
|
||||
info.name
|
||||
);
|
||||
}
|
||||
let samples = scene_msaa_request(saved_samples);
|
||||
@@ -1185,7 +1186,7 @@ fn main() -> eframe::Result {
|
||||
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
|
||||
});
|
||||
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::note!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
app.msaa_supported = supported;
|
||||
@@ -1346,7 +1347,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
|
||||
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
solidcraft_engine::note!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
|
||||
@@ -1372,7 +1373,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
solidcraft_engine::note!("SolidCraft: the window could not open: {error}");
|
||||
// This failure was handled, so neither the marker nor the journal should call it a crash.
|
||||
if let Some(dir) = settings {
|
||||
startup::started(dir);
|
||||
@@ -1385,7 +1386,7 @@ fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backen
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
solidcraft_engine::note!("SolidCraft: {note}");
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
@@ -1791,7 +1792,7 @@ mod viewport_tests {
|
||||
viewport.fullscreen = Some(fullscreen);
|
||||
viewport.native_pixels_per_point = Some(retina);
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
|
||||
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
|
||||
ctx.run_ui(raw, |ui| super::content_ui(app, ui)).textures_delta.clear();
|
||||
};
|
||||
// Enter, leave, and enter again: no stale inset when returning to a normal window.
|
||||
for fullscreen in [false, true, false, true] {
|
||||
|
||||
@@ -434,7 +434,7 @@ mod tests {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
|
||||
app.home.open = false;
|
||||
let ctx = Context::default();
|
||||
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
|
||||
ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {}).textures_delta.clear();
|
||||
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
|
||||
let queue = |bridge: &NavBridge| {
|
||||
let mut shared = bridge.shared.lock().unwrap();
|
||||
|
||||
@@ -1914,6 +1914,7 @@ mod tests {
|
||||
|
||||
fn wheel() -> egui::Event {
|
||||
egui::Event::MouseWheel {
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: egui::vec2(0.0, -1.0),
|
||||
modifiers: Default::default(),
|
||||
|
||||
@@ -357,7 +357,7 @@ pub fn relaunch_with_version_compat() -> Result<bool, String> {
|
||||
return Ok(false);
|
||||
}
|
||||
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
solidcraft_engine::note!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
|
||||
let status = std::process::Command::new(&exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
|
||||
@@ -55,8 +55,7 @@ pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, fon
|
||||
painter.ctx().add_plugin(LabelAtlas);
|
||||
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
|
||||
let atlas = painter.ctx().fonts(|f| f.font_image_size());
|
||||
let primitives =
|
||||
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
|
||||
let primitives = painter.ctx().tessellate(vec![egui::epaint::ClippedShape::new(Rect::EVERYTHING, text)], painter.ctx().pixels_per_point());
|
||||
for primitive in primitives {
|
||||
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
|
||||
mesh.texture_id = LABEL_PIXELS;
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "تعطيل المجلدات المحددة",
|
||||
"Suppressed": "مُعطّل",
|
||||
"Surface": "السطح",
|
||||
"Surface or face": "سطح أو وجه",
|
||||
"Surface or faces": "سطح أو أوجه",
|
||||
"Surface, or faces to turn over": "السطح، أو الأوجه المراد قلبها",
|
||||
"Surfaces": "الأسطح",
|
||||
"Swap Ends": "تبديل الطرفين",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "الهدف",
|
||||
"Target Body": "الجسم الهدف",
|
||||
"Target Face (optional)": "الوجه الهدف (اختياري)",
|
||||
"Target edges": "الحواف المستهدفة",
|
||||
"Tear": "تمزيق",
|
||||
"Technical details": "التفاصيل التقنية",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "الرسم المتقطع دليل مستوى محفوظ. حدد وجهًا جديدًا لإصلاح الارتباط.",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "يجب أن يكون الاتجاه محورًا أو منحنى رسم مستقيمًا يقع في مستوى الرسم",
|
||||
"The mirror line cannot also be mirrored": "لا يمكن عكس خط المرآة نفسه",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "يجب أن يكون خط المرآة منحنى مستقيمًا من هذا الرسم، وليس أحد محاور نقطة الأصل",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "محور الأصل عمودي على مستوى الرسم، لذا ليس له خط في المستوى",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "نسخة المؤلف المحفوظة مفقودة. حدد موضع مؤلف صالحًا قبل إعادة الربط.",
|
||||
"Then": "ثم",
|
||||
"These features will be deleted:": "ستُحذف هذه الميزات:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "جسم الحافة المحددة لم يعد موجودًا",
|
||||
"add a key for a joint first": "أضف أولًا مفتاحًا لمفصل",
|
||||
"ai.connected_toast": "أصبح SolidCraft متاحًا في {clients}. أعد تشغيلها لاستخدامه (التفضيلات > المساعد).",
|
||||
"ai.http.allow": "السماح باتصال HTTP غير آمن",
|
||||
"ai.http.confirm": "السماح لهذه النقطة الطرفية",
|
||||
"ai.http.warning": "سيُرسَل مفتاح API والمحادثة دون تشفير. يمكن لأي شخص لديه وصول إلى الشبكة قراءتهما أو تغييرهما. استخدم هذا الخيار فقط على شبكة تثق بها.",
|
||||
"ai.prefs.allow_control": "السماح لوكلاء الذكاء الاصطناعي بالاتصال",
|
||||
"ai.prefs.allow_control_note": "قناة محلية (MCP). لا شيء يغادر هذا الحاسوب.\n\nيفتح قناة تحكم على هذا الحاسوب فقط (127.0.0.1، المنفذ 27878 أو أول منفذ متاح بعده). على البرنامج الذي يتصل أن يقدّم أيضًا رمزًا سريًا لا يقرؤه إلا حساب المستخدم الخاص بك، ويُنشأ من جديد عند كل تشغيل. يُطبّق في المرة القادمة التي يبدأ فيها SolidCraft. عند الإيقاف: لا شيء يستمع ما لم تشغّل SolidCraft مع --control.",
|
||||
"ai.prefs.availability": "التوفر",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "تتبّع الكاميرا",
|
||||
"ai.prefs.follow_camera_off": "تتبّع الكاميرا التلقائي متوقف.",
|
||||
"ai.prefs.follow_camera_on": "تتجه الكاميرا نحو الرسومات التي ينشئها الوكيل.",
|
||||
"ai.prefs.instructions": "تعليمات مخصصة",
|
||||
"ai.prefs.instructions_note": "تُرسَل إلى الوكيل في بداية كل محادثة، بعد تعليمات SolidCraft الخاصة: طريقتك المفضلة في العمل والقواعد التي يجب الالتزام بها. يسري أي تغيير من المحادثة التالية (اتصال أو محادثة جديدة).",
|
||||
"ai.prefs.intro": "وكلاء الذكاء الاصطناعي في SolidCraft.",
|
||||
"ai.prefs.make_default": "تعيين افتراضيًا",
|
||||
"ai.prefs.no_agents": "لا يوجد وكلاء بعد. أضف وكيلًا أو حدّث القائمة للعثور على الوكلاء المثبّتين.",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "تحريك فاصل مقارنة التصيير",
|
||||
"cmd.render.result.discard": "تجاهل نتيجة التصيير",
|
||||
"cmd.render.result.select": "تحديد نتيجة التصيير",
|
||||
"cmd.render.surface.import": "استيراد خرائط السطح",
|
||||
"cmd.render.texture.list": "عرض صور السطح المضمّنة",
|
||||
"cmd.render.texture.remove": "إزالة صورة السطح المضمّنة",
|
||||
"cmd.render_aging_describe": "تقادم السطح بالطبقات",
|
||||
"cmd.render_backends": "محركات التصيير",
|
||||
"cmd.render_batch": "تصيير كل اللقطات",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "علامة التبويب السابقة",
|
||||
"cmd.view_rename": "إعادة تسمية عرض مسمّى",
|
||||
"cmd.view_render_frame": "أدلة إطار التصيير",
|
||||
"cmd.view_ribbon_tabs_only": "علامات تبويب الشريط فقط",
|
||||
"cmd.view_right": "العرض الأيمن",
|
||||
"cmd.view_save": "عرض مسمّى جديد",
|
||||
"cmd.view_shots_strip": "شريط اللقطات",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: في XWayland ترفض البوابة النافذة الأصلية.",
|
||||
"file_load.built": "بُني حتى الآن: {built} من {total} ميزة.",
|
||||
"file_load.converting": "تحويل الجسم الصلب {done} من {total}",
|
||||
"file_load.inserted": "تم إدراج {name}",
|
||||
"file_load.inserting": "إدراج الهندسة",
|
||||
"file_load.link_item": "{name} ({done} من {total})",
|
||||
"file_load.opened": "تم فتح {name}",
|
||||
"file_load.opened_in_tab": "تم فتح {name} في علامة تبويب جديدة",
|
||||
"file_load.opening": "فتح التصميم",
|
||||
"file_load.preparing_thumbnail": "تجهيز الصورة المصغّرة...",
|
||||
"file_load.preparing_view": "تجهيز العرض...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "يقرأ مسارات مخطط SVG إلى رسم بالمقياس والدوران والموضع الذي تحدده. استخدمه لتحويل شعار أو محيط متجهي إلى منحنيات يمكنك بثقها.",
|
||||
"help.sketch.inspect": "يفتح تقريرًا عن الرسم المفتوح: عدد درجات الحرية المتبقية وهل هو مقيد بالكامل. استخدمه لمعرفة ما لا يزال يتحرك قبل إنهاء الرسم.",
|
||||
"help.sketch.intersect": "يضيف المنحنيات أو النقاط التي تقطع فيها أجسام أو أوجه أو حواف أو رسوم أخرى مستوى الرسم. تبقى مرتبطة وتتبع النموذج عندما يتغير.",
|
||||
"help.sketch.intersection_curve": "ينشئ منحنيات ثلاثية الأبعاد حيث يلتقي جسمان أو وجهان، مرتبطة بهما. استخدمها مسارات أو قضبان توجيه تتبع النموذج.",
|
||||
"help.sketch.intersection_curve": "ينشئ منحنيات ثلاثية الأبعاد حيث يلتقي جسمان أو وجهان أو منحنيات رسوم أخرى، مرتبطة بهما. استخدمها مسارات أو قضبان توجيه تتبع النموذج.",
|
||||
"help.sketch.isoparametric_curve": "ينشئ منحنى ثلاثي الأبعاد على اتجاه U أو V للوجه عبر نقطة تختارها، مرتبطًا بالوجه. استخدمه مسارًا أو خط تقسيم يتبع السطح.",
|
||||
"help.sketch.line": "يرسم خطوطًا متصلة نقطة بنقطة، كسلسلة مفتوحة أو محيط مغلق. هو الأداة الأساسية لمعظم المقاطع.",
|
||||
"help.sketch.line.midpoint": "يرسم خطًا من منتصفه إلى أحد طرفيه، ويعكس الطرف الآخر. استخدمه للخطوط المتمركزة على نقطة، مثل حافة متناظرة.",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "يدير العرض أو يُزيحه أو يغيّر تكبيره بخطوات، مثل ربع دورة إلى اليسار. استخدمه لتحريك الكاميرا بدقة بدلًا من السحب.",
|
||||
"help.view.navigation_bar": "يُظهر شريط التنقل أسفل منطقة العرض أو يخفيه. أخفِه لمساحة أكبر، وأظهره للوصول إلى أدوات العرض بالفأرة.",
|
||||
"help.view.rename": "يمنح عرضًا مسمّى محفوظًا اسمًا جديدًا. يبقى العرض نفسه كما هو.",
|
||||
"help.view.ribbon_tabs_only": "يطوي الشريط إلى صف علامات التبويب، فتحصل منطقة العرض على المساحة التي كانت تشغلها لوحاته. انقر علامة تبويب لإظهار لوحاتها فوق منطقة العرض؛ واختر هذا الأمر مرة أخرى لإبقاء الشريط كاملًا ظاهرًا.",
|
||||
"help.view.save": "يحفظ موضع الكاميرا الحالي كعرض مسمّى في التصميم. استخدمه للعودة إلى الزاوية نفسها لاحقًا، مثلًا للقطات الشاشة.",
|
||||
"help.view.spin": "يدوّر العرض حول النموذج باستمرار حتى النقرة أو المفتاح التالي. استخدمه لعرض النموذج من جميع الجهات.",
|
||||
"help.view.tool": "يثبّت أداة كاميرا، التدوير أو الإزاحة أو التكبير، بحيث يحرّك السحب بزر الفأرة الأيسر العرض حتى توقفها. استخدمه على لوحة اللمس أو بدون زر فأرة أوسط.",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "السابق",
|
||||
"prefs.choose_font": "اختيار ملف خط…",
|
||||
"prefs.click": "نقر",
|
||||
"prefs.command_dialog_values": "مربعات حوار الأوامر",
|
||||
"prefs.contact_shading": "تظليل التلامس الناعم",
|
||||
"prefs.contact_shading_note": "تظليل ناعم حيث تقترب القطع من بعضها.",
|
||||
"prefs.context_menu": "القائمة السياقية",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "الأسطح والحواف",
|
||||
"prefs.graphics_page.view_bar": "أيضًا في شريط العرض",
|
||||
"prefs.graphics_presets_note": "الإعدادات المسبقة تضبط الخيارات أدناه معًا.",
|
||||
"prefs.ground_shadow_follows_model": "ظل الأرض يتبع النموذج",
|
||||
"prefs.ground_shadow_follows_model_help": "ضع الظل أسفل أدنى جسم مرئي. عند التعطيل، يبقى ظل النماذج المرفوعة على مستوى الأرض عند نقطة الأصل.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "إخفاء رموز القيود عند التصغير",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "تختفي رموز القيود من العرض عندما يُصغَّر الرسم بقدر يجعلها تزحمه. معطّل: تبقى بحجم ثابت على الشاشة عند أي مستوى تكبير.",
|
||||
"prefs.history": "السجل",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "بطاقة",
|
||||
"prefs.layout_page.column": "عمود",
|
||||
"prefs.layout_page.hover": "عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus": "تبديل قوائم اللوحات عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus_help": "انقر على اسم لوحة في الشريط لفتح قائمتها. مرّر المؤشر فوق اسم لوحة أخرى للتبديل. حرّك المؤشر خارج الشريط والقائمة المفتوحة لإغلاقها.",
|
||||
"prefs.layout_page.icons_labels": "الأيقونات + التسميات",
|
||||
"prefs.layout_page.ok_cancel": "موافق وإلغاء",
|
||||
"prefs.layout_page.on_hover": "عند مرور المؤشر",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "أنعم",
|
||||
"prefs.quick_measure": "قياس سريع في زاوية العرض",
|
||||
"prefs.quick_measure_note": "مقاس العناصر المحددة في الزاوية اليمنى السفلية من العرض: طول الحافة (أو Ø وR للحافة الدائرية)، ومساحة الوجه، وموضع النقطة، والمسافة والزاوية بين عنصرين، ومجموع عدة حواف أو أوجه. النقر ينسخه.",
|
||||
"prefs.remember_dialog_values": "تذكّر قيم مربعات حوار الأوامر",
|
||||
"prefs.remember_dialog_values.help": "معطّل افتراضيًا. عند تفعيله، بعد نجاح الأمر، يُعاد استخدام قيمه وخياراته الآمنة في المرة التالية التي تفتح فيها الأمر نفسه، حتى إغلاق SolidCraft.",
|
||||
"prefs.render_previous_geometry_budget": "استخدام ميزانية الهندسة السابقة لمعاينة التصيير",
|
||||
"prefs.render_previous_geometry_budget_help": "استعادة حد الهندسة الثابت السابق (512 ميبيبايت على سطح المكتب 64 بت، و128 ميبيبايت على المنصات الأخرى) بدلًا من الميزانية المعتمدة على الجهاز.",
|
||||
"prefs.reports_saved": "{n} محفوظة",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "النقر على زر الشريط المضيء لأداة الرسم المستخدمة ينهي الأداة، فيعود النقر في العرض إلى التحديد. عند الإيقاف: يبدأ النقر الأداة من جديد.",
|
||||
"prefs.ribbon_reveal": "إظهار مكان نتائج البحث في الشريط",
|
||||
"prefs.ribbon_reveal_note": "عند تمرير المؤشر فوق نتيجة في «البحث في الأوامر» يظهر مكان الأمر في الشريط: تبويبه أو زره أو صفه في القائمة المنسدلة للوحة. ويومض هناك الأمر الذي بدأ من البحث، ويُعلِّم الأمر الجاري من قائمة منسدلة لوحتَه.",
|
||||
"prefs.ribbon_tabs_only": "إظهار علامات تبويب الشريط فقط",
|
||||
"prefs.ribbon_tabs_only_note": "يُطوى الشريط إلى صف علامات التبويب. يؤدي النقر على علامة تبويب إلى إظهار لوحاتها فوق منطقة العرض حتى يبدأ أمر أو يغادرها المؤشر. يبدّل الأمر «علامات تبويب الشريط فقط» في تبويب العرض، أو اختصاره، هذا الخيار.",
|
||||
"prefs.right_click": "نقر أيمن",
|
||||
"prefs.saving": "البدء والحفظ",
|
||||
"prefs.scale_note": "أو Ctrl + / Ctrl − في النافذة. الأزرار 28 px عند 100%.",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "الامتداد",
|
||||
"render.stage_panels.fit_save": "ملاءمة وحفظ",
|
||||
"render.stage_panels.gpu_required": "يتطلب تتبّع المسار على GPU",
|
||||
"render.stage_panels.grain_angle": "زاوية عروق الخشب",
|
||||
"render.stage_panels.keep": "احتفاظ",
|
||||
"render.stage_panels.kept": "ما يُحتفظ به",
|
||||
"render.stage_panels.kept_fields": "الكاميرا، وأخذ العيّنات، والأبعاد، والروابط البصرية، وهندسة CAD، والخصائص الهندسية.",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "مسار عشوائي للتشتت الداخلي",
|
||||
"render.subsurface_help": "للعوازل المغلقة فقط. التشتت وامتصاص RGB معاملان منفصلان لكل mm. تتشتت المسارات داخل الهندسة الفعلية، ثم تنكسر أو تنعكس عند الحد. عطّل نفاذ الظل المستقيم وإزالة تشويش السطح والحجم الخارجي؛ واستخدم طبقة فرينل واحدة. ليس مظلل انتشار ولا شبكة كثافة ولا مادة مقيسة. الأوساط الكثيفة قد تحتاج مزيدًا من العيّنات وعمق المسار.",
|
||||
"render.subsurface_rate": "معدل اصطدامات التشتت",
|
||||
"render.surface_import.author": "المؤلف",
|
||||
"render.surface_import.busy": "جارٍ استيراد الصورة…",
|
||||
"render.surface_import.complete": "اختر صورة وأدخل مؤلفها ومصدرها وترخيصها.",
|
||||
"render.surface_import.embedded": "صورة مستوردة",
|
||||
"render.surface_import.failed": "لم يكتمل استيراد الصورة.",
|
||||
"render.surface_import.license": "الترخيص / الملكية",
|
||||
"render.surface_import.raytrace_required": "تتطلب الصور تتبع الأشعة على CPU أو GPU. اختره في الإخراج.",
|
||||
"render.surface_import.repeat_u": "تكرار U",
|
||||
"render.surface_import.repeat_v": "تكرار V",
|
||||
"render.surface_import.save": "حفظ الصورة على الجسم",
|
||||
"render.surface_import.saved_cpu": "احفظ الخرائط لاستخدامها في التصيير على CPU.",
|
||||
"render.surface_import.source": "المصدر",
|
||||
"render.surface_import.title": "استيراد صورة",
|
||||
"render.tone_aces_fitted": "ACES مُلاءَم",
|
||||
"render.tone_reinhard": "راينهارد",
|
||||
"render.transmission_scope": "يدعم زجاج GPU الأوساط المنفصلة المعتمدة. لا تتوفر الأوساط المتداخلة غير المقيدة ولا مقدّر نفاذ خالٍ من التشويش.",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "لوح نموذجي",
|
||||
"sample.vents.about": "شقوق في نمط مستطيلي: الصفوف والأعمدة والخطوة معاملات",
|
||||
"sample.vents.name": "لوح تهوية",
|
||||
"scale.per_axis": "لكل محور",
|
||||
"scale.uniform": "موحّد",
|
||||
"search.command_keys": "Enter تشغيل Shift+Enter تشغيل مع الإبقاء مفتوحًا Esc إغلاق",
|
||||
"search.commands": "الأوامر",
|
||||
"search.design": "في هذا التصميم",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "النقر يحدد الأجسام كاملة",
|
||||
"ui.click_then_press_key": "انقر ثم اضغط المفتاح الجديد",
|
||||
"ui.close": "إغلاق",
|
||||
"ui.close_tab": "إغلاق علامة التبويب",
|
||||
"ui.coil": "ملف حلزوني",
|
||||
"ui.coincident": "متطابق",
|
||||
"ui.collapse": "طي",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "المعامل",
|
||||
"ui.fade": "تلاشٍ",
|
||||
"ui.fade_length": "طول التلاشي",
|
||||
"ui.fair_iterations": "عدد التكرارات",
|
||||
"ui.fair_keep_edges": "الحفاظ على الحواف",
|
||||
"ui.fair_strength": "شدة التنعيم",
|
||||
"ui.fair_surface_title": "تنعيم السطح",
|
||||
"ui.fair_tolerance_default": "0.001 من حجم الجسم",
|
||||
"ui.feature_history": "سجل الميزات",
|
||||
"ui.feature_options": "خيارات الميزة",
|
||||
"ui.features": "الميزات",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "فحص",
|
||||
"ui.instances": "النسخ",
|
||||
"ui.intersect": "تقاطع",
|
||||
"ui.intersection_curve_second_pick": "منحنى التقاطع: انقر على الوجه أو المنحنى الثاني (انقر بزر الماوس الأيمن للبدء من جديد)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "عزل",
|
||||
"ui.join": "ضم",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "السحب الحر للمُعالج أيضًا مع Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "عند التفعيل: Alt/Option أو Ctrl/Cmd على أسهم البثق / أداة التحويل يعطّل التقاط القيم. عند الإيقاف: Ctrl/Cmd فقط.",
|
||||
"ui.manufacture": "التصنيع",
|
||||
"ui.match_surface_title": "مطابقة السطح",
|
||||
"ui.material_review.axes": "محاور محلية للمادة؛ لم يُحدد اتجاه للجسم.",
|
||||
"ui.material_review.claims": "المصادر والتراخيص ادعاءات غير موثّقة. القيم المفقودة تبقى مجهولة.",
|
||||
"ui.material_review.compressive_strength": "مقاومة الانضغاط",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "مراجعة بيانات المادة الفيزيائية",
|
||||
"ui.material_review.unknown": "مجهول",
|
||||
"ui.material_review.yield_strength": "مقاومة الخضوع",
|
||||
"ui.mcp_agents": "الوكلاء عبر MCP",
|
||||
"ui.mcp_compact": "مخرجات مضغوطة",
|
||||
"ui.mcp_compact_note": "للوكلاء المتصلين عبر خادم MCP (solidcraft-cli mcp): يلخّص inspect_design الرسومات، ولا تُنسَّق نتائج الأدوات، فتستهلك رموزًا أقل. يسري على جلسات الوكلاء التي تبدأ بعد التغيير؛ ويفعّله أيضًا --compact أو SOLIDCRAFT_MCP_COMPACT=1.",
|
||||
"ui.measure": "قياس",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF مع وسوم الأوجه",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "الخيارات",
|
||||
"ui.orbit": "تدوير العرض",
|
||||
"ui.orbit_around": "التدوير حول",
|
||||
"ui.organize": "تنظيم",
|
||||
"ui.orientation": "الاتجاه",
|
||||
"ui.origin": "نقطة الأصل",
|
||||
"ui.palette_construction_mode": "وضع الخطوط المساعدة (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "إضافة مرشح…",
|
||||
"ui.point_cloud_auto_range": "نطاق تلقائي",
|
||||
"ui.point_cloud_color_by": "التلوين حسب",
|
||||
"ui.point_cloud_dots": "نقاط منفردة",
|
||||
"ui.point_cloud_file_colors": "ألوان الملف",
|
||||
"ui.point_cloud_filter_by": "التصفية حسب",
|
||||
"ui.point_cloud_height_high": "أعلى ارتفاع",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Učitava putanje SVG crteža u skicu, skalirane, zakrenute i postavljene gdje zadate. Koristite ga da logotip ili vektorski obris pretvorite u krive koje možete ekstrudirati.",
|
||||
"help.sketch.inspect": "Otvara izvještaj o otvorenoj skici: koliko je stepeni slobode preostalo i je li potpuno ograničena. Koristite ga da saznate šta se još pomjera prije nego što završite skicu.",
|
||||
"help.sketch.intersect": "Dodaje krive ili tačke gdje tijela, lica, ivice ili druge skice sijeku ravan skice. Ostaju povezane i prate model kada se promijeni.",
|
||||
"help.sketch.intersection_curve": "Pravi 3D krive tamo gdje se dva tijela ili lica sastaju, povezane s njima. Koristite ih kao putanje ili vodilice koje prate model.",
|
||||
"help.sketch.intersection_curve": "Pravi 3D krive tamo gdje se dva tijela, lica ili krive drugih skica sastaju, povezane s njima. Koristite ih kao putanje ili vodilice koje prate model.",
|
||||
"help.sketch.isoparametric_curve": "Pravi 3D krivu u smjeru U ili V lica kroz tačku koju izaberete, povezanu s licem. Koristite je kao putanju ili liniju razdvajanja koja prati površ.",
|
||||
"help.sketch.line": "Crta povezane linije tačku po tačku, kao otvoreni lanac ili zatvoren obris. To je osnovni alat za većinu profila.",
|
||||
"help.sketch.line.midpoint": "Crta liniju od njene središnje tačke do jednog kraja, a drugi kraj se preslikava. Koristite je za linije centrirane na tački, npr. simetričnu ivicu.",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Načte cesty výkresu SVG do náčrtu, ve zvoleném měřítku, natočení a poloze. Použijte to k přeměně loga nebo vektorového obrysu na křivky, které lze vysunout.",
|
||||
"help.sketch.inspect": "Otevře zprávu o otevřeném náčrtu: kolik stupňů volnosti zbývá a zda je plně zavazbený. Použijte to ke zjištění, co se ještě hýbe, než náčrt dokončíte.",
|
||||
"help.sketch.intersect": "Přidá křivky nebo body, kde tělesa, plochy, hrany nebo jiné náčrty protínají rovinu náčrtu. Zůstanou propojené a sledují model, když se změní.",
|
||||
"help.sketch.intersection_curve": "Vytvoří 3D křivky tam, kde se setkávají dvě tělesa nebo plochy, propojené s nimi. Použijte je jako cesty nebo vodicí dráhy, které sledují model.",
|
||||
"help.sketch.intersection_curve": "Vytvoří 3D křivky tam, kde se setkávají dvě tělesa, plochy nebo křivky jiných náčrtů, propojené s nimi. Použijte je jako cesty nebo vodicí dráhy, které sledují model.",
|
||||
"help.sketch.isoparametric_curve": "Vytvoří 3D křivku ve směru U nebo V plochy bodem, který vyberete, propojenou s plochou. Použijte ji jako cestu nebo dělicí čáru, která sleduje povrch.",
|
||||
"help.sketch.line": "Kreslí spojené úsečky bod po bodu, jako otevřený řetězec nebo uzavřený obrys. Je to základní nástroj pro většinu profilů.",
|
||||
"help.sketch.line.midpoint": "Nakreslí úsečku od jejího středu k jednomu konci, přičemž druhý konec se zrcadlí. Použijte to pro úsečky vystředěné na bod, například souměrnou hranu.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Ukázková deska",
|
||||
"sample.vents.about": "Drážky v obdélníkovém poli: řádky, sloupce a rozteč jsou parametry",
|
||||
"sample.vents.name": "Větrací deska",
|
||||
"scale.per_axis": "Podle os",
|
||||
"scale.uniform": "Rovnoměrně",
|
||||
"search.command_keys": "Enter spustit Shift+Enter spustit a ponechat otevřené Esc zavřít",
|
||||
"search.commands": "Příkazy",
|
||||
"search.design": "V tomto návrhu",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Læser stierne i en SVG-tegning ind i en skitse, skaleret, drejet og placeret, som du angiver. Brug den til at gøre et logo eller en vektorkontur til kurver, du kan ekstrudere.",
|
||||
"help.sketch.inspect": "Åbner en rapport om den åbne skitse: hvor mange frihedsgrader der er tilbage, og om den er fuldt bundet. Brug den til at finde ud af, hvad der stadig kan bevæge sig, før du afslutter skitsen.",
|
||||
"help.sketch.intersect": "Tilføjer de kurver eller punkter, hvor emner, flader, kanter eller andre skitser krydser skitseplanet. De forbliver linkede og følger modellen, når den ændres.",
|
||||
"help.sketch.intersection_curve": "Opretter 3D-kurver, hvor to emner eller flader mødes, linket til dem. Brug dem som baner eller styrekurver, der følger modellen.",
|
||||
"help.sketch.intersection_curve": "Opretter 3D-kurver, hvor to emner, flader eller kurver fra andre skitser mødes, linket til dem. Brug dem som baner eller styrekurver, der følger modellen.",
|
||||
"help.sketch.isoparametric_curve": "Opretter en 3D-kurve langs en flades U- eller V-retning gennem et punkt, du vælger, linket til fladen. Brug den som bane eller delelinje, der følger overfladen.",
|
||||
"help.sketch.line": "Tegner forbundne linjer punkt for punkt som en åben kæde eller en lukket kontur. Det er det grundlæggende værktøj til de fleste profiler.",
|
||||
"help.sketch.line.midpoint": "Tegner en linje fra dens midtpunkt ud til den ene ende, og den anden ende spejles. Brug den til linjer centreret om et punkt, f.eks. en symmetrisk kant.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Eksempelplade",
|
||||
"sample.vents.about": "Slidser i et rektangulært mønster: Rækker, kolonner og deling er parametre",
|
||||
"sample.vents.name": "Ventilationsplade",
|
||||
"scale.per_axis": "Per akse",
|
||||
"scale.uniform": "Ensartet",
|
||||
"search.command_keys": "Enter kør Shift+Enter kør og hold åben Esc luk",
|
||||
"search.commands": "Kommandoer",
|
||||
"search.design": "I dette design",
|
||||
|
||||
+50
-1
@@ -628,6 +628,8 @@
|
||||
"Suppress Selected Folders": "Ausgewählte Ordner unterdrücken",
|
||||
"Suppressed": "Unterdrückt",
|
||||
"Surface": "Flächenkörper",
|
||||
"Surface or face": "Flächenkörper oder Fläche",
|
||||
"Surface or faces": "Flächenkörper oder Flächen",
|
||||
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
|
||||
"Surfaces": "Flächen",
|
||||
"Swap Ends": "Enden vertauschen",
|
||||
@@ -641,6 +643,7 @@
|
||||
"Target": "Ziel",
|
||||
"Target Body": "Zielkörper",
|
||||
"Target Face (optional)": "Zielfläche (optional)",
|
||||
"Target edges": "Zielkanten",
|
||||
"Tear": "Reißen",
|
||||
"Technical details": "Technische Details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
|
||||
@@ -648,6 +651,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Die Richtung muss eine Achse oder eine gerade Skizzenkurve in der Skizzenebene sein",
|
||||
"The mirror line cannot also be mirrored": "Die Spiegellinie kann nicht gleichzeitig gespiegelt werden",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Die Spiegellinie muss eine gerade Kurve dieser Skizze sein, keine Ursprungsachse",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Die Ursprungsachse steht senkrecht auf der Skizzenebene und hat daher keine Linie in ihr",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
|
||||
"Then": "Then",
|
||||
"These features will be deleted:": "Diese Merkmale werden gelöscht:",
|
||||
@@ -739,6 +743,9 @@
|
||||
"a picked edge's body is gone": "der Körper einer gewählten Kante existiert nicht mehr",
|
||||
"add a key for a joint first": "zuerst einen Schlüssel für ein Gelenk hinzufügen",
|
||||
"ai.connected_toast": "SolidCraft ist jetzt in {clients} verfügbar. Starten Sie diese neu, um es zu nutzen (Einstellungen > Assistent).",
|
||||
"ai.http.allow": "Unsicheres HTTP erlauben",
|
||||
"ai.http.confirm": "Für diesen Endpunkt erlauben",
|
||||
"ai.http.warning": "Der API-Schlüssel und die Unterhaltung werden unverschlüsselt gesendet. Jeder mit Zugriff auf das Netzwerk kann sie lesen oder verändern. Verwenden Sie dies nur in einem vertrauenswürdigen Netzwerk.",
|
||||
"ai.prefs.allow_control": "KI-Agenten verbinden lassen",
|
||||
"ai.prefs.allow_control_note": "Ein lokaler Kanal (MCP). Nichts verlässt diesen Computer.\n\nÖffnet einen Steuerkanal nur auf diesem Computer (127.0.0.1, Port 27878 oder der nächste freie). Ein verbindendes Programm muss zusätzlich ein geheimes Token vorweisen, das nur Ihr Benutzerkonto lesen kann und bei jedem Start neu erzeugt wird. Wirkt ab dem nächsten Start von SolidCraft. Aus: Es wartet nichts auf Verbindungen, außer Sie starten SolidCraft mit --control.",
|
||||
"ai.prefs.availability": "Verfügbarkeit",
|
||||
@@ -1074,6 +1081,9 @@
|
||||
"cmd.render.compare.divider": "Render-Vergleichstrenner verschieben",
|
||||
"cmd.render.result.discard": "Renderergebnis verwerfen",
|
||||
"cmd.render.result.select": "Renderergebnis auswählen",
|
||||
"cmd.render.surface.import": "Oberflächentexturen importieren",
|
||||
"cmd.render.texture.list": "Eingebettete Oberflächenbilder auflisten",
|
||||
"cmd.render.texture.remove": "Eingebettetes Oberflächenbild entfernen",
|
||||
"cmd.render_aging_describe": "Mehrschichtige Oberflächenalterung",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.render_batch": "Alle Aufnahmen rendern",
|
||||
@@ -1446,8 +1456,11 @@
|
||||
"file.open.portal_fallback.xwayland": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Unter XWayland lehnt das Portal das Elternfenster ab.",
|
||||
"file_load.built": "Bisher erstellt: {built} von {total} Merkmalen.",
|
||||
"file_load.converting": "Volumenkörper {done} von {total} wird konvertiert",
|
||||
"file_load.inserted": "{name} eingefügt",
|
||||
"file_load.inserting": "Geometrie wird eingefügt",
|
||||
"file_load.link_item": "{name} ({done} von {total})",
|
||||
"file_load.opened": "{name} geöffnet",
|
||||
"file_load.opened_in_tab": "{name} in neuem Tab geöffnet",
|
||||
"file_load.opening": "Design wird geöffnet",
|
||||
"file_load.preparing_thumbnail": "Vorschaubild wird vorbereitet...",
|
||||
"file_load.preparing_view": "Ansicht wird vorbereitet...",
|
||||
@@ -1683,7 +1696,7 @@
|
||||
"help.sketch.insert_svg": "Liest die Pfade einer SVG-Zeichnung in eine Skizze ein, skaliert, gedreht und an der gewünschten Stelle. Damit machen Sie aus einem Logo oder einer Vektorkontur Kurven, die sich extrudieren lassen.",
|
||||
"help.sketch.inspect": "Öffnet einen Bericht zur offenen Skizze: wie viele Freiheitsgrade übrig sind und ob sie vollständig bestimmt ist. Damit sehen Sie, was sich noch bewegt, bevor Sie die Skizze abschließen.",
|
||||
"help.sketch.intersect": "Fügt die Kurven oder Punkte hinzu, an denen Körper, Flächen, Kanten oder andere Skizzen die Skizzenebene schneiden. Sie bleiben verknüpft und folgen Änderungen am Modell.",
|
||||
"help.sketch.intersection_curve": "Erzeugt 3D-Kurven, wo sich zwei Körper oder Flächen treffen, mit diesen verknüpft. Als Pfade oder Leitkurven, die dem Modell folgen.",
|
||||
"help.sketch.intersection_curve": "Erzeugt 3D-Kurven, wo sich zwei Körper, Flächen oder Kurven anderer Skizzen treffen, mit diesen verknüpft. Als Pfade oder Leitkurven, die dem Modell folgen.",
|
||||
"help.sketch.isoparametric_curve": "Erzeugt eine 3D-Kurve in U- oder V-Richtung einer Fläche durch einen gewählten Punkt, mit der Fläche verknüpft. Als Pfad oder Trennlinie, die der Fläche folgt.",
|
||||
"help.sketch.line": "Zeichnet verbundene Linien Punkt für Punkt, als offene Kette oder geschlossene Kontur. Das Grundwerkzeug für die meisten Profile.",
|
||||
"help.sketch.line.midpoint": "Zeichnet eine Linie von ihrem Mittelpunkt zu einem Ende, das andere Ende gespiegelt. Für Linien, die auf einem Punkt mittig liegen, etwa eine symmetrische Kante.",
|
||||
@@ -2293,6 +2306,7 @@
|
||||
"prefs.carousel_previous": "Zurück",
|
||||
"prefs.choose_font": "Schriftdatei auswählen…",
|
||||
"prefs.click": "Klicken",
|
||||
"prefs.command_dialog_values": "Befehlsdialoge",
|
||||
"prefs.contact_shading": "Weiche Kontaktschattierung",
|
||||
"prefs.contact_shading_note": "Erzeugt weiche Schatten an Stellen, an denen sich Bauteile nahekommen.",
|
||||
"prefs.context_menu": "Kontextmenü",
|
||||
@@ -2388,6 +2402,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "Flächen und Kanten",
|
||||
"prefs.graphics_page.view_bar": "Auch in der Ansichtsleiste",
|
||||
"prefs.graphics_presets_note": "Mit den Profilen werden die unten aufgeführten Optionen gemeinsam eingestellt.",
|
||||
"prefs.ground_shadow_follows_model": "Bodenschatten folgt dem Modell",
|
||||
"prefs.ground_shadow_follows_model_help": "Den Schatten unter dem niedrigsten sichtbaren Körper platzieren. Wenn deaktiviert, bleibt der Schatten angehobener Modelle auf der Bodenebene durch den Ursprung.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "Bindungssymbole bei starkem Herauszoomen ausblenden",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "Bindungssymbole verschwinden, wenn die Skizze so weit herausgezoomt ist, dass sie sie überladen würden. Aus: sie behalten bei jedem Zoom eine feste Bildschirmgröße.",
|
||||
"prefs.history": "Verlauf",
|
||||
@@ -2423,6 +2439,8 @@
|
||||
"prefs.layout_page.card": "Karte",
|
||||
"prefs.layout_page.column": "Spalte",
|
||||
"prefs.layout_page.hover": "Bei Mauszeiger",
|
||||
"prefs.layout_page.hover_panel_menus": "Bereichsmenüs beim Darüberfahren wechseln",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Klicken Sie auf einen Bereichsnamen, um das Menü zu öffnen. Bewegen Sie den Zeiger auf einen anderen Bereichsnamen, um das Menü zu wechseln. Außerhalb des Menübands und des geöffneten Menüs schließt es sich.",
|
||||
"prefs.layout_page.icons_labels": "Symbole + Beschriftungen",
|
||||
"prefs.layout_page.ok_cancel": "OK und Abbrechen",
|
||||
"prefs.layout_page.on_hover": "Bei Mauszeiger",
|
||||
@@ -2525,6 +2543,8 @@
|
||||
"prefs.quality_smoother": "Stärker geglättet",
|
||||
"prefs.quick_measure": "Schnellmessung in der Ecke der Ansicht",
|
||||
"prefs.quick_measure_note": "Zeigt unten rechts in der Ansicht die Größe der Auswahl an: bei einer Kante die Länge, bei einer kreisförmigen Kante zusätzlich Durchmesser (Ø) und Radius (R), bei einer Fläche den Flächeninhalt, bei einem Punkt die Position, bei zwei Elementen Abstand und Winkel sowie bei mehreren Kanten oder Flächen die Gesamtlänge bzw. Gesamtfläche. Ein Klick kopiert den Messwert.",
|
||||
"prefs.remember_dialog_values": "Befehlsdialogwerte merken",
|
||||
"prefs.remember_dialog_values.help": "Standardmäßig aus. Wenn aktiviert, werden nach erfolgreicher Ausführung eines Befehls seine sicheren Werte und Optionen beim nächsten Öffnen desselben Befehls wiederverwendet, bis SolidCraft geschlossen wird.",
|
||||
"prefs.render_previous_geometry_budget": "Bisheriges Geometriebudget für die Render-Vorschau verwenden",
|
||||
"prefs.render_previous_geometry_budget_help": "Stellt die bisherige feste Obergrenze für die Geometrie wieder her (512 MiB auf 64-Bit-Desktop-Systemen, andernfalls 128 MiB) anstelle des geräteabhängigen Budgets.",
|
||||
"prefs.reports_saved": "{n} gespeichert",
|
||||
@@ -3876,6 +3896,7 @@
|
||||
"render.stage_panels.extent": "Ausdehnung",
|
||||
"render.stage_panels.fit_save": "Einpassen und speichern",
|
||||
"render.stage_panels.gpu_required": "Benötigt GPU-Pfadverfolgung",
|
||||
"render.stage_panels.grain_angle": "Maserungswinkel",
|
||||
"render.stage_panels.keep": "Behalten",
|
||||
"render.stage_panels.kept": "Beibehalten",
|
||||
"render.stage_panels.kept_fields": "Kamera, Abtastung, Abmessungen, optische Bindungen, CAD-Geometrie und technische Eigenschaften.",
|
||||
@@ -3994,6 +4015,19 @@
|
||||
"render.subsurface_enable": "Random Walk für innere Streuung",
|
||||
"render.subsurface_help": "Nur für geschlossene Dielektrika. Streuung und RGB-Absorption sind getrennte Koeffizienten pro mm. Pfade streuen innerhalb der tatsächlichen Geometrie und werden dann an der Grenzfläche gebrochen oder reflektiert. Deaktivieren Sie die Transmission gerader Schattenstrahlen, das Entrauschen von Oberflächen und das äußere Volumen; verwenden Sie eine Fresnel-Schicht. Kein Diffusions-Shader, kein Dichtegitter und kein gemessenes Material. Dichte Medien können mehr Samples und Pfadtiefe benötigen.",
|
||||
"render.subsurface_rate": "Streukollisionsrate",
|
||||
"render.surface_import.author": "Autor",
|
||||
"render.surface_import.busy": "Bild wird importiert…",
|
||||
"render.surface_import.complete": "Wählen Sie ein Bild und geben Sie Autor, Quelle und Lizenz an.",
|
||||
"render.surface_import.embedded": "Importiertes Bild",
|
||||
"render.surface_import.failed": "Der Bildimport wurde nicht abgeschlossen.",
|
||||
"render.surface_import.license": "Lizenz / Eigentum",
|
||||
"render.surface_import.raytrace_required": "Bilder erfordern CPU- oder GPU-Raytracing. Wählen Sie es unter Ausgabe.",
|
||||
"render.surface_import.repeat_u": "Wiederholung U",
|
||||
"render.surface_import.repeat_v": "Wiederholung V",
|
||||
"render.surface_import.save": "Bild am Körper speichern",
|
||||
"render.surface_import.saved_cpu": "Speichern Sie Texturen für das CPU-Rendering.",
|
||||
"render.surface_import.source": "Quelle",
|
||||
"render.surface_import.title": "Bild importieren",
|
||||
"render.tone_aces_fitted": "ACES angepasst",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "GPU-Glas unterstützt zertifizierte getrennte Medien. Unbeschränkt verschachtelte Medien und ein rauschfreier Transmissionsschätzer werden nicht bereitgestellt.",
|
||||
@@ -4070,6 +4104,8 @@
|
||||
"sample.plate.name": "Beispielplatte",
|
||||
"sample.vents.about": "Langlöcher in einem Rechteckmuster: Zeilen, Spalten und Abstand sind Parameter",
|
||||
"sample.vents.name": "Lüftungsplatte",
|
||||
"scale.per_axis": "Je Achse",
|
||||
"scale.uniform": "Gleichmäßig",
|
||||
"search.command_keys": "Eingabe ausführen Umschalt+Eingabe ausführen und offen lassen Esc schließen",
|
||||
"search.commands": "Befehle",
|
||||
"search.design": "In diesem Design",
|
||||
@@ -4712,6 +4748,7 @@
|
||||
"ui.click_selects_whole_bodies": "Klick wählt ganze Körper aus",
|
||||
"ui.click_then_press_key": "Klicken, dann die neue Taste drücken",
|
||||
"ui.close": "Schließen",
|
||||
"ui.close_tab": "Tab schließen",
|
||||
"ui.coil": "Schraube",
|
||||
"ui.coincident": "Koinzident",
|
||||
"ui.collapse": "Einklappen",
|
||||
@@ -4927,6 +4964,11 @@
|
||||
"ui.factor": "Faktor",
|
||||
"ui.fade": "Auslauf",
|
||||
"ui.fade_length": "Auslauflänge",
|
||||
"ui.fair_iterations": "Iterationen",
|
||||
"ui.fair_keep_edges": "Kanten behalten",
|
||||
"ui.fair_strength": "Stärke",
|
||||
"ui.fair_surface_title": "FLÄCHENBEGRADIGUNG",
|
||||
"ui.fair_tolerance_default": "0,001 der Körpergröße",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
"ui.feature_options": "Funktionsoptionen",
|
||||
"ui.features": "Funktionen",
|
||||
@@ -5109,6 +5151,7 @@
|
||||
"ui.inspect": "Prüfen",
|
||||
"ui.instances": "Instanzen",
|
||||
"ui.intersect": "Schnitt",
|
||||
"ui.intersection_curve_second_pick": "Schnittkurve: zweite Fläche oder Kurve anklicken (Rechtsklick zum Neubeginn)",
|
||||
"ui.isolate": "Isolieren",
|
||||
"ui.join": "Verbinden",
|
||||
"ui.joint_play": "▶ Abspielen",
|
||||
@@ -5136,6 +5179,7 @@
|
||||
"ui.manipulator_free_alt": "Freies Ziehen des Manipulators auch mit Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "An: Alt/Option oder Strg/Cmd auf Extrusions-/Gizmo-Pfeilen deaktiviert das Einrasten von Werten. Aus: nur Strg/Cmd.",
|
||||
"ui.manufacture": "Fertigung",
|
||||
"ui.match_surface_title": "FLÄCHENANPASSUNG",
|
||||
"ui.material_review.axes": "Materiallokale Achsen; keine Körperausrichtung zugewiesen.",
|
||||
"ui.material_review.claims": "Quellen und Lizenzen sind ungeprüfte Angaben. Fehlende Werte bleiben unbekannt.",
|
||||
"ui.material_review.compressive_strength": "Druckfestigkeit",
|
||||
@@ -5158,6 +5202,9 @@
|
||||
"ui.material_review.title": "Physikalische Materialdaten prüfen",
|
||||
"ui.material_review.unknown": "Unbekannt",
|
||||
"ui.material_review.yield_strength": "Streckgrenze",
|
||||
"ui.mcp_agents": "Agenten über MCP",
|
||||
"ui.mcp_compact": "Kompakte Ausgabe",
|
||||
"ui.mcp_compact_note": "Für Agenten, die über den MCP-Server (solidcraft-cli mcp) verbunden sind: inspect_design fasst Skizzen zusammen und Werkzeugergebnisse werden nicht formatiert, sodass sie weniger Tokens verbrauchen. Gilt für Agentensitzungen, die nach der Änderung beginnen; --compact oder SOLIDCRAFT_MCP_COMPACT=1 schalten es ebenfalls ein.",
|
||||
"ui.measure": "Messen",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF mit Flächenmarken",
|
||||
@@ -5199,6 +5246,7 @@
|
||||
"ui.options": "Optionen",
|
||||
"ui.orbit": "Drehen",
|
||||
"ui.orbit_around": "Umkreisen um",
|
||||
"ui.organize": "Organisieren",
|
||||
"ui.orientation": "Ausrichtung",
|
||||
"ui.origin": "Ursprung",
|
||||
"ui.palette_construction_mode": "Konstruktionsmodus (X)",
|
||||
@@ -5243,6 +5291,7 @@
|
||||
"ui.point_cloud_add_filter": "Filter hinzufügen…",
|
||||
"ui.point_cloud_auto_range": "Bereich automatisch",
|
||||
"ui.point_cloud_color_by": "Färben nach",
|
||||
"ui.point_cloud_dots": "Einzelpunkte",
|
||||
"ui.point_cloud_file_colors": "Farben der Datei",
|
||||
"ui.point_cloud_filter_by": "Filtern nach",
|
||||
"ui.point_cloud_height_high": "Oberste Höhe",
|
||||
|
||||
@@ -235,6 +235,8 @@
|
||||
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
|
||||
"render.studio_center": "Centre X, Y, Z (mm)",
|
||||
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
|
||||
"render.surface_import.complete": "Choose an image and enter its author, source and licence.",
|
||||
"render.surface_import.license": "Licence / ownership",
|
||||
"sketch.hint.arc.center_point": "Arc: click centre and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.circle.center": "Circle: click the centre, then a point on the circle",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to centre the second on the first",
|
||||
@@ -266,6 +268,7 @@
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
|
||||
"ui.organize": "Organise",
|
||||
"ui.point_cloud_color_by": "Colour By",
|
||||
"ui.point_cloud_file_colors": "File Colours",
|
||||
"ui.point_cloud_no_file_colors": "The file has no colours",
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "Suppress Selected Folders",
|
||||
"Suppressed": "Suppressed",
|
||||
"Surface": "Surface",
|
||||
"Surface or face": "Surface or face",
|
||||
"Surface or faces": "Surface or faces",
|
||||
"Surface, or faces to turn over": "Surface, or faces to turn over",
|
||||
"Surfaces": "Surfaces",
|
||||
"Swap Ends": "Swap Ends",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "Target",
|
||||
"Target Body": "Target Body",
|
||||
"Target Face (optional)": "Target Face (optional)",
|
||||
"Target edges": "Target edges",
|
||||
"Tear": "Tear",
|
||||
"Technical details": "Technical details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "The direction must be an axis or a straight sketch curve that lies in the sketch plane",
|
||||
"The mirror line cannot also be mirrored": "The mirror line cannot also be mirrored",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "The mirror line must be a straight curve of this sketch, not an origin axis",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "The origin axis is perpendicular to the sketch plane, so it has no line in the plane",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "The saved author occurrence is missing. Select a valid author placement before rebinding.",
|
||||
"Then": "Then",
|
||||
"These features will be deleted:": "These features will be deleted:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "a picked edge's body is gone",
|
||||
"add a key for a joint first": "add a key for a joint first",
|
||||
"ai.connected_toast": "SolidCraft is now available in {clients}. Restart them to use it (Preferences > Assistant).",
|
||||
"ai.http.allow": "Allow insecure HTTP",
|
||||
"ai.http.confirm": "Allow for this endpoint",
|
||||
"ai.http.warning": "The API key and conversation will be sent unencrypted. Anyone with access to the network may read or change them. Use this only on a network you trust.",
|
||||
"ai.prefs.allow_control": "Let AI agents connect",
|
||||
"ai.prefs.allow_control_note": "A local channel (MCP). Nothing leaves this computer.\n\nOpens a control channel on this computer only (127.0.0.1, port 27878 or the next free one). A program that connects must also show a secret token that only your user account can read, made fresh at every start. Takes effect the next time SolidCraft starts. Off: nothing listens unless you start SolidCraft with --control.",
|
||||
"ai.prefs.availability": "Availability",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "Follow the camera",
|
||||
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
|
||||
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
|
||||
"ai.prefs.instructions": "Custom instructions",
|
||||
"ai.prefs.instructions_note": "Sent to the agent at the start of every chat, after SolidCraft's own instructions: how you like to work, rules to keep to. A change applies from the next chat (Connect or New chat).",
|
||||
"ai.prefs.intro": "AI agents in SolidCraft.",
|
||||
"ai.prefs.make_default": "Make default",
|
||||
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "Move Render Comparison Divider",
|
||||
"cmd.render.result.discard": "Discard Render Result",
|
||||
"cmd.render.result.select": "Select Render Result",
|
||||
"cmd.render.surface.import": "Import Surface Maps",
|
||||
"cmd.render.texture.list": "List Embedded Surface Images",
|
||||
"cmd.render.texture.remove": "Remove Embedded Surface Image",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_backends": "Render Backends",
|
||||
"cmd.render_batch": "Render All Shots",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "Previous Tab",
|
||||
"cmd.view_rename": "Rename Named View",
|
||||
"cmd.view_render_frame": "Render Frame Guides",
|
||||
"cmd.view_ribbon_tabs_only": "Ribbon Tabs Only",
|
||||
"cmd.view_right": "Right View",
|
||||
"cmd.view_save": "New Named View",
|
||||
"cmd.view_shots_strip": "Shots Strip",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "The file chooser that opened is not the desktop's own: under XWayland the portal refuses the parent window.",
|
||||
"file_load.built": "Built so far: {built} of {total} features.",
|
||||
"file_load.converting": "Converting solid {done} of {total}",
|
||||
"file_load.inserted": "Inserted {name}",
|
||||
"file_load.inserting": "Inserting geometry",
|
||||
"file_load.link_item": "{name} ({done} of {total})",
|
||||
"file_load.opened": "Opened {name}",
|
||||
"file_load.opened_in_tab": "Opened {name} in a new tab",
|
||||
"file_load.opening": "Opening design",
|
||||
"file_load.preparing_thumbnail": "Preparing the thumbnail...",
|
||||
"file_load.preparing_view": "Preparing the view...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "Reads the paths of an SVG drawing into a sketch, scaled, turned and placed where you set. Use it to turn a logo or vector outline into curves you can extrude.",
|
||||
"help.sketch.inspect": "Opens a report on the open sketch: how many degrees of freedom are left and whether it is fully constrained. Use it to find out what still moves before you finish the sketch.",
|
||||
"help.sketch.intersect": "Adds the curves or points where bodies, faces, edges or other sketches cross the sketch plane. They stay linked and follow the model when it changes.",
|
||||
"help.sketch.intersection_curve": "Creates 3D curves where two bodies or faces meet, linked to them. Use them as paths or guide rails that follow the model.",
|
||||
"help.sketch.intersection_curve": "Creates 3D curves where two bodies, faces or curves of other sketches meet, linked to them. Use them as paths or guide rails that follow the model.",
|
||||
"help.sketch.isoparametric_curve": "Creates a 3D curve along a face's U or V direction through a point you pick, linked to the face. Use it as a path or split line that follows the surface.",
|
||||
"help.sketch.line": "Draws connected lines point by point, as an open chain or a closed outline. It is the basic tool for most profiles.",
|
||||
"help.sketch.line.midpoint": "Draws a line from its midpoint out to one end, with the other end mirrored. Use it for lines centred on a point, such as a symmetric edge.",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "Turns, pans or zooms the view by steps, such as a quarter turn left. Use it to move the camera precisely instead of dragging.",
|
||||
"help.view.navigation_bar": "Shows or hides the navigation bar at the bottom of the viewport. Hide it for more room, show it to reach the view tools by mouse.",
|
||||
"help.view.rename": "Gives a saved named view a new name. The view itself stays the same.",
|
||||
"help.view.ribbon_tabs_only": "Folds the ribbon to its tab row, so the view gets the room its panels took. Click a tab to show its panels over the view; choose this again to keep the whole ribbon shown.",
|
||||
"help.view.save": "Saves the current camera position as a named view in the design. Use it to come back to the same angle later, for example for screenshots.",
|
||||
"help.view.spin": "Orbits the view around the model continuously until the next click or key. Use it to present a model from all sides.",
|
||||
"help.view.tool": "Latches a camera tool, orbit, pan or zoom, so that dragging with the left mouse button moves the view until you switch it off. Use it on a trackpad or without a middle mouse button.",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "Previous",
|
||||
"prefs.choose_font": "Choose font file…",
|
||||
"prefs.click": "Click",
|
||||
"prefs.command_dialog_values": "Command dialogs",
|
||||
"prefs.contact_shading": "Soft contact shading",
|
||||
"prefs.contact_shading_note": "Soft shading where parts come close.",
|
||||
"prefs.context_menu": "Context menu",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "Surfaces and edges",
|
||||
"prefs.graphics_page.view_bar": "Also in the view bar",
|
||||
"prefs.graphics_presets_note": "Presets set the choices below together.",
|
||||
"prefs.ground_shadow_follows_model": "Ground shadow follows the model",
|
||||
"prefs.ground_shadow_follows_model_help": "Place the shadow below the lowest visible body. When off, raised models keep their shadow on the origin ground plane.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "Hide constraint glyphs when zoomed out",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "Constraint glyphs leave the view when the sketch is zoomed out far enough that they would clutter it. Off: they stay at a fixed screen size at every zoom.",
|
||||
"prefs.history": "History",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "Card",
|
||||
"prefs.layout_page.column": "Column",
|
||||
"prefs.layout_page.hover": "Hover",
|
||||
"prefs.layout_page.hover_panel_menus": "Switch panel menus on hover",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Click a ribbon panel name to open its menu. Hover another panel name to switch. Move outside the ribbon and open menu to close it.",
|
||||
"prefs.layout_page.icons_labels": "Icons + labels",
|
||||
"prefs.layout_page.ok_cancel": "OK and Cancel",
|
||||
"prefs.layout_page.on_hover": "On hover",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "smoother",
|
||||
"prefs.quick_measure": "Quick measurement in the view's corner",
|
||||
"prefs.quick_measure_note": "The selection's size in the view's bottom right corner: an edge's length, or Ø and R for a round one, a face's area, a point's position, two items' distance and angle, several edges' or faces' total. A click copies it.",
|
||||
"prefs.remember_dialog_values": "Remember command dialog values",
|
||||
"prefs.remember_dialog_values.help": "Off by default. When on, after a command succeeds, its safe values and options are reused the next time you open the same command, until SolidCraft closes.",
|
||||
"prefs.render_previous_geometry_budget": "Use previous Render preview geometry budget",
|
||||
"prefs.render_previous_geometry_budget_help": "Restore the previous fixed geometry limit (512 MiB on 64-bit desktop, 128 MiB elsewhere) instead of the device-based budget.",
|
||||
"prefs.reports_saved": "{n} saved",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "A click on the lit ribbon button of the sketch tool in use ends the tool, so clicks in the view select again. Off: the click starts the tool again.",
|
||||
"prefs.ribbon_reveal": "Show search results in the ribbon",
|
||||
"prefs.ribbon_reveal_note": "Hovering a result in Search commands shows where the command sits in the ribbon: its tab, its button or its row in a panel's drop-down. A command started from the search blinks there, and a running command from a drop-down marks its panel.",
|
||||
"prefs.ribbon_tabs_only": "Show only the ribbon's tabs",
|
||||
"prefs.ribbon_tabs_only_note": "The ribbon folds to its tab row. Clicking a tab shows its panels over the view until a command starts or the pointer leaves them. Ribbon Tabs Only in the View tab, or its shortcut, switches this.",
|
||||
"prefs.right_click": "Right click",
|
||||
"prefs.saving": "Start and save",
|
||||
"prefs.scale_note": "Also Ctrl + / Ctrl − in the window. Buttons are 28 px at 100%.",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "Extent",
|
||||
"render.stage_panels.fit_save": "Fit and save",
|
||||
"render.stage_panels.gpu_required": "Needs GPU path tracing",
|
||||
"render.stage_panels.grain_angle": "Grain angle",
|
||||
"render.stage_panels.keep": "Keep",
|
||||
"render.stage_panels.kept": "Kept",
|
||||
"render.stage_panels.kept_fields": "Camera, sampling, dimensions, optical bindings, CAD geometry and engineering properties.",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "Interior scattering random walk",
|
||||
"render.subsurface_help": "Closed dielectric only. Scattering and RGB absorption are separate per-mm coefficients. Paths scatter inside actual geometry, then refract or reflect at the boundary. Disable straight shadow transmission, surface denoising and exterior volume; use one Fresnel stratum. Not a diffusion shader, density grid or measured material. Dense media may need more samples and path depth.",
|
||||
"render.subsurface_rate": "Scattering collision rate",
|
||||
"render.surface_import.author": "Author",
|
||||
"render.surface_import.busy": "Importing image…",
|
||||
"render.surface_import.complete": "Choose an image and enter its author, source and license.",
|
||||
"render.surface_import.embedded": "Imported image",
|
||||
"render.surface_import.failed": "The image import did not finish.",
|
||||
"render.surface_import.license": "License / ownership",
|
||||
"render.surface_import.raytrace_required": "Images require CPU or GPU ray tracing. Choose it in Output.",
|
||||
"render.surface_import.repeat_u": "Repeat U",
|
||||
"render.surface_import.repeat_v": "Repeat V",
|
||||
"render.surface_import.save": "Save image on body",
|
||||
"render.surface_import.saved_cpu": "Save maps to use them with CPU rendering.",
|
||||
"render.surface_import.source": "Source",
|
||||
"render.surface_import.title": "Import image",
|
||||
"render.tone_aces_fitted": "ACES fitted",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "GPU glass supports certified disjoint media. Unrestricted nested media and a noise-free transmission estimator are not provided.",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "Sample Plate",
|
||||
"sample.vents.about": "Slots in a rectangular pattern: rows, columns and pitch are parameters",
|
||||
"sample.vents.name": "Vent Plate",
|
||||
"scale.per_axis": "Per axis",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter run Shift+Enter run and keep open Esc close",
|
||||
"search.commands": "Commands",
|
||||
"search.design": "In this design",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "Click selects whole bodies",
|
||||
"ui.click_then_press_key": "Click, then press the new key",
|
||||
"ui.close": "Close",
|
||||
"ui.close_tab": "Close tab",
|
||||
"ui.coil": "Coil",
|
||||
"ui.coincident": "Coincident",
|
||||
"ui.collapse": "Collapse",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "Factor",
|
||||
"ui.fade": "Fade",
|
||||
"ui.fade_length": "Fade Length",
|
||||
"ui.fair_iterations": "Iterations",
|
||||
"ui.fair_keep_edges": "Keep edges",
|
||||
"ui.fair_strength": "Strength",
|
||||
"ui.fair_surface_title": "FAIR SURFACE",
|
||||
"ui.fair_tolerance_default": "0.001 of body size",
|
||||
"ui.feature_history": "Feature history",
|
||||
"ui.feature_options": "Feature Options",
|
||||
"ui.features": "Features",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "Inspect",
|
||||
"ui.instances": "Instances",
|
||||
"ui.intersect": "Intersect",
|
||||
"ui.intersection_curve_second_pick": "Intersection curve: click the second face or curve (right-click to start over)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "Isolate",
|
||||
"ui.join": "Join",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "Manipulator free drag also on Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "On: Alt/Option or Ctrl/Cmd on extrude / gizmo arrows disables value snapping. Off: Ctrl/Cmd only.",
|
||||
"ui.manufacture": "Manufacture",
|
||||
"ui.match_surface_title": "MATCH SURFACE",
|
||||
"ui.material_review.axes": "Material-local axes; no body orientation is assigned.",
|
||||
"ui.material_review.claims": "Sources and licenses are unverified claims. Missing values remain unknown.",
|
||||
"ui.material_review.compressive_strength": "Compressive strength",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "Review Physical Material Data",
|
||||
"ui.material_review.unknown": "Unknown",
|
||||
"ui.material_review.yield_strength": "Yield strength",
|
||||
"ui.mcp_agents": "Agents through MCP",
|
||||
"ui.mcp_compact": "Compact output",
|
||||
"ui.mcp_compact_note": "For agents connected through the MCP server (solidcraft-cli mcp): inspect_design summarises sketches and tool results are not pretty-printed, so they use fewer tokens. Applies to agent sessions started after the change; --compact or SOLIDCRAFT_MCP_COMPACT=1 also turn it on.",
|
||||
"ui.measure": "Measure",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF with face tags",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "Options",
|
||||
"ui.orbit": "Orbit",
|
||||
"ui.orbit_around": "Orbit around",
|
||||
"ui.organize": "Organize",
|
||||
"ui.orientation": "Orientation",
|
||||
"ui.origin": "Origin",
|
||||
"ui.palette_construction_mode": "Construction mode (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "Add filter…",
|
||||
"ui.point_cloud_auto_range": "Automatic Range",
|
||||
"ui.point_cloud_color_by": "Color By",
|
||||
"ui.point_cloud_dots": "Dots",
|
||||
"ui.point_cloud_file_colors": "File Colors",
|
||||
"ui.point_cloud_filter_by": "Filter by",
|
||||
"ui.point_cloud_height_high": "Highest",
|
||||
|
||||
@@ -4079,6 +4079,8 @@
|
||||
"sample.plate.name": "Placa de ejemplo",
|
||||
"sample.vents.about": "Ranuras en un patrón rectangular: las filas, las columnas y el paso son parámetros",
|
||||
"sample.vents.name": "Placa de ventilación",
|
||||
"scale.per_axis": "Por eje",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Intro ejecutar Mayús+Intro ejecutar y mantener abierto Esc cerrar",
|
||||
"search.commands": "Comandos",
|
||||
"search.design": "En este diseño",
|
||||
@@ -4750,6 +4752,7 @@
|
||||
"ui.click_selects_whole_bodies": "Un clic selecciona cuerpos completos",
|
||||
"ui.click_then_press_key": "Haz clic y pulsa la nueva tecla",
|
||||
"ui.close": "Cerrar",
|
||||
"ui.close_tab": "Cerrar pestaña",
|
||||
"ui.coil": "Hélice",
|
||||
"ui.coincident": "Coincidente",
|
||||
"ui.collapse": "Contraer",
|
||||
|
||||
@@ -4077,6 +4077,8 @@
|
||||
"sample.plate.name": "Esimerkkilevy",
|
||||
"sample.vents.about": "Urat suorakulmaisessa kuviossa: rivit, sarakkeet ja jako ovat parametreja",
|
||||
"sample.vents.name": "Tuuletuslevy",
|
||||
"scale.per_axis": "Akseleittain",
|
||||
"scale.uniform": "Tasainen",
|
||||
"search.command_keys": "Enter suorita Shift+Enter suorita ja pidä auki Esc sulje",
|
||||
"search.commands": "Komennot",
|
||||
"search.design": "Tässä mallissa",
|
||||
|
||||
@@ -1624,7 +1624,7 @@
|
||||
"help.sketch.insert_svg": "Lit les tracés d'un dessin SVG dans une esquisse, mis à l'échelle, tournés et placés comme vous le définissez. À utiliser pour transformer un logo ou un contour vectoriel en courbes que vous pouvez extruder.",
|
||||
"help.sketch.inspect": "Ouvre un rapport sur l'esquisse ouverte : combien de degrés de liberté il reste et si elle est entièrement contrainte. À utiliser pour savoir ce qui bouge encore avant de terminer l'esquisse.",
|
||||
"help.sketch.intersect": "Ajoute les courbes ou points où des corps, faces, arêtes ou d'autres esquisses traversent le plan d'esquisse. Ils restent liés et suivent le modèle lorsqu'il change.",
|
||||
"help.sketch.intersection_curve": "Crée des courbes 3D là où deux corps ou faces se rencontrent, liées à eux. À utiliser comme trajectoires ou rails guides qui suivent le modèle.",
|
||||
"help.sketch.intersection_curve": "Crée des courbes 3D là où deux corps, faces ou courbes d'autres esquisses se rencontrent, liées à eux. À utiliser comme trajectoires ou rails guides qui suivent le modèle.",
|
||||
"help.sketch.isoparametric_curve": "Crée une courbe 3D le long de la direction U ou V d'une face, par un point que vous choisissez, liée à la face. À utiliser comme trajectoire ou ligne de partage qui suit la surface.",
|
||||
"help.sketch.line": "Dessine des lignes connectées point par point, en chaîne ouverte ou en contour fermé. C'est l'outil de base de la plupart des profils.",
|
||||
"help.sketch.line.midpoint": "Dessine une ligne de son milieu vers une extrémité, l'autre extrémité étant son symétrique. À utiliser pour des lignes centrées sur un point, comme une arête symétrique.",
|
||||
@@ -3814,6 +3814,8 @@
|
||||
"sample.plate.name": "Plaque d'exemple",
|
||||
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.name": "Plaque d'aération",
|
||||
"scale.per_axis": "Par axe",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
|
||||
"search.commands": "Commandes",
|
||||
"search.design": "Dans cette conception",
|
||||
@@ -4443,6 +4445,7 @@
|
||||
"ui.click_selects_whole_bodies": "Un clic sélectionne des corps entiers",
|
||||
"ui.click_then_press_key": "Cliquer, puis appuyer sur la nouvelle touche",
|
||||
"ui.close": "Fermer",
|
||||
"ui.close_tab": "Fermer l’onglet",
|
||||
"ui.coil": "Hélice",
|
||||
"ui.coincident": "Coïncidence",
|
||||
"ui.collapse": "Réduire",
|
||||
@@ -4926,6 +4929,7 @@
|
||||
"ui.point_cloud_add_filter": "Ajouter un filtre…",
|
||||
"ui.point_cloud_auto_range": "Plage automatique",
|
||||
"ui.point_cloud_color_by": "Colorer par",
|
||||
"ui.point_cloud_dots": "Points isolés",
|
||||
"ui.point_cloud_file_colors": "Couleurs du fichier",
|
||||
"ui.point_cloud_filter_by": "Filtrer par",
|
||||
"ui.point_cloud_height_high": "Plus haut",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Le os trazados dun debuxo SVG nun bosquexo, escalados, xirados e colocados onde indiques. Úsao para converter un logotipo ou un contorno vectorial en curvas que podes extruír.",
|
||||
"help.sketch.inspect": "Abre un informe do bosquexo aberto: cantos graos de liberdade quedan e se está totalmente restrinxido. Úsao para saber o que aínda se move antes de rematar o bosquexo.",
|
||||
"help.sketch.intersect": "Engade as curvas ou os puntos onde corpos, caras, arestas ou outros bosquexos cruzan o plano do bosquexo. Quedan ligados e seguen o modelo cando cambia.",
|
||||
"help.sketch.intersection_curve": "Crea curvas 3D onde se atopan dous corpos ou caras, ligadas a eles. Úsaas como traxectorias ou guías que seguen o modelo.",
|
||||
"help.sketch.intersection_curve": "Crea curvas 3D onde se atopan dous corpos, caras ou curvas doutros bosquexos, ligadas a eles. Úsaas como traxectorias ou guías que seguen o modelo.",
|
||||
"help.sketch.isoparametric_curve": "Crea unha curva 3D na dirección U ou V dunha cara por un punto que escollas, ligada á cara. Úsaa como traxectoria ou liña de división que segue a superficie.",
|
||||
"help.sketch.line": "Debuxa liñas conectadas punto a punto, como cadea aberta ou como contorno pechado. É a ferramenta básica para a maioría dos perfís.",
|
||||
"help.sketch.line.midpoint": "Debuxa unha liña desde o seu punto medio ata un extremo, co outro extremo reflectido. Úsao para liñas centradas nun punto, como unha aresta simétrica.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Placa de exemplo",
|
||||
"sample.vents.about": "Ranuras nun patrón rectangular: filas, columnas e paso son parámetros",
|
||||
"sample.vents.name": "Placa de ventilación",
|
||||
"scale.per_axis": "Por eixo",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Intro executar Maiús+Intro executar e manter aberto Esc pechar",
|
||||
"search.commands": "Ordes",
|
||||
"search.design": "Neste deseño",
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "השבתת התיקיות שנבחרו",
|
||||
"Suppressed": "מושבת",
|
||||
"Surface": "משטח",
|
||||
"Surface or face": "משטח או פאה",
|
||||
"Surface or faces": "משטח או פאות",
|
||||
"Surface, or faces to turn over": "משטח, או פאות להיפוך",
|
||||
"Surfaces": "משטחים",
|
||||
"Swap Ends": "החלפת קצוות",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "יעד",
|
||||
"Target Body": "גוף יעד",
|
||||
"Target Face (optional)": "פאת יעד (אופציונלי)",
|
||||
"Target edges": "מקצועות יעד",
|
||||
"Tear": "קריעה",
|
||||
"Technical details": "פרטים טכניים",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "הסקיצה המקווקוות מסמנת מישור שמור. יש לבחור פאה חדשה כדי לתקן את ההצמדה.",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "הכיוון חייב להיות ציר או עקומת סקיצה ישרה הנמצאת במישור הסקיצה",
|
||||
"The mirror line cannot also be mirrored": "אי אפשר לשקף גם את קו השיקוף עצמו",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "קו השיקוף חייב להיות עקומה ישרה של הסקיצה הזו, ולא אחד מצירי הראשית",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "ציר הראשית ניצב למישור הסקיצה, ולכן אין לו קו במישור",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "מופע היצירה השמור חסר. יש לבחור מיקום יצירה תקין לפני הקישור מחדש.",
|
||||
"Then": "אז",
|
||||
"These features will be deleted:": "הפיצ'רים הבאים יימחקו:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "הגוף של המקצוע שנבחר כבר לא קיים",
|
||||
"add a key for a joint first": "יש להוסיף תחילה מפתח למפרק",
|
||||
"ai.connected_toast": "SolidCraft זמין עכשיו ב־{clients}. יש להפעיל אותם מחדש כדי להשתמש בו (הגדרות > עוזר).",
|
||||
"ai.http.allow": "אפשר HTTP לא מאובטח",
|
||||
"ai.http.confirm": "אפשר עבור נקודת קצה זו",
|
||||
"ai.http.warning": "מפתח ה־API והשיחה יישלחו ללא הצפנה. כל מי שיש לו גישה לרשת יכול לקרוא או לשנות אותם. יש להשתמש באפשרות זו רק ברשת מהימנה.",
|
||||
"ai.prefs.allow_control": "לאפשר לסוכני AI להתחבר",
|
||||
"ai.prefs.allow_control_note": "ערוץ מקומי (MCP). שום דבר לא יוצא מהמחשב הזה.\n\nפותח ערוץ בקרה במחשב זה בלבד (127.0.0.1, פורט 27878 או הפנוי הבא). תוכנה שמתחברת צריכה גם להציג אסימון סודי שרק חשבון המשתמש שלך יכול לקרוא, ונוצר מחדש בכל הפעלה. ייכנס לתוקף בהפעלה הבאה של SolidCraft. כבוי: שום דבר אינו מאזין, אלא אם SolidCraft מופעל עם --control.",
|
||||
"ai.prefs.availability": "זמינות",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "לעקוב אחר המצלמה",
|
||||
"ai.prefs.follow_camera_off": "מעקב המצלמה האוטומטי כבוי.",
|
||||
"ai.prefs.follow_camera_on": "המצלמה פונה אל הסקיצות שהסוכן יוצר.",
|
||||
"ai.prefs.instructions": "הוראות מותאמות אישית",
|
||||
"ai.prefs.instructions_note": "נשלחות לסוכן בתחילת כל צ'אט, אחרי ההוראות של SolidCraft עצמה: דרך העבודה המועדפת עליך וכללים שיש להקפיד עליהם. שינוי חל מהצ'אט הבא (התחברות או צ'אט חדש).",
|
||||
"ai.prefs.intro": "סוכני AI ב־SolidCraft.",
|
||||
"ai.prefs.make_default": "לקבוע כברירת מחדל",
|
||||
"ai.prefs.no_agents": "אין עדיין סוכנים. אפשר להוסיף סוכן או לרענן כדי למצוא סוכנים מותקנים.",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "הזז את מחיצת ההשוואה של הרינדור",
|
||||
"cmd.render.result.discard": "השלך תוצאת רינדור",
|
||||
"cmd.render.result.select": "בחר תוצאת רינדור",
|
||||
"cmd.render.surface.import": "ייבוא מפות משטח",
|
||||
"cmd.render.texture.list": "הצגת תמונות משטח מוטמעות",
|
||||
"cmd.render.texture.remove": "הסרת תמונת משטח מוטמעת",
|
||||
"cmd.render_aging_describe": "הזדקנות משטח בשכבות",
|
||||
"cmd.render_backends": "מנועי הדמיה",
|
||||
"cmd.render_batch": "רינדור כל הצילומים",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "הכרטיסייה הקודמת",
|
||||
"cmd.view_rename": "שינוי שם תצוגה שמורה",
|
||||
"cmd.view_render_frame": "מדריכי מסגרת רינדור",
|
||||
"cmd.view_ribbon_tabs_only": "רק לשוניות הסרט",
|
||||
"cmd.view_right": "תצוגה ימנית",
|
||||
"cmd.view_save": "תצוגה שמורה חדשה",
|
||||
"cmd.view_shots_strip": "סרגל צילומים",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "בורר הקבצים שנפתח אינו של שולחן העבודה: תחת XWayland הפורטל דוחה את חלון האב.",
|
||||
"file_load.built": "נבנו עד כה: {built} מתוך {total} פיצ'רים.",
|
||||
"file_load.converting": "המרת גוף מוצק {done} מתוך {total}",
|
||||
"file_load.inserted": "{name} הוכנס",
|
||||
"file_load.inserting": "הוספת גאומטריה",
|
||||
"file_load.link_item": "{name} ({done} מתוך {total})",
|
||||
"file_load.opened": "{name} נפתח",
|
||||
"file_load.opened_in_tab": "{name} נפתח בלשונית חדשה",
|
||||
"file_load.opening": "פותח את התכן",
|
||||
"file_load.preparing_thumbnail": "מכין את התמונה הממוזערת...",
|
||||
"file_load.preparing_view": "מכין את התצוגה...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "קריאת הנתיבים של שרטוט SVG לתוך סקיצה, בקנה המידה, בסיבוב ובמיקום שנקבעו. כך הופכים לוגו או קו מתאר וקטורי לעקומות שניתן לשחל.",
|
||||
"help.sketch.inspect": "פותח דוח על הסקיצה הפתוחה: כמה דרגות חופש נותרו והאם היא מאולצת במלואה. השתמשו בו כדי לגלות מה עדיין זז לפני שמסיימים את הסקיצה.",
|
||||
"help.sketch.intersect": "מוסיף את העקומות או הנקודות שבהן גופים, פאות, קצוות או סקיצות אחרות חותכים את מישור הסקיצה. הן נשארות מקושרות ועוקבות אחר המודל כשהוא משתנה.",
|
||||
"help.sketch.intersection_curve": "יוצר עקומות תלת־ממדיות במקום שבו שני גופים או פאות נפגשים, מקושרות אליהם. מתאים למסלולים או למסילות הנחיה שעוקבים אחר המודל.",
|
||||
"help.sketch.intersection_curve": "יוצר עקומות תלת־ממדיות במקום שבו שני גופים, פאות או עקומות של סקיצות אחרות נפגשים, מקושרות אליהם. מתאים למסלולים או למסילות הנחיה שעוקבים אחר המודל.",
|
||||
"help.sketch.isoparametric_curve": "יוצר עקומה תלת־ממדית לאורך כיוון U או V של פאה דרך נקודה שבוחרים, מקושרת לפאה. מתאים למסלול או לקו פיצול שעוקבים אחר המשטח.",
|
||||
"help.sketch.line": "משרטט ישרים מחוברים נקודה אחר נקודה, כשרשרת פתוחה או כקו מתאר סגור. זה הכלי הבסיסי לרוב הפרופילים.",
|
||||
"help.sketch.line.midpoint": "משרטט ישר מנקודת האמצע שלו אל אחד הקצוות, כשהקצה השני משוקף. מתאים לישרים הממורכזים סביב נקודה, כמו קצה סימטרי.",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "סיבוב, הזזה או זום של התצוגה בצעדים, כמו רבע סיבוב שמאלה. מתאים להזזה מדויקת של המצלמה במקום גרירה.",
|
||||
"help.view.navigation_bar": "הצגה או הסתרה של סרגל הניווט בתחתית חלון התצוגה. אפשר להסתיר אותו לשטח נוסף, או להציג אותו כדי להגיע אל כלי התצוגה בעכבר.",
|
||||
"help.view.rename": "מתן שם חדש לתצוגה שמורה. התצוגה עצמה נשארת כפי שהיא.",
|
||||
"help.view.ribbon_tabs_only": "מקפל את הסרט לשורת הלשוניות שלו, כך שחלון התצוגה מקבל את המקום שתפסו הלוחות. לחיצה על לשונית מציגה את הלוחות שלה מעל התצוגה; בחירה חוזרת בפקודה משאירה את הסרט כולו מוצג.",
|
||||
"help.view.save": "שמירת מיקום המצלמה הנוכחי כתצוגה בשם בתכן. מתאים לחזרה לאותה זווית בהמשך, למשל לצילומי מסך.",
|
||||
"help.view.spin": "הקפת המודל בתצוגה באופן רציף עד ללחיצה או למקש הבא. מתאים להצגת מודל מכל הצדדים.",
|
||||
"help.view.tool": "נעילת כלי מצלמה, מסלול, הזזה או זום, כך שגרירה בכפתור השמאלי של העכבר מזיזה את התצוגה עד שמכבים אותו. מתאים למשטח מגע או כשאין כפתור עכבר אמצעי.",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "הקודם",
|
||||
"prefs.choose_font": "בחירת קובץ גופן…",
|
||||
"prefs.click": "לחיצה",
|
||||
"prefs.command_dialog_values": "תיבות דו-שיח של פקודות",
|
||||
"prefs.contact_shading": "הצללת מגע רכה",
|
||||
"prefs.contact_shading_note": "הצללה רכה במקומות שבהם חלקים מתקרבים.",
|
||||
"prefs.context_menu": "תפריט הקשר",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "משטחים וקצוות",
|
||||
"prefs.graphics_page.view_bar": "גם בסרגל התצוגה",
|
||||
"prefs.graphics_presets_note": "הגדרות קבועות מראש קובעות יחד את הבחירות שלמטה.",
|
||||
"prefs.ground_shadow_follows_model": "צל הקרקע עוקב אחר המודל",
|
||||
"prefs.ground_shadow_follows_model_help": "מקם את הצל מתחת לגוף הגלוי הנמוך ביותר. כשהאפשרות כבויה, הצל של מודלים מוגבהים נשאר במישור הקרקע העובר בראשית הצירים.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "הסתרת סמלי אילוצים בהקטנה",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "סמלי האילוצים נעלמים כשהשרטוט מוקטן מספיק כדי שלא יעמיסו על התצוגה. כבוי: הם נשארים בגודל מסך קבוע בכל זום.",
|
||||
"prefs.history": "ציר הזמן",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "כרטיס",
|
||||
"prefs.layout_page.column": "עמודה",
|
||||
"prefs.layout_page.hover": "מעבר עכבר",
|
||||
"prefs.layout_page.hover_panel_menus": "מעבר בין תפריטי לוחות בריחוף",
|
||||
"prefs.layout_page.hover_panel_menus_help": "לחצו על שם לוח ברצועת הכלים כדי לפתוח את התפריט שלו. העבירו את הסמן מעל שם של לוח אחר כדי לעבור לתפריט שלו. הזיזו את הסמן אל מחוץ לרצועת הכלים ולתפריט הפתוח כדי לסגור אותו.",
|
||||
"prefs.layout_page.icons_labels": "סמלים + תוויות",
|
||||
"prefs.layout_page.ok_cancel": "אישור וביטול",
|
||||
"prefs.layout_page.on_hover": "בעת מעבר עכבר",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "חלק יותר",
|
||||
"prefs.quick_measure": "מדידה מהירה בפינת התצוגה",
|
||||
"prefs.quick_measure_note": "מציג את מידות הבחירה בפינה הימנית התחתונה של התצוגה, מעל ההיסטוריה: אורך מקצוע (או Ø ו-R למקצוע עגול), שטח פאה, מיקום נקודה, המרחק והזווית בין שני פריטים, והסכום של כמה מקצועות או פאות. לחיצה מעתיקה אותו.",
|
||||
"prefs.remember_dialog_values": "זכור ערכים בתיבות דו-שיח של פקודות",
|
||||
"prefs.remember_dialog_values.help": "כבוי כברירת מחדל. כשהוא פועל, לאחר הצלחת פקודה, הערכים והאפשרויות הבטוחים שלה ישמשו שוב בפעם הבאה שתפתח את אותה פקודה, עד לסגירת SolidCraft.",
|
||||
"prefs.render_previous_geometry_budget": "שימוש בתקציב הגאומטריה הקודם לתצוגה מקדימה של רינדור",
|
||||
"prefs.render_previous_geometry_budget_help": "שחזור מגבלת הגאומטריה הקבועה הקודמת (512 MiB במחשב שולחני של 64 סיביות, 128 MiB בפלטפורמות אחרות) במקום התקציב המבוסס על ההתקן.",
|
||||
"prefs.reports_saved": "{n} שמורים",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "לחיצה על לחצן הסרט המודגש של כלי השרטוט שבשימוש מסיימת את הכלי, כך שלחיצות בתצוגה בוחרות שוב. כבוי: הלחיצה מפעילה את הכלי מחדש.",
|
||||
"prefs.ribbon_reveal": "הצגת תוצאות חיפוש בסרט",
|
||||
"prefs.ribbon_reveal_note": "מעבר עם הסמן מעל תוצאה בחיפוש פקודות מראה היכן הפקודה נמצאת בסרט: הלשונית, הכפתור או השורה שלה בתפריט הנפתח של לוח. פקודה שהופעלה מהחיפוש מהבהבת שם, ופקודה פעילה מתפריט נפתח מסמנת את הלוח שלה.",
|
||||
"prefs.ribbon_tabs_only": "הצגת לשוניות הסרט בלבד",
|
||||
"prefs.ribbon_tabs_only_note": "הסרט מתקפל לשורת הלשוניות שלו. לחיצה על לשונית מציגה את הלוחות שלה מעל התצוגה עד שפקודה מתחילה או שהמצביע עוזב אותם. הפקודה ״רק לשוניות הסרט״ בלשונית תצוגה, או קיצור המקשים שלה, מחליפה הגדרה זו.",
|
||||
"prefs.right_click": "לחיצה ימנית",
|
||||
"prefs.saving": "הפעלה ושמירה",
|
||||
"prefs.scale_note": "גם Ctrl + / Ctrl − בחלון. הכפתורים בגודל 28 px ב־100%.",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "ממדי התחום",
|
||||
"render.stage_panels.fit_save": "התאמה ושמירה",
|
||||
"render.stage_panels.gpu_required": "נדרש מעקב נתיב ב-GPU",
|
||||
"render.stage_panels.grain_angle": "זווית סיבי העץ",
|
||||
"render.stage_panels.keep": "שמור",
|
||||
"render.stage_panels.kept": "נשמר",
|
||||
"render.stage_panels.kept_fields": "מצלמה, דגימה, ממדים, קישורים אופטיים, גאומטריית CAD ותכונות הנדסיות.",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "הילוך אקראי לפיזור פנימי",
|
||||
"render.subsurface_help": "לדיאלקטרי סגור בלבד. פיזור ובליעת RGB הם מקדמים נפרדים לכל mm. המסלולים מתפזרים בתוך הגאומטריה בפועל, ואז נשברים או מוחזרים בגבול. יש לבטל העברת צל ישרה, הסרת רעש משטחית ונפח חיצוני; ולהשתמש בשכבת פרנל אחת. אינו שיידר דיפוזיה, אינו רשת צפיפות ואינו חומר מדוד. תווך צפוף עשוי לדרוש יותר דגימות ועומק מסלול.",
|
||||
"render.subsurface_rate": "קצב התנגשויות הפיזור",
|
||||
"render.surface_import.author": "יוצר",
|
||||
"render.surface_import.busy": "מתבצע ייבוא תמונה…",
|
||||
"render.surface_import.complete": "בחר תמונה והזן את היוצר, המקור והרישיון שלה.",
|
||||
"render.surface_import.embedded": "תמונה מיובאת",
|
||||
"render.surface_import.failed": "ייבוא התמונה לא הושלם.",
|
||||
"render.surface_import.license": "רישיון / בעלות",
|
||||
"render.surface_import.raytrace_required": "תמונות מחייבות מעקב קרניים ב־CPU או ב־GPU. יש לבחור זאת תחת פלט.",
|
||||
"render.surface_import.repeat_u": "חזרת U",
|
||||
"render.surface_import.repeat_v": "חזרת V",
|
||||
"render.surface_import.save": "שמירת תמונה על הגוף",
|
||||
"render.surface_import.saved_cpu": "יש לשמור את המפות לשימוש בהדמיה ב־CPU.",
|
||||
"render.surface_import.source": "מקור",
|
||||
"render.surface_import.title": "ייבוא תמונה",
|
||||
"render.tone_aces_fitted": "ACES מותאם",
|
||||
"render.tone_reinhard": "ריינהרד",
|
||||
"render.transmission_scope": "זכוכית GPU תומכת בתווכים נפרדים מאושרים. תווכים מקוננים ללא הגבלה ומעריך העברה ללא רעש אינם מסופקים.",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "לוח לדוגמה",
|
||||
"sample.vents.about": "חריצים במערך מלבני: מספר השורות, מספר העמודות והפסיעה הם פרמטרים",
|
||||
"sample.vents.name": "לוח אוורור",
|
||||
"scale.per_axis": "לפי ציר",
|
||||
"scale.uniform": "אחיד",
|
||||
"search.command_keys": "Enter הרצה Shift+Enter הרצה והשארה פתוח Esc סגירה",
|
||||
"search.commands": "פקודות",
|
||||
"search.design": "בתכן זה",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "לחיצה בוחרת גופים שלמים",
|
||||
"ui.click_then_press_key": "יש ללחוץ ואז להקיש על המקש החדש",
|
||||
"ui.close": "סגירה",
|
||||
"ui.close_tab": "סגירת הלשונית",
|
||||
"ui.coil": "סליל",
|
||||
"ui.coincident": "מתלכד",
|
||||
"ui.collapse": "כיווץ",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "מקדם",
|
||||
"ui.fade": "דעיכה",
|
||||
"ui.fade_length": "אורך דעיכה",
|
||||
"ui.fair_iterations": "מספר איטרציות",
|
||||
"ui.fair_keep_edges": "שמירה על המקצועות",
|
||||
"ui.fair_strength": "עוצמה",
|
||||
"ui.fair_surface_title": "החלקת משטח",
|
||||
"ui.fair_tolerance_default": "0.001 מגודל הגוף",
|
||||
"ui.feature_history": "ציר הזמן",
|
||||
"ui.feature_options": "אפשרויות פיצ'ר",
|
||||
"ui.features": "פיצ'רים",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "בדיקה",
|
||||
"ui.instances": "מופעים",
|
||||
"ui.intersect": "חיתוך משותף",
|
||||
"ui.intersection_curve_second_pick": "עקומת חיתוך: יש ללחוץ על הפאה או העקומה השנייה (לחיצה ימנית להתחלה מחדש)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "בידוד",
|
||||
"ui.join": "איחוד",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "גרירה חופשית של הידית גם עם Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "פעיל: Alt/Option או Ctrl/Cmd על חיצי השליפה / הידית מבטלים הצמדת ערכים. כבוי: Ctrl/Cmd בלבד.",
|
||||
"ui.manufacture": "ייצור",
|
||||
"ui.match_surface_title": "התאמת משטח",
|
||||
"ui.material_review.axes": "צירים מקומיים של החומר; לא הוגדרה אוריינטציה לגוף.",
|
||||
"ui.material_review.claims": "המקורות והרישיונות הם טענות שלא אומתו. ערכים חסרים נשארים לא ידועים.",
|
||||
"ui.material_review.compressive_strength": "חוזק לחיצה",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "בדיקת נתוני חומר פיזיקליים",
|
||||
"ui.material_review.unknown": "לא ידוע",
|
||||
"ui.material_review.yield_strength": "חוזק כניעה",
|
||||
"ui.mcp_agents": "סוכנים דרך MCP",
|
||||
"ui.mcp_compact": "פלט מקוצר",
|
||||
"ui.mcp_compact_note": "לסוכנים שמחוברים דרך שרת ה-MCP (solidcraft-cli mcp): inspect_design מסכם סקיצות ותוצאות הכלים אינן מעוצבות, כך שהן צורכות פחות טוקנים. חל על הפעלות סוכן שמתחילות אחרי השינוי; גם --compact או SOLIDCRAFT_MCP_COMPACT=1 מפעילים אותו.",
|
||||
"ui.measure": "מדידה",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF עם תגי פאות",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "אפשרויות",
|
||||
"ui.orbit": "סיבוב מבט",
|
||||
"ui.orbit_around": "סיבוב מבט סביב",
|
||||
"ui.organize": "ארגון",
|
||||
"ui.orientation": "אוריינטציה",
|
||||
"ui.origin": "ראשית",
|
||||
"ui.palette_construction_mode": "מצב עזר (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "הוספת מסנן…",
|
||||
"ui.point_cloud_auto_range": "טווח אוטומטי",
|
||||
"ui.point_cloud_color_by": "צביעה לפי",
|
||||
"ui.point_cloud_dots": "נקודות בודדות",
|
||||
"ui.point_cloud_file_colors": "צבעי הקובץ",
|
||||
"ui.point_cloud_filter_by": "סינון לפי",
|
||||
"ui.point_cloud_height_high": "הגובה המרבי",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Čita putanje SVG crteža u skicu, skalirane, zakrenute i postavljene kako zadate. Upotrijebite ga za pretvaranje logotipa ili vektorskog obrisa u krivulje koje možete ekstrudirati.",
|
||||
"help.sketch.inspect": "Otvara izvješće o otvorenoj skici: koliko je stupnjeva slobode preostalo i je li potpuno ograničena. Upotrijebite ga kako biste saznali što se još pomiče prije dovršetka skice.",
|
||||
"help.sketch.intersect": "Dodaje krivulje ili točke u kojima tijela, lica, bridovi ili druge skice presijecaju ravninu skice. Ostaju povezane i prate model kad se promijeni.",
|
||||
"help.sketch.intersection_curve": "Izrađuje 3D krivulje na mjestu gdje se dva tijela ili lica susreću, povezane s njima. Upotrijebite ih kao putanje ili vodilice koje prate model.",
|
||||
"help.sketch.intersection_curve": "Izrađuje 3D krivulje na mjestu gdje se dva tijela, lica ili krivulje drugih skica susreću, povezane s njima. Upotrijebite ih kao putanje ili vodilice koje prate model.",
|
||||
"help.sketch.isoparametric_curve": "Izrađuje 3D krivulju u smjeru U ili V lica kroz točku koju odaberete, povezanu s licem. Upotrijebite je kao putanju ili liniju razdvajanja koja prati plohu.",
|
||||
"help.sketch.line": "Crta povezane linije točku po točku, kao otvoreni lanac ili zatvoreni obris. To je osnovni alat za većinu profila.",
|
||||
"help.sketch.line.midpoint": "Crta liniju od njezine središnje točke do jednog kraja, a drugi kraj zrcali. Upotrijebite ga za linije centrirane na točku, primjerice simetričan brid.",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Beolvassa egy SVG-rajz útvonalait egy vázlatba, az Ön által megadott léptékkel, elforgatással és helyen. Használja egy logó vagy vektoros körvonal kihúzható görbékké alakítására.",
|
||||
"help.sketch.inspect": "Jelentést nyit a megnyitott vázlatról: hány szabadságfok maradt, és teljesen kényszerezett-e. Használja annak kiderítésére, mi mozdul még, mielőtt befejezi a vázlatot.",
|
||||
"help.sketch.intersect": "Hozzáadja azokat a görbéket vagy pontokat, ahol testek, lapok, élek vagy más vázlatok metszik a vázlatsíkot. Kapcsoltak maradnak, és követik a modellt, ha az megváltozik.",
|
||||
"help.sketch.intersection_curve": "3D görbéket hoz létre ott, ahol két test vagy lap találkozik, hozzájuk kapcsolva. Használja őket útvonalként vagy vezérgörbeként, amely követi a modellt.",
|
||||
"help.sketch.intersection_curve": "3D görbéket hoz létre ott, ahol két test, lap vagy más vázlatok görbéi találkozik, hozzájuk kapcsolva. Használja őket útvonalként vagy vezérgörbeként, amely követi a modellt.",
|
||||
"help.sketch.isoparametric_curve": "3D görbét hoz létre egy lap U vagy V iránya mentén egy kiválasztott ponton át, a laphoz kapcsolva. Használja útvonalként vagy osztóvonalként, amely követi a felületet.",
|
||||
"help.sketch.line": "Összefüggő vonalakat rajzol pontról pontra, nyitott láncként vagy zárt körvonalként. Ez a legtöbb profil alapeszköze.",
|
||||
"help.sketch.line.midpoint": "Vonalat rajzol a felezőpontjától az egyik végéig, a másik véget tükrözve. Használja egy pontra középen álló vonalakhoz, például szimmetrikus élhez.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Mintalemez",
|
||||
"sample.vents.about": "Hornyok téglalap mintában: a sorok, oszlopok és osztás paraméterek",
|
||||
"sample.vents.name": "Szellőzőlemez",
|
||||
"scale.per_axis": "Tengelyenként",
|
||||
"scale.uniform": "Egyenletes",
|
||||
"search.command_keys": "Enter futtatás Shift+Enter futtatás és nyitva tartás Esc bezárás",
|
||||
"search.commands": "Parancsok",
|
||||
"search.design": "Ebben a tervben",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Membaca path gambar SVG ke dalam sketsa, diskalakan, diputar, dan ditempatkan sesuai pengaturan Anda. Gunakan untuk mengubah logo atau kontur vektor menjadi kurva yang dapat diekstrusi.",
|
||||
"help.sketch.inspect": "Membuka laporan tentang sketsa yang terbuka: berapa derajat kebebasan yang tersisa dan apakah sketsa terbatasi penuh. Gunakan untuk mengetahui apa yang masih bergerak sebelum Anda menyelesaikan sketsa.",
|
||||
"help.sketch.intersect": "Menambahkan kurva atau titik tempat bodi, muka, tepi, atau sketsa lain memotong bidang sketsa. Semuanya tetap tertaut dan mengikuti model saat model berubah.",
|
||||
"help.sketch.intersection_curve": "Membuat kurva 3D di tempat dua bodi atau muka bertemu, tertaut padanya. Gunakan sebagai jalur atau rel pemandu yang mengikuti model.",
|
||||
"help.sketch.intersection_curve": "Membuat kurva 3D di tempat dua bodi, muka, atau kurva sketsa lain bertemu, tertaut padanya. Gunakan sebagai jalur atau rel pemandu yang mengikuti model.",
|
||||
"help.sketch.isoparametric_curve": "Membuat kurva 3D sepanjang arah U atau V sebuah muka melalui titik yang Anda pilih, tertaut ke muka itu. Gunakan sebagai jalur atau garis pisah yang mengikuti permukaan.",
|
||||
"help.sketch.line": "Menggambar garis-garis tersambung titik demi titik, sebagai rantai terbuka atau kontur tertutup. Ini alat dasar untuk sebagian besar profil.",
|
||||
"help.sketch.line.midpoint": "Menggambar garis dari titik tengahnya ke satu ujung, dengan ujung lainnya dicerminkan. Gunakan untuk garis yang dipusatkan pada sebuah titik, seperti tepi simetris.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Pelat Contoh",
|
||||
"sample.vents.about": "Slot dalam pola persegi panjang: baris, kolom, dan pitch adalah parameter",
|
||||
"sample.vents.name": "Pelat Ventilasi",
|
||||
"scale.per_axis": "Per sumbu",
|
||||
"scale.uniform": "Seragam",
|
||||
"search.command_keys": "Enter jalankan Shift+Enter jalankan dan tetap buka Esc tutup",
|
||||
"search.commands": "Perintah",
|
||||
"search.design": "Di desain ini",
|
||||
|
||||
@@ -1625,7 +1625,7 @@
|
||||
"help.sketch.insert_svg": "Legge i tracciati di un disegno SVG in uno schizzo, scalati, ruotati e posizionati come imposti. Usalo per trasformare un logo o un contorno vettoriale in curve che puoi estrudere.",
|
||||
"help.sketch.inspect": "Apre un rapporto sullo schizzo aperto: quanti gradi di libertà restano e se è completamente vincolato. Usalo per scoprire cosa si muove ancora prima di terminare lo schizzo.",
|
||||
"help.sketch.intersect": "Aggiunge le curve o i punti in cui corpi, facce, spigoli o altri schizzi attraversano il piano dello schizzo. Restano collegati e seguono il modello quando cambia.",
|
||||
"help.sketch.intersection_curve": "Crea curve 3D dove si incontrano due corpi o facce, collegate a essi. Usale come percorsi o guide che seguono il modello.",
|
||||
"help.sketch.intersection_curve": "Crea curve 3D dove si incontrano due corpi, facce o curve di altri schizzi, collegate a essi. Usale come percorsi o guide che seguono il modello.",
|
||||
"help.sketch.isoparametric_curve": "Crea una curva 3D lungo la direzione U o V di una faccia passante per un punto che scegli, collegata alla faccia. Usala come percorso o linea di divisione che segue la superficie.",
|
||||
"help.sketch.line": "Disegna linee collegate punto per punto, come catena aperta o contorno chiuso. È lo strumento di base per la maggior parte dei profili.",
|
||||
"help.sketch.line.midpoint": "Disegna una linea dal suo punto medio fino a un'estremità, con l'altra estremità speculare. Usalo per linee centrate su un punto, come un bordo simmetrico.",
|
||||
@@ -3813,6 +3813,8 @@
|
||||
"sample.plate.name": "Piastra di esempio",
|
||||
"sample.vents.about": "Asole in ripetizione rettangolare: righe, colonne e passo sono parametri",
|
||||
"sample.vents.name": "Piastra di ventilazione",
|
||||
"scale.per_axis": "Per asse",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Invio esegui Maiusc+Invio esegui e tieni aperto Esc chiudi",
|
||||
"search.commands": "Comandi",
|
||||
"search.design": "In questo progetto",
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "選択したフォルダーを抑制",
|
||||
"Suppressed": "抑制",
|
||||
"Surface": "サーフェス",
|
||||
"Surface or face": "サーフェスまたは面",
|
||||
"Surface or faces": "サーフェスまたは面",
|
||||
"Surface, or faces to turn over": "サーフェス、または裏返す面",
|
||||
"Surfaces": "サーフェス",
|
||||
"Swap Ends": "端点を入れ替え",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "ターゲット",
|
||||
"Target Body": "ターゲットボディ",
|
||||
"Target Face (optional)": "対象面 (オプション)",
|
||||
"Target edges": "ターゲットのエッジ",
|
||||
"Tear": "引き裂き",
|
||||
"Technical details": "技術情報",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "方向は、軸、またはスケッチ平面上にある直線のスケッチ曲線にしてください",
|
||||
"The mirror line cannot also be mirrored": "ミラー線自体をミラーすることはできません",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "ミラー線は、原点の軸ではなく、このスケッチの直線にしてください",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点軸はスケッチ平面に垂直なため、平面内に線がありません",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存された作成元のオカレンスが見つかりません。再バインドする前に有効な作成元配置を選択してください。",
|
||||
"Then": "次を実行 (Then)",
|
||||
"These features will be deleted:": "以下のフィーチャーが削除されます:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "選択したエッジのボディが存在しなくなりました",
|
||||
"add a key for a joint first": "先にジョイントのキーを追加してください",
|
||||
"ai.connected_toast": "SolidCraft が {clients} で使えるようになりました。使うには再起動してください(設定 > アシスタント)。",
|
||||
"ai.http.allow": "安全でない HTTP を許可",
|
||||
"ai.http.confirm": "このエンドポイントで許可",
|
||||
"ai.http.warning": "API キーと会話は暗号化されずに送信されます。ネットワークにアクセスできる人は、内容を読み取ったり改ざんしたりする可能性があります。信頼できるネットワークでのみ使用してください。",
|
||||
"ai.prefs.allow_control": "AIエージェントの接続を許可",
|
||||
"ai.prefs.allow_control_note": "ローカルチャネル(MCP)です。このコンピューターから外には何も送信しません。\n\nこのコンピューター内だけのコントロールチャンネル(127.0.0.1、ポート 27878 または次に空いているポート)を開きます。接続するプログラムは、起動のたびに作られ、あなたのユーザーアカウントだけが読める秘密のトークンも提示する必要があります。次に SolidCraft を起動したときから有効です。オフ:--control を付けて起動しない限り、何も待ち受けません。",
|
||||
"ai.prefs.availability": "利用設定",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "カメラを追従",
|
||||
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
|
||||
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
|
||||
"ai.prefs.instructions": "カスタム指示",
|
||||
"ai.prefs.instructions_note": "各チャットの開始時に、SolidCraft 自身の指示の後に続けてエージェントへ送られます。作業の進め方や守ってほしいルールを書いてください。変更は次のチャット(接続または新しいチャット)から適用されます。",
|
||||
"ai.prefs.intro": "SolidCraftのAIエージェント。",
|
||||
"ai.prefs.make_default": "既定にする",
|
||||
"ai.prefs.no_agents": "エージェントはまだありません。追加するか、更新してインストール済みのエージェントを検索してください。",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "レンダー比較の境界を移動",
|
||||
"cmd.render.result.discard": "レンダリング結果を破棄",
|
||||
"cmd.render.result.select": "レンダリング結果を選択",
|
||||
"cmd.render.surface.import": "サーフェスマップをインポート",
|
||||
"cmd.render.texture.list": "埋め込みサーフェス画像の一覧",
|
||||
"cmd.render.texture.remove": "埋め込みサーフェス画像を削除",
|
||||
"cmd.render_aging_describe": "レイヤー式表面エイジング",
|
||||
"cmd.render_backends": "レンダリング バックエンド",
|
||||
"cmd.render_batch": "すべてのショットをレンダリング",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "前のタブ",
|
||||
"cmd.view_rename": "名前付きビューの名前を変更",
|
||||
"cmd.view_render_frame": "レンダーフレームガイド",
|
||||
"cmd.view_ribbon_tabs_only": "リボンのタブのみ",
|
||||
"cmd.view_right": "右側面ビュー",
|
||||
"cmd.view_save": "新規名前付きビュー",
|
||||
"cmd.view_shots_strip": "ショットストリップ",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "開いたファイル選択はデスクトップのものではありません。XWayland ではポータルが親ウィンドウを拒否します。",
|
||||
"file_load.built": "構築済み: 全{total}フィーチャー中{built}件。",
|
||||
"file_load.converting": "ソリッドを変換中: {done}/{total}",
|
||||
"file_load.inserted": "{name} を挿入しました",
|
||||
"file_load.inserting": "ジオメトリを挿入中",
|
||||
"file_load.link_item": "{name} ({done}/{total})",
|
||||
"file_load.opened": "{name} を開きました",
|
||||
"file_load.opened_in_tab": "{name} を新しいタブで開きました",
|
||||
"file_load.opening": "デザインを開いています",
|
||||
"file_load.preparing_thumbnail": "サムネイルを準備中...",
|
||||
"file_load.preparing_view": "ビューを準備中...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "SVG 図面のパスをスケッチに読み込み、指定した位置に拡大縮小・回転して配置します。ロゴやベクター輪郭を、押し出しできる曲線にするときに使います。",
|
||||
"help.sketch.inspect": "開いているスケッチのレポートを開きます。残りの自由度と、完全に拘束されているかどうかを表示します。スケッチを終える前に、まだ動く部分を確認するときに使います。",
|
||||
"help.sketch.intersect": "ボディ、面、エッジ、ほかのスケッチがスケッチ平面と交差する曲線または点を追加します。リンクが保たれ、モデルが変わると追従します。",
|
||||
"help.sketch.intersection_curve": "2つのボディまたは面が接する位置に、それらにリンクした 3D 曲線を作成します。モデルに追従するパスやガイドレールとして使います。",
|
||||
"help.sketch.intersection_curve": "2つのボディ、面、またはほかのスケッチの曲線が接する位置に、それらにリンクした 3D 曲線を作成します。モデルに追従するパスやガイドレールとして使います。",
|
||||
"help.sketch.isoparametric_curve": "選んだ点を通る面の U または V 方向に沿った 3D 曲線を、面にリンクして作成します。表面に追従するパスや分割線として使います。",
|
||||
"help.sketch.line": "点を順に指定して、つながった直線を開いた連なりまたは閉じた外形として描きます。ほとんどの輪郭に使う基本のツールです。",
|
||||
"help.sketch.line.midpoint": "中点から一方の端まで直線を描き、もう一方の端は反対側に対称に作られます。対称なエッジなど、点を中心にした直線に使います。",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "左に90度回転するなど、ステップごとにビューを回転、移動、ズームします。ドラッグではなく、カメラを正確に動かすときに使います。",
|
||||
"help.view.navigation_bar": "ビューポート下部のナビゲーションバーを表示または非表示にします。広く使いたいときは非表示に、マウスでビューツールを使うときは表示にします。",
|
||||
"help.view.rename": "保存した名前付きビューの名前を変更します。ビュー自体は変わりません。",
|
||||
"help.view.ribbon_tabs_only": "リボンをタブの行だけに折りたたみ、パネルが使っていた場所をビューに充てます。タブをクリックするとそのパネルがビューの上に表示されます。もう一度選ぶとリボン全体を表示したままにします。",
|
||||
"help.view.save": "現在のカメラ位置を名前付きビューとしてデザインに保存します。スクリーンショット用など、後で同じ角度に戻るときに使います。",
|
||||
"help.view.spin": "次のクリックまたはキー操作まで、モデルのまわりでビューを連続して周回させます。モデルをあらゆる方向から見せるときに使います。",
|
||||
"help.view.tool": "オービット、パン、ズームのカメラツールを固定し、オフにするまで左ボタンのドラッグでビューを動かせるようにします。トラックパッドや、中ボタンのないマウスで使います。",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "前へ",
|
||||
"prefs.choose_font": "フォントファイルを選択…",
|
||||
"prefs.click": "クリック",
|
||||
"prefs.command_dialog_values": "コマンドダイアログ",
|
||||
"prefs.contact_shading": "柔らかな接触陰影",
|
||||
"prefs.contact_shading_note": "部品が近接する部分に柔らかな陰影を付けます。",
|
||||
"prefs.context_menu": "コンテキストメニュー",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "サーフェスとエッジ",
|
||||
"prefs.graphics_page.view_bar": "ビューバーにも表示",
|
||||
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
|
||||
"prefs.ground_shadow_follows_model": "地面の影をモデルに追従させる",
|
||||
"prefs.ground_shadow_follows_model_help": "表示中のボディの最下部に影を配置します。オフの場合、持ち上げたモデルの影は原点を通る地面上に残ります。",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "ズームアウト時に拘束グリフを隠す",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "スケッチを十分にズームアウトすると、拘束グリフは表示されず画面を散らかしません。オフ: ズームに関係なく固定の画面サイズのままです。",
|
||||
"prefs.history": "履歴",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "カード",
|
||||
"prefs.layout_page.column": "列",
|
||||
"prefs.layout_page.hover": "ホバー時",
|
||||
"prefs.layout_page.hover_panel_menus": "ポイントしてパネルメニューを切り替える",
|
||||
"prefs.layout_page.hover_panel_menus_help": "リボンのパネル名をクリックしてメニューを開きます。別のパネル名にポインターを合わせると切り替わります。リボンと開いているメニューの外に移動すると閉じます。",
|
||||
"prefs.layout_page.icons_labels": "アイコン+ラベル",
|
||||
"prefs.layout_page.ok_cancel": "OK とキャンセル",
|
||||
"prefs.layout_page.on_hover": "ホバー時",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "より滑らか",
|
||||
"prefs.quick_measure": "ビューの隅にクイック測定",
|
||||
"prefs.quick_measure_note": "選択したものの寸法をビューの右下、履歴の上に表示します: エッジの長さ(円形のものは Ø と R)、面の面積、点の位置、2 つの項目の距離と角度、複数のエッジや面の合計。クリックでコピーします。",
|
||||
"prefs.remember_dialog_values": "コマンドダイアログの値を記憶",
|
||||
"prefs.remember_dialog_values.help": "既定ではオフです。オンにすると、コマンドが成功した後、SolidCraft を閉じるまで、同じコマンドを次に開いたときに安全な値とオプションを再利用します。",
|
||||
"prefs.render_previous_geometry_budget": "レンダープレビューで以前のジオメトリ予算を使用",
|
||||
"prefs.render_previous_geometry_budget_help": "デバイスに基づく予算の代わりに、以前の固定ジオメトリ上限(64ビットデスクトップでは512 MiB、その他では128 MiB)に戻します。",
|
||||
"prefs.reports_saved": "{n} 件保存済み",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "使用中のスケッチツールの点灯したリボンボタンをクリックするとツールが終了し、ビューのクリックで再び選択できます。オフ: クリックするとツールが最初から開始されます。",
|
||||
"prefs.ribbon_reveal": "検索結果をリボンに表示",
|
||||
"prefs.ribbon_reveal_note": "コマンド検索で結果にポインターを合わせると、そのコマンドがリボンのどこにあるか(タブ、ボタン、またはパネルのドロップダウン内の行)を表示します。検索から開始したコマンドはそこで点滅し、ドロップダウンから実行中のコマンドはそのパネルに印が付きます。",
|
||||
"prefs.ribbon_tabs_only": "リボンのタブだけを表示",
|
||||
"prefs.ribbon_tabs_only_note": "リボンをタブの行だけに折りたたみます。タブをクリックすると、コマンドが始まるかポインターが離れるまで、そのパネルがビューの上に表示されます。表示タブの「リボンのタブのみ」またはそのショートカットで切り替えます。",
|
||||
"prefs.right_click": "右クリック",
|
||||
"prefs.saving": "起動と保存",
|
||||
"prefs.scale_note": "ウィンドウ内では Ctrl + / Ctrl − でも変更できます。100% のボタンは 28 px です。",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "範囲",
|
||||
"render.stage_panels.fit_save": "フィットして保存",
|
||||
"render.stage_panels.gpu_required": "GPUパストレーシングが必要です",
|
||||
"render.stage_panels.grain_angle": "木目の角度",
|
||||
"render.stage_panels.keep": "保持",
|
||||
"render.stage_panels.kept": "保持",
|
||||
"render.stage_panels.kept_fields": "カメラ、サンプリング、画像寸法、光学設定、CAD形状、工学的特性。",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "内部散乱ランダムウォーク",
|
||||
"render.subsurface_help": "閉じた誘電体専用。散乱とRGB吸収は別々の毎mm係数です。実際の形状内で散乱し、境界で屈折・反射します。直線透過影、表面ノイズ除去、外部ボリュームを無効にし、フレネル層数を1にします。拡散シェーダー、密度グリッド、実測物性ではありません。高密度ではサンプル数と深度が必要です。",
|
||||
"render.subsurface_rate": "散乱衝突率",
|
||||
"render.surface_import.author": "作者",
|
||||
"render.surface_import.busy": "画像をインポート中…",
|
||||
"render.surface_import.complete": "画像を選び、作者、出典、ライセンスを入力してください。",
|
||||
"render.surface_import.embedded": "インポートした画像",
|
||||
"render.surface_import.failed": "画像のインポートが完了しませんでした。",
|
||||
"render.surface_import.license": "ライセンス / 所有権",
|
||||
"render.surface_import.raytrace_required": "画像にはCPUまたはGPUのレイトレーシングが必要です。出力で選択してください。",
|
||||
"render.surface_import.repeat_u": "U方向の繰り返し",
|
||||
"render.surface_import.repeat_v": "V方向の繰り返し",
|
||||
"render.surface_import.save": "ボディに画像を保存",
|
||||
"render.surface_import.saved_cpu": "CPUレンダリングで使うにはマップを保存してください。",
|
||||
"render.surface_import.source": "出典",
|
||||
"render.surface_import.title": "画像をインポート",
|
||||
"render.tone_aces_fitted": "ACES近似",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "GPUガラスは検証された非交差媒体に対応します。無制限の入れ子媒体やノイズのない透過推定器は提供しません。",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "サンプル プレート",
|
||||
"sample.vents.about": "矩形パターンに並んだスロット: 行数、列数、ピッチはパラメータ",
|
||||
"sample.vents.name": "通気プレート",
|
||||
"scale.per_axis": "軸ごと",
|
||||
"scale.uniform": "均等",
|
||||
"search.command_keys": "Enter 実行 Shift+Enter 実行して開いたままにする Esc 閉じる",
|
||||
"search.commands": "コマンド",
|
||||
"search.design": "このデザイン内",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "クリックでボディ全体を選択",
|
||||
"ui.click_then_press_key": "クリックした後、割り当てるキーを押してください",
|
||||
"ui.close": "閉じる",
|
||||
"ui.close_tab": "タブを閉じる",
|
||||
"ui.coil": "コイル",
|
||||
"ui.coincident": "一致",
|
||||
"ui.collapse": "折りたたむ",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "倍率",
|
||||
"ui.fade": "切り上げ",
|
||||
"ui.fade_length": "切り上げ長さ",
|
||||
"ui.fair_iterations": "反復回数",
|
||||
"ui.fair_keep_edges": "エッジを保持",
|
||||
"ui.fair_strength": "強度",
|
||||
"ui.fair_surface_title": "サーフェスフェアリング",
|
||||
"ui.fair_tolerance_default": "ボディサイズの 0.001 倍",
|
||||
"ui.feature_history": "フィーチャー履歴",
|
||||
"ui.feature_options": "フィーチャーオプション",
|
||||
"ui.features": "フィーチャー",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "検査",
|
||||
"ui.instances": "インスタンス",
|
||||
"ui.intersect": "交差",
|
||||
"ui.intersection_curve_second_pick": "交差曲線: 2 つ目の面または曲線をクリックします (右クリックでやり直し)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "分離表示",
|
||||
"ui.join": "結合",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "マニピュレータのフリードラッグを Alt/Option でも有効",
|
||||
"ui.manipulator_free_alt_tip": "オン: 押し出し/ギズモ矢印で Alt/Option または Ctrl/Cmd が値スナップを無効にします。オフ: Ctrl/Cmd のみ。",
|
||||
"ui.manufacture": "製造",
|
||||
"ui.match_surface_title": "サーフェス一致",
|
||||
"ui.material_review.axes": "材料ローカル軸。ボディの方向は割り当てません。",
|
||||
"ui.material_review.claims": "出典とライセンスの申告は未確認です。欠落値は不明のままです。",
|
||||
"ui.material_review.compressive_strength": "圧縮強度",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "物理材料データを確認",
|
||||
"ui.material_review.unknown": "不明",
|
||||
"ui.material_review.yield_strength": "降伏強度",
|
||||
"ui.mcp_agents": "MCP 経由のエージェント",
|
||||
"ui.mcp_compact": "コンパクトな出力",
|
||||
"ui.mcp_compact_note": "MCP サーバー (solidcraft-cli mcp) 経由で接続したエージェント向け: inspect_design はスケッチを要約し、ツールの結果は整形されないため、トークンが少なくて済みます。変更後に開始したエージェントのセッションから適用されます。--compact または SOLIDCRAFT_MCP_COMPACT=1 でも有効になります。",
|
||||
"ui.measure": "計測",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "面タグ付き 3MF",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "オプション",
|
||||
"ui.orbit": "回転",
|
||||
"ui.orbit_around": "回転中心",
|
||||
"ui.organize": "整理",
|
||||
"ui.orientation": "向き",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_construction_mode": "補助線モード (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "フィルターを追加…",
|
||||
"ui.point_cloud_auto_range": "範囲を自動設定",
|
||||
"ui.point_cloud_color_by": "色の付け方",
|
||||
"ui.point_cloud_dots": "単独の点",
|
||||
"ui.point_cloud_file_colors": "ファイルの色",
|
||||
"ui.point_cloud_filter_by": "フィルター",
|
||||
"ui.point_cloud_height_high": "最高の高さ",
|
||||
|
||||
+808
-46
File diff suppressed because it is too large
Load Diff
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Membaca laluan lukisan SVG ke dalam lakaran, diskalakan, diputar dan diletakkan mengikut tetapan anda. Gunakan untuk menukar logo atau garis luar vektor kepada lengkung yang boleh disemperit.",
|
||||
"help.sketch.inspect": "Membuka laporan tentang lakaran yang terbuka: berapa banyak darjah kebebasan yang tinggal dan sama ada ia terkekang sepenuhnya. Gunakan untuk mengetahui apa yang masih bergerak sebelum anda menamatkan lakaran.",
|
||||
"help.sketch.intersect": "Menambah lengkung atau titik tempat jasad, muka, tepi atau lakaran lain melintasi satah lakaran. Ia kekal terpaut dan mengikut model apabila model berubah.",
|
||||
"help.sketch.intersection_curve": "Mencipta lengkung 3D tempat dua jasad atau muka bertemu, terpaut kepadanya. Gunakan sebagai laluan atau rel pemandu yang mengikut model.",
|
||||
"help.sketch.intersection_curve": "Mencipta lengkung 3D tempat dua jasad, muka atau lengkung lakaran lain bertemu, terpaut kepadanya. Gunakan sebagai laluan atau rel pemandu yang mengikut model.",
|
||||
"help.sketch.isoparametric_curve": "Mencipta lengkung 3D di sepanjang arah U atau V muka melalui titik yang anda pilih, terpaut kepada muka itu. Gunakan sebagai laluan atau garis belahan yang mengikut permukaan.",
|
||||
"help.sketch.line": "Melukis garis bersambung titik demi titik, sebagai rantaian terbuka atau garis luar tertutup. Ia ialah alat asas bagi kebanyakan profil.",
|
||||
"help.sketch.line.midpoint": "Melukis garis daripada titik tengahnya ke satu hujung, dengan hujung satu lagi dicerminkan. Gunakan untuk garis yang berpusat pada satu titik, seperti tepi simetri.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Plat Contoh",
|
||||
"sample.vents.about": "Corak slot segi empat tepat: baris, lajur dan pic sebagai parameter",
|
||||
"sample.vents.name": "Plat Pengudaraan",
|
||||
"scale.per_axis": "Mengikut paksi",
|
||||
"scale.uniform": "Seragam",
|
||||
"search.command_keys": "Enter jalankan Shift+Enter jalankan dan kekalkan terbuka Esc tutup",
|
||||
"search.commands": "Arahan",
|
||||
"search.design": "Dalam reka bentuk ini",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Leser banene fra en SVG-tegning inn i en skisse, skalert, dreid og plassert slik du angir. Bruk det til å gjøre en logo eller vektorkontur om til kurver du kan ekstrudere.",
|
||||
"help.sketch.inspect": "Åpner en rapport om den åpne skissen: hvor mange frihetsgrader som gjenstår og om den er fullstendig bundet. Bruk det til å finne ut hva som fortsatt beveger seg før du fullfører skissen.",
|
||||
"help.sketch.intersect": "Legger til kurvene eller punktene der kropper, flater, kanter eller andre skisser krysser skisseplanet. De forblir koblet og følger modellen når den endres.",
|
||||
"help.sketch.intersection_curve": "Oppretter 3D-kurver der to kropper eller flater møtes, koblet til dem. Bruk dem som baner eller føringskurver som følger modellen.",
|
||||
"help.sketch.intersection_curve": "Oppretter 3D-kurver der to kropper, flater eller kurver fra andre skisser møtes, koblet til dem. Bruk dem som baner eller føringskurver som følger modellen.",
|
||||
"help.sketch.isoparametric_curve": "Oppretter en 3D-kurve langs U- eller V-retningen til en flate gjennom et punkt du velger, koblet til flaten. Bruk den som en bane eller delelinje som følger flaten.",
|
||||
"help.sketch.line": "Tegner sammenhengende linjer punkt for punkt, som en åpen kjede eller en lukket kontur. Det er det grunnleggende verktøyet for de fleste profiler.",
|
||||
"help.sketch.line.midpoint": "Tegner en linje fra midtpunktet og ut til den ene enden, med den andre enden speilet. Bruk det til linjer sentrert på et punkt, for eksempel en symmetrisk kant.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Eksempelplate",
|
||||
"sample.vents.about": "Spor i et rektangulært mønster: rader, kolonner og stigning er parametere",
|
||||
"sample.vents.name": "Ventilasjonsplate",
|
||||
"scale.per_axis": "Per akse",
|
||||
"scale.uniform": "Jevn",
|
||||
"search.command_keys": "Enter kjør Shift+Enter kjør og behold åpen Esc lukk",
|
||||
"search.commands": "Kommandoer",
|
||||
"search.design": "I dette designet",
|
||||
|
||||
@@ -1625,7 +1625,7 @@
|
||||
"help.sketch.insert_svg": "Leest de paden van een SVG-tekening in een schets, geschaald, gedraaid en geplaatst waar je instelt. Gebruik dit om een logo of vectorcontour om te zetten in curven die je kunt extruderen.",
|
||||
"help.sketch.inspect": "Opent een rapport over de geopende schets: hoeveel vrijheidsgraden er nog over zijn en of hij volledig beperkt is. Gebruik dit om te zien wat nog beweegt voordat je de schets voltooit.",
|
||||
"help.sketch.intersect": "Voegt de curven of punten toe waar lichamen, vlakken, randen of andere schetsen het schetsvlak kruisen. Ze blijven gekoppeld en volgen het model wanneer dat verandert.",
|
||||
"help.sketch.intersection_curve": "Maakt 3D-curven waar twee lichamen of vlakken samenkomen, eraan gekoppeld. Gebruik ze als paden of geleiders die het model volgen.",
|
||||
"help.sketch.intersection_curve": "Maakt 3D-curven waar twee lichamen, vlakken of curven van andere schetsen samenkomen, eraan gekoppeld. Gebruik ze als paden of geleiders die het model volgen.",
|
||||
"help.sketch.isoparametric_curve": "Maakt een 3D-curve langs de U- of V-richting van een vlak door een punt dat je kiest, gekoppeld aan het vlak. Gebruik dit als pad of splitslijn die het oppervlak volgt.",
|
||||
"help.sketch.line": "Tekent verbonden lijnen punt voor punt, als open keten of gesloten contour. Het is het basisgereedschap voor de meeste profielen.",
|
||||
"help.sketch.line.midpoint": "Tekent een lijn vanuit zijn middelpunt naar één uiteinde, waarbij het andere uiteinde wordt gespiegeld. Gebruik dit voor lijnen die op een punt zijn gecentreerd, zoals een symmetrische rand.",
|
||||
@@ -3818,6 +3818,8 @@
|
||||
"sample.plate.name": "Voorbeeldplaat",
|
||||
"sample.vents.about": "Sleuven in een rechthoekig patroon: rijen, kolommen en spoed zijn parameters",
|
||||
"sample.vents.name": "Ventilatieplaat",
|
||||
"scale.per_axis": "Per as",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter uitvoeren Shift+Enter uitvoeren en open houden Esc sluiten",
|
||||
"search.commands": "Opdrachten",
|
||||
"search.design": "In dit ontwerp",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ai.prefs.instructions": "Custom instructions",
|
||||
"ai.prefs.instructions_note": "Sent to the agent at the start of every chat, after SolidCraft's own instructions: how you like to work, rules to keep to. A change applies from the next chat (Connect or New chat)."
|
||||
},
|
||||
"ar": {
|
||||
"ai.prefs.instructions": "تعليمات مخصصة",
|
||||
"ai.prefs.instructions_note": "تُرسَل إلى الوكيل في بداية كل محادثة، بعد تعليمات SolidCraft الخاصة: طريقتك المفضلة في العمل والقواعد التي يجب الالتزام بها. يسري أي تغيير من المحادثة التالية (اتصال أو محادثة جديدة)."
|
||||
},
|
||||
"he": {
|
||||
"ai.prefs.instructions": "הוראות מותאמות אישית",
|
||||
"ai.prefs.instructions_note": "נשלחות לסוכן בתחילת כל צ'אט, אחרי ההוראות של SolidCraft עצמה: דרך העבודה המועדפת עליך וכללים שיש להקפיד עליהם. שינוי חל מהצ'אט הבא (התחברות או צ'אט חדש)."
|
||||
},
|
||||
"ja": {
|
||||
"ai.prefs.instructions": "カスタム指示",
|
||||
"ai.prefs.instructions_note": "各チャットの開始時に、SolidCraft 自身の指示の後に続けてエージェントへ送られます。作業の進め方や守ってほしいルールを書いてください。変更は次のチャット(接続または新しいチャット)から適用されます。"
|
||||
},
|
||||
"vi": {
|
||||
"ai.prefs.instructions": "Hướng dẫn tùy chỉnh",
|
||||
"ai.prefs.instructions_note": "Được gửi cho agent khi bắt đầu mỗi cuộc trò chuyện, sau hướng dẫn riêng của SolidCraft: cách bạn muốn làm việc, các quy tắc cần tuân theo. Thay đổi có hiệu lực từ cuộc trò chuyện tiếp theo (Kết nối hoặc Cuộc trò chuyện mới)."
|
||||
},
|
||||
"zh": {
|
||||
"ai.prefs.instructions": "自定义指令",
|
||||
"ai.prefs.instructions_note": "在每次对话开始时发送给代理,位于 SolidCraft 自身的指令之后:你喜欢的工作方式、需要遵守的规则。更改从下一次对话(连接或新对话)起生效。"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"prefs.layout_page.hover_panel_menus": "تبديل قوائم اللوحات عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus_help": "انقر على اسم لوحة في الشريط لفتح قائمتها. مرّر المؤشر فوق اسم لوحة أخرى للتبديل. حرّك المؤشر خارج الشريط والقائمة المفتوحة لإغلاقها."
|
||||
},
|
||||
"de": {
|
||||
"prefs.layout_page.hover_panel_menus": "Bereichsmenüs beim Darüberfahren wechseln",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Klicken Sie auf einen Bereichsnamen, um das Menü zu öffnen. Bewegen Sie den Zeiger auf einen anderen Bereichsnamen, um das Menü zu wechseln. Außerhalb des Menübands und des geöffneten Menüs schließt es sich."
|
||||
},
|
||||
"en": {
|
||||
"prefs.layout_page.hover_panel_menus": "Switch panel menus on hover",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Click a ribbon panel name to open its menu. Hover another panel name to switch. Move outside the ribbon and open menu to close it."
|
||||
},
|
||||
"he": {
|
||||
"prefs.layout_page.hover_panel_menus": "מעבר בין תפריטי לוחות בריחוף",
|
||||
"prefs.layout_page.hover_panel_menus_help": "לחצו על שם לוח ברצועת הכלים כדי לפתוח את התפריט שלו. העבירו את הסמן מעל שם של לוח אחר כדי לעבור לתפריט שלו. הזיזו את הסמן אל מחוץ לרצועת הכלים ולתפריט הפתוח כדי לסגור אותו."
|
||||
},
|
||||
"ja": {
|
||||
"prefs.layout_page.hover_panel_menus": "ポイントしてパネルメニューを切り替える",
|
||||
"prefs.layout_page.hover_panel_menus_help": "リボンのパネル名をクリックしてメニューを開きます。別のパネル名にポインターを合わせると切り替わります。リボンと開いているメニューの外に移動すると閉じます。"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.layout_page.hover_panel_menus": "포인터를 올려 패널 메뉴 전환",
|
||||
"prefs.layout_page.hover_panel_menus_help": "리본의 패널 이름을 클릭하여 메뉴를 엽니다. 다른 패널 이름에 포인터를 올리면 메뉴가 전환됩니다. 리본과 열린 메뉴 밖으로 포인터를 이동하면 메뉴가 닫힙니다."
|
||||
},
|
||||
"vi": {
|
||||
"prefs.layout_page.hover_panel_menus": "Chuyển menu nhóm khi di chuột",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Nhấp vào tên nhóm trên dải công cụ để mở menu. Di chuột lên tên nhóm khác để chuyển menu. Di chuyển con trỏ ra ngoài dải công cụ và menu đang mở để đóng menu."
|
||||
},
|
||||
"zh": {
|
||||
"prefs.layout_page.hover_panel_menus": "悬停切换面板菜单",
|
||||
"prefs.layout_page.hover_panel_menus_help": "点击功能区的面板名称打开菜单。将指针悬停在另一面板名称上即可切换菜单。将指针移到功能区和已打开菜单之外即可关闭菜单。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "The origin axis is perpendicular to the sketch plane, so it has no line in the plane"
|
||||
},
|
||||
"ar": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "محور الأصل عمودي على مستوى الرسم، لذا ليس له خط في المستوى"
|
||||
},
|
||||
"de": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Die Ursprungsachse steht senkrecht auf der Skizzenebene und hat daher keine Linie in ihr"
|
||||
},
|
||||
"he": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "ציר הראשית ניצב למישור הסקיצה, ולכן אין לו קו במישור"
|
||||
},
|
||||
"ja": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点軸はスケッチ平面に垂直なため、平面内に線がありません"
|
||||
},
|
||||
"ru": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Ось начала координат перпендикулярна плоскости эскиза, поэтому линии в этой плоскости нет"
|
||||
},
|
||||
"vi": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Trục gốc vuông góc với mặt phẳng phác thảo nên không có đường thẳng trong mặt phẳng này"
|
||||
},
|
||||
"zh": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点轴垂直于草图平面,因此在该平面内没有直线"
|
||||
}
|
||||
}
|
||||
@@ -1408,6 +1408,8 @@
|
||||
"sample.plate.name": "Placa de exemplo",
|
||||
"sample.vents.about": "Rasgos em padrão retangular: linhas, colunas e passo são parâmetros",
|
||||
"sample.vents.name": "Placa de ventilação",
|
||||
"scale.per_axis": "Por eixo",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Enter executar Shift+Enter executar e manter aberto Esc fechar",
|
||||
"search.commands": "Comandos",
|
||||
"search.design": "Neste projeto",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Citește traseele unui desen SVG într-o schiță, scalate, rotite și plasate așa cum stabiliți. Folosiți-o pentru a transforma un logo sau un contur vectorial în curbe pe care le puteți extruda.",
|
||||
"help.sketch.inspect": "Deschide un raport despre schița deschisă: câte grade de libertate au rămas și dacă este complet constrânsă. Folosiți-l pentru a afla ce se mai mișcă înainte de a finaliza schița.",
|
||||
"help.sketch.intersect": "Adaugă curbele sau punctele în care corpuri, fețe, muchii sau alte schițe taie planul schiței. Rămân legate și urmează modelul când acesta se schimbă.",
|
||||
"help.sketch.intersection_curve": "Creează curbe 3D acolo unde se întâlnesc două corpuri sau fețe, legate de ele. Folosiți-le ca trasee sau ghidaje care urmează modelul.",
|
||||
"help.sketch.intersection_curve": "Creează curbe 3D acolo unde se întâlnesc două corpuri, fețe sau curbe ale altor schițe, legate de ele. Folosiți-le ca trasee sau ghidaje care urmează modelul.",
|
||||
"help.sketch.isoparametric_curve": "Creează o curbă 3D pe direcția U sau V a unei fețe, printr-un punct ales de dumneavoastră, legată de față. Folosiți-o ca traseu sau linie de separare care urmează suprafața.",
|
||||
"help.sketch.line": "Desenează linii conectate punct cu punct, ca lanț deschis sau contur închis. Este instrumentul de bază pentru majoritatea profilurilor.",
|
||||
"help.sketch.line.midpoint": "Desenează o linie de la mijlocul ei spre un capăt, celălalt capăt fiind în oglindă. Folosiți-o pentru linii centrate pe un punct, de exemplu o muchie simetrică.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Placă exemplu",
|
||||
"sample.vents.about": "Canale într-o matrice dreptunghiulară: rândurile, coloanele și pasul sunt parametri",
|
||||
"sample.vents.name": "Placă de ventilație",
|
||||
"scale.per_axis": "Pe axe",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter execută Shift+Enter execută și păstrează deschis Esc închide",
|
||||
"search.commands": "Comenzi",
|
||||
"search.design": "În acest proiect",
|
||||
|
||||
@@ -647,6 +647,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Направление должно быть осью или прямолинейной кривой эскиза, лежащей в плоскости эскиза",
|
||||
"The mirror line cannot also be mirrored": "Линию зеркала нельзя одновременно отражать",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Линия зеркала должна быть прямолинейной кривой этого эскиза, а не осью начала координат",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Ось начала координат перпендикулярна плоскости эскиза, поэтому линии в этой плоскости нет",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Сохранённое исходное вхождение отсутствует. Выберите корректное исходное положение перед перепривязкой.",
|
||||
"Then": "Затем",
|
||||
"These features will be deleted:": "Эти операции будут удалены:",
|
||||
@@ -1610,7 +1611,7 @@
|
||||
"help.sketch.insert_svg": "Читает контуры рисунка SVG в эскиз, масштабируя, поворачивая и размещая их так, как вы зададите. Используйте, чтобы превратить логотип или векторный контур в кривые, которые можно выдавить.",
|
||||
"help.sketch.inspect": "Открывает отчёт об открытом эскизе: сколько осталось степеней свободы и определён ли он полностью. Используйте, чтобы выяснить, что ещё двигается, прежде чем завершить эскиз.",
|
||||
"help.sketch.intersect": "Добавляет кривые или точки, где тела, грани, рёбра или другие эскизы пересекают плоскость эскиза. Они остаются связанными и следуют за моделью при её изменении.",
|
||||
"help.sketch.intersection_curve": "Создаёт 3D-кривые там, где встречаются два тела или две грани, связанные с ними. Используйте как траектории или направляющие, следующие за моделью.",
|
||||
"help.sketch.intersection_curve": "Создаёт 3D-кривые там, где встречаются два тела, две грани или кривые других эскизов, связанные с ними. Используйте как траектории или направляющие, следующие за моделью.",
|
||||
"help.sketch.isoparametric_curve": "Создаёт 3D-кривую вдоль направления U или V грани через выбранную точку, связанную с гранью. Используйте как траекторию или линию разделения, следующую за поверхностью.",
|
||||
"help.sketch.line": "Рисует соединённые линии точка за точкой, как открытую цепочку или замкнутый контур. Это основной инструмент для большинства профилей.",
|
||||
"help.sketch.line.midpoint": "Рисует линию от её середины до одного конца, второй конец отражается зеркально. Используйте для линий с центром в точке, например симметричного ребра.",
|
||||
@@ -3750,6 +3751,8 @@
|
||||
"sample.plate.name": "Образец пластины",
|
||||
"sample.vents.about": "Пазы в прямоугольном массиве: число рядов, столбцов и шаг заданы параметрами",
|
||||
"sample.vents.name": "Вентиляционная пластина",
|
||||
"scale.per_axis": "По осям",
|
||||
"scale.uniform": "Равномерно",
|
||||
"search.command_keys": "Enter выполнить Shift+Enter выполнить и оставить открытым Esc закрыть",
|
||||
"search.commands": "Команды",
|
||||
"search.design": "В этом дизайне",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Учитава путање SVG цртежа у скицу, скалиране, окренуте и постављене како зададете. Користите га да логотип или векторски обрис претворите у криве које можете екструдирати.",
|
||||
"help.sketch.inspect": "Отвара извештај о отвореној скици: колико је степени слободе остало и да ли је потпуно ограничена. Користите га да сазнате шта се још помера пре него што завршите скицу.",
|
||||
"help.sketch.intersect": "Додаје криве или тачке у којима тела, лица, ивице или друге скице секу раван скице. Оне остају повезане и прате модел када се он промени.",
|
||||
"help.sketch.intersection_curve": "Прави 3D криве тамо где се два тела или лица састају, повезане са њима. Користите их као путање или водилице које прате модел.",
|
||||
"help.sketch.intersection_curve": "Прави 3D криве тамо где се два тела, лица или криве других скица састају, повезане са њима. Користите их као путање или водилице које прате модел.",
|
||||
"help.sketch.isoparametric_curve": "Прави 3D криву дуж U или V смера лица кроз тачку коју изаберете, повезану са лицем. Користите је као путању или линију поделе која прати површ.",
|
||||
"help.sketch.line": "Црта повезане линије тачку по тачку, као отворени ланац или затворени обрис. То је основни алат за већину профила.",
|
||||
"help.sketch.line.midpoint": "Црта линију од њене средишње тачке до једног краја, а други крај се пресликава. Користите је за линије центриране на тачки, на пример за симетричну ивицу.",
|
||||
|
||||
@@ -1622,7 +1622,7 @@
|
||||
"help.sketch.insert_svg": "Läser in banorna från en SVG-ritning i en skiss, skalade, vridna och placerade där du anger. Använd det för att göra en logotyp eller en vektorkontur till kurvor som du kan extrudera.",
|
||||
"help.sketch.inspect": "Öppnar en rapport om den öppna skissen: hur många frihetsgrader som återstår och om den är fullt definierad. Använd det för att ta reda på vad som fortfarande rör sig innan du avslutar skissen.",
|
||||
"help.sketch.intersect": "Lägger till de kurvor eller punkter där kroppar, ytor, kanter eller andra skisser korsar skissplanet. De förblir länkade och följer modellen när den ändras.",
|
||||
"help.sketch.intersection_curve": "Skapar 3D-kurvor där två kroppar eller ytor möts, länkade till dem. Använd dem som banor eller styrkurvor som följer modellen.",
|
||||
"help.sketch.intersection_curve": "Skapar 3D-kurvor där två kroppar, ytor eller kurvor från andra skisser möts, länkade till dem. Använd dem som banor eller styrkurvor som följer modellen.",
|
||||
"help.sketch.isoparametric_curve": "Skapar en 3D-kurva längs en ytas U- eller V-riktning genom en punkt som du väljer, länkad till ytan. Använd den som en bana eller delningslinje som följer ytan.",
|
||||
"help.sketch.line": "Ritar sammanhängande linjer punkt för punkt, som en öppen kedja eller en sluten kontur. Det är det grundläggande verktyget för de flesta profiler.",
|
||||
"help.sketch.line.midpoint": "Ritar en linje från dess mittpunkt ut till ena änden, med den andra änden speglad. Använd det för linjer centrerade på en punkt, till exempel en symmetrisk kant.",
|
||||
@@ -3808,6 +3808,8 @@
|
||||
"sample.plate.name": "Exempelplatta",
|
||||
"sample.vents.about": "Spår i ett rektangulärt mönster: rader, kolumner och delning är parametrar",
|
||||
"sample.vents.name": "Ventilationsplatta",
|
||||
"scale.per_axis": "Per axel",
|
||||
"scale.uniform": "Likformig",
|
||||
"search.command_keys": "Enter kör Shift+Enter kör och behåll öppen Esc stäng",
|
||||
"search.commands": "Kommandon",
|
||||
"search.design": "I denna konstruktion",
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"help.sketch.insert_svg": "Bir SVG çiziminin yollarını, belirlediğiniz ölçekte, açıda ve konumda bir eskize okur. Bir logoyu veya vektör dış hattını ekstrüzyon yapabileceğiniz eğrilere dönüştürmek için kullanın.",
|
||||
"help.sketch.inspect": "Açık eskiz için bir rapor açar: kaç serbestlik derecesi kaldığını ve tam kısıtlı olup olmadığını. Eskizi bitirmeden önce nelerin hâlâ hareket ettiğini öğrenmek için kullanın.",
|
||||
"help.sketch.intersect": "Gövdelerin, yüzlerin, kenarların veya diğer eskizlerin eskiz düzlemini kestiği eğrileri ya da noktaları ekler. Bağlı kalır ve model değiştiğinde onu izler.",
|
||||
"help.sketch.intersection_curve": "İki gövdenin veya yüzün buluştuğu yerde, onlara bağlı 3B eğriler oluşturur. Bunları modeli izleyen yollar veya kılavuz raylar olarak kullanın.",
|
||||
"help.sketch.intersection_curve": "İki gövdenin, yüzün veya diğer eskizlerin eğrilerinin buluştuğu yerde, onlara bağlı 3B eğriler oluşturur. Bunları modeli izleyen yollar veya kılavuz raylar olarak kullanın.",
|
||||
"help.sketch.isoparametric_curve": "Bir yüzün U veya V yönü boyunca, seçtiğiniz bir noktadan geçen ve yüze bağlı bir 3B eğri oluşturur. Yüzeyi izleyen bir yol veya bölme çizgisi olarak kullanın.",
|
||||
"help.sketch.line": "Bağlantılı çizgileri nokta nokta, açık bir zincir veya kapalı bir dış hat olarak çizer. Çoğu profil için temel araçtır.",
|
||||
"help.sketch.line.midpoint": "Bir çizgiyi orta noktasından bir uca doğru çizer; diğer uç yansıtılır. Bir nokta etrafında ortalanmış çizgiler, örneğin simetrik bir kenar için kullanın.",
|
||||
@@ -3807,6 +3807,8 @@
|
||||
"sample.plate.name": "Örnek plaka",
|
||||
"sample.vents.about": "Dikdörtgen desende yuvalar: satırlar, sütunlar ve aralık parametredir",
|
||||
"sample.vents.name": "Havalandırma plakası",
|
||||
"scale.per_axis": "Eksen başına",
|
||||
"scale.uniform": "Tekdüze",
|
||||
"search.command_keys": "Enter çalıştır Shift+Enter çalıştır ve açık tut Esc kapat",
|
||||
"search.commands": "Komutlar",
|
||||
"search.design": "Bu tasarımda",
|
||||
|
||||
@@ -1681,7 +1681,7 @@
|
||||
"help.sketch.insert_svg": "Зчитує контури малюнка SVG в ескіз, масштабовані, повернуті й розміщені так, як ви задасте. Використовуйте, щоб перетворити логотип або векторний контур на криві, які можна видавити.",
|
||||
"help.sketch.inspect": "Відкриває звіт про відкритий ескіз: скільки ступенів вільності залишилося і чи він повністю обмежений. Використовуйте його, щоб дізнатися, що ще рухається, перш ніж завершити ескіз.",
|
||||
"help.sketch.intersect": "Додає криві чи точки, де тіла, грані, ребра або інші ескізи перетинають площину ескізу. Вони залишаються пов'язаними й слідують за моделлю, коли вона змінюється.",
|
||||
"help.sketch.intersection_curve": "Створює 3D-криві там, де сходяться два тіла чи грані, пов'язані з ними. Використовуйте їх як траєкторії або напрямні, що слідують за моделлю.",
|
||||
"help.sketch.intersection_curve": "Створює 3D-криві там, де сходяться два тіла, грані чи криві інших ескізів, пов'язані з ними. Використовуйте їх як траєкторії або напрямні, що слідують за моделлю.",
|
||||
"help.sketch.isoparametric_curve": "Створює 3D-криву, пов'язану з гранню, вздовж напрямку U або V грані через вибрану вами точку. Використовуйте як траєкторію або лінію розділення, що слідує за поверхнею.",
|
||||
"help.sketch.line": "Малює з'єднані прямі точка за точкою — як розімкнений ланцюг або замкнений контур. Це основний інструмент для більшості профілів.",
|
||||
"help.sketch.line.midpoint": "Малює пряму від її середини до одного кінця, а другий кінець відображається дзеркально. Використовуйте для прямих, центрованих на точці, наприклад симетричного краю.",
|
||||
@@ -4074,6 +4074,8 @@
|
||||
"sample.plate.name": "Зразкова пластина",
|
||||
"sample.vents.about": "Пази в прямокутному масиві: рядки, стовпці та крок є параметрами",
|
||||
"sample.vents.name": "Вентиляційна пластина",
|
||||
"scale.per_axis": "По осях",
|
||||
"scale.uniform": "Рівномірно",
|
||||
"search.command_keys": "Enter виконати Shift+Enter виконати й залишити відкритим Esc закрити",
|
||||
"search.commands": "Команди",
|
||||
"search.design": "У цьому проєкті",
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "Bỏ kích hoạt các thư mục đã chọn",
|
||||
"Suppressed": "Bị tạm nén",
|
||||
"Surface": "Bề mặt",
|
||||
"Surface or face": "Bề mặt hoặc mặt",
|
||||
"Surface or faces": "Bề mặt hoặc các mặt",
|
||||
"Surface, or faces to turn over": "Mặt cong, hoặc các mặt cần lật hướng",
|
||||
"Surfaces": "Các bề mặt",
|
||||
"Swap Ends": "Hoán đổi hai đầu",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "Mục tiêu",
|
||||
"Target Body": "Khối mục tiêu",
|
||||
"Target Face (optional)": "Mặt đích (tùy chọn)",
|
||||
"Target edges": "Các cạnh đích",
|
||||
"Tear": "Xé rách mép chấn",
|
||||
"Technical details": "Chi tiết kỹ thuật",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Phác thảo nét đứt là đường dẫn mặt phẳng đã lưu. Hãy chọn một bề mặt mới để sửa lại liên kết.",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Hướng phải là một trục hoặc một đường cong phác thảo thẳng nằm trong mặt phẳng phác thảo",
|
||||
"The mirror line cannot also be mirrored": "Đường đối xứng không thể đồng thời được lấy đối xứng",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Đường đối xứng phải là một đường cong thẳng của phác thảo này, không phải trục gốc",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Trục gốc vuông góc với mặt phẳng phác thảo nên không có đường thẳng trong mặt phẳng này",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Bản thể tác giả đã lưu bị thiếu. Hãy chọn một vị trí tác giả hợp lệ trước khi gán lại.",
|
||||
"Then": "Thì",
|
||||
"These features will be deleted:": "Các tính năng sau sẽ bị xóa:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "thân của một cạnh đã chọn không còn nữa",
|
||||
"add a key for a joint first": "hãy thêm một khóa cho khớp trước",
|
||||
"ai.connected_toast": "SolidCraft hiện đã sẵn sàng trong {clients}. Khởi động lại ứng dụng để sử dụng (Cài đặt > Trợ lý ảo).",
|
||||
"ai.http.allow": "Cho phép HTTP không bảo mật",
|
||||
"ai.http.confirm": "Cho phép với điểm cuối này",
|
||||
"ai.http.warning": "Khóa API và cuộc trò chuyện sẽ được gửi mà không mã hóa. Bất kỳ ai có quyền truy cập mạng đều có thể đọc hoặc thay đổi chúng. Chỉ sử dụng trên mạng mà bạn tin cậy.",
|
||||
"ai.prefs.allow_control": "Cho phép tác nhân AI kết nối",
|
||||
"ai.prefs.allow_control_note": "Kênh cục bộ (MCP). Không có gì rời khỏi máy tính này.\n\nMở kênh điều khiển chỉ trên máy tính này (127.0.0.1, cổng 27878 hoặc cổng trống tiếp theo). Chương trình kết nối phải xuất trình mã token bảo mật chỉ tài khoản người dùng của bạn đọc được, được tạo mới mỗi lần khởi động. Có hiệu lực vào lần khởi động tiếp theo của SolidCraft. Khi tắt: không lắng nghe trừ khi bạn bật.",
|
||||
"ai.prefs.availability": "Khả dụng",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "Theo camera",
|
||||
"ai.prefs.follow_camera_off": "Tự động xoay camera theo phác thảo đang tắt.",
|
||||
"ai.prefs.follow_camera_on": "Camera sẽ tự động xoay vuông góc với phác thảo do agent tạo.",
|
||||
"ai.prefs.instructions": "Hướng dẫn tùy chỉnh",
|
||||
"ai.prefs.instructions_note": "Được gửi cho agent khi bắt đầu mỗi cuộc trò chuyện, sau hướng dẫn riêng của SolidCraft: cách bạn muốn làm việc, các quy tắc cần tuân theo. Thay đổi có hiệu lực từ cuộc trò chuyện tiếp theo (Kết nối hoặc Cuộc trò chuyện mới).",
|
||||
"ai.prefs.intro": "Tác nhân AI trong SolidCraft.",
|
||||
"ai.prefs.make_default": "Đặt làm mặc định",
|
||||
"ai.prefs.no_agents": "Chưa có agent nào. Thêm một agent hoặc làm mới để tìm các agent đã cài đặt.",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "Di chuyển vạch so sánh kết xuất",
|
||||
"cmd.render.result.discard": "Bỏ kết quả kết xuất",
|
||||
"cmd.render.result.select": "Chọn kết quả kết xuất",
|
||||
"cmd.render.surface.import": "Nhập bản đồ bề mặt",
|
||||
"cmd.render.texture.list": "Liệt kê ảnh bề mặt được nhúng",
|
||||
"cmd.render.texture.remove": "Xóa ảnh bề mặt được nhúng",
|
||||
"cmd.render_aging_describe": "Lão hóa bề mặt phân lớp",
|
||||
"cmd.render_backends": "Kết xuất hình ảnh Sauends",
|
||||
"cmd.render_batch": "Kết xuất mọi cảnh chụp",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "Thẻ trước",
|
||||
"cmd.view_rename": "Đổi tên góc nhìn đã lưu",
|
||||
"cmd.view_render_frame": "Đường dẫn khung kết xuất",
|
||||
"cmd.view_ribbon_tabs_only": "Chỉ các thẻ ribbon",
|
||||
"cmd.view_right": "Góc nhìn bên phải",
|
||||
"cmd.view_save": "Tạo góc nhìn đã lưu mới",
|
||||
"cmd.view_shots_strip": "Dải cảnh chụp",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "Bộ chọn tệp đã mở không phải của riêng desktop: dưới XWayland, portal từ chối cửa sổ cha.",
|
||||
"file_load.built": "Đã dựng được: {built} trên {total} tính năng.",
|
||||
"file_load.converting": "Đang chuyển đổi khối đặc {done} trên {total}",
|
||||
"file_load.inserted": "Đã chèn {name}",
|
||||
"file_load.inserting": "Đang chèn hình học",
|
||||
"file_load.link_item": "{name} ({done} trên {total})",
|
||||
"file_load.opened": "Đã mở {name}",
|
||||
"file_load.opened_in_tab": "Đã mở {name} trong thẻ mới",
|
||||
"file_load.opening": "Đang mở thiết kế",
|
||||
"file_load.preparing_thumbnail": "Đang chuẩn bị ảnh thu nhỏ...",
|
||||
"file_load.preparing_view": "Đang chuẩn bị góc nhìn...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "Đọc các đường dẫn của bản vẽ SVG vào phác thảo, theo tỉ lệ, góc xoay và vị trí bạn đặt. Dùng để biến logo hoặc đường bao véc tơ thành đường cong có thể đùn.",
|
||||
"help.sketch.inspect": "Mở báo cáo về bản phác thảo đang mở: còn bao nhiêu bậc tự do và đã ràng buộc hoàn toàn chưa. Dùng nó để biết phần nào vẫn còn di chuyển được trước khi hoàn tất bản phác thảo.",
|
||||
"help.sketch.intersect": "Thêm các đường cong hoặc điểm nơi khối, mặt, cạnh hoặc phác thảo khác cắt qua mặt phẳng phác thảo. Chúng vẫn được liên kết và thay đổi theo mô hình.",
|
||||
"help.sketch.intersection_curve": "Tạo đường cong 3D tại nơi hai khối hoặc hai mặt gặp nhau, liên kết với chúng. Dùng làm đường dẫn hoặc ray dẫn hướng thay đổi theo mô hình.",
|
||||
"help.sketch.intersection_curve": "Tạo đường cong 3D tại nơi hai khối, hai mặt hoặc đường cong của các phác thảo khác gặp nhau, liên kết với chúng. Dùng làm đường dẫn hoặc ray dẫn hướng thay đổi theo mô hình.",
|
||||
"help.sketch.isoparametric_curve": "Tạo đường cong 3D theo hướng U hoặc V của một mặt qua điểm bạn chọn, liên kết với mặt đó. Dùng làm đường dẫn hoặc đường tách thay đổi theo bề mặt.",
|
||||
"help.sketch.line": "Vẽ các đường thẳng nối liền từng điểm một, thành chuỗi hở hoặc đường bao khép kín. Đây là công cụ cơ bản cho hầu hết biên dạng.",
|
||||
"help.sketch.line.midpoint": "Vẽ đường thẳng từ trung điểm ra một đầu, đầu kia đối xứng qua trung điểm. Dùng cho đường thẳng có tâm tại một điểm, như cạnh đối xứng.",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "Xoay, dịch hoặc thu phóng khung nhìn theo từng bước, chẳng hạn xoay một phần tư vòng sang trái. Dùng để điều khiển camera chính xác thay vì kéo chuột.",
|
||||
"help.view.navigation_bar": "Hiện hoặc ẩn thanh điều hướng ở cuối khung nhìn. Ẩn đi để có thêm chỗ, hiện ra để dùng các công cụ xem bằng chuột.",
|
||||
"help.view.rename": "Đặt tên mới cho một góc nhìn đã lưu. Bản thân góc nhìn không đổi.",
|
||||
"help.view.ribbon_tabs_only": "Thu gọn ribbon chỉ còn hàng thẻ, để khung nhìn có thêm chỗ mà các bảng từng chiếm. Bấm một thẻ để hiện các bảng của nó đè lên khung nhìn; chọn lại lệnh này để luôn hiện toàn bộ ribbon.",
|
||||
"help.view.save": "Lưu vị trí camera hiện tại thành một góc nhìn có tên trong thiết kế. Dùng để quay lại đúng góc đó sau này, chẳng hạn khi chụp ảnh màn hình.",
|
||||
"help.view.spin": "Quay khung nhìn quanh mô hình liên tục cho đến cú nhấp hoặc phím tiếp theo. Dùng để trình bày mô hình từ mọi phía.",
|
||||
"help.view.tool": "Giữ cố định một công cụ camera (xoay quanh, dịch hoặc thu phóng) để kéo bằng nút chuột trái sẽ di chuyển khung nhìn cho đến khi bạn tắt nó. Dùng trên bàn di chuột hoặc khi không có nút chuột giữa.",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "Trước",
|
||||
"prefs.choose_font": "Chọn tệp phông chữ…",
|
||||
"prefs.click": "Nhấp chuột",
|
||||
"prefs.command_dialog_values": "Hộp thoại lệnh",
|
||||
"prefs.contact_shading": "Đổ bóng tiếp xúc mềm",
|
||||
"prefs.contact_shading_note": "Tạo bóng đổ mềm tại những vị trí các chi tiết tiếp giáp gần nhau.",
|
||||
"prefs.context_menu": "Menu ngữ cảnh",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "Bề mặt và cạnh",
|
||||
"prefs.graphics_page.view_bar": "Cũng có trên thanh khung nhìn",
|
||||
"prefs.graphics_presets_note": "Các cấu hình định sẵn sẽ thiết lập đồng thời các tùy chọn bên dưới.",
|
||||
"prefs.ground_shadow_follows_model": "Bóng trên mặt đất di chuyển theo mô hình",
|
||||
"prefs.ground_shadow_follows_model_help": "Đặt bóng bên dưới khối hiển thị thấp nhất. Khi tắt, bóng của mô hình được nâng lên vẫn nằm trên mặt phẳng đất đi qua gốc tọa độ.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "Ẩn biểu tượng ràng buộc khi thu nhỏ",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "Biểu tượng ràng buộc biến mất khi bản phác được thu nhỏ đủ xa để không làm rối khung nhìn. Tắt: chúng giữ kích thước màn hình cố định ở mọi mức thu phóng.",
|
||||
"prefs.history": "Lịch sử",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "Thẻ",
|
||||
"prefs.layout_page.column": "Cột",
|
||||
"prefs.layout_page.hover": "Rê chuột",
|
||||
"prefs.layout_page.hover_panel_menus": "Chuyển menu nhóm khi di chuột",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Nhấp vào tên nhóm trên dải công cụ để mở menu. Di chuột lên tên nhóm khác để chuyển menu. Di chuyển con trỏ ra ngoài dải công cụ và menu đang mở để đóng menu.",
|
||||
"prefs.layout_page.icons_labels": "Biểu tượng + nhãn",
|
||||
"prefs.layout_page.ok_cancel": "Đồng ý và Hủy bỏ",
|
||||
"prefs.layout_page.on_hover": "Khi rê chuột",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "mượt mà hơn",
|
||||
"prefs.quick_measure": "Đo nhanh ở góc khung nhìn",
|
||||
"prefs.quick_measure_note": "Hiển thị kích thước của đối tượng được chọn ở góc dưới bên phải khung nhìn, phía trên Lịch sử: chiều dài của cạnh (hoặc Ø và R với cạnh tròn), diện tích của mặt, vị trí của điểm, khoảng cách và góc giữa hai đối tượng, tổng của nhiều cạnh hoặc mặt. Nhấp để sao chép.",
|
||||
"prefs.remember_dialog_values": "Ghi nhớ giá trị hộp thoại lệnh",
|
||||
"prefs.remember_dialog_values.help": "Tắt theo mặc định. Khi bật, sau khi lệnh thành công, các giá trị và tùy chọn an toàn của lệnh sẽ được dùng lại vào lần tiếp theo bạn mở cùng lệnh đó, cho đến khi SolidCraft đóng.",
|
||||
"prefs.render_previous_geometry_budget": "Dùng ngân sách hình học trước đây cho bản xem trước kết xuất",
|
||||
"prefs.render_previous_geometry_budget_help": "Khôi phục giới hạn hình học cố định trước đây (512 MiB trên máy tính 64 bit, 128 MiB trên các nền tảng khác) thay cho ngân sách dựa trên thiết bị.",
|
||||
"prefs.reports_saved": "Đã lưu {n}",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "Nhấp vào nút ruy-băng đang sáng của công cụ phác thảo đang dùng sẽ kết thúc công cụ, để các cú nhấp trong khung nhìn lại chọn đối tượng. Tắt: cú nhấp khởi động lại công cụ.",
|
||||
"prefs.ribbon_reveal": "Hiện kết quả tìm kiếm trên ribbon",
|
||||
"prefs.ribbon_reveal_note": "Khi rê chuột lên một kết quả trong Tìm lệnh, ribbon cho biết lệnh nằm ở đâu: tab, nút hoặc dòng của nó trong menu thả xuống của một bảng. Lệnh bắt đầu từ tìm kiếm sẽ nhấp nháy ở đó, và lệnh đang chạy từ menu thả xuống sẽ đánh dấu bảng của nó.",
|
||||
"prefs.ribbon_tabs_only": "Chỉ hiện các thẻ của ribbon",
|
||||
"prefs.ribbon_tabs_only_note": "Ribbon thu gọn chỉ còn hàng thẻ. Bấm một thẻ sẽ hiện các bảng của nó đè lên khung nhìn cho đến khi một lệnh bắt đầu hoặc con trỏ rời khỏi chúng. Lệnh Chỉ các thẻ ribbon trong thẻ Xem, hoặc phím tắt của nó, bật tắt tùy chọn này.",
|
||||
"prefs.right_click": "Nhấp chuột phải",
|
||||
"prefs.saving": "Khởi động và lưu",
|
||||
"prefs.scale_note": "Cũng có thể dùng Ctrl + / Ctrl − trong cửa sổ. Các nút có kích thước 28 px ở mức 100%.",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "Phạm vi",
|
||||
"render.stage_panels.fit_save": "Khớp và lưu",
|
||||
"render.stage_panels.gpu_required": "Cần dò đường đi của tia bằng GPU",
|
||||
"render.stage_panels.grain_angle": "Góc vân gỗ",
|
||||
"render.stage_panels.keep": "Giữ lại",
|
||||
"render.stage_panels.kept": "Được giữ lại",
|
||||
"render.stage_panels.kept_fields": "Camera, lấy mẫu, kích thước, liên kết quang học, hình học CAD và đặc tính kỹ thuật.",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "Bước đi ngẫu nhiên tán xạ bên trong",
|
||||
"render.subsurface_help": "Chỉ dành cho chất điện môi kín. Hệ số tán xạ và hấp thụ RGB là các hệ số riêng biệt trên mỗi mm. Tia sáng tán xạ bên trong hình học thực tế, sau đó khúc xạ hoặc phản xạ tại ranh giới. Hãy tắt bóng đổ thẳng, khử nhiễu bề mặt và thể tích bên ngoài; dùng một phân tầng Fresnel. Đây không phải bộ đổ bóng khuếch tán, lưới mật độ hay vật liệu đo đạc. Môi trường đậm đặc có thể cần nhiều mẫu và độ sâu tia lớn hơn.",
|
||||
"render.subsurface_rate": "Tần suất va chạm tán xạ",
|
||||
"render.surface_import.author": "Tác giả",
|
||||
"render.surface_import.busy": "Đang nhập ảnh…",
|
||||
"render.surface_import.complete": "Chọn ảnh rồi nhập tác giả, nguồn và giấy phép.",
|
||||
"render.surface_import.embedded": "Ảnh đã nhập",
|
||||
"render.surface_import.failed": "Quá trình nhập ảnh chưa hoàn tất.",
|
||||
"render.surface_import.license": "Giấy phép / quyền sở hữu",
|
||||
"render.surface_import.raytrace_required": "Ảnh cần dò tia trên CPU hoặc GPU. Hãy chọn trong phần Đầu ra.",
|
||||
"render.surface_import.repeat_u": "Chu kỳ U",
|
||||
"render.surface_import.repeat_v": "Chu kỳ V",
|
||||
"render.surface_import.save": "Lưu ảnh trên khối",
|
||||
"render.surface_import.saved_cpu": "Lưu bản đồ để dùng khi kết xuất bằng CPU.",
|
||||
"render.surface_import.source": "Nguồn",
|
||||
"render.surface_import.title": "Nhập ảnh",
|
||||
"render.tone_aces_fitted": "ACES khớp",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "Thủy tinh GPU hỗ trợ các môi trường rời rạc đã xác thực. Không cung cấp môi trường lồng nhau không giới hạn hay ước tính truyền dẫn không nhiễu.",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "Tấm mẫu",
|
||||
"sample.vents.about": "Các rãnh then trong mảng chữ nhật: số hàng, số cột và bước rãnh là các tham số",
|
||||
"sample.vents.name": "Tấm khe thông gió",
|
||||
"scale.per_axis": "Theo từng trục",
|
||||
"scale.uniform": "Đồng đều",
|
||||
"search.command_keys": "Enter chạy Shift+Enter chạy và giữ mở Esc đóng",
|
||||
"search.commands": "Lệnh",
|
||||
"search.design": "Trong thiết kế này",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "Nhấp chuột chọn toàn bộ thân khối",
|
||||
"ui.click_then_press_key": "Nhấp chuột, sau đó nhấn phím mới",
|
||||
"ui.close": "Đóng",
|
||||
"ui.close_tab": "Đóng thẻ",
|
||||
"ui.coil": "Lò xo xoắn",
|
||||
"ui.coincident": "Trùng điểm",
|
||||
"ui.collapse": "Thu gọn",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "Hệ số",
|
||||
"ui.fade": "Thoát ren",
|
||||
"ui.fade_length": "Chiều dài thoát ren",
|
||||
"ui.fair_iterations": "Số lần lặp",
|
||||
"ui.fair_keep_edges": "Giữ các cạnh",
|
||||
"ui.fair_strength": "Cường độ",
|
||||
"ui.fair_surface_title": "LÀM MƯỢT BỀ MẶT",
|
||||
"ui.fair_tolerance_default": "0,001 lần kích thước thân khối",
|
||||
"ui.feature_history": "Lịch sử tính năng",
|
||||
"ui.feature_options": "Tùy chọn tính năng",
|
||||
"ui.features": "Các tính năng",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "Kiểm tra",
|
||||
"ui.instances": "Số bản",
|
||||
"ui.intersect": "Giao nhau",
|
||||
"ui.intersection_curve_second_pick": "Đường cong giao tuyến: nhấp vào bề mặt hoặc đường cong thứ hai (nhấp chuột phải để bắt đầu lại)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "Cô lập đối tượng",
|
||||
"ui.join": "Nối liền",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "Kéo tự do bộ thao tác cũng bằng Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "Bật: Alt/Option hoặc Ctrl/Cmd trên các mũi tên đùn / gizmo sẽ tắt bước nhảy giá trị. Tắt: chỉ Ctrl/Cmd.",
|
||||
"ui.manufacture": "Gia công chế tạo",
|
||||
"ui.match_surface_title": "KHỚP BỀ MẶT",
|
||||
"ui.material_review.axes": "Các trục cục bộ của vật liệu; chưa gán hướng cho thân khối.",
|
||||
"ui.material_review.claims": "Nguồn gốc và giấy phép là thông tin chưa được kiểm chứng. Các giá trị thiếu vẫn ở dạng chưa biết.",
|
||||
"ui.material_review.compressive_strength": "Độ bền nén",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "Xem xét dữ liệu thuộc tính vật liệu",
|
||||
"ui.material_review.unknown": "Chưa biết",
|
||||
"ui.material_review.yield_strength": "Giới hạn chảy",
|
||||
"ui.mcp_agents": "Tác tử qua MCP",
|
||||
"ui.mcp_compact": "Đầu ra gọn",
|
||||
"ui.mcp_compact_note": "Dành cho các tác tử kết nối qua máy chủ MCP (solidcraft-cli mcp): inspect_design tóm tắt các bản phác thảo và kết quả công cụ không được định dạng đẹp, nên tốn ít token hơn. Áp dụng cho các phiên tác tử bắt đầu sau khi thay đổi; --compact hoặc SOLIDCRAFT_MCP_COMPACT=1 cũng bật nó.",
|
||||
"ui.measure": "Đo lường",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF kèm thẻ bề mặt",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "Tùy chọn",
|
||||
"ui.orbit": "Xoay quanh tâm (Orbit)",
|
||||
"ui.orbit_around": "Xoay quanh",
|
||||
"ui.organize": "Sắp xếp",
|
||||
"ui.orientation": "Định hướng",
|
||||
"ui.origin": "Gốc tọa độ",
|
||||
"ui.palette_construction_mode": "Chế độ đường dựng (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "Thêm bộ lọc…",
|
||||
"ui.point_cloud_auto_range": "Phạm vi tự động",
|
||||
"ui.point_cloud_color_by": "Tô màu theo",
|
||||
"ui.point_cloud_dots": "Điểm đơn lẻ",
|
||||
"ui.point_cloud_file_colors": "Màu trong tệp",
|
||||
"ui.point_cloud_filter_by": "Lọc theo",
|
||||
"ui.point_cloud_height_high": "Cao nhất",
|
||||
|
||||
@@ -1624,7 +1624,7 @@
|
||||
"help.sketch.insert_svg": "將 SVG 圖形的路徑讀入草圖,並依您的設定縮放、旋轉和放置。用於把標誌或向量輪廓轉成可擠出的曲線。",
|
||||
"help.sketch.inspect": "開啟目前草圖的報告:還剩多少個自由度,以及是否已完全拘束。用於在完成草圖之前找出哪些部分仍會移動。",
|
||||
"help.sketch.intersect": "加入實體、面、邊或其他草圖與草圖平面相交的曲線或點。它們保持連結,並在模型變更時跟隨。",
|
||||
"help.sketch.intersection_curve": "在兩個實體或面相接處建立 3D 曲線,並與它們連結。用作跟隨模型的路徑或導軌。",
|
||||
"help.sketch.intersection_curve": "在兩個實體、面或其他草圖的曲線相接處建立 3D 曲線,並與它們連結。用作跟隨模型的路徑或導軌。",
|
||||
"help.sketch.isoparametric_curve": "通過您選取的點,沿面的 U 或 V 方向建立 3D 曲線,並與該面連結。用作跟隨曲面的路徑或分割線。",
|
||||
"help.sketch.line": "逐點畫出相連的直線,可為開放的鏈或封閉的輪廓。這是大多數輪廓的基本工具。",
|
||||
"help.sketch.line.midpoint": "從中點向一端畫直線,另一端對稱映射。用於以一點為中心的直線,例如對稱的邊。",
|
||||
@@ -3813,6 +3813,8 @@
|
||||
"sample.plate.name": "範例板",
|
||||
"sample.vents.about": "依矩形陣列排列的槽:列數、欄數和間距均為參數",
|
||||
"sample.vents.name": "通風板",
|
||||
"scale.per_axis": "依軸",
|
||||
"scale.uniform": "等比",
|
||||
"search.command_keys": "Enter 執行 Shift+Enter 執行並保持開啟 Esc 關閉",
|
||||
"search.commands": "命令",
|
||||
"search.design": "此設計中",
|
||||
@@ -4443,6 +4445,7 @@
|
||||
"ui.click_selects_whole_bodies": "點擊選擇整個實體",
|
||||
"ui.click_then_press_key": "點擊,然後按新按鍵",
|
||||
"ui.close": "關閉",
|
||||
"ui.close_tab": "關閉分頁",
|
||||
"ui.coil": "線圈",
|
||||
"ui.coincident": "重合",
|
||||
"ui.collapse": "摺疊",
|
||||
|
||||
+56
-1
@@ -629,6 +629,8 @@
|
||||
"Suppress Selected Folders": "抑制所选文件夹",
|
||||
"Suppressed": "已抑制",
|
||||
"Surface": "曲面",
|
||||
"Surface or face": "曲面或面",
|
||||
"Surface or faces": "曲面或面",
|
||||
"Surface, or faces to turn over": "曲面,或要翻转的面",
|
||||
"Surfaces": "曲面",
|
||||
"Swap Ends": "交换端点",
|
||||
@@ -642,6 +644,7 @@
|
||||
"Target": "目标",
|
||||
"Target Body": "目标实体",
|
||||
"Target Face (optional)": "目标面(可选)",
|
||||
"Target edges": "目标边",
|
||||
"Tear": "撕裂",
|
||||
"Technical details": "技术详情",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
@@ -649,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "方向必须是轴,或位于草图平面内的直线草图曲线",
|
||||
"The mirror line cannot also be mirrored": "镜像线本身不能再被镜像",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "镜像线必须是此草图中的直线曲线,不能是原点轴",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点轴垂直于草图平面,因此在该平面内没有直线",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。",
|
||||
"Then": "操作",
|
||||
"These features will be deleted:": "以下特征将被删除:",
|
||||
@@ -740,6 +744,9 @@
|
||||
"a picked edge's body is gone": "所选边所属的实体已不存在",
|
||||
"add a key for a joint first": "请先为接头添加关键帧",
|
||||
"ai.connected_toast": "SolidCraft 现已可在 {clients} 中使用。请重启它们(设置 > 助手)。",
|
||||
"ai.http.allow": "允许不安全的 HTTP",
|
||||
"ai.http.confirm": "允许此端点",
|
||||
"ai.http.warning": "API 密钥和对话将以未加密的方式发送。任何能够访问该网络的人都可能读取或篡改它们。请仅在可信网络上使用。",
|
||||
"ai.prefs.allow_control": "允许AI代理连接",
|
||||
"ai.prefs.allow_control_note": "本地通道(MCP)。任何内容都不会离开此计算机。\n\n仅在本机开放控制通道(127.0.0.1,端口 27878 或下一个空闲端口)。连接的程序还必须出示一个每次启动时新生成、只有你的用户账户能读取的秘密令牌。下次启动 SolidCraft 时生效。关闭:除非用 --control 启动,否则不监听任何端口。",
|
||||
"ai.prefs.availability": "可用性",
|
||||
@@ -751,6 +758,8 @@
|
||||
"ai.prefs.follow_camera": "跟随相机",
|
||||
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
|
||||
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
|
||||
"ai.prefs.instructions": "自定义指令",
|
||||
"ai.prefs.instructions_note": "在每次对话开始时发送给代理,位于 SolidCraft 自身的指令之后:你喜欢的工作方式、需要遵守的规则。更改从下一次对话(连接或新对话)起生效。",
|
||||
"ai.prefs.intro": "SolidCraft中的AI代理。",
|
||||
"ai.prefs.make_default": "设为默认",
|
||||
"ai.prefs.no_agents": "尚无代理。添加代理或刷新以查找已安装的代理。",
|
||||
@@ -1081,6 +1090,9 @@
|
||||
"cmd.render.compare.divider": "移动渲染比较分隔线",
|
||||
"cmd.render.result.discard": "丢弃渲染结果",
|
||||
"cmd.render.result.select": "选择渲染结果",
|
||||
"cmd.render.surface.import": "导入表面贴图",
|
||||
"cmd.render.texture.list": "列出嵌入的表面图像",
|
||||
"cmd.render.texture.remove": "移除嵌入的表面图像",
|
||||
"cmd.render_aging_describe": "分层表面老化",
|
||||
"cmd.render_backends": "渲染后端",
|
||||
"cmd.render_batch": "渲染所有镜头",
|
||||
@@ -1324,6 +1336,7 @@
|
||||
"cmd.view_previous_tab": "上一个标签页",
|
||||
"cmd.view_rename": "重命名命名视图",
|
||||
"cmd.view_render_frame": "渲染构图辅助线",
|
||||
"cmd.view_ribbon_tabs_only": "仅功能区选项卡",
|
||||
"cmd.view_right": "右视图",
|
||||
"cmd.view_save": "新建命名视图",
|
||||
"cmd.view_shots_strip": "镜头条",
|
||||
@@ -1454,8 +1467,11 @@
|
||||
"file.open.portal_fallback.xwayland": "打开的文件选择器并非桌面自带:在 XWayland 下,门户会拒绝父窗口。",
|
||||
"file_load.built": "已构建:{built}/{total} 个特征。",
|
||||
"file_load.converting": "正在转换实体 {done}/{total}",
|
||||
"file_load.inserted": "已插入 {name}",
|
||||
"file_load.inserting": "正在插入几何体",
|
||||
"file_load.link_item": "{name} ({done}/{total})",
|
||||
"file_load.opened": "已打开 {name}",
|
||||
"file_load.opened_in_tab": "已在新标签页中打开 {name}",
|
||||
"file_load.opening": "正在打开设计",
|
||||
"file_load.preparing_thumbnail": "正在准备缩略图...",
|
||||
"file_load.preparing_view": "正在准备视图...",
|
||||
@@ -1691,7 +1707,7 @@
|
||||
"help.sketch.insert_svg": "将 SVG 图形中的路径读入草图,并按设定进行缩放、旋转和放置。用于把标志或矢量轮廓变成可拉伸的曲线。",
|
||||
"help.sketch.inspect": "打开当前草图的报告:显示剩余的自由度以及是否已完全约束。在完成草图之前,用它找出仍然可以移动的部分。",
|
||||
"help.sketch.intersect": "添加实体、面、边或其他草图与草图平面相交处的曲线或点。它们保持关联,并随模型变化。",
|
||||
"help.sketch.intersection_curve": "在两个实体或面相交处创建与它们关联的三维曲线。用作跟随模型的路径或导轨。",
|
||||
"help.sketch.intersection_curve": "在两个实体、面或其他草图的曲线相交处创建与它们关联的三维曲线。用作跟随模型的路径或导轨。",
|
||||
"help.sketch.isoparametric_curve": "通过你选取的点,沿面的 U 或 V 方向创建与该面关联的三维曲线。用作跟随曲面的路径或分割线。",
|
||||
"help.sketch.line": "逐点绘制相连的直线,可以是开放链或封闭轮廓。这是绘制大多数轮廓的基本工具。",
|
||||
"help.sketch.line.midpoint": "从中点向一端绘制直线,另一端随之对称生成。用于以某点为中心的直线,例如对称的边。",
|
||||
@@ -1797,6 +1813,7 @@
|
||||
"help.view.navigate": "按步骤旋转、平移或缩放视图,例如向左转四分之一圈。用于精确移动相机,而不是靠拖动。",
|
||||
"help.view.navigation_bar": "显示或隐藏视口底部的导航栏。隐藏可腾出空间,显示便于用鼠标使用视图工具。",
|
||||
"help.view.rename": "为已保存的命名视图改名。视图本身保持不变。",
|
||||
"help.view.ribbon_tabs_only": "将功能区折叠为选项卡行,让视图得到面板原先占用的空间。单击选项卡可在视图上方显示其面板;再次选择此命令则始终显示完整的功能区。",
|
||||
"help.view.save": "将当前相机位置作为命名视图保存到设计中。用于以后回到同一角度,例如截图。",
|
||||
"help.view.spin": "让视图持续绕模型环绕,直到下一次点击或按键。用于从各个方向展示模型。",
|
||||
"help.view.tool": "锁定一个相机工具(环绕、平移或缩放),之后用鼠标左键拖动即可移动视图,直到关闭。用于触控板或没有鼠标中键时。",
|
||||
@@ -2316,6 +2333,7 @@
|
||||
"prefs.carousel_previous": "上一个",
|
||||
"prefs.choose_font": "选择字体文件…",
|
||||
"prefs.click": "单击",
|
||||
"prefs.command_dialog_values": "命令对话框",
|
||||
"prefs.contact_shading": "柔和接触阴影",
|
||||
"prefs.contact_shading_note": "在零件相近处添加柔和阴影。",
|
||||
"prefs.context_menu": "上下文菜单",
|
||||
@@ -2411,6 +2429,8 @@
|
||||
"prefs.graphics_page.surfaces_edges": "曲面与边",
|
||||
"prefs.graphics_page.view_bar": "也可在视图栏中设置",
|
||||
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
|
||||
"prefs.ground_shadow_follows_model": "地面阴影跟随模型",
|
||||
"prefs.ground_shadow_follows_model_help": "将阴影放在可见实体的最低点下方。关闭时,抬高模型的阴影仍位于经过原点的地面上。",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "缩小时隐藏约束符号",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "草图缩小到一定程度时隐藏约束符号,以免遮挡草图。关闭:任意缩放比例下符号仍保持固定屏幕大小。",
|
||||
"prefs.history": "历史",
|
||||
@@ -2452,6 +2472,8 @@
|
||||
"prefs.layout_page.card": "卡片",
|
||||
"prefs.layout_page.column": "列",
|
||||
"prefs.layout_page.hover": "悬停",
|
||||
"prefs.layout_page.hover_panel_menus": "悬停切换面板菜单",
|
||||
"prefs.layout_page.hover_panel_menus_help": "点击功能区的面板名称打开菜单。将指针悬停在另一面板名称上即可切换菜单。将指针移到功能区和已打开菜单之外即可关闭菜单。",
|
||||
"prefs.layout_page.icons_labels": "图标+标签",
|
||||
"prefs.layout_page.ok_cancel": "确定和取消",
|
||||
"prefs.layout_page.on_hover": "悬停时",
|
||||
@@ -2554,6 +2576,8 @@
|
||||
"prefs.quality_smoother": "更平滑",
|
||||
"prefs.quick_measure": "在视图角落显示快速测量",
|
||||
"prefs.quick_measure_note": "在视图右下角、历史记录上方显示所选对象的尺寸:边的长度(圆形边显示 Ø 和 R)、面的面积、点的位置、两个对象之间的距离和角度、多条边或多个面的总和。点击即可复制。",
|
||||
"prefs.remember_dialog_values": "记住命令对话框的值",
|
||||
"prefs.remember_dialog_values.help": "默认关闭。开启后,命令成功执行后,在关闭 SolidCraft 前,下次打开同一命令时会重用其安全的值和选项。",
|
||||
"prefs.render_previous_geometry_budget": "使用先前的渲染预览几何内存预算",
|
||||
"prefs.render_previous_geometry_budget_help": "恢复先前的固定几何内存上限(64位桌面为512 MiB,其他平台为128 MiB),替代基于设备的预算。",
|
||||
"prefs.reports_saved": "已保存 {n} 份",
|
||||
@@ -2562,6 +2586,8 @@
|
||||
"prefs.ribbon_click_ends_tool_note": "单击正在使用的草图工具的高亮功能区按钮会结束该工具,在视图中单击即可再次选择。关闭:单击会重新启动该工具。",
|
||||
"prefs.ribbon_reveal": "在功能区中显示搜索结果",
|
||||
"prefs.ribbon_reveal_note": "将指针悬停在“搜索命令”的结果上时,会显示该命令在功能区中的位置:其选项卡、按钮或面板下拉菜单中的行。从搜索启动的命令会在那里闪烁,从下拉菜单运行的命令会标记其面板。",
|
||||
"prefs.ribbon_tabs_only": "仅显示功能区的选项卡",
|
||||
"prefs.ribbon_tabs_only_note": "功能区折叠为选项卡行。单击选项卡会在视图上方显示其面板,直到命令开始或指针离开面板。“视图”选项卡中的“仅功能区选项卡”或其快捷键可切换此项。",
|
||||
"prefs.right_click": "右键单击",
|
||||
"prefs.saving": "启动和保存",
|
||||
"prefs.scale_note": "也可在窗口中使用 Ctrl + / Ctrl −。100% 时按钮高度为 28 像素。",
|
||||
@@ -3914,6 +3940,7 @@
|
||||
"render.stage_panels.extent": "范围",
|
||||
"render.stage_panels.fit_save": "适配并保存",
|
||||
"render.stage_panels.gpu_required": "需要 GPU 路径追踪",
|
||||
"render.stage_panels.grain_angle": "木纹角度",
|
||||
"render.stage_panels.keep": "保留",
|
||||
"render.stage_panels.kept": "保留",
|
||||
"render.stage_panels.kept_fields": "相机、采样、图像尺寸、光学绑定、CAD 几何体和工程属性。",
|
||||
@@ -4032,6 +4059,19 @@
|
||||
"render.subsurface_enable": "内部散射随机游走",
|
||||
"render.subsurface_help": "仅适用于封闭介质。散射和RGB吸收使用独立的每毫米系数。路径在实际几何内部散射,并在边界折射或反射。关闭直线透射阴影、表面降噪和外部体积;使用一个菲涅耳分层。这不是扩散着色器、密度网格或实测材料。高密度可能需要更多样本和路径深度。",
|
||||
"render.subsurface_rate": "散射碰撞率",
|
||||
"render.surface_import.author": "作者",
|
||||
"render.surface_import.busy": "正在导入图像…",
|
||||
"render.surface_import.complete": "请选择图像并填写作者、来源和许可。",
|
||||
"render.surface_import.embedded": "导入的图像",
|
||||
"render.surface_import.failed": "图像导入未完成。",
|
||||
"render.surface_import.license": "许可 / 所有权",
|
||||
"render.surface_import.raytrace_required": "图像需要 CPU 或 GPU 光线追踪。请在输出中选择。",
|
||||
"render.surface_import.repeat_u": "U 方向重复",
|
||||
"render.surface_import.repeat_v": "V 方向重复",
|
||||
"render.surface_import.save": "将图像保存到实体",
|
||||
"render.surface_import.saved_cpu": "保存贴图以用于 CPU 渲染。",
|
||||
"render.surface_import.source": "来源",
|
||||
"render.surface_import.title": "导入图像",
|
||||
"render.tone_aces_fitted": "ACES 拟合",
|
||||
"render.tone_reinhard": "Reinhard",
|
||||
"render.transmission_scope": "GPU 玻璃支持经过验证且互不相交的介质;不支持任意嵌套介质,也不提供无噪声透射估计器。",
|
||||
@@ -4108,6 +4148,8 @@
|
||||
"sample.plate.name": "示例板",
|
||||
"sample.vents.about": "按矩形阵列排列的槽:行数、列数和间距均为参数",
|
||||
"sample.vents.name": "通风板",
|
||||
"scale.per_axis": "按轴",
|
||||
"scale.uniform": "等比",
|
||||
"search.command_keys": "Enter 运行 Shift+Enter 运行并保持打开 Esc 关闭",
|
||||
"search.commands": "命令",
|
||||
"search.design": "此设计中",
|
||||
@@ -4780,6 +4822,7 @@
|
||||
"ui.click_selects_whole_bodies": "单击选择整个实体",
|
||||
"ui.click_then_press_key": "单击,然后按新按键",
|
||||
"ui.close": "关闭",
|
||||
"ui.close_tab": "关闭标签页",
|
||||
"ui.coil": "螺旋",
|
||||
"ui.coincident": "重合",
|
||||
"ui.collapse": "折叠",
|
||||
@@ -4997,6 +5040,11 @@
|
||||
"ui.factor": "系数",
|
||||
"ui.fade": "收尾",
|
||||
"ui.fade_length": "收尾长度",
|
||||
"ui.fair_iterations": "迭代次数",
|
||||
"ui.fair_keep_edges": "保持边",
|
||||
"ui.fair_strength": "强度",
|
||||
"ui.fair_surface_title": "曲面光顺",
|
||||
"ui.fair_tolerance_default": "实体尺寸的 0.001 倍",
|
||||
"ui.feature_history": "特征历史",
|
||||
"ui.feature_options": "特征选项",
|
||||
"ui.features": "特征",
|
||||
@@ -5182,6 +5230,7 @@
|
||||
"ui.inspect": "检查",
|
||||
"ui.instances": "实例数",
|
||||
"ui.intersect": "相交",
|
||||
"ui.intersection_curve_second_pick": "相交曲线:单击第二个面或曲线(右键单击重新开始)",
|
||||
"ui.iso_4753": "ISO 4753",
|
||||
"ui.isolate": "隔离",
|
||||
"ui.join": "连接",
|
||||
@@ -5210,6 +5259,7 @@
|
||||
"ui.manipulator_free_alt": "操纵器自由拖动也适用于 Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "开:在拉伸/小控件箭头上按 Alt/Option 或 Ctrl/Cmd 可关闭数值捕捉。关:仅 Ctrl/Cmd。",
|
||||
"ui.manufacture": "制造",
|
||||
"ui.match_surface_title": "曲面匹配",
|
||||
"ui.material_review.axes": "材料局部坐标轴;不指定实体方向。",
|
||||
"ui.material_review.claims": "来源和许可证声明未经核实。缺失值保持未知。",
|
||||
"ui.material_review.compressive_strength": "抗压强度",
|
||||
@@ -5232,6 +5282,9 @@
|
||||
"ui.material_review.title": "检查物理材料数据",
|
||||
"ui.material_review.unknown": "未知",
|
||||
"ui.material_review.yield_strength": "屈服强度",
|
||||
"ui.mcp_agents": "通过 MCP 连接的代理",
|
||||
"ui.mcp_compact": "紧凑输出",
|
||||
"ui.mcp_compact_note": "适用于通过 MCP 服务器 (solidcraft-cli mcp) 连接的代理:inspect_design 汇总草图,工具结果不做美化排版,从而占用更少的 token。更改后新开始的代理会话生效;--compact 或 SOLIDCRAFT_MCP_COMPACT=1 也会开启它。",
|
||||
"ui.measure": "测量",
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "带面标签的 3MF",
|
||||
@@ -5273,6 +5326,7 @@
|
||||
"ui.options": "选项",
|
||||
"ui.orbit": "环绕",
|
||||
"ui.orbit_around": "环绕",
|
||||
"ui.organize": "整理",
|
||||
"ui.orientation": "定向",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_construction_mode": "构造模式 (X)",
|
||||
@@ -5317,6 +5371,7 @@
|
||||
"ui.point_cloud_add_filter": "添加筛选…",
|
||||
"ui.point_cloud_auto_range": "自动范围",
|
||||
"ui.point_cloud_color_by": "着色方式",
|
||||
"ui.point_cloud_dots": "独立点",
|
||||
"ui.point_cloud_file_colors": "文件颜色",
|
||||
"ui.point_cloud_filter_by": "筛选依据",
|
||||
"ui.point_cloud_height_high": "最高高度",
|
||||
|
||||
+166
-23
@@ -883,10 +883,10 @@ fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
// Point clouds (`view/clouds.rs`): each point is a round dot a few pixels across, facing the
|
||||
// screen, depth-tested and written like the model's faces, so the model hides the points behind
|
||||
// it and they hide it. The cloud's style colours them by their own colour, a ramp or a palette,
|
||||
// and shows only those whose filtered values lie in its ranges or sets.
|
||||
// Point clouds and their cells (`view/clouds.rs`): dots, segments as beads (or lines when they
|
||||
// have no size) and triangles, from one record per element. The cloud's style colours them by
|
||||
// their own colour, a ramp or a palette, and shows only those whose filtered values lie in its
|
||||
// ranges or sets; values vary across an element, so a range cuts it where its value crosses an end.
|
||||
|
||||
struct Cloud {
|
||||
style: vec4<f32>, // x: colour (0 own, 1 ramp, 2 palette), y: dot size (px), z, w: ramp range
|
||||
@@ -895,43 +895,88 @@ struct Cloud {
|
||||
@group(1) @binding(3) var<uniform> cloud: Cloud;
|
||||
|
||||
struct CIn {
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(1) v: f32,
|
||||
@location(2) f: vec4<f32>,
|
||||
@location(3) rgb: vec4<f32>,
|
||||
@location(0) a: vec3<f32>,
|
||||
@location(1) b: vec3<f32>,
|
||||
@location(2) c: vec3<f32>,
|
||||
@location(8) before: vec3<f32>,
|
||||
@location(9) after: vec3<f32>,
|
||||
@location(3) v: vec3<f32>,
|
||||
@location(4) f0: vec3<f32>,
|
||||
@location(5) f1: vec3<f32>,
|
||||
@location(10) f2: vec3<f32>,
|
||||
@location(11) f3: vec3<f32>,
|
||||
@location(6) size: vec2<f32>,
|
||||
@location(7) rgb: vec4<f32>,
|
||||
};
|
||||
|
||||
struct COut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) wp: vec3<f32>,
|
||||
@location(1) v: f32,
|
||||
@location(2) f: vec4<f32>,
|
||||
@location(3) rgb: vec3<f32>,
|
||||
@location(4) at: vec2<f32>, // the place on its disc
|
||||
@location(5) @interpolate(flat) has: u32,
|
||||
@location(1) n: vec3<f32>, // zero: not lit
|
||||
@location(2) v: f32,
|
||||
@location(3) f: vec4<f32>,
|
||||
@location(4) rgb: vec3<f32>,
|
||||
@location(5) at: vec2<f32>, // a dot's place on its disc
|
||||
@location(6) @interpolate(flat) has: u32,
|
||||
};
|
||||
|
||||
fn cloud_out(i: CIn, w: vec3<f32>, wp: vec3<f32>, pos: vec4<f32>, n: vec3<f32>) -> COut {
|
||||
var o: COut;
|
||||
o.pos = pos;
|
||||
o.wp = wp;
|
||||
o.n = n;
|
||||
o.v = dot(w, i.v);
|
||||
o.f = vec4<f32>(dot(w, i.f0), dot(w, i.f1), dot(w, i.f2), dot(w, i.f3));
|
||||
o.rgb = i.rgb.rgb;
|
||||
o.at = vec2<f32>(0.0);
|
||||
o.has = u32(round(i.rgb.a * 255.0));
|
||||
return o;
|
||||
}
|
||||
|
||||
fn behind(c: vec4<f32>) -> bool {
|
||||
return near_z(c) < 0.0;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_cloud_dot(@builtin(vertex_index) vi: u32, i: CIn) -> COut {
|
||||
var xs = array<f32, 6>(-1.0, 1.0, 1.0, -1.0, 1.0, -1.0);
|
||||
var ys = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||
let at = vec2<f32>(xs[vi], ys[vi]);
|
||||
let size = max(cloud.style.y, 1.0);
|
||||
let centre = u.vp * vec4<f32>(i.p, 1.0);
|
||||
var o: COut;
|
||||
o.pos = vec4<f32>(centre.xy + at * size / u.screen.xy * centre.w, centre.z, centre.w);
|
||||
// Behind the eye: nothing.
|
||||
if (near_z(centre) < 0.0) { o.pos = vec4<f32>(0.0, 0.0, 0.0, -1.0); }
|
||||
o.wp = i.p;
|
||||
o.v = i.v;
|
||||
o.f = i.f;
|
||||
o.rgb = i.rgb.rgb;
|
||||
o.has = u32(round(i.rgb.a * 255.0));
|
||||
let centre = u.vp * vec4<f32>(i.a, 1.0);
|
||||
var pos = vec4<f32>(centre.xy + at * size / u.screen.xy * centre.w, centre.z, centre.w);
|
||||
if (behind(centre)) { pos = vec4<f32>(0.0, 0.0, 0.0, -1.0); }
|
||||
var o = cloud_out(i, vec3<f32>(1.0, 0.0, 0.0), i.a, pos, vec3<f32>(0.0));
|
||||
// Dots under two pixels are squares: a disc that small loses its centre pixel.
|
||||
o.at = at * select(1.0, 0.0, size < 2.0);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// The least half-width of a segment on screen, in pixels.
|
||||
const MIN_HALF_PX: f32 = 0.75;
|
||||
|
||||
/// The frame of a bead along `d`: along it, across it level, and up, its height axis.
|
||||
fn bead_frame(d: vec3<f32>) -> mat3x3<f32> {
|
||||
var t = vec3<f32>(1.0, 0.0, 0.0);
|
||||
if (length(d) > 1e-9) { t = d / length(d); }
|
||||
var side = cross(vec3<f32>(0.0, 0.0, 1.0), t);
|
||||
if (length(side) < 1e-6) { side = vec3<f32>(1.0, 0.0, 0.0); }
|
||||
side = normalize(side);
|
||||
return mat3x3<f32>(t, side, cross(t, side));
|
||||
}
|
||||
|
||||
/// Corner `j` (0…9) of a bead's cross-section, a stadium: its place across and up, and its
|
||||
/// normal. Corners 0…4 go up the right half-disc, 5…9 down the left one, so 4–5 is the flat top
|
||||
/// and 9–0 the flat bottom.
|
||||
fn stadium(j: u32, size: vec2<f32>) -> vec4<f32> {
|
||||
let k = j % 10u;
|
||||
let right = k < 5u;
|
||||
let a = -1.5707963 + 0.7853982 * f32(k % 5u) + select(3.1415927, 0.0, right);
|
||||
let r = 0.5 * size.y;
|
||||
let flat_half = 0.5 * max(size.x - size.y, 0.0);
|
||||
return vec4<f32>(select(-flat_half, flat_half, right) + r * cos(a), r * sin(a), cos(a), sin(a));
|
||||
}
|
||||
|
||||
/// Whether filter `f` keeps the value `v`.
|
||||
fn cloud_keeps(f: vec4<f32>, v: f32) -> bool {
|
||||
if (f.x < 0.5) { return true; }
|
||||
@@ -940,6 +985,101 @@ fn cloud_keeps(f: vec4<f32>, v: f32) -> bool {
|
||||
return k >= 0 && k < 32 && ((bitcast<u32>(f.w) >> u32(k)) & 1u) != 0u;
|
||||
}
|
||||
|
||||
/// Whether filter `f` leaves out a whole element with values `v` at its corners: all on one
|
||||
/// side of a range, or one value at every corner, not kept. A set of values with corners that
|
||||
/// differ may keep part of the element: its fragments decide.
|
||||
fn cloud_drops(f: vec4<f32>, v: vec3<f32>) -> bool {
|
||||
if (f.x > 1.5) { return all(v == vec3<f32>(v.x)) && !cloud_keeps(f, v.x); }
|
||||
return f.x > 0.5 && (all(v < vec3<f32>(f.y)) || all(v > vec3<f32>(f.z)));
|
||||
}
|
||||
|
||||
/// Whether a filter leaves out the whole element.
|
||||
fn filtered_out(i: CIn) -> bool {
|
||||
let has = u32(round(i.rgb.a * 255.0));
|
||||
let values = array<vec3<f32>, 4>(i.f0, i.f1, i.f2, i.f3);
|
||||
for (var k = 0u; k < 4u; k++) {
|
||||
if (((has >> k) & 1u) != 0u && cloud_drops(cloud.filters[k], values[k])) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The part in front of the near plane of the segment between clip-space `a` and `b`, as
|
||||
/// fractions of it from `a`; empty (first above second) when all of it is behind. Clip-space
|
||||
/// coordinates are affine along a line, so the crossing is exact.
|
||||
fn in_front(a: vec4<f32>, b: vec4<f32>) -> vec2<f32> {
|
||||
let za = near_z(a);
|
||||
let zb = near_z(b);
|
||||
if (za < 0.0 && zb < 0.0) { return vec2<f32>(1.0, 0.0); }
|
||||
let t = za / (za - zb);
|
||||
return vec2<f32>(select(0.0, t, za < 0.0), select(1.0, t, zb < 0.0));
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_cloud_segment(@builtin(vertex_index) vi: u32, i: CIn) -> COut {
|
||||
// Corner `vi % 10` of the cross-section at the start (`vi < 10`) or the end; the index
|
||||
// buffer joins them into the bead's sides.
|
||||
let pa = u.vp * vec4<f32>(i.a, 1.0);
|
||||
let pb = u.vp * vec4<f32>(i.b, 1.0);
|
||||
let part = in_front(pa, pb);
|
||||
if (filtered_out(i) || part.x > part.y) {
|
||||
return cloud_out(i, vec3<f32>(1.0, 0.0, 0.0), i.a, vec4<f32>(0.0, 0.0, 0.0, -1.0), vec3<f32>(0.0));
|
||||
}
|
||||
// Where the near plane cuts it, the bead ends there, square; its numbers are exact at `e`.
|
||||
let at_end = vi >= 10u;
|
||||
let e = select(part.x, part.y, at_end);
|
||||
let cut = e != select(0.0, 1.0, at_end);
|
||||
let f = bead_frame(i.b - i.a);
|
||||
let c = stadium(vi % 10u, i.size);
|
||||
let axis = mix(i.a, i.b, e);
|
||||
// Where it meets the next or the previous bead, the cross-section lies in the plane halfway
|
||||
// between their directions, stretched across to keep its width: both beads end on the same
|
||||
// section. A turn sharper than 120° ends square instead.
|
||||
let other = select(i.a - i.before, i.after - i.b, at_end);
|
||||
var across_dir = f[1];
|
||||
var stretch = 1.0;
|
||||
if (!cut && length(other) > 1e-9) {
|
||||
let g = bead_frame(other);
|
||||
let mid = f[1] + g[1];
|
||||
if (length(mid) > 1e-6 && dot(normalize(mid), f[1]) > 0.5) {
|
||||
across_dir = normalize(mid);
|
||||
stretch = 1.0 / dot(across_dir, f[1]);
|
||||
}
|
||||
}
|
||||
let wp = axis + across_dir * (c.x * stretch) + f[2] * c.y;
|
||||
let ca = u.vp * vec4<f32>(axis, 1.0);
|
||||
let cw = u.vp * vec4<f32>(wp, 1.0);
|
||||
let half = u.screen.xy * 0.5;
|
||||
var pos = cw;
|
||||
// At least a pixel and a half across: a bead seen from afar, or a line without size. A corner
|
||||
// behind the eye has no place on the screen and needs no widening: the rasteriser clips it.
|
||||
if (cw.w > 0.0) {
|
||||
let c0 = mix(pa, pb, part.x);
|
||||
let c1 = mix(pa, pb, part.y);
|
||||
let along = (c1.xy / c1.w - c0.xy / c0.w) * half;
|
||||
var across = vec2<f32>(-along.y, along.x);
|
||||
if (length(across) < 1e-6) { across = vec2<f32>(1.0, 0.0); }
|
||||
across = normalize(across);
|
||||
var px = (cw.xy / cw.w - ca.xy / ca.w) * half;
|
||||
if (length(px) < MIN_HALF_PX * abs(c.z)) {
|
||||
px = across * c.z * MIN_HALF_PX;
|
||||
pos = vec4<f32>((ca.xy / ca.w + px / half) * cw.w, cw.z, cw.w);
|
||||
}
|
||||
}
|
||||
let n = select(f[1] * c.z + f[2] * c.w, vec3<f32>(0.0), i.size.y <= 0.0);
|
||||
return cloud_out(i, vec3<f32>(1.0 - e, e, 0.0), wp, pos, n);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_cloud_triangle(@builtin(vertex_index) vi: u32, i: CIn) -> COut {
|
||||
// Not `w[vi] = 1.0`: the Windows D3D12 shader compiler cannot write through a dynamic vector index.
|
||||
let w = vec3<f32>(f32(vi == 0u), f32(vi == 1u), f32(vi == 2u));
|
||||
let wp = i.a * w.x + i.b * w.y + i.c * w.z;
|
||||
var n = cross(i.b - i.a, i.c - i.a);
|
||||
if (length(n) > 0.0) { n = normalize(n); }
|
||||
// The true clip-space corner, even behind the eye: the rasteriser clips a triangle exactly.
|
||||
return cloud_out(i, w, wp, u.vp * vec4<f32>(wp, 1.0), n);
|
||||
}
|
||||
|
||||
fn cloud_ramp(t: f32) -> vec3<f32> {
|
||||
var stops = array<vec3<f32>, 5>(
|
||||
vec3<f32>(40.0, 70.0, 200.0), vec3<f32>(30.0, 170.0, 230.0), vec3<f32>(60.0, 200.0, 90.0),
|
||||
@@ -974,5 +1114,8 @@ fn fs_cloud(o: COut) -> @location(0) vec4<f32> {
|
||||
} else if ((o.has & 16u) != 0u && mode > 0.5) {
|
||||
c = cloud_ramp((o.v - cloud.style.z) / max(cloud.style.w - cloud.style.z, 1e-12));
|
||||
}
|
||||
if (length(o.n) > 0.5) {
|
||||
c = c * (0.45 + 0.55 * abs(dot(normalize(o.n), normalize(u.back.xyz))));
|
||||
}
|
||||
return out_color(vec4<f32>(c, 1.0));
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
| assets/locales/pending/ribbon-panel-hover.json | SolidCraft contributors | Original ribbon panel menu preference labels and translations | MIT OR Apache-2.0 |
|
||||
@@ -1487,7 +1487,7 @@ pub enum FeatureKind {
|
||||
},
|
||||
/// Plastic lip (raised band) or groove (recess) along the rim face picked at `face`.
|
||||
Lip {
|
||||
face: Position,
|
||||
face: FacePick,
|
||||
#[walk(length = "Width")]
|
||||
width: Expr,
|
||||
#[walk(length = "Height")]
|
||||
@@ -3910,6 +3910,7 @@ impl Document {
|
||||
/// Read a design file's JSON (any format this build knows; older ones are upgraded).
|
||||
pub fn from_json(s: &str) -> Result<Document> {
|
||||
crate::render_resources::check_json_budget(s)?;
|
||||
crate::render_texture_resources::check_json_budget(s)?;
|
||||
let mut d = crate::format::read(s)?;
|
||||
if d.features.len() > MAX_FEATURES || d.params.len() > MAX_PARAMS {
|
||||
return Err(DocError::Invalid("document too large".into()));
|
||||
|
||||
+93
-6
@@ -784,7 +784,7 @@ impl ModelState {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
serde_json::to_string(&s.plane).unwrap_or_default().hash(&mut h);
|
||||
let mut sketch = s.sketch.clone();
|
||||
quantize(&mut sketch);
|
||||
fingerprint_sketch(&mut sketch);
|
||||
serde_json::to_string(&sketch).unwrap_or_default().hash(&mut h);
|
||||
s.report.ok().hash(&mut h);
|
||||
self.sketch_keys.insert(feature, h.finish());
|
||||
@@ -1253,6 +1253,13 @@ fn quantize(sk: &mut solidcraft_sketch::Sketch) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The stored solve as a cache key: on the [`QUANTUM`] grid, without the editor state
|
||||
/// (see [`solidcraft_sketch::Sketch::clear_id_counters`]).
|
||||
fn fingerprint_sketch(sk: &mut solidcraft_sketch::Sketch) {
|
||||
quantize(sk);
|
||||
sk.clear_id_counters();
|
||||
}
|
||||
|
||||
/// A feature as fingerprinted: its definition, a sketch's stored solve quantized, an imported
|
||||
/// file's text or mesh payload by its content digest.
|
||||
/// Hash [`feature_json`] of `f` into `h`. Every evaluation fingerprints every feature, and in a
|
||||
@@ -1319,7 +1326,7 @@ fn feature_json(f: &Feature, steps: &ImportDigests) -> String {
|
||||
if let FeatureKind::Sketch { .. } = &f.kind {
|
||||
let mut g = f.clone();
|
||||
if let FeatureKind::Sketch { sketch, .. } = &mut g.kind {
|
||||
quantize(sketch);
|
||||
fingerprint_sketch(sketch);
|
||||
}
|
||||
return serde_json::to_string(&g).unwrap_or_default();
|
||||
}
|
||||
@@ -1570,6 +1577,9 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
||||
#[derive(Default)]
|
||||
struct PlacementReads {
|
||||
all: bool,
|
||||
/// A body named that `st` lacks: a further placement's (`Block (Part:2)`), found through
|
||||
/// its occurrence (#9957), or gone.
|
||||
unlisted: bool,
|
||||
components: std::collections::BTreeSet<u64>,
|
||||
paths: Vec<Vec<u64>>,
|
||||
bodies: std::collections::BTreeSet<String>,
|
||||
@@ -1608,14 +1618,17 @@ impl PlacementReads {
|
||||
}
|
||||
}
|
||||
|
||||
/// The component of body `name` in `st`; every component when the body is not there (a
|
||||
/// projection then takes the body nearest its point, wherever that is).
|
||||
/// The component of body `name` in `st`; every component and every placement when the body
|
||||
/// is not there (a further placement's body, or the body nearest its point, wherever that is).
|
||||
fn body(&mut self, doc: &Document, st: Option<&ModelState>, name: &str) {
|
||||
match st.and_then(|st| st.body(name)) {
|
||||
Some(b) => {
|
||||
self.components.insert(doc.model_body_component(b));
|
||||
}
|
||||
None => self.all = true,
|
||||
None => {
|
||||
self.all = true;
|
||||
self.unlisted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1851,6 +1864,9 @@ fn placement_digest(doc: &Document, f: &Feature, st: Option<&ModelState>) -> u64
|
||||
}
|
||||
}
|
||||
hash_placed_bodies(doc, st, &reads.bodies, &inverse, &mut h);
|
||||
if reads.unlisted {
|
||||
placements_hash(doc).hash(&mut h);
|
||||
}
|
||||
// Legacy and component-wide reads still need the conservative offset dependency. Body
|
||||
// reads above already include just their own offsets, in the frame that evaluates them.
|
||||
if reads.all || !reads.components.is_empty() || !reads.paths.is_empty() {
|
||||
@@ -6726,6 +6742,24 @@ fn replay(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A moved copy is copied again as a body: each instance is the copy the Move made,
|
||||
// placed by the pattern (#5931).
|
||||
if let FeatureKind::Move { copy: true, .. } = &src.kind {
|
||||
let made: Vec<ModelBody> = st.bodies.iter().filter(|b| b.feature == src.id).cloned().collect();
|
||||
if made.is_empty() {
|
||||
return Err(DocError::Invalid(format!("{} made no copy to repeat", src.name)));
|
||||
}
|
||||
let mut output = 0;
|
||||
for (k, m) in mats.iter().enumerate() {
|
||||
for mb in &made {
|
||||
let moved = moved_copy(mb, kernel::transform_matrix(&mb.body, *m)?, first + k + 1);
|
||||
let name = copy_body_name(st, f, output, &mb.name);
|
||||
st.bodies.push(ModelBody::traced_new(name, moved, f.id));
|
||||
output += 1;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (op, targets) = feature_op(src);
|
||||
// A primitive's or a hole's targets are its participants (#1108): the copies change
|
||||
// those bodies only, like the feature itself (#2755).
|
||||
@@ -7232,7 +7266,7 @@ fn eval_feature(
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::ReplaceFace { .. }
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. } => more::eval(doc, vals, f, st),
|
||||
| FeatureKind::Remove { .. } => more::eval(doc, vals, f, st, warning),
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
let fst = match anchor {
|
||||
@@ -7243,6 +7277,14 @@ fn eval_feature(
|
||||
None if !plane_reads_geometry(plane) && sketch.links.is_empty() => std::borrow::Cow::Borrowed(st),
|
||||
None => state_in_frame(doc, st, f.component)?,
|
||||
};
|
||||
// A further placement a link reads is placed in its frame too (#9957).
|
||||
let named: Vec<&str> = sketch.links.iter().flat_map(|l| l.source.bodies()).collect();
|
||||
let fst = match anchor.map(|anchor| anchor.author_frame(doc, f.component)) {
|
||||
_ if named.is_empty() => fst,
|
||||
Some(Ok(frame)) => with_placed_copies(doc, st, fst, &frame, &named),
|
||||
Some(Err(_)) => fst,
|
||||
None => with_placed_copies(doc, st, fst, &doc.component_transform(f.component), &named),
|
||||
};
|
||||
let (plane, face_warning) = match (anchor, plane) {
|
||||
(Some(_), plane) => doc.resolve_attached_plane_noted(vals, plane, f.component, st, &fst)?,
|
||||
// A flat face of a sheet: the panel's exact plane, from its flat region and fold.
|
||||
@@ -8480,6 +8522,51 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
|
||||
Ok(Cow::Owned(out))
|
||||
}
|
||||
|
||||
/// `framed` (the model `st` in the frame that `frame` places in the world) with the further
|
||||
/// placements `names` names (`Block (Part:2)`), where they are in the world. A component's model
|
||||
/// holds its first placement only, so a sketch projecting a copy's face found no body of that
|
||||
/// name and took the first placement's (#9957). Only the named ones are placed: placing every
|
||||
/// body of an assembly for each sketch made sketch edits as slow as the design is big.
|
||||
pub fn with_placed_copies<'a>(
|
||||
doc: &Document,
|
||||
st: &ModelState,
|
||||
framed: std::borrow::Cow<'a, ModelState>,
|
||||
frame: &Mat,
|
||||
names: &[&str],
|
||||
) -> std::borrow::Cow<'a, ModelState> {
|
||||
let missing: Vec<&str> = names.iter().copied().filter(|n| !n.is_empty() && framed.body(n).is_none()).collect();
|
||||
if missing.is_empty() || doc.occurrences.is_empty() {
|
||||
return framed;
|
||||
}
|
||||
let Some(inv) = crate::mat_inverse(frame) else { return framed };
|
||||
let mut by_component: std::collections::HashMap<u64, Vec<&ModelBody>> = std::collections::HashMap::new();
|
||||
for (b, owner) in st.bodies.iter().zip(doc.model_body_components(st)) {
|
||||
by_component.entry(owner).or_default().push(b);
|
||||
}
|
||||
let world = crate::WorldNames::new(doc, st);
|
||||
let mut placed: Vec<ModelBody> = Vec::new();
|
||||
for (comp, path, m) in doc.placements() {
|
||||
for b in by_component.get(&comp).into_iter().flatten() {
|
||||
let name = world.name(&b.name, comp, &path);
|
||||
if !missing.contains(&name.as_str()) || placed.iter().any(|p| p.name == name) {
|
||||
continue;
|
||||
}
|
||||
let bm = doc.model_body_offset(b).map(|o| crate::mat_mul(&m, o)).unwrap_or(m);
|
||||
if let Ok(mut copy) = st.body_in_frame(b, &crate::mat_mul(&inv, &bm)) {
|
||||
copy.name = name;
|
||||
copy.source_name = Some(b.definition_name().to_string());
|
||||
placed.push(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
if placed.is_empty() {
|
||||
return framed;
|
||||
}
|
||||
let mut out = framed.into_owned();
|
||||
out.bodies.extend(placed);
|
||||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
|
||||
/// The model placed in the world: each component's bodies, sketches, threads and construction
|
||||
/// geometry through every occurrence of it (bodies of the first placement keep their names;
|
||||
/// further instances get the occurrence path in brackets). Without components this is the model
|
||||
|
||||
@@ -1759,3 +1759,30 @@ fn mesh_metadata_and_order_remain_part_of_the_fingerprint() {
|
||||
same_as_fresh(&doc, &model);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_added_and_deleted_sketch_entity_keeps_its_fingerprint() {
|
||||
let mut doc = Document::new("sketch fingerprint");
|
||||
let id = doc.add_feature(line_sketch(PlaneRef::Origin { name: "XY".into() }, Vec2::new(10.0, 0.0)), None).unwrap();
|
||||
let mut model = Model::new();
|
||||
model.evaluate(&doc);
|
||||
let key = model.state().sketch_keys.get(&id).copied();
|
||||
let json = super::feature_json(doc.feature(id).unwrap(), &super::ImportDigests::default());
|
||||
|
||||
// Make and delete an id: the counters advance, but the geometry is the same.
|
||||
{
|
||||
let sk = doc.sketch_mut(id).unwrap();
|
||||
let a = sk.add_point(Vec2::new(2.0, 2.0), None).unwrap();
|
||||
let b = sk.add_point(Vec2::new(3.0, 2.0), None).unwrap();
|
||||
let line = sk.add_line_pts(a, b, None).unwrap();
|
||||
sk.remove_curves(&[line]);
|
||||
sk.remove_points(&[b]);
|
||||
sk.remove_points(&[a]);
|
||||
}
|
||||
|
||||
let json_after = super::feature_json(doc.feature(id).unwrap(), &super::ImportDigests::default());
|
||||
assert_eq!(json, json_after, "deleted ids are not geometry");
|
||||
model.evaluate(&doc);
|
||||
assert_eq!(model.state().sketch_keys.get(&id).copied(), key);
|
||||
assert_eq!((model.last_recomputed, model.last_restored), (0, 0));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::sheet;
|
||||
use solidcore_record::{Loop2, Seg2};
|
||||
use solidcraft_geom::{Aabb3, Ring};
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face } => {
|
||||
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
|
||||
@@ -59,7 +59,20 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
if st.bodies.is_empty() {
|
||||
return Err(DocError::Invalid("there is no body to emboss".into()));
|
||||
}
|
||||
apply_op(doc, st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
|
||||
// #10940: an emboss or deboss whose tool lies clear of the body (a sketch on the far
|
||||
// side of the material, a depth that stops short) used to report success and change
|
||||
// nothing. Say so: the body is compared before and after, as the command's reply does.
|
||||
let before: Vec<Body> = st.bodies.iter().map(|b| b.body.clone()).collect();
|
||||
apply_op(doc, st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))?;
|
||||
let unchanged = st.bodies.len() == before.len() && st.bodies.iter().zip(&before).all(|(b, old)| old.same(&b.body));
|
||||
if unchanged {
|
||||
let what = if *deboss { "deboss" } else { "emboss" };
|
||||
append_warning(
|
||||
warning,
|
||||
format!("the {what} changed nothing; check where its profile lies, which side of the face the sketch is on, and the depth"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Rib { .. } => {
|
||||
let tools = rib_tools(vals, st, f, None)?;
|
||||
|
||||
@@ -63,13 +63,16 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
let mut tools = Vec::new();
|
||||
for n in bodies {
|
||||
let b = st.body(n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
tools.push(kernel::thicken(&b.body, t, *symmetric)?);
|
||||
let b = st.body(n).cloned().ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
// Kept face, offset and edge walls. Carry that history so a sketch can attach
|
||||
// to the solid's faces and follow the thickness (#7641).
|
||||
let thickened = kernel::thicken(&b.body, t, *symmetric)?;
|
||||
tools.push(Traced::step(thickened, &[&b.traced()]));
|
||||
}
|
||||
if tools.is_empty() {
|
||||
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
|
||||
}
|
||||
apply_op(doc, st, f, tools, *operation, &crate::plain(targets))
|
||||
apply_traced(doc, st, f, tools, *operation, &crate::plain(targets))
|
||||
}
|
||||
FeatureKind::SurfaceTrim { body, plane, tool, keep, keep_more, curves, with, keep_with, stitch, extend } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
|
||||
@@ -67,6 +67,7 @@ impl FeatureKind {
|
||||
| FeatureKind::ReplaceFace { body, .. }
|
||||
| FeatureKind::SurfaceOffset { body, .. }
|
||||
| FeatureKind::Boss { body, .. }
|
||||
| FeatureKind::Lip { body, .. }
|
||||
| FeatureKind::SnapFit { body, .. }
|
||||
| FeatureKind::Rest { body, .. } => body.as_deref(),
|
||||
FeatureKind::SheetConvert { body, .. } | FeatureKind::SurfaceRuled { body, .. } => Some(body.as_ref()),
|
||||
|
||||
@@ -150,6 +150,7 @@ boss
|
||||
face points: [0, 0, 0]
|
||||
lip
|
||||
moves: face[0], face[1], face[2]
|
||||
face points: [0, 0, 0]
|
||||
snap_fit
|
||||
moves: direction[0], direction[2], hook[0], hook[1], position[0], position[1], position[2]
|
||||
face points: [0, 0, 0]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Saves exact encoded texture bytes, inert provenance and explicit saved surface roles.
|
||||
//! Older designs have no embedded textures and retain their built-in surface bindings.
|
||||
//! The last release asks for an update instead of silently dropping the new resources.
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A sketch's intersection link can save a curve of another sketch as either side (`a` or `b`,
|
||||
//! `{"type": "sketch_curve", sketch, curve}`), standing in for that curve's extrusion along its
|
||||
//! sketch's normal (#11048). An older design has only bodies and faces there, and opens as before.
|
||||
//! The last release refuses a design that has one (it asks for an update) instead of opening the
|
||||
//! link as lost.
|
||||
@@ -0,0 +1,6 @@
|
||||
//! A point cloud keeps the segments and triangles its file draws over its points (#8817):
|
||||
//! `segments` and `triangles` in its `complex`, the indices of their points, and a column of named
|
||||
//! numbers may lie on them (`on`: `segment` or `triangle`; a column that does not say is on the
|
||||
//! points, as every design saved before this has it). Older designs need no transformation: their
|
||||
//! clouds are points alone. The last release refuses a design saved with this (the newer format
|
||||
//! number) instead of opening its clouds as loose points without their edges and faces.
|
||||
@@ -77,6 +77,7 @@ pub mod render_optics;
|
||||
pub mod render_resources;
|
||||
pub mod render_settings;
|
||||
pub mod render_surface;
|
||||
pub mod render_texture_resources;
|
||||
pub mod sheet;
|
||||
pub mod sketch_equation;
|
||||
pub mod text_params;
|
||||
@@ -92,8 +93,8 @@ pub use eval::{
|
||||
blend_target, curve_pick_error, draft_corner_options, edge_names_for, edge_refs, face_point_names, face_point_state, hole_direction_at,
|
||||
hole_on_surface, import_body_names, join_pieces_added, map_wire, metric_thread_sizes, object_height, parse_metric_thread, pattern_matrices,
|
||||
revolve_extent, revolve_extent_for_handle, revolve_extent_with_convention, select_profiles, sketch_hole_direction, sketch_line_plane,
|
||||
state_in_frame, state_in_frame_at, suggested_thread, suggested_thread_in, tangent_face_chain, through_all_distance, world_state,
|
||||
world_state_reusing,
|
||||
state_in_frame, state_in_frame_at, suggested_thread, suggested_thread_in, tangent_face_chain, through_all_distance, with_placed_copies,
|
||||
world_state, world_state_reusing,
|
||||
};
|
||||
pub use eval::{EvalCache, KeptResults};
|
||||
pub use expr::Expr;
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expr::{self, Expr, Kind, ParamDef, Value};
|
||||
use crate::sheet::rule_references;
|
||||
use crate::walk::Walk;
|
||||
use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result};
|
||||
|
||||
@@ -797,13 +798,14 @@ impl Document {
|
||||
}
|
||||
}
|
||||
let uses = |e: &str| expr::references(e).iter().any(|r| r == name);
|
||||
let rule_uses = |field: &str, e: &str| rule_references(field, e).iter().any(|r| r == name);
|
||||
for r in &self.sheet.rules {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(_, e, _)| uses(e)) {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(f, e, _)| rule_uses(f, e)) {
|
||||
out.push(ParamUser::Rule { kind: "sheet metal".into(), name: r.name.clone(), field: field.into() });
|
||||
}
|
||||
}
|
||||
for r in &self.plastic.rules {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(_, e, _)| uses(e)) {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(f, e, _)| rule_uses(f, e)) {
|
||||
out.push(ParamUser::Rule { kind: "plastic".into(), name: r.name.clone(), field: field.into() });
|
||||
}
|
||||
}
|
||||
@@ -855,6 +857,21 @@ impl Document {
|
||||
if self.name_taken(new) {
|
||||
return Err(DocError::Invalid(format!("there is already a parameter `{new}`")));
|
||||
}
|
||||
// Outside a rule's own thickness, `Thickness` is the rule's thickness (#9623): renaming to
|
||||
// it would turn those references into the rule's thickness and change their values.
|
||||
if new == "Thickness" {
|
||||
let uses_old = |field: &str, e: &str| field != "thickness" && expr::references(e).iter().any(|r| r == old);
|
||||
let sheet = self.sheet.rules.iter().flat_map(|r| r.fields().into_iter().map(move |(f, e, _)| ("sheet metal", &r.name, f, e)));
|
||||
let plastic = self.plastic.rules.iter().flat_map(|r| r.fields().into_iter().map(move |(f, e, _)| ("plastic", &r.name, f, e)));
|
||||
let list: Vec<String> =
|
||||
sheet.chain(plastic).filter(|(_, _, f, e)| uses_old(f, e)).map(|(kind, name, f, _)| format!("{kind} rule {name} ({f})")).collect();
|
||||
if !list.is_empty() {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"`{old}` can't be renamed to `Thickness`: {} would read the rule's thickness instead",
|
||||
list.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut n = 0;
|
||||
let text = self.text_param(old).is_some();
|
||||
for p in &mut self.text_params {
|
||||
@@ -964,7 +981,8 @@ impl Document {
|
||||
}
|
||||
// Sheet metal and plastic rules and canvas planes hold expressions too (#4420).
|
||||
let rules = self.sheet.rules.iter_mut().flat_map(|r| r.fields_mut()).chain(self.plastic.rules.iter_mut().flat_map(|r| r.fields_mut()));
|
||||
for (_, e) in rules {
|
||||
// `Thickness` outside a rule's own thickness is the rule's, not the parameter (#9623).
|
||||
for (_, e) in rules.filter(|(field, _)| old != "Thickness" || *field == "thickness") {
|
||||
let r = expr::rename_reference(e, old, new);
|
||||
if r != *e {
|
||||
*e = r;
|
||||
@@ -1025,10 +1043,10 @@ impl Document {
|
||||
}
|
||||
// Sheet metal and plastic features rebuild when their rules' parameters change (#4420).
|
||||
if f.kind.is_sheet() {
|
||||
names.extend(self.sheet.rules.iter().flat_map(|r| r.fields()).flat_map(|(_, e, _)| expr::references(e)));
|
||||
names.extend(self.sheet.rules.iter().flat_map(|r| r.fields()).flat_map(|(f, e, _)| rule_references(f, e)));
|
||||
}
|
||||
if f.kind.is_plastic() {
|
||||
names.extend(self.plastic.rules.iter().flat_map(|r| r.fields()).flat_map(|(_, e, _)| expr::references(e)));
|
||||
names.extend(self.plastic.rules.iter().flat_map(|r| r.fields()).flat_map(|(f, e, _)| rule_references(f, e)));
|
||||
}
|
||||
out.extend(names.into_iter().map(|n| (format!("Feature:{}", f.name), n)));
|
||||
}
|
||||
@@ -1052,9 +1070,9 @@ impl Document {
|
||||
}
|
||||
// A rule that uses it changes every sheet metal (or plastic) feature: the evaluation
|
||||
// hashes the rules into their fingerprint.
|
||||
let uses = |e: &String| expr::references(e).iter().any(|r| affected.contains(r));
|
||||
let sheet = self.sheet.rules.iter().any(|r| r.fields().iter().any(|(_, e, _)| uses(e)));
|
||||
let plastic = self.plastic.rules.iter().any(|r| r.fields().iter().any(|(_, e, _)| uses(e)));
|
||||
let uses = |field: &str, e: &str| rule_references(field, e).iter().any(|r| affected.contains(r));
|
||||
let sheet = self.sheet.rules.iter().any(|r| r.fields().iter().any(|(f, e, _)| uses(f, e)));
|
||||
let plastic = self.plastic.rules.iter().any(|r| r.fields().iter().any(|(f, e, _)| uses(f, e)));
|
||||
let mut out = Vec::new();
|
||||
for f in &self.features {
|
||||
let mut hit = f.param_names.iter().any(|n| affected.contains(n));
|
||||
|
||||
@@ -167,7 +167,7 @@ impl Document {
|
||||
return Err(DocError::Invalid("the plastic thickness must be positive".into()));
|
||||
}
|
||||
let with_t = |e: &str, k: Kind| -> Result<f64> {
|
||||
let f = |n: &str| if n == "Thickness" && !vals.contains_key("Thickness") { Ok(Value::length(t)) } else { look(n) };
|
||||
let f = |n: &str| if n == "Thickness" { Ok(Value::length(t)) } else { look(n) };
|
||||
let v = expr::eval_with(e, &f)?.to_kind(k)?;
|
||||
if !v.is_finite() || v < 0.0 {
|
||||
return Err(DocError::Invalid(format!("plastic rule `{}`: `{e}` must not be negative", r.name)));
|
||||
|
||||
@@ -12,7 +12,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use solidcore_record::Vec3;
|
||||
|
||||
use crate::canvas::{base64_decode, base64_encode};
|
||||
pub use crate::point_cloud_complex::{Column, Complex, Filter, Floats, Keep, MAX_COLUMNS, MAX_NAMES, Names};
|
||||
pub use crate::point_cloud_complex::{Column, Complex, Filter, Floats, Indices, Keep, MAX_CELLS, MAX_COLUMNS, MAX_NAMES, Names, On};
|
||||
use crate::{DocError, IDENTITY, Mat, Result};
|
||||
|
||||
/// Most points one cloud may hold.
|
||||
@@ -71,24 +71,26 @@ pub struct PointCloud {
|
||||
pub height_range: Option<[f64; 2]>,
|
||||
#[serde(default = "yes")]
|
||||
pub visible: bool,
|
||||
/// The file's other properties of the points.
|
||||
/// Segments and triangles over the points, and the file's other properties.
|
||||
#[serde(default, skip_serializing_if = "Complex::is_empty")]
|
||||
pub complex: Complex,
|
||||
/// The property `color_by: property` colours by.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color_property: Option<String>,
|
||||
/// Only the points every filter keeps are shown.
|
||||
/// Only the elements within every filter's range are shown.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
/// Where a property's values come from.
|
||||
/// Where a property's values come from on one kind of element.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Source<'a> {
|
||||
/// A coordinate of each point in the design.
|
||||
/// A coordinate of each corner in the design.
|
||||
Coordinate(usize),
|
||||
/// A column of the file: each point's.
|
||||
Column(&'a [f32]),
|
||||
/// A column on the points: each corner's.
|
||||
Corners(&'a [f32]),
|
||||
/// A column on the element itself.
|
||||
Element(&'a [f32]),
|
||||
}
|
||||
|
||||
/// How a cloud's points are coloured.
|
||||
@@ -239,16 +241,21 @@ impl PointCloud {
|
||||
ColorBy::File | ColorBy::Single => None,
|
||||
}
|
||||
}
|
||||
/// Where the values of `name` are: a coordinate of each point or a column of the file.
|
||||
pub fn source(&self, name: &str) -> Option<Source<'_>> {
|
||||
/// Where the values of `name` are on elements of kind `on`: a coordinate of each corner, a
|
||||
/// column on the points (each corner's, varying across the element) or one on the element.
|
||||
pub fn source(&self, name: &str, on: On) -> Option<Source<'_>> {
|
||||
if let Some(k) = COORDINATES.iter().position(|n| *n == name) {
|
||||
return Some(Source::Coordinate(k));
|
||||
}
|
||||
self.complex.column(name).map(|col| Source::Column(&col.values.0))
|
||||
if let Some(col) = self.complex.column(name, on) {
|
||||
return Some(Source::Element(&col.values.0));
|
||||
}
|
||||
self.complex.column(name, On::Point).map(|col| Source::Corners(&col.values.0))
|
||||
}
|
||||
/// Every property the cloud can be coloured and filtered by: its coordinates, then its columns.
|
||||
pub fn property_names(&self) -> impl Iterator<Item = &str> {
|
||||
COORDINATES.into_iter().chain(self.complex.columns.iter().map(|c| c.name.as_str()))
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
COORDINATES.into_iter().chain(self.complex.columns.iter().map(|c| c.name.as_str())).filter(move |n| seen.insert(*n))
|
||||
}
|
||||
/// The smallest and largest value of a property, the coordinates in the design.
|
||||
pub fn span(&self, property: &str) -> Option<[f64; 2]> {
|
||||
@@ -273,6 +280,19 @@ impl PointCloud {
|
||||
self.filters.iter().filter(|f| f.property == property).filter_map(Filter::range).fold(span, |[lo, hi], [a, b]| [lo.max(a), hi.min(b)]);
|
||||
Some(if shown[0] <= shown[1] { crate::point_cloud_distribution::ramp_range(shown) } else { span })
|
||||
}
|
||||
/// Points no segment or triangle ends on: those drawn as dots.
|
||||
pub fn isolated_points(&self) -> usize {
|
||||
if self.complex.segments.is_empty() && self.complex.triangles.is_empty() {
|
||||
return self.len();
|
||||
}
|
||||
let mut used = vec![false; self.len()];
|
||||
for i in self.complex.segments.0.iter().chain(self.complex.triangles.0.iter()) {
|
||||
if let Some(u) = used.get_mut(*i as usize) {
|
||||
*u = true;
|
||||
}
|
||||
}
|
||||
used.iter().filter(|u| !**u).count()
|
||||
}
|
||||
/// The heights the height ramp runs between now: the cloud's own, else the automatic ones.
|
||||
pub fn height_range_now(&self) -> Option<[f64; 2]> {
|
||||
self.height_range.or_else(|| self.auto_height_range())
|
||||
@@ -281,7 +301,7 @@ impl PointCloud {
|
||||
pub fn shade(&self, range: Option<[f64; 2]>) -> impl Fn(usize) -> [u8; 3] + '_ {
|
||||
let single = self.color.unwrap_or(PLAIN).map(byte);
|
||||
let plain = PLAIN.map(byte);
|
||||
let column = self.color_property.as_deref().and_then(|n| self.complex.column(n));
|
||||
let column = self.color_property.as_deref().and_then(|n| self.complex.column(n, On::Point));
|
||||
move |i| match self.color_by {
|
||||
ColorBy::File => self.colors.get(i).unwrap_or(plain),
|
||||
ColorBy::Single => single,
|
||||
@@ -558,9 +578,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ramp_spans_what_is_shown_unless_set() {
|
||||
fn the_ramp_spans_what_is_shown_unless_set_and_isolated_points_are_counted() {
|
||||
let mut c = cloud(vec![0.0; 9], Vec::new());
|
||||
c.complex.columns.push(Column { name: "t".into(), integer: false, values: Floats(vec![0.0, 10.0, 20.0].into()) });
|
||||
c.complex.segments = Indices(vec![0, 1].into());
|
||||
c.complex.columns.push(Column { name: "t".into(), on: On::Point, integer: false, values: Floats(vec![0.0, 10.0, 20.0].into()) });
|
||||
(c.color_by, c.color_property) = (ColorBy::Property, Some("t".into()));
|
||||
assert_eq!(c.color_range(), Some([0.0, 20.0]), "the whole span");
|
||||
c.filters = vec![Filter { property: "t".into(), keep: Keep::Range { range: [5.0, 50.0] } }];
|
||||
@@ -569,6 +590,7 @@ mod tests {
|
||||
assert_eq!(c.color_range(), Some([5.0, 20.0]), "another property's filter changes nothing");
|
||||
c.height_range = Some([1.0, 2.0]);
|
||||
assert_eq!(c.color_range(), Some([1.0, 2.0]), "a range set is kept");
|
||||
assert_eq!(c.isolated_points(), 1, "point 2 ends no segment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! What a point cloud holds besides its points' places and colours: named numbers on its points
|
||||
//! (a PLY file's other vertex properties), to colour the cloud by and to filter it with. Whole
|
||||
//! What a point cloud holds besides its points: segments and triangles between them, and named
|
||||
//! numbers on its points, segments and triangles (a PLY file's properties), to colour the cloud
|
||||
//! by and to filter it with. A point's numbers vary along the segments and across the triangles
|
||||
//! that meet there; a segment's or a triangle's are constant on it. A segment with `width` and
|
||||
//! `height` numbers is drawn as a bead of that cross-section; without them, as a line. Whole
|
||||
//! numbers may have names (`role: 0 outer wall, 1 inner wall` in a PLY file's comments): they
|
||||
//! are labels, filtered by a set of values rather than a range.
|
||||
|
||||
@@ -11,13 +14,41 @@ use crate::canvas::{base64_decode, base64_encode};
|
||||
use crate::point_cloud::MAX_POINTS;
|
||||
use crate::{DocError, Result};
|
||||
|
||||
/// Most named numbers per point.
|
||||
/// Most segments or triangles in one cloud.
|
||||
pub const MAX_CELLS: usize = 20_000_000;
|
||||
/// Most named numbers per point, segment and triangle together.
|
||||
pub const MAX_COLUMNS: usize = 64;
|
||||
|
||||
/// One named number on every point.
|
||||
/// The elements a column of numbers is on.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum On {
|
||||
#[default]
|
||||
Point,
|
||||
Segment,
|
||||
Triangle,
|
||||
}
|
||||
|
||||
impl On {
|
||||
fn is_point(&self) -> bool {
|
||||
*self == On::Point
|
||||
}
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
On::Point => "point",
|
||||
On::Segment => "segment",
|
||||
On::Triangle => "triangle",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One named number on every element of a kind.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Column {
|
||||
pub name: String,
|
||||
/// Points unless said: a design saved with numbers on points alone says nothing.
|
||||
#[serde(default, skip_serializing_if = "On::is_point")]
|
||||
pub on: On,
|
||||
/// Whole numbers (labels, counts): coloured by a palette rather than a ramp.
|
||||
#[serde(default)]
|
||||
pub integer: bool,
|
||||
@@ -33,10 +64,16 @@ impl Column {
|
||||
}
|
||||
}
|
||||
|
||||
/// The complex a cloud's points form: named numbers on the points, and the names of
|
||||
/// whole-numbered ones. Points alone are a complex of dimension 0.
|
||||
/// The complex a cloud's points span: segments and triangles over them, and named numbers on
|
||||
/// all three. Without segments or triangles, the points alone are a complex of dimension 0.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Complex {
|
||||
/// Point indices, two per segment.
|
||||
#[serde(default, skip_serializing_if = "Indices::is_empty")]
|
||||
pub segments: Indices,
|
||||
/// Point indices, three per triangle, counter-clockwise seen from its front.
|
||||
#[serde(default, skip_serializing_if = "Indices::is_empty")]
|
||||
pub triangles: Indices,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub columns: Vec<Column>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
@@ -78,33 +115,95 @@ impl Names {
|
||||
|
||||
impl Complex {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.columns.is_empty() && self.names.is_empty()
|
||||
self.segments.is_empty() && self.triangles.is_empty() && self.columns.is_empty() && self.names.is_empty()
|
||||
}
|
||||
/// The column named `name`.
|
||||
pub fn column(&self, name: &str) -> Option<&Column> {
|
||||
self.columns.iter().find(|c| c.name == name)
|
||||
/// The subcomplex spanned by the points `kept` marks: those points, numbered in order, with
|
||||
/// their numbers, and the segments and triangles all of whose corners are among them, with
|
||||
/// theirs. Also how many segments and triangles it leaves out.
|
||||
pub fn induced(&self, kept: &[bool]) -> (Complex, u64) {
|
||||
let mut next = 0u32;
|
||||
let renumbered: Vec<Option<u32>> = kept
|
||||
.iter()
|
||||
.map(|k| {
|
||||
k.then(|| {
|
||||
next += 1;
|
||||
next - 1
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let restrict = |indices: &Indices, n: usize| {
|
||||
let (mut ids, mut whole) = (Vec::new(), Vec::new());
|
||||
for corners in indices.0.chunks_exact(n) {
|
||||
let new: Option<Vec<u32>> = corners.iter().map(|i| renumbered.get(*i as usize).copied().flatten()).collect();
|
||||
whole.push(new.is_some());
|
||||
ids.extend(new.unwrap_or_default());
|
||||
}
|
||||
(Indices(ids.into()), whole)
|
||||
};
|
||||
let (segments, on_segments) = restrict(&self.segments, 2);
|
||||
let (triangles, on_triangles) = restrict(&self.triangles, 3);
|
||||
let left_out = on_segments.iter().chain(&on_triangles).filter(|whole| !**whole).count() as u64;
|
||||
let columns = self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mask = match c.on {
|
||||
On::Point => kept,
|
||||
On::Segment => &on_segments,
|
||||
On::Triangle => &on_triangles,
|
||||
};
|
||||
let values: Vec<f32> = c.values.0.iter().zip(mask).filter(|(_, k)| **k).map(|(v, _)| *v).collect();
|
||||
Column { values: Floats(values.into()), ..c.clone() }
|
||||
})
|
||||
.collect();
|
||||
(Complex { segments, triangles, columns, names: self.names.clone() }, left_out)
|
||||
}
|
||||
/// The smallest and largest value named `name`.
|
||||
pub fn segment_count(&self) -> usize {
|
||||
self.segments.0.len() / 2
|
||||
}
|
||||
pub fn triangle_count(&self) -> usize {
|
||||
self.triangles.0.len() / 3
|
||||
}
|
||||
/// The column named `name` on elements of kind `on`: a name may be on several kinds.
|
||||
pub fn column(&self, name: &str, on: On) -> Option<&Column> {
|
||||
self.columns.iter().find(|c| c.name == name && c.on == on)
|
||||
}
|
||||
/// The smallest and largest value named `name`, on whichever elements carry it.
|
||||
pub fn span(&self, name: &str) -> Option<[f64; 2]> {
|
||||
self.column(name).and_then(Column::span)
|
||||
self.columns.iter().filter(|c| c.name == name).filter_map(Column::span).reduce(|[a, b], [c, d]| [a.min(c), b.max(d)])
|
||||
}
|
||||
/// The names of `property`'s values, when it is a label.
|
||||
pub fn names(&self, property: &str) -> Option<&Names> {
|
||||
self.names.iter().find(|n| n.property == property).filter(|_| self.integer(property))
|
||||
}
|
||||
/// Whether the values named `name` are whole numbers.
|
||||
/// Whether the values named `name` are whole numbers wherever they are.
|
||||
pub fn integer(&self, name: &str) -> bool {
|
||||
self.column(name).is_some_and(|c| c.integer)
|
||||
let mut named = self.columns.iter().filter(|c| c.name == name).peekable();
|
||||
named.peek().is_some() && named.all(|c| c.integer)
|
||||
}
|
||||
/// Every column has one number per point, the cloud having `points` points.
|
||||
/// How many elements of a kind there are, the cloud having `points` points.
|
||||
pub fn count(&self, on: On, points: usize) -> usize {
|
||||
match on {
|
||||
On::Point => points,
|
||||
On::Segment => self.segment_count(),
|
||||
On::Triangle => self.triangle_count(),
|
||||
}
|
||||
}
|
||||
/// Every index names a point, and every column has one number per element.
|
||||
pub fn check(&self, points: usize) -> Result<()> {
|
||||
let bad = |what: String| Err(DocError::Invalid(format!("point cloud properties: {what}")));
|
||||
if self.columns.len() > MAX_COLUMNS {
|
||||
return bad("too many properties".into());
|
||||
let bad = |what: String| Err(DocError::Invalid(format!("point cloud complex: {what}")));
|
||||
if !self.segments.0.len().is_multiple_of(2) || !self.triangles.0.len().is_multiple_of(3) {
|
||||
return bad("segments are not pairs or triangles not triples".into());
|
||||
}
|
||||
if self.segment_count() > MAX_CELLS || self.triangle_count() > MAX_CELLS || self.columns.len() > MAX_COLUMNS {
|
||||
return bad("too many segments, triangles or properties".into());
|
||||
}
|
||||
if self.segments.0.iter().chain(self.triangles.0.iter()).any(|i| *i as usize >= points) {
|
||||
return bad("an index names a missing point".into());
|
||||
}
|
||||
for c in &self.columns {
|
||||
if c.values.0.len() != points {
|
||||
return bad(format!("property `{}` does not have one number per point", c.name));
|
||||
if c.values.0.len() != self.count(c.on, points) {
|
||||
return bad(format!("property `{}` does not have one number per {}", c.name, c.on.name()));
|
||||
}
|
||||
}
|
||||
if self.names.len() > MAX_COLUMNS || self.names.iter().any(|n| n.values.is_empty() || n.values.len() > MAX_NAMES) {
|
||||
@@ -114,8 +213,8 @@ impl Complex {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which points are shown: those whose property lies in a range (both ends included), or, for a
|
||||
/// label, is one of a set of values.
|
||||
/// Which elements are shown: those whose property lies in a range (both ends included), or, for
|
||||
/// a label, is one of a set of values.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Filter {
|
||||
pub property: String,
|
||||
@@ -140,10 +239,26 @@ impl Filter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Indices into a cloud's points, shared between copies of the design.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Indices(pub Arc<[u32]>);
|
||||
|
||||
impl Indices {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Numbers, shared between copies of the design.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Floats(pub Arc<[f32]>);
|
||||
|
||||
impl PartialEq for Indices {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0) || self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Floats {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0) || self.0.iter().map(|v| v.to_bits()).eq(other.0.iter().map(|v| v.to_bits()))
|
||||
@@ -167,6 +282,18 @@ fn words<'de, D: Deserializer<'de>>(d: D, max: usize) -> std::result::Result<Vec
|
||||
Ok(b.as_chunks::<4>().0.to_vec())
|
||||
}
|
||||
|
||||
impl Serialize for Indices {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&base64_encode(&self.0.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Indices {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
|
||||
Ok(Indices(words(d, 3 * MAX_CELLS)?.into_iter().map(u32::from_le_bytes).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Floats {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&base64_encode(&self.0.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>()))
|
||||
@@ -175,7 +302,7 @@ impl Serialize for Floats {
|
||||
|
||||
impl<'de> Deserialize<'de> for Floats {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
|
||||
Ok(Floats(words(d, MAX_POINTS)?.into_iter().map(f32::from_le_bytes).collect()))
|
||||
Ok(Floats(words(d, MAX_POINTS.max(MAX_CELLS))?.into_iter().map(f32::from_le_bytes).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,20 +311,26 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn properties_round_trip_and_are_checked_against_the_points() {
|
||||
fn a_complex_round_trips_and_is_checked_against_its_points() {
|
||||
let complex = Complex {
|
||||
segments: Indices(vec![0, 1, 1, 2].into()),
|
||||
triangles: Indices(vec![0, 1, 2].into()),
|
||||
columns: vec![
|
||||
Column { name: "t".into(), integer: false, values: Floats(vec![0.0, 0.5, 1.0].into()) },
|
||||
Column { name: "role".into(), integer: true, values: Floats(vec![0.0, 4.0, 4.0].into()) },
|
||||
Column { name: "t".into(), on: On::Point, integer: false, values: Floats(vec![0.0, 0.5, 1.0].into()) },
|
||||
Column { name: "role".into(), on: On::Segment, integer: true, values: Floats(vec![0.0, 4.0].into()) },
|
||||
],
|
||||
names: Vec::new(),
|
||||
};
|
||||
let back: Complex = serde_json::from_str(&serde_json::to_string(&complex).unwrap()).unwrap();
|
||||
assert_eq!(back, complex);
|
||||
let points: Column = serde_json::from_str(r#"{"name": "t", "values": ""}"#).unwrap();
|
||||
assert!(!serde_json::to_string(&points).unwrap().contains("\"on\""), "a column on the points does not say so");
|
||||
assert_eq!(points.on, On::Point, "a column without `on` is on the points");
|
||||
complex.check(3).unwrap();
|
||||
assert!(complex.check(2).is_err(), "three numbers for two points");
|
||||
assert!(complex.check(2).is_err(), "index 2 names a missing point");
|
||||
assert_eq!(complex.span("role"), Some([0.0, 4.0]));
|
||||
assert!(complex.integer("role") && !complex.integer("t") && !complex.integer("nope"));
|
||||
assert!(complex.column("role", On::Segment).is_some() && complex.column("role", On::Point).is_none());
|
||||
// Named values make a label, and a filter keeps a range or a set of them.
|
||||
let names = Names::parse("role: 0 outer wall, 4 bridge, -1 none").unwrap();
|
||||
assert_eq!((names.name(4), names.name(-1), names.name(2)), (Some("bridge"), Some("none"), None));
|
||||
@@ -211,5 +344,8 @@ mod tests {
|
||||
let range: Filter = serde_json::from_str(r#"{"property": "t", "range": [1, 3]}"#).unwrap();
|
||||
assert_eq!((set.range(), range.range()), (None, Some([1.0, 3.0])));
|
||||
assert_eq!(serde_json::to_value(&set).unwrap(), serde_json::json!({"property": "role", "values": [0, 4]}));
|
||||
let mut short = complex.clone();
|
||||
short.columns[1].values = Floats(vec![0.0].into());
|
||||
assert!(short.check(3).is_err(), "one role for two segments");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,30 +3,79 @@
|
||||
//! colours the view draws, and the values where a label changes along a property, which the
|
||||
//! tracks snap to.
|
||||
//!
|
||||
//! Each point weighs the same, the cloud weighing 1. A range filter keeps the points between its
|
||||
//! ends, as the view does; a filter by values keeps those whose rounded value it holds; the
|
||||
//! colour sorts the points kept into the colours they are drawn in. The values the points kept
|
||||
//! take, from the least to the greatest, are the ramp's automatic range: the colours span exactly
|
||||
//! what is shown.
|
||||
//! Each element weighs its measure: a dot 1, a segment its length, a triangle its area, each kind
|
||||
//! divided by its own total so that kinds of different dimension weigh alike. On an element every
|
||||
//! property is linear (the same everywhere, or its corners' values interpolated), so filters,
|
||||
//! colours and the histogram all cut it the same way, where a linear function crosses a value. A
|
||||
//! range filter keeps the part between its ends, as the view does; a filter by values keeps the
|
||||
//! parts whose rounded value it holds; the colour sorts the parts into the colours they are drawn
|
||||
//! in. Each part spreads its measure exactly over the values it covers of the histogram's
|
||||
//! property: a dot's at one value, a segment's evenly, a triangle's as the tent its corners'
|
||||
//! values make. The values the parts kept take, from the least to the greatest, are the ramp's
|
||||
//! automatic range: the colours span exactly what is shown.
|
||||
|
||||
use crate::point_cloud::{ColorBy, Filter, Keep, PALETTE, PointCloud, Source};
|
||||
use crate::point_cloud::{ColorBy, Filter, Keep, On, PALETTE, PointCloud, Source};
|
||||
|
||||
/// Most bins of one histogram.
|
||||
pub const MAX_BINS: usize = 8192;
|
||||
/// How far past its first band one part of an element is split by one cut. A ramp of [`MAX_BINS`]
|
||||
/// bands, with the one below it and the one above it, has no more than this to cross, so its
|
||||
/// measure is whole; only the whole numbers have more (a hostile file's values may cross billions).
|
||||
const MAX_BANDS: usize = MAX_BINS + 1;
|
||||
/// Most corners of a part of a triangle: each cut adds at most one to its three.
|
||||
const MAX_CORNERS: usize = 24;
|
||||
|
||||
/// What the histograms need of a cloud's geometry, worked out once for its points.
|
||||
/// What the histograms need of a cloud's geometry, worked out once for its points and cells.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Geometry {
|
||||
/// Each point in the design.
|
||||
world: Vec<[f64; 3]>,
|
||||
/// The points drawn as dots: those no segment or triangle ends on.
|
||||
dots: Vec<u32>,
|
||||
/// Each segment's share of all segments' length, and each triangle's of their area.
|
||||
segments: Vec<f64>,
|
||||
triangles: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
pub fn of(c: &PointCloud) -> Geometry {
|
||||
Geometry { world: (0..c.len()).map(|i| c.world_point(i).map_or([0.0; 3], |p| [p.x, p.y, p.z])).collect() }
|
||||
let world: Vec<[f64; 3]> = (0..c.len()).map(|i| c.world_point(i).map_or([0.0; 3], |p| [p.x, p.y, p.z])).collect();
|
||||
let mut used = vec![false; world.len()];
|
||||
for i in c.complex.segments.0.iter().chain(c.complex.triangles.0.iter()) {
|
||||
if let Some(u) = used.get_mut(*i as usize) {
|
||||
*u = true;
|
||||
}
|
||||
}
|
||||
let dots = (0..world.len() as u32).filter(|i| !used.get(*i as usize).copied().unwrap_or(true)).collect();
|
||||
let at = |i: u32| world.get(i as usize).copied().unwrap_or_default();
|
||||
let sub = |a: [f64; 3], b: [f64; 3]| [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let norm = |v: [f64; 3]| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
let segments = c.complex.segments.0.as_chunks::<2>().0.iter().map(|[a, b]| norm(sub(at(*a), at(*b)))).collect();
|
||||
let triangles = c
|
||||
.complex
|
||||
.triangles
|
||||
.0
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|[a, b, d]| {
|
||||
let (u, v) = (sub(at(*a), at(*b)), sub(at(*a), at(*d)));
|
||||
0.5 * norm([u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]])
|
||||
})
|
||||
.collect();
|
||||
Geometry { world, dots, segments: shares(segments), triangles: shares(triangles) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Each measure over their total (none for a total that is not a positive number).
|
||||
fn shares(mut m: Vec<f64>) -> Vec<f64> {
|
||||
let total: f64 = m.iter().filter(|v| v.is_finite()).sum();
|
||||
for v in &mut m {
|
||||
*v = if total > 0.0 && v.is_finite() { *v / total } else { 0.0 };
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// How a histogram is split by colour, into slots; the last slot holds what has no value to be
|
||||
/// coloured by.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -82,11 +131,11 @@ pub struct Histogram {
|
||||
pub bins: usize,
|
||||
/// `slots[s][b]`: the measure of slot `s` in bin `b`.
|
||||
pub slots: Vec<Vec<f64>>,
|
||||
/// The least and the greatest value the points kept take, within the axis or not.
|
||||
/// The least and the greatest value the parts kept take, within the axis or not.
|
||||
pub extent: Option<[f64; 2]>,
|
||||
}
|
||||
|
||||
/// Where values are cut into bands, by increasing values.
|
||||
/// Where a linear function is cut into bands, by increasing values.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Cut {
|
||||
/// Below (band 0), within (1, both ends included) and above (2) a range.
|
||||
@@ -115,6 +164,123 @@ impl Cut {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The values band `k` holds, from and to (`None`: without end).
|
||||
fn bounds(&self, k: usize) -> (Option<f64>, Option<f64>) {
|
||||
match *self {
|
||||
Cut::Range([a, b]) => match k {
|
||||
0 => (None, Some(a)),
|
||||
1 => (Some(a), Some(b)),
|
||||
_ => (Some(b), None),
|
||||
},
|
||||
Cut::Grid { start, step, count } => {
|
||||
((k > 0).then_some(start + k.saturating_sub(1) as f64 * step), (k < count).then_some(start + k as f64 * step))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A part of an element in its own coordinates: a dot, a stretch `[s₀, s₁]` of a segment (0 at
|
||||
/// its first point), or a convex polygon in a triangle (each corner the weights of the
|
||||
/// triangle's second and third points).
|
||||
// Parts live in two buffers reused element after element: boxing a polygon would allocate per
|
||||
// triangle.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Part {
|
||||
Dot,
|
||||
Stretch([f64; 2]),
|
||||
Polygon(Polygon),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Polygon {
|
||||
at: [[f64; 2]; MAX_CORNERS],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Polygon {
|
||||
fn triangle() -> Polygon {
|
||||
let mut at = [[0.0; 2]; MAX_CORNERS];
|
||||
at[1] = [1.0, 0.0];
|
||||
at[2] = [0.0, 1.0];
|
||||
Polygon { at, len: 3 }
|
||||
}
|
||||
fn corners(&self) -> &[[f64; 2]] {
|
||||
self.at.get(..self.len).unwrap_or(&[])
|
||||
}
|
||||
/// The part where `g ≥ 0`.
|
||||
fn clip(&self, g: impl Fn([f64; 2]) -> f64) -> Polygon {
|
||||
let mut out = Polygon { at: [[0.0; 2]; MAX_CORNERS], len: 0 };
|
||||
let mut push = |p: [f64; 2]| {
|
||||
if let Some(slot) = out.at.get_mut(out.len) {
|
||||
*slot = p;
|
||||
out.len += 1;
|
||||
}
|
||||
};
|
||||
let c = self.corners();
|
||||
for (k, p) in c.iter().enumerate() {
|
||||
let q = c.get((k + 1) % c.len()).copied().unwrap_or(*p);
|
||||
let (gp, gq) = (g(*p), g(q));
|
||||
if gp >= 0.0 {
|
||||
push(*p);
|
||||
}
|
||||
if (gp >= 0.0) != (gq >= 0.0) {
|
||||
let f = gp / (gp - gq);
|
||||
push([p[0] + (q[0] - p[0]) * f, p[1] + (q[1] - p[1]) * f]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// A linear function on an element: its values at the corners.
|
||||
fn at(f: [f64; 3], p: [f64; 2]) -> f64 {
|
||||
f[0] + (f[1] - f[0]) * p[0] + (f[2] - f[0]) * p[1]
|
||||
}
|
||||
|
||||
/// The bands of `cut` that `part` meets where `f` takes its values, each with the part in it.
|
||||
fn split(part: &Part, f: [f64; 3], cut: Cut, mut visit: impl FnMut(usize, Part)) {
|
||||
match part {
|
||||
Part::Dot => visit(cut.band(f[0]), Part::Dot),
|
||||
Part::Stretch([s0, s1]) => {
|
||||
let (a, b) = (at(f, [*s0, 0.0]), at(f, [*s1, 0.0]));
|
||||
let (lo, hi) = (a.min(b), a.max(b));
|
||||
let (k0, k1) = (cut.band(lo), cut.band(hi));
|
||||
if k0 == k1 || !(hi > lo) {
|
||||
return visit(k0, *part);
|
||||
}
|
||||
let s = |x: f64| s0 + (x - a) / (b - a) * (s1 - s0);
|
||||
for k in k0..=k1.min(k0 + MAX_BANDS) {
|
||||
let (from, to) = cut.bounds(k);
|
||||
let (x0, x1) = (from.map_or(lo, |v| v.max(lo)), to.map_or(hi, |v| v.min(hi)));
|
||||
if x1 > x0 {
|
||||
let (t0, t1) = (s(x0), s(x1));
|
||||
visit(k, Part::Stretch([t0.min(t1), t0.max(t1)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Part::Polygon(p) => {
|
||||
let values = p.corners().iter().map(|c| at(f, *c));
|
||||
let (lo, hi) = values.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| (lo.min(v), hi.max(v)));
|
||||
let (k0, k1) = (cut.band(lo), cut.band(hi));
|
||||
if k0 == k1 || !(hi > lo) {
|
||||
return visit(k0, *part);
|
||||
}
|
||||
for k in k0..=k1.min(k0 + MAX_BANDS) {
|
||||
let (from, to) = cut.bounds(k);
|
||||
let mut q = *p;
|
||||
if let Some(v) = from {
|
||||
q = q.clip(|c| at(f, c) - v);
|
||||
}
|
||||
if let Some(v) = to {
|
||||
q = q.clip(|c| v - at(f, c));
|
||||
}
|
||||
if q.len >= 3 {
|
||||
visit(k, Part::Polygon(q));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A filter as a cut and the bands it keeps.
|
||||
@@ -130,8 +296,7 @@ impl Kept<'_> {
|
||||
Keep::Values { values } => Kept { cut: INTEGERS, values: Some(values) },
|
||||
}
|
||||
}
|
||||
fn keeps(&self, v: f64) -> bool {
|
||||
let band = self.cut.band(v);
|
||||
fn keeps(&self, band: usize) -> bool {
|
||||
match self.values {
|
||||
None => band == 1,
|
||||
Some(values) => integer(band).is_some_and(|v| values.iter().any(|k| i64::from(*k) == v)),
|
||||
@@ -139,14 +304,133 @@ impl Kept<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of `s` at point `i`.
|
||||
fn value(s: Source, i: usize, world: &[[f64; 3]]) -> f64 {
|
||||
/// The values of `s` at an element's corners: element `e` of a kind, on points `ids`.
|
||||
fn corners(s: Source, e: usize, ids: [u32; 3], world: &[[f64; 3]]) -> [f64; 3] {
|
||||
match s {
|
||||
Source::Coordinate(k) => world.get(i).and_then(|p| p.get(k)).copied().unwrap_or(f64::NAN),
|
||||
Source::Column(v) => v.get(i).map_or(f64::NAN, |x| f64::from(*x)),
|
||||
Source::Coordinate(k) => ids.map(|i| world.get(i as usize).and_then(|p| p.get(k)).copied().unwrap_or(f64::NAN)),
|
||||
Source::Corners(v) => ids.map(|i| v.get(i as usize).map_or(f64::NAN, |x| f64::from(*x))),
|
||||
Source::Element(v) => [v.get(e).map_or(f64::NAN, |x| f64::from(*x)); 3],
|
||||
}
|
||||
}
|
||||
|
||||
/// Measure spread over bins: what falls in a bin directly, and evenly spread measure as ramps
|
||||
/// (`k·max(0, x − c)` summed into its distribution function at each bin edge).
|
||||
struct Bins {
|
||||
lo: f64,
|
||||
hi: f64,
|
||||
width: f64,
|
||||
n: usize,
|
||||
direct: Vec<f64>,
|
||||
slope: Vec<f64>,
|
||||
offset: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Bins {
|
||||
fn new([lo, hi]: [f64; 2], n: usize, slots: usize) -> Bins {
|
||||
Bins {
|
||||
lo,
|
||||
hi,
|
||||
width: (hi - lo) / n as f64,
|
||||
n,
|
||||
direct: vec![0.0; slots * n],
|
||||
slope: vec![0.0; slots * (n + 2)],
|
||||
offset: vec![0.0; slots * (n + 2)],
|
||||
}
|
||||
}
|
||||
fn edge(&self, j: usize) -> f64 {
|
||||
if j >= self.n { self.hi } else { self.lo + j as f64 * self.width }
|
||||
}
|
||||
/// The bin holding `x`, the last bin holding its upper end too.
|
||||
fn bin(&self, x: f64) -> Option<usize> {
|
||||
if !(x >= self.lo && x <= self.hi) {
|
||||
return None;
|
||||
}
|
||||
Some((((x - self.lo) / self.width).floor() as usize).min(self.n - 1))
|
||||
}
|
||||
fn add(&mut self, slot: usize, j: usize, m: f64) {
|
||||
if let Some(d) = self.direct.get_mut(slot * self.n + j) {
|
||||
*d += m;
|
||||
}
|
||||
}
|
||||
fn point(&mut self, slot: usize, x: f64, m: f64) {
|
||||
if let Some(j) = self.bin(x) {
|
||||
self.add(slot, j, m);
|
||||
}
|
||||
}
|
||||
/// A ramp of slope `k` from `c`: it reaches the first edge at or after `c`.
|
||||
fn ramp(&mut self, slot: usize, c: f64, k: f64) {
|
||||
let j = ((c - self.lo) / self.width).ceil().clamp(0.0, (self.n + 1) as f64) as usize;
|
||||
let at = slot * (self.n + 2) + j;
|
||||
if let (Some(s), Some(o)) = (self.slope.get_mut(at), self.offset.get_mut(at)) {
|
||||
*s += k;
|
||||
*o += k * c;
|
||||
}
|
||||
}
|
||||
/// `m` spread evenly over `[a, b]`.
|
||||
fn even(&mut self, slot: usize, a: f64, b: f64, m: f64) {
|
||||
if !(b > a) {
|
||||
return self.point(slot, a, m);
|
||||
}
|
||||
let k = m / (b - a);
|
||||
self.ramp(slot, a, k);
|
||||
self.ramp(slot, b, -k);
|
||||
}
|
||||
/// `m` spread as a triangle's measure over its corners' values `q₀ ≤ q₁ ≤ q₂`: rising to
|
||||
/// `q₁` and falling to `q₂`.
|
||||
fn tent(&mut self, slot: usize, [q0, q1, q2]: [f64; 3], m: f64) {
|
||||
if !(q2 > q0) {
|
||||
return self.point(slot, q0, m);
|
||||
}
|
||||
let below = |x: f64| {
|
||||
if x <= q0 {
|
||||
0.0
|
||||
} else if x >= q2 {
|
||||
m
|
||||
} else if x <= q1 {
|
||||
m * (x - q0) * (x - q0) / ((q2 - q0) * (q1 - q0))
|
||||
} else {
|
||||
m * (1.0 - (q2 - x) * (q2 - x) / ((q2 - q0) * (q2 - q1)))
|
||||
}
|
||||
};
|
||||
let (Some(j0), Some(j1)) = (self.bin(q0.max(self.lo)), self.bin(q2.min(self.hi))) else { return };
|
||||
for j in j0..=j1 {
|
||||
let (a, b) = (self.edge(j).max(q0), self.edge(j + 1).min(q2));
|
||||
self.add(slot, j, below(b) - below(a));
|
||||
}
|
||||
}
|
||||
fn finish(self, slots: usize) -> Vec<Vec<f64>> {
|
||||
(0..slots)
|
||||
.map(|s| {
|
||||
let (mut slope, mut offset, mut before) = (0.0, 0.0, 0.0);
|
||||
(0..=self.n)
|
||||
.filter_map(|j| {
|
||||
let at = s * (self.n + 2) + j;
|
||||
slope += self.slope.get(at).copied().unwrap_or(0.0);
|
||||
offset += self.offset.get(at).copied().unwrap_or(0.0);
|
||||
let f = slope * self.edge(j) - offset;
|
||||
let spread = (j > 0).then_some(f - before);
|
||||
before = f;
|
||||
spread.map(|d| d + self.direct.get(s * self.n + j - 1).copied().unwrap_or(0.0))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The kinds of element, each with its elements' points and shares of measure.
|
||||
fn kinds<'a>(c: &'a PointCloud, g: &'a Geometry) -> [(On, Box<dyn Iterator<Item = (usize, [u32; 3], f64)> + 'a>); 3] {
|
||||
let dot = if g.dots.is_empty() { 0.0 } else { 1.0 / g.dots.len() as f64 };
|
||||
[
|
||||
(On::Point, Box::new(g.dots.iter().map(move |i| (*i as usize, [*i; 3], dot)))),
|
||||
(
|
||||
On::Segment,
|
||||
Box::new(c.complex.segments.0.as_chunks::<2>().0.iter().zip(&g.segments).enumerate().map(|(e, ([a, b], m))| (e, [*a, *b, *b], *m))),
|
||||
),
|
||||
(On::Triangle, Box::new(c.complex.triangles.0.as_chunks::<3>().0.iter().zip(&g.triangles).enumerate().map(|(e, (ids, m))| (e, *ids, *m)))),
|
||||
]
|
||||
}
|
||||
|
||||
/// How much of the cloud lies in each of `bins` bins over `axis` of `property`, by `colour`,
|
||||
/// within every one of `filters`; `None` for an axis or a number of bins it can't have.
|
||||
pub fn histogram(
|
||||
@@ -167,39 +451,68 @@ pub fn histogram(
|
||||
return None;
|
||||
}
|
||||
let slots = colour.slots();
|
||||
let mut out = vec![vec![0.0; bins]; slots];
|
||||
let mut extent = None;
|
||||
let Some(p) = c.source(property) else { return Some(Histogram { bins, slots: out, extent }) };
|
||||
// A point is not filtered by a property the cloud doesn't have, as in the view.
|
||||
let kept: Vec<(Kept, Source)> = filters.iter().filter_map(|f| Some((Kept::of(f), c.source(&f.property)?))).collect();
|
||||
let coloured = colour.property().and_then(|n| c.source(n));
|
||||
let mut out = Bins::new(axis, bins, slots);
|
||||
let kept: Vec<Kept> = filters.iter().map(Kept::of).collect();
|
||||
let (colour_cut, plain) = (colour.cut(), slots - 1);
|
||||
let m = 1.0 / g.world.len().max(1) as f64;
|
||||
let width = (axis[1] - axis[0]) / bins as f64;
|
||||
for i in 0..g.world.len() {
|
||||
if !kept.iter().all(|(k, s)| k.keeps(value(*s, i, &g.world))) {
|
||||
continue;
|
||||
}
|
||||
let v = value(p, i, &g.world);
|
||||
if !v.is_finite() {
|
||||
continue;
|
||||
}
|
||||
extent = Some(extent.map_or([v, v], |[lo, hi]: [f64; 2]| [lo.min(v), hi.max(v)]));
|
||||
if !(v >= axis[0] && v <= axis[1]) {
|
||||
continue;
|
||||
}
|
||||
let slot = match (colour_cut, coloured) {
|
||||
(Some(cut), Some(s)) => colour.slot(cut.band(value(s, i, &g.world))),
|
||||
(Some(_), None) => plain,
|
||||
(None, _) => 0,
|
||||
let (mut parts, mut next) = (Vec::new(), Vec::new());
|
||||
let mut extent = None;
|
||||
for (on, elements) in kinds(c, g) {
|
||||
let Some(p) = c.source(property, on) else { continue };
|
||||
let filtered: Vec<Option<Source>> = filters.iter().map(|f| c.source(&f.property, on)).collect();
|
||||
let coloured = colour.property().and_then(|n| c.source(n, on));
|
||||
let start = match on {
|
||||
On::Point => Part::Dot,
|
||||
On::Segment => Part::Stretch([0.0, 1.0]),
|
||||
On::Triangle => Part::Polygon(Polygon::triangle()),
|
||||
};
|
||||
// The last bin holds the axis' upper end too.
|
||||
let bin = (((v - axis[0]) / width).floor() as usize).min(bins - 1);
|
||||
if let Some(b) = out.get_mut(slot).and_then(|s| s.get_mut(bin)) {
|
||||
*b += m;
|
||||
for (e, ids, m) in elements {
|
||||
if m <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
parts.clear();
|
||||
parts.push(start);
|
||||
for (k, s) in kept.iter().zip(&filtered) {
|
||||
// An element without the filtered property is not filtered by it, as in the view.
|
||||
let Some(s) = s else { continue };
|
||||
let f = corners(*s, e, ids, &g.world);
|
||||
next.clear();
|
||||
for part in &parts {
|
||||
split(part, f, k.cut, |band, q| {
|
||||
if k.keeps(band) {
|
||||
next.push(q);
|
||||
}
|
||||
});
|
||||
}
|
||||
std::mem::swap(&mut parts, &mut next);
|
||||
}
|
||||
let pv = corners(p, e, ids, &g.world);
|
||||
let colour_values = coloured.map(|s| corners(s, e, ids, &g.world));
|
||||
for part in &parts {
|
||||
// A linear function is least and greatest at a part's corners.
|
||||
for v in part_values(part, pv) {
|
||||
if v.is_finite() {
|
||||
extent = Some(extent.map_or([v, v], |[lo, hi]: [f64; 2]| [lo.min(v), hi.max(v)]));
|
||||
}
|
||||
}
|
||||
match (colour_cut, colour_values) {
|
||||
(Some(cut), Some(f)) => split(part, f, cut, |band, q| spread(&mut out, colour.slot(band), &q, pv, m)),
|
||||
(Some(_), None) => spread(&mut out, plain, part, pv, m),
|
||||
(None, _) => spread(&mut out, 0, part, pv, m),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Histogram { bins, slots: out, extent })
|
||||
Some(Histogram { bins, slots: out.finish(slots), extent })
|
||||
}
|
||||
|
||||
/// The values a linear function `f` takes at a part's corners.
|
||||
fn part_values(part: &Part, f: [f64; 3]) -> impl Iterator<Item = f64> + '_ {
|
||||
let (ends, corners): (Option<[f64; 2]>, &[[f64; 2]]) = match part {
|
||||
Part::Dot => (Some([f[0], f[0]]), &[]),
|
||||
Part::Stretch([s0, s1]) => (Some([at(f, [*s0, 0.0]), at(f, [*s1, 0.0])]), &[]),
|
||||
Part::Polygon(p) => (None, p.corners()),
|
||||
};
|
||||
ends.into_iter().flatten().chain(corners.iter().map(move |c| at(f, *c)))
|
||||
}
|
||||
|
||||
/// A range for a ramp from the values it spans: a single value gets one unit around it.
|
||||
@@ -225,16 +538,50 @@ pub fn color_range_by(c: &PointCloud, extent: impl FnOnce(&str, [f64; 2]) -> Opt
|
||||
extent(property, axis).map(ramp_range).or_else(|| c.color_range())
|
||||
}
|
||||
|
||||
/// The values of `property` where the whole-numbered `label` changes, point after point in order
|
||||
/// of `property`, lowest first.
|
||||
/// Spread a part's measure (`m`, the whole element's share) over the values `f` takes on it.
|
||||
fn spread(out: &mut Bins, slot: usize, part: &Part, f: [f64; 3], m: f64) {
|
||||
match part {
|
||||
Part::Dot => out.point(slot, f[0], m),
|
||||
Part::Stretch([s0, s1]) => {
|
||||
let (a, b) = (at(f, [*s0, 0.0]), at(f, [*s1, 0.0]));
|
||||
out.even(slot, a.min(b), a.max(b), m * (s1 - s0));
|
||||
}
|
||||
Part::Polygon(p) => {
|
||||
let c = p.corners();
|
||||
let Some(first) = c.first() else { return };
|
||||
for w in c.get(1..).unwrap_or(&[]).windows(2) {
|
||||
let [b, d] = [w[0], w[1]];
|
||||
// The share of the triangle's area: twice the area in its own coordinates.
|
||||
let area = ((b[0] - first[0]) * (d[1] - first[1]) - (b[1] - first[1]) * (d[0] - first[0])).abs();
|
||||
let mut q = [at(f, *first), at(f, b), at(f, d)];
|
||||
q.sort_by(f64::total_cmp);
|
||||
out.tent(slot, q, m * area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The values of `property` where the whole-numbered `label` changes, element after element in
|
||||
/// order of `property` (each element at its smallest value, with its label there), lowest first.
|
||||
pub fn label_changes(c: &PointCloud, g: &Geometry, label: &str, property: &str) -> Vec<f64> {
|
||||
let (Some(p), Some(l)) = (c.source(property), c.source(label)) else { return Vec::new() };
|
||||
let mut at: Vec<(f64, i64)> = (0..g.world.len())
|
||||
.filter_map(|i| {
|
||||
let (v, l) = (value(p, i, &g.world), value(l, i, &g.world));
|
||||
v.is_finite().then(|| (v, if l.is_finite() { l.round() as i64 } else { i64::MIN }))
|
||||
})
|
||||
.collect();
|
||||
let mut at: Vec<(f64, i64)> = Vec::new();
|
||||
for (on, elements) in kinds(c, g) {
|
||||
let (Some(p), Some(l)) = (c.source(property, on), c.source(label, on)) else { continue };
|
||||
let n = match on {
|
||||
On::Point => 1,
|
||||
On::Segment => 2,
|
||||
On::Triangle => 3,
|
||||
};
|
||||
for (e, ids, _) in elements {
|
||||
let (pv, lv) = (corners(p, e, ids, &g.world), corners(l, e, ids, &g.world));
|
||||
let lowest = (0..n).min_by(|i, j| pv[*i].total_cmp(&pv[*j])).unwrap_or(0);
|
||||
let v = pv[lowest];
|
||||
if v.is_finite() {
|
||||
let l = lv[lowest];
|
||||
at.push((v, if l.is_finite() { l.round() as i64 } else { i64::MIN }));
|
||||
}
|
||||
}
|
||||
}
|
||||
at.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut out: Vec<f64> = at.windows(2).filter(|w| w[0].1 != w[1].1).map(|w| w[1].0).collect();
|
||||
out.dedup();
|
||||
|
||||
@@ -2,10 +2,10 @@ use solidcore_record::Vec3;
|
||||
|
||||
use super::*;
|
||||
use crate::IDENTITY;
|
||||
use crate::point_cloud::{ColorBy, Colors, Column, Complex, Floats, Points};
|
||||
use crate::point_cloud::{ColorBy, Colors, Column, Complex, Floats, Indices, Points};
|
||||
|
||||
/// A cloud of `points` with `columns` on them.
|
||||
fn cloud(points: &[[f32; 3]], columns: Vec<Column>) -> PointCloud {
|
||||
/// A cloud of `points`, with `segments` and `triangles` between them and `columns` on them.
|
||||
fn cloud(points: &[[f32; 3]], segments: &[u32], triangles: &[u32], columns: Vec<Column>) -> PointCloud {
|
||||
PointCloud {
|
||||
id: 1,
|
||||
name: "c".into(),
|
||||
@@ -21,14 +21,14 @@ fn cloud(points: &[[f32; 3]], columns: Vec<Column>) -> PointCloud {
|
||||
color: None,
|
||||
height_range: None,
|
||||
visible: true,
|
||||
complex: Complex { columns, names: Vec::new() },
|
||||
complex: Complex { segments: Indices(segments.to_vec().into()), triangles: Indices(triangles.to_vec().into()), columns, names: Vec::new() },
|
||||
color_property: None,
|
||||
filters: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn column(name: &str, integer: bool, values: &[f32]) -> Column {
|
||||
Column { name: name.into(), integer, values: Floats(values.to_vec().into()) }
|
||||
fn column(name: &str, on: On, integer: bool, values: &[f32]) -> Column {
|
||||
Column { name: name.into(), on, integer, values: Floats(values.to_vec().into()) }
|
||||
}
|
||||
|
||||
fn range(property: &str, range: [f64; 2]) -> Filter {
|
||||
@@ -39,27 +39,19 @@ fn close(a: &[f64], b: &[f64]) -> bool {
|
||||
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
|
||||
}
|
||||
|
||||
/// Ten points along x, `t` the same as x.
|
||||
fn row() -> PointCloud {
|
||||
let points: Vec<[f32; 3]> = (0..10).map(|i| [i as f32 + 0.5, 0.0, 0.0]).collect();
|
||||
let t: Vec<f32> = points.iter().map(|p| p[0]).collect();
|
||||
cloud(&points, vec![column("t", false, &t)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_point_weighs_alike_and_a_range_keeps_those_within_it() {
|
||||
let c = row();
|
||||
fn a_segment_spreads_its_length_evenly_over_what_a_range_keeps() {
|
||||
// t runs 0 to 10 along a segment 10 long.
|
||||
let c = cloud(&[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]], &[0, 1], &[], vec![column("t", On::Point, false, &[0.0, 10.0])]);
|
||||
let g = Geometry::of(&c);
|
||||
let all = histogram(&c, &g, "t", [0.0, 10.0], 5, &Colour::One, &[]).unwrap();
|
||||
assert!(close(&all.slots[0], &[0.2; 5]));
|
||||
// Kept from t = 2 to 5: three points, ends included.
|
||||
let kept = histogram(&c, &g, "t", [0.0, 10.0], 5, &Colour::One, &[range("t", [2.0, 5.0])]).unwrap();
|
||||
assert!(close(&kept.slots[0], &[0.0, 0.2, 0.1, 0.0, 0.0]));
|
||||
// Filtered by another property: x from 0 to 4 keeps t from 0 to 4.
|
||||
let all = histogram(&c, &g, "t", [0.0, 10.0], 10, &Colour::One, &[]).unwrap();
|
||||
assert!(close(&all.slots[0], &[0.1; 10]));
|
||||
// Kept from t = 2.5 to 5: the part is cut at the iso-values, as the view cuts it.
|
||||
let kept = histogram(&c, &g, "t", [0.0, 10.0], 10, &Colour::One, &[range("t", [2.5, 5.0])]).unwrap();
|
||||
assert!(close(&kept.slots[0], &[0.0, 0.0, 0.05, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0]));
|
||||
// Filtered by another property along the same segment: x from 0 to 4 keeps t from 0 to 4.
|
||||
let by_x = histogram(&c, &g, "t", [0.0, 10.0], 5, &Colour::One, &[range("x", [0.0, 4.0])]).unwrap();
|
||||
assert!(close(&by_x.slots[0], &[0.2, 0.2, 0.0, 0.0, 0.0]));
|
||||
// A filter on a property the cloud doesn't have keeps everything, as in the view.
|
||||
assert_eq!(histogram(&c, &g, "t", [0.0, 10.0], 5, &Colour::One, &[range("nope", [0.0, 1.0])]), Some(all));
|
||||
// An axis or a number of bins it can't have.
|
||||
for (axis, bins) in [([1.0, 1.0], 4), ([0.0, f64::NAN], 4), ([0.0, 1.0], 0), ([0.0, 1.0], MAX_BINS + 1)] {
|
||||
assert!(histogram(&c, &g, "t", axis, bins, &Colour::One, &[]).is_none());
|
||||
@@ -67,37 +59,82 @@ fn each_point_weighs_alike_and_a_range_keeps_those_within_it() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colours_split_what_is_kept() {
|
||||
// Four points, roles 0, 3, 3 and -1, t from 0 to 3.
|
||||
let c = cloud(
|
||||
&[[0.0; 3], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
|
||||
vec![column("t", false, &[0.0, 1.0, 2.0, 3.0]), column("role", true, &[0.0, 3.0, 3.0, -1.0])],
|
||||
);
|
||||
fn a_triangle_spreads_its_area_as_a_tent_over_its_corners_values() {
|
||||
// t is 0, 1 and 2 at the corners: the measure rises to t = 1 and falls to 2.
|
||||
let c = cloud(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], &[], &[0, 1, 2], vec![column("t", On::Point, false, &[0.0, 1.0, 2.0])]);
|
||||
let g = Geometry::of(&c);
|
||||
let colour = Colour::Palette { property: "role" };
|
||||
let h = histogram(&c, &g, "t", [0.0, 4.0], 4, &colour, &[]).unwrap();
|
||||
assert_eq!(h.slots.len(), colour.slots());
|
||||
assert!(close(&h.slots[0], &[0.25, 0.0, 0.0, 0.0]));
|
||||
assert!(close(&h.slots[3], &[0.0, 0.25, 0.25, 0.0]));
|
||||
assert!(close(&h.slots[PALETTE.len()], &[0.0, 0.0, 0.0, 0.25]), "a negative label");
|
||||
// A filter by values keeps role 3 only.
|
||||
let three = Filter { property: "role".into(), keep: Keep::Values { values: vec![3] } };
|
||||
let kept = histogram(&c, &g, "t", [0.0, 4.0], 4, &colour, &[three]).unwrap();
|
||||
let total: f64 = kept.slots.iter().flatten().sum();
|
||||
assert!((total - 0.5).abs() < 1e-9 && close(&kept.slots[3], &h.slots[3]));
|
||||
// A ramp over t in two bands, and coloured by a property the cloud doesn't have: plain.
|
||||
let ramp = Colour::Ramp { property: "t", range: [0.0, 4.0], bands: 2 };
|
||||
let r = histogram(&c, &g, "t", [0.0, 4.0], 4, &ramp, &[]).unwrap();
|
||||
assert!(close(&r.slots[0], &[0.25, 0.25, 0.0, 0.0]) && close(&r.slots[1], &[0.0, 0.0, 0.25, 0.25]));
|
||||
let none = histogram(&c, &g, "t", [0.0, 4.0], 4, &Colour::Palette { property: "nope" }, &[]).unwrap();
|
||||
assert!(close(&none.slots[PALETTE.len() + 1], &[0.25; 4]));
|
||||
let h = histogram(&c, &g, "t", [0.0, 2.0], 4, &Colour::One, &[]).unwrap();
|
||||
// The distribution function: x²/2 up to 1, then 1 − (2 − x)²/2.
|
||||
assert!(close(&h.slots[0], &[0.125, 0.375, 0.375, 0.125]));
|
||||
// Cut at t ≤ 1: half the area is left, all below 1.
|
||||
let half = histogram(&c, &g, "t", [0.0, 2.0], 4, &Colour::One, &[range("t", [0.0, 1.0])]).unwrap();
|
||||
assert!(close(&half.slots[0], &[0.125, 0.375, 0.0, 0.0]));
|
||||
// Cut by x across t: the part where x ≤ 0.5, its area 3/4 of the triangle's.
|
||||
let x = histogram(&c, &g, "t", [0.0, 2.0], 1, &Colour::One, &[range("x", [0.0, 0.5])]).unwrap();
|
||||
assert!(close(&x.slots[0], &[0.75]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_change_where_the_next_point_has_another() {
|
||||
// Six points in order of t, layers 0 0 1 1 2 2.
|
||||
fn kinds_weigh_alike_and_colours_split_what_is_kept() {
|
||||
// Two segments, roles 0 and 3, and a dot; t along each segment, the dot at t = 5.
|
||||
let c = cloud(
|
||||
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [4.0, 0.0, 0.0], [9.0, 9.0, 9.0]],
|
||||
&[0, 1, 2, 3],
|
||||
&[],
|
||||
vec![
|
||||
column("t", On::Point, false, &[0.0, 1.0, 1.0, 4.0, 5.0]),
|
||||
column("role", On::Segment, true, &[0.0, 3.0]),
|
||||
column("role", On::Point, true, &[0.0, 0.0, 0.0, 0.0, 2.0]),
|
||||
],
|
||||
);
|
||||
let g = Geometry::of(&c);
|
||||
let colour = Colour::Palette { property: "role" };
|
||||
let h = histogram(&c, &g, "t", [0.0, 5.0], 5, &colour, &[]).unwrap();
|
||||
assert_eq!(h.slots.len(), colour.slots());
|
||||
// The segments share their kind's 1 by length (1 and 3), the dot has its kind's 1.
|
||||
assert!(close(&h.slots[0], &[0.25, 0.0, 0.0, 0.0, 0.0]));
|
||||
assert!(close(&h.slots[3], &[0.0, 0.25, 0.25, 0.25, 0.0]));
|
||||
assert!(close(&h.slots[2], &[0.0, 0.0, 0.0, 0.0, 1.0]));
|
||||
// A filter by values keeps role 3 only; the dot has a role too and is left out.
|
||||
let three = Filter { property: "role".into(), keep: Keep::Values { values: vec![3] } };
|
||||
let kept = histogram(&c, &g, "t", [0.0, 5.0], 5, &colour, &[three]).unwrap();
|
||||
let total: f64 = kept.slots.iter().flatten().sum();
|
||||
assert!((total - 0.75).abs() < 1e-9 && close(&kept.slots[3], &h.slots[3]));
|
||||
// A ramp over t in two bands splits the long segment where t crosses 2.5.
|
||||
let ramp = Colour::Ramp { property: "t", range: [0.0, 5.0], bands: 2 };
|
||||
let r = histogram(&c, &g, "t", [0.0, 5.0], 5, &ramp, &[]).unwrap();
|
||||
assert!(close(&r.slots[0], &[0.25, 0.25, 0.125, 0.0, 0.0]) && close(&r.slots[1], &[0.0, 0.0, 0.125, 0.25, 1.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ramp_of_the_most_bands_keeps_all_of_an_element() {
|
||||
// Across every band of the finest ramp the API accepts, nothing is lost: a segment and a
|
||||
// triangle, within the ramp and running past both its ends.
|
||||
let t = |values: &[f32]| vec![column("t", On::Point, false, values)];
|
||||
let ramp = Colour::Ramp { property: "t", range: [0.0, 1.0], bands: MAX_BINS };
|
||||
for c in [
|
||||
cloud(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], &[0, 1], &[], t(&[0.0, 1.0])),
|
||||
cloud(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], &[], &[0, 1, 2], t(&[0.0, 1.0, 0.5])),
|
||||
cloud(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], &[0, 1], &[], t(&[-0.5, 1.5])),
|
||||
cloud(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], &[], &[0, 1, 2], t(&[-0.5, 1.5, 0.5])),
|
||||
] {
|
||||
let g = Geometry::of(&c);
|
||||
let h = histogram(&c, &g, "t", [-1.0, 2.0], 4, &ramp, &[]).unwrap();
|
||||
let total: f64 = h.slots.iter().flatten().sum();
|
||||
assert!((total - 1.0).abs() < 1e-9, "{total}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_change_where_the_next_element_has_another() {
|
||||
// Five segments in order of t, layers 0 0 1 1 2.
|
||||
let points: Vec<[f32; 3]> = (0..6).map(|i| [i as f32, 0.0, 0.0]).collect();
|
||||
let c = cloud(&points, vec![column("t", false, &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]), column("layer", true, &[0.0, 0.0, 1.0, 1.0, 2.0, 2.0])]);
|
||||
let c = cloud(
|
||||
&points,
|
||||
&[0, 1, 1, 2, 2, 3, 3, 4, 4, 5],
|
||||
&[],
|
||||
vec![column("t", On::Point, false, &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]), column("layer", On::Segment, true, &[0.0, 0.0, 1.0, 1.0, 2.0])],
|
||||
);
|
||||
let g = Geometry::of(&c);
|
||||
assert_eq!(label_changes(&c, &g, "layer", "t"), vec![2.0, 4.0]);
|
||||
assert!(label_changes(&c, &g, "nope", "t").is_empty());
|
||||
@@ -105,21 +142,21 @@ fn labels_change_where_the_next_point_has_another() {
|
||||
|
||||
#[test]
|
||||
fn the_automatic_range_is_what_every_filter_keeps() {
|
||||
// Filtered by x from 2 to 4, coloured by t: the range is what is kept of t, though no filter
|
||||
// is on t (its span within filters on t would be all of it).
|
||||
let mut c = cloud(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [4.0, 0.0, 0.0], [20.0, 0.0, 0.0]], vec![column("t", false, &[0.0, 3.0, 5.0, 7.0])]);
|
||||
// t runs 0 to 10 along x; filtered by x from 2 to 4, coloured by t: the range is 2 to 4,
|
||||
// though no filter is on t (its span within filters on t would be 0 to 10).
|
||||
let mut c = cloud(&[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [20.0, 0.0, 0.0]], &[0, 1], &[], vec![column("t", On::Point, false, &[0.0, 10.0, 7.0])]);
|
||||
c.color_by = ColorBy::Property;
|
||||
c.color_property = Some("t".into());
|
||||
let g = Geometry::of(&c);
|
||||
let x = [range("x", [2.0, 4.0])];
|
||||
assert_eq!(c.color_range(), Some([0.0, 7.0]));
|
||||
assert_eq!(color_range(&c, &g, &x), Some([3.0, 5.0]));
|
||||
// The point at x = 20 alone: one value, one unit wide.
|
||||
assert_eq!(c.color_range(), Some([0.0, 10.0]));
|
||||
assert_eq!(color_range(&c, &g, &x), Some([2.0, 4.0]));
|
||||
// The dot at x = 20 is kept by nothing; by itself it would be one value, one unit wide.
|
||||
assert_eq!(color_range(&c, &g, &[range("x", [19.0, 21.0])]), Some([6.5, 7.5]));
|
||||
// Nothing kept: the cloud's own bound; a range set is kept as it is.
|
||||
assert_eq!(color_range(&c, &g, &[range("x", [50.0, 60.0])]), Some([0.0, 7.0]));
|
||||
assert_eq!(color_range(&c, &g, &[range("x", [50.0, 60.0])]), Some([0.0, 10.0]));
|
||||
c.height_range = Some([1.0, 2.0]);
|
||||
assert_eq!(color_range(&c, &g, &x), Some([1.0, 2.0]));
|
||||
let h = histogram(&c, &g, "t", [0.0, 1.0], 4, &Colour::One, &x).unwrap();
|
||||
assert_eq!(h.extent, Some([3.0, 5.0]), "the extent reaches past the axis");
|
||||
assert_eq!(h.extent, Some([2.0, 4.0]), "the extent reaches past the axis");
|
||||
}
|
||||
|
||||
+12
-12
@@ -55,17 +55,21 @@ pub struct Resolved {
|
||||
static CACHE: std::sync::Mutex<Vec<(String, Resolved)>> = std::sync::Mutex::new(Vec::new());
|
||||
const CACHE_SIZE: usize = 64;
|
||||
|
||||
fn is_sketch_curve(src: &LinkSource) -> bool {
|
||||
matches!(src, LinkSource::SketchCurve { .. })
|
||||
}
|
||||
|
||||
fn cache_key(st: &ModelState, plane: &Plane, kind: LinkKind, src: &LinkSource) -> Option<String> {
|
||||
// An intersection with a sketch curve (#11048) changes with that sketch, not with the meshes.
|
||||
let body_based = matches!(
|
||||
src,
|
||||
LinkSource::Edge { .. }
|
||||
| LinkSource::Face { .. }
|
||||
| LinkSource::Body { .. }
|
||||
| LinkSource::Vertex { .. }
|
||||
| LinkSource::Intersection { .. }
|
||||
| LinkSource::Iso { .. }
|
||||
| LinkSource::Spun { .. }
|
||||
);
|
||||
) || matches!(src, LinkSource::Intersection { a, b } if !is_sketch_curve(a) && !is_sketch_curve(b));
|
||||
if !body_based {
|
||||
return None;
|
||||
}
|
||||
@@ -225,16 +229,12 @@ fn resolve_uncached(
|
||||
Vec::new()
|
||||
}
|
||||
LinkSource::Intersection { a, b } => {
|
||||
// Where the bodies (or faces) meet, as the edges SMLib's imprint of one by the other
|
||||
// makes: each its exact record curve, its points at the display tolerance.
|
||||
let (ba, fa) = crate::project3d::part_of(st, a).ok_or_else(|| lost(&a.describe()))?;
|
||||
let (bb, fb) = crate::project3d::part_of(st, b).ok_or_else(|| lost(&b.describe()))?;
|
||||
let chord = (ba.body.size().max(bb.body.size()) * 1e-3).max(1e-3);
|
||||
for e in
|
||||
crate::kernel::meeting_edges(&ba.body, fa.as_ref().map(std::slice::from_ref), &bb.body, fb.as_ref().map(std::slice::from_ref), chord)?
|
||||
{
|
||||
wires.push(e.points);
|
||||
exact.push(vec![WireCurve { curve: e.curve, interval: e.interval }]);
|
||||
// Where the bodies, faces or other sketches' curves meet, as the edges SMLib's imprint
|
||||
// of one by the other makes: each its exact record curve, its points at the display
|
||||
// tolerance.
|
||||
for (pts, curves) in crate::project3d::meeting(st, a, b)? {
|
||||
wires.push(pts);
|
||||
exact.push(curves);
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
+233
-7
@@ -1,27 +1,253 @@
|
||||
//! 3D sketch curves from the model: curves projected onto faces (on their display meshes), the
|
||||
//! bodies and faces an intersection curve is taken on, and spun profiles.
|
||||
//! sides an intersection curve is taken on (bodies, faces and other sketches' curves), and spun profiles.
|
||||
|
||||
use solidcore_record::{Plane, Vec2, Vec3};
|
||||
use solidcore_record::{Plane, Seg2, Vec2, Vec3};
|
||||
use solidcraft_geom::Mesh;
|
||||
use solidcraft_geom::prelude::*;
|
||||
use solidcraft_sketch::LinkSource;
|
||||
use solidcraft_sketch::{LinkSource, WireCurve};
|
||||
|
||||
use crate::eval::ModelState;
|
||||
|
||||
/// The body of a body or face reference, and the face (its index in the body's face order) for a
|
||||
/// face reference: the face nearest its point, on the named body or else the nearest body.
|
||||
pub(crate) fn part_of<'a>(st: &'a ModelState, src: &LinkSource) -> Option<(&'a crate::ModelBody, Option<usize>)> {
|
||||
/// The body of a body or face reference and, for a face reference, the faces (indices in the
|
||||
/// body's face order) of the freeform surface its face is part of ([`smooth_patch`]), on the named
|
||||
/// body or else the nearest body, so a spline wall of one face per span meets as a whole (#9009).
|
||||
fn part_of<'a>(st: &'a ModelState, src: &LinkSource) -> Option<(&'a crate::ModelBody, Option<Vec<usize>>)> {
|
||||
match src {
|
||||
LinkSource::Body { body } => Some((st.body(body)?, None)),
|
||||
LinkSource::Face { body, at } => {
|
||||
let b = st.body(body).or_else(|| st.bodies.iter().min_by(|x, y| dist_to(&x.mesh(), *at).total_cmp(&dist_to(&y.mesh(), *at))))?;
|
||||
let (f, _, _) = crate::project::nearest_face(&b.mesh(), *at)?;
|
||||
Some((b, usize::try_from(f).ok()))
|
||||
Some((b, Some(smooth_patch(&b.body, usize::try_from(f).ok()?))))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces one freeform surface with `face` makes, sorted: `face` and the spline faces joined to
|
||||
/// it at least tangentially (G1, measured on the exact surfaces), never across a crease. A
|
||||
/// fit-point spline's wall is one face per span, and a pick on any span means the whole wall; a
|
||||
/// plane, a fillet or any other analytic face is the face alone, as are faces whose seams cannot
|
||||
/// be measured.
|
||||
fn smooth_patch(body: &crate::kernel::Body, face: usize) -> Vec<usize> {
|
||||
use solidcore_record::brep::Surface;
|
||||
let r = body.record();
|
||||
let freeform = |f: usize| r.faces.get(f).is_some_and(|f| matches!(f.surface, Surface::Spline { .. } | Surface::RationalSpline { .. }));
|
||||
if !freeform(face) {
|
||||
return vec![face];
|
||||
}
|
||||
// The two-sided edges between freeform faces, the only seams worth measuring.
|
||||
let between: Vec<(usize, [usize; 2])> = r
|
||||
.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ei, e)| {
|
||||
let mut fs: Vec<usize> = e.uses.iter().map(|u| u.face as usize).collect();
|
||||
fs.sort_unstable();
|
||||
fs.dedup();
|
||||
match fs[..] {
|
||||
[x, y] if freeform(x) && freeform(y) => Some((ei, [x, y])),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let edges: Vec<usize> = between.iter().map(|(ei, _)| *ei).collect();
|
||||
let tangent: std::collections::BTreeSet<usize> = if edges.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
crate::kernel::seam_diagnostics(body, &edges, crate::kernel::MIN_SAMPLES, crate::kernel::ContinuityTolerances::default())
|
||||
.map(|rep| {
|
||||
rep.seams
|
||||
.iter()
|
||||
.filter(|s| matches!(s.verdict, crate::kernel::SeamVerdict::Smooth | crate::kernel::SeamVerdict::CurvatureJump))
|
||||
.map(|s| s.edge)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let mut patch = std::collections::BTreeSet::from([face]);
|
||||
let mut todo = vec![face];
|
||||
while let Some(f) = todo.pop() {
|
||||
for (_, pair) in between.iter().filter(|(ei, pair)| tangent.contains(ei) && pair.contains(&f)) {
|
||||
for next in pair {
|
||||
if patch.insert(*next) {
|
||||
todo.push(*next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
patch.into_iter().collect()
|
||||
}
|
||||
|
||||
/// One side of an intersection curve: a body (or a face patch of it), or a curve of another sketch.
|
||||
enum Side<'a> {
|
||||
Part(&'a crate::ModelBody, Option<Vec<usize>>),
|
||||
Curve { plane: Plane, segs: Vec<Seg2>, pts: Vec<Vec3> },
|
||||
}
|
||||
|
||||
fn side_of<'a>(st: &'a ModelState, src: &LinkSource) -> Option<Side<'a>> {
|
||||
if let Some((b, f)) = part_of(st, src) {
|
||||
return Some(Side::Part(b, f));
|
||||
}
|
||||
let LinkSource::SketchCurve { sketch, curve } = src else { return None };
|
||||
let ss = st.sketch(*sketch)?;
|
||||
let ci = ss.sketch.curve_index(curve)?;
|
||||
let segs = ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci));
|
||||
let pts: Vec<Vec3> = ss.sketch.segs(ci).iter().flat_map(|s| s.polyline(1e-3)).map(|p| ss.plane.to_world(p)).collect();
|
||||
(!segs.is_empty() && pts.len() >= 2).then_some(Side::Curve { plane: ss.plane, segs, pts })
|
||||
}
|
||||
|
||||
/// Bounds of a point set; `None` when empty or not finite.
|
||||
fn bounds_of<'a>(pts: impl IntoIterator<Item = &'a Vec3>) -> Option<(Vec3, Vec3)> {
|
||||
let mut it = pts.into_iter();
|
||||
let first = *it.next()?;
|
||||
let (lo, hi) = it.fold((first, first), |(l, h), p| (l.min(*p), h.max(*p)));
|
||||
(lo.is_finite() && hi.is_finite()).then_some((lo, hi))
|
||||
}
|
||||
|
||||
/// The offsets along `plane`'s unit normal `n` that cover the box `lo`..`hi`, padded.
|
||||
fn span_over(plane: &Plane, n: Vec3, lo: Vec3, hi: Vec3) -> (f64, f64) {
|
||||
let corners =
|
||||
(0..8).map(|i| Vec3::new(if i & 1 == 0 { lo.x } else { hi.x }, if i & 2 == 0 { lo.y } else { hi.y }, if i & 4 == 0 { lo.z } else { hi.z }));
|
||||
let (s0, s1) = corners.map(|c| (c - plane.origin).dot(n)).fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), s| (l.min(s), h.max(s)));
|
||||
let pad = (hi - lo).len() * 0.02 + 1e-3;
|
||||
(s0 - pad, s1 + pad)
|
||||
}
|
||||
|
||||
/// Where intersection sides `a` and `b` meet, as whole curves ([`join_meeting`]): the edges
|
||||
/// SMLib's imprint of one by the other makes, each its exact record curve, its points at the
|
||||
/// display tolerance. A sketch curve stands in for its extrusion along its sketch's normal, both
|
||||
/// ways and as far as the other side reaches: a sheet made here for this alone and never added to
|
||||
/// the model (#11048).
|
||||
pub(crate) fn meeting(st: &ModelState, a: &LinkSource, b: &LinkSource) -> crate::Result<Vec<(Vec<Vec3>, Vec<WireCurve>)>> {
|
||||
let lost = |s: &LinkSource| crate::DocError::Unknown(format!("{} no longer exists", s.describe()));
|
||||
let (sa, sb) = (side_of(st, a).ok_or_else(|| lost(a))?, side_of(st, b).ok_or_else(|| lost(b))?);
|
||||
let unit = |p: &Plane, s: &LinkSource| p.normal().normalized().ok_or_else(|| lost(s));
|
||||
// How far each curve is swept: across the other side's bounds, or for two curves as far as a
|
||||
// crossing can lie (a + s·na = b + t·nb with a, b within the curves' bounds, so |s|·sin ≤ |a − b|).
|
||||
let (ra, rb) = match (&sa, &sb) {
|
||||
(Side::Curve { plane: pa, pts: qa, .. }, Side::Curve { plane: pb, pts: qb, .. }) => {
|
||||
let sin = unit(pa, a)?.cross(unit(pb, b)?).len();
|
||||
if sin < 1e-6 {
|
||||
return Err(crate::DocError::Invalid("the two curves' sketches are parallel, so their extrusions never cross in a curve".into()));
|
||||
}
|
||||
let (lo, hi) = bounds_of(qa.iter().chain(qb)).ok_or_else(|| lost(a))?;
|
||||
let reach = (hi - lo).len() / sin * 1.05 + 1e-3;
|
||||
((-reach, reach), (-reach, reach))
|
||||
}
|
||||
(Side::Curve { plane, .. }, Side::Part(mb, _)) => {
|
||||
let m = mb.body.exact_bounds()?;
|
||||
(span_over(plane, unit(plane, a)?, m.min, m.max), (0.0, 0.0))
|
||||
}
|
||||
(Side::Part(mb, _), Side::Curve { plane, .. }) => {
|
||||
let m = mb.body.exact_bounds()?;
|
||||
((0.0, 0.0), span_over(plane, unit(plane, b)?, m.min, m.max))
|
||||
}
|
||||
(Side::Part(..), Side::Part(..)) => ((0.0, 0.0), (0.0, 0.0)),
|
||||
};
|
||||
let sheet = |s: &Side, (s0, s1): (f64, f64)| -> crate::Result<Option<crate::kernel::Body>> {
|
||||
match s {
|
||||
Side::Curve { plane, segs, .. } => Ok(Some(crate::kernel::extrude_sheet(plane, std::slice::from_ref(segs), s0, s1)?)),
|
||||
Side::Part(..) => Ok(None),
|
||||
}
|
||||
};
|
||||
let (ka, kb) = (sheet(&sa, ra)?, sheet(&sb, rb)?);
|
||||
let (Some((ba, fa)), Some((bb, fb))) = (body_of(&sa, ka.as_ref()), body_of(&sb, kb.as_ref())) else { return Err(lost(a)) };
|
||||
let chord = (ba.size().max(bb.size()) * 1e-3).max(1e-3);
|
||||
// The pieces end at the imprint's shared vertices, which the kernel places within its
|
||||
// intersection tolerance; ten times that, as when it sews sheets, joins them and nothing else.
|
||||
let tol = 10.0 * ba.record().tolerance.max(bb.record().tolerance).max(1e-9);
|
||||
Ok(join_meeting(crate::kernel::meeting_edges(ba, fa, bb, fb, chord)?, tol))
|
||||
}
|
||||
|
||||
/// The kernel body a side is intersected as: its model body (and face patch), or the curve's `sheet`.
|
||||
fn body_of<'b>(side: &'b Side<'b>, sheet: Option<&'b crate::kernel::Body>) -> Option<(&'b crate::kernel::Body, Option<&'b [usize]>)> {
|
||||
match side {
|
||||
Side::Part(mb, f) => Some((&mb.body, f.as_deref())),
|
||||
Side::Curve { .. } => sheet.map(|k| (k, None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The meeting edges as whole curves: edges whose ends meet (within `tol`) joined end to end, each
|
||||
/// curve its points and its exact pieces in order along them. The imprint splits a crossing at
|
||||
/// every face boundary of either side, so one crossing of two spline walls comes as one edge per
|
||||
/// span; it is one curve.
|
||||
fn join_meeting(edges: Vec<crate::kernel::MeetingEdge>, tol: f64) -> Vec<(Vec<Vec3>, Vec<WireCurve>)> {
|
||||
let mut left: Vec<(Vec<Vec3>, Vec<WireCurve>)> =
|
||||
edges.into_iter().filter(|e| !e.points.is_empty()).map(|e| (e.points, vec![WireCurve { curve: e.curve, interval: e.interval }])).collect();
|
||||
let flip = |(mut p, c): (Vec<Vec3>, Vec<WireCurve>)| {
|
||||
p.reverse();
|
||||
(p, c.iter().rev().map(reversed).collect::<Vec<_>>())
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
while let Some(mut cur) = left.pop() {
|
||||
while let (Some(cs), Some(ce)) = (cur.0.first().copied(), cur.0.last().copied()) {
|
||||
if cur.0.len() > 2 && cs.dist(ce) < tol {
|
||||
break;
|
||||
}
|
||||
let ends = |w: &(Vec<Vec3>, Vec<WireCurve>)| w.0.first().copied().zip(w.0.last().copied());
|
||||
let Some(i) = left.iter().position(|w| ends(w).is_some_and(|(s, e)| [s, e].iter().any(|q| q.dist(ce) < tol || q.dist(cs) < tol))) else {
|
||||
break;
|
||||
};
|
||||
let next = left.swap_remove(i);
|
||||
let Some((s, e)) = ends(&next) else { break };
|
||||
// Append after the end or prepend before the start, turning `next` to run on.
|
||||
let (mut head, tail) = if s.dist(ce) < tol {
|
||||
(cur, next)
|
||||
} else if e.dist(ce) < tol {
|
||||
(cur, flip(next))
|
||||
} else if e.dist(cs) < tol {
|
||||
(next, cur)
|
||||
} else {
|
||||
(flip(next), cur)
|
||||
};
|
||||
head.0.extend(tail.0.into_iter().skip(1));
|
||||
head.1.extend(tail.1);
|
||||
cur = head;
|
||||
}
|
||||
out.push(cur);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `w` run the other way: the same points, its parameter reversed (record curves have no
|
||||
/// direction flag, so the curve itself is rewritten).
|
||||
fn reversed(w: &WireCurve) -> WireCurve {
|
||||
use solidcore_record::brep::Curve;
|
||||
let [t0, t1] = w.interval;
|
||||
let mirror = |knots: &[f64]| {
|
||||
let (a, b) = (knots.first().copied().unwrap_or(0.0), knots.last().copied().unwrap_or(0.0));
|
||||
(knots.iter().rev().map(|k| a + b - k).collect::<Vec<_>>(), a + b)
|
||||
};
|
||||
let (curve, interval) = match &w.curve {
|
||||
Curve::Line { start, end } => (Curve::Line { start: *end, end: *start }, [1.0 - t1, 1.0 - t0]),
|
||||
Curve::Circle { frame, radius } => (Curve::Circle { frame: flipped(frame), radius: *radius }, [-t1, -t0]),
|
||||
Curve::Ellipse { frame, radii } => (Curve::Ellipse { frame: flipped(frame), radii: *radii }, [-t1, -t0]),
|
||||
Curve::Polyline { points } => {
|
||||
let n = points.len().saturating_sub(1) as f64;
|
||||
(Curve::Polyline { points: points.iter().rev().copied().collect() }, [n - t1, n - t0])
|
||||
}
|
||||
Curve::Spline { knots, points } => {
|
||||
let (k, s) = mirror(knots);
|
||||
(Curve::Spline { knots: k, points: points.iter().rev().copied().collect() }, [s - t1, s - t0])
|
||||
}
|
||||
Curve::RationalSpline { knots, homogeneous } => {
|
||||
let (k, s) = mirror(knots);
|
||||
(Curve::RationalSpline { knots: k, homogeneous: homogeneous.iter().rev().copied().collect() }, [s - t1, s - t0])
|
||||
}
|
||||
Curve::SurfaceIntersection { surfaces, leader, tolerance } => {
|
||||
let r = reversed(&WireCurve { curve: (**leader).clone(), interval: w.interval });
|
||||
(Curve::SurfaceIntersection { surfaces: surfaces.clone(), leader: Box::new(r.curve), tolerance: *tolerance }, r.interval)
|
||||
}
|
||||
};
|
||||
WireCurve { curve, interval }
|
||||
}
|
||||
|
||||
/// `f` with its y axis turned over, so a circle or ellipse on it runs the other way.
|
||||
fn flipped(f: &solidcore_record::brep::Frame) -> solidcore_record::brep::Frame {
|
||||
let [x, y, z] = f.y_axis;
|
||||
solidcore_record::brep::Frame { origin: f.origin, x_axis: f.x_axis, y_axis: [-x, -y, -z] }
|
||||
}
|
||||
|
||||
fn dist_to(m: &Mesh, p: Vec3) -> f64 {
|
||||
crate::project::nearest_face(m, p).map(|x| x.2).unwrap_or(f64::INFINITY)
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ pub fn encode_hdr_base64(bytes: &[u8]) -> Result<String> {
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn decode_base64(value: &str, maximum: usize) -> Result<Vec<u8>> {
|
||||
pub(crate) fn decode_base64(value: &str, maximum: usize) -> Result<Vec<u8>> {
|
||||
let length = base64_length(value, maximum)?;
|
||||
let padding = if value.ends_with("==") { 2 } else { usize::from(value.ends_with('=')) };
|
||||
let mut bytes = Vec::new();
|
||||
@@ -420,7 +420,7 @@ impl<'de> DeserializeSeed<'de> for EncodedBudget {
|
||||
// RawValue borrows the JSON source without building serde_json's unescaped String scratch.
|
||||
// Count base64 characters (including legal JSON slash/Unicode escapes) before any payload
|
||||
// allocation. Full canonical padding, stream, radiance and digest checks still follow.
|
||||
fn json_base64_length(raw: &str, maximum: usize) -> Result<usize> {
|
||||
pub(crate) fn json_base64_length(raw: &str, maximum: usize) -> Result<usize> {
|
||||
let text = raw.strip_prefix('"').and_then(|s| s.strip_suffix('"')).ok_or_else(|| invalid("HDR data must be a base64 string"))?;
|
||||
let mut bytes = text.as_bytes().iter().copied();
|
||||
let mut count = 0usize;
|
||||
|
||||
@@ -121,8 +121,10 @@ impl RenderSetup {
|
||||
if !self.stage.is_empty() && matches!(self.job, Some(RenderJob::Raster { .. })) {
|
||||
return Err(DocError::Invalid("passive stage geometry requires raytracing; raster cannot silently discard it".into()));
|
||||
}
|
||||
if !self.surfaces.is_empty() && !matches!(self.job, Some(RenderJob::GpuPath { .. })) {
|
||||
return Err(DocError::Invalid("saved surfaces require an explicit GPU job; unsupported methods cannot discard bindings".into()));
|
||||
if !self.surfaces.is_empty() && !matches!(self.job, Some(RenderJob::CpuRaytrace { .. } | RenderJob::GpuPath { .. })) {
|
||||
return Err(DocError::Invalid(
|
||||
"saved surfaces require an explicit CPU raytrace or GPU path job; unsupported methods cannot discard bindings".into(),
|
||||
));
|
||||
}
|
||||
let (width, height) = (u64::from(self.width), u64::from(self.height));
|
||||
if width == 0 || height == 0 {
|
||||
@@ -185,6 +187,8 @@ pub struct RenderSettings {
|
||||
pub setups: BTreeMap<String, RenderSetup>,
|
||||
#[serde(skip_serializing_if = "crate::render_resources::HdrResources::is_empty")]
|
||||
pub resources: crate::render_resources::HdrResources,
|
||||
#[serde(skip_serializing_if = "crate::render_texture_resources::TextureResources::is_empty")]
|
||||
pub texture_resources: crate::render_texture_resources::TextureResources,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -192,12 +196,13 @@ pub struct RenderSettings {
|
||||
struct UncheckedSettings {
|
||||
setups: BTreeMap<String, RenderSetup>,
|
||||
resources: crate::render_resources::HdrResources,
|
||||
texture_resources: crate::render_texture_resources::TextureResources,
|
||||
}
|
||||
|
||||
impl TryFrom<UncheckedSettings> for RenderSettings {
|
||||
type Error = DocError;
|
||||
fn try_from(value: UncheckedSettings) -> Result<Self> {
|
||||
let settings = Self { setups: value.setups, resources: value.resources };
|
||||
let settings = Self { setups: value.setups, resources: value.resources, texture_resources: value.texture_resources };
|
||||
settings.validate()?;
|
||||
Ok(settings)
|
||||
}
|
||||
@@ -205,7 +210,7 @@ impl TryFrom<UncheckedSettings> for RenderSettings {
|
||||
|
||||
impl RenderSettings {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.setups.is_empty() && self.resources.is_empty()
|
||||
self.setups.is_empty() && self.resources.is_empty() && self.texture_resources.is_empty()
|
||||
}
|
||||
|
||||
pub fn validate_name(name: &str) -> Result<()> {
|
||||
@@ -228,6 +233,17 @@ impl RenderSettings {
|
||||
}
|
||||
|
||||
pub fn validate_resources(&self, setup: &RenderSetup) -> Result<()> {
|
||||
for surface in setup.surfaces.iter() {
|
||||
if let crate::render_surface::SurfaceSource::Embedded(source) = &surface.source {
|
||||
for (role, id) in source.role_resources() {
|
||||
let resource = self.texture_resources.required(&id)?;
|
||||
resource.validate_role(role)?;
|
||||
if matches!(setup.job, Some(RenderJob::CpuRaytrace { .. })) {
|
||||
resource.validate_cpu_encoding()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match &setup.job {
|
||||
Some(RenderJob::CpuRaytrace { options }) => {
|
||||
if let Some(hdr) = options.hdr {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Portable built-in surface intent. Targets use definition/feature/occurrence identity,
|
||||
//! never a captured mesh ordinal. Frames are definition-local millimetres. Encoded map
|
||||
//! hashes pin the installed built-in resource revision; no paths or device objects are saved.
|
||||
//! Portable opaque surface intent with built-in or design-owned encoded sources.
|
||||
//! Targets use definition/feature/occurrence identity, never a captured mesh ordinal.
|
||||
//! Frames are definition-local millimetres; no paths or device objects are saved.
|
||||
use crate::{DocError, Result, render_resources::HdrId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_render::job::textures::PlanarFrame;
|
||||
@@ -72,10 +72,74 @@ impl BuiltinSurfaceSource {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SavedSurface {
|
||||
pub target: RenderBodyTarget,
|
||||
pub source: BuiltinSurfaceSource,
|
||||
pub source: SurfaceSource,
|
||||
pub local_frame: PlanarFrame,
|
||||
pub normal_strength: f64,
|
||||
}
|
||||
|
||||
/// Built-in records retain their original wire shape. Embedded records explicitly
|
||||
/// identify each role; one digest may have several independently prepared uses.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SurfaceSource {
|
||||
Builtin(BuiltinSurfaceSource),
|
||||
Embedded(EmbeddedSurfaceSource),
|
||||
}
|
||||
impl From<BuiltinSurfaceSource> for SurfaceSource {
|
||||
fn from(source: BuiltinSurfaceSource) -> Self {
|
||||
Self::Builtin(source)
|
||||
}
|
||||
}
|
||||
impl SurfaceSource {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::Builtin(source) => source.validate(),
|
||||
Self::Embedded(source) => source.validate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EmbeddedSurfaceKind {
|
||||
Embedded,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EmbeddedSurfaceSource {
|
||||
pub kind: EmbeddedSurfaceKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_color: Option<HdrId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub normal_open_gl: Option<HdrId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub roughness: Option<HdrId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metallic: Option<HdrId>,
|
||||
#[serde(default)]
|
||||
pub flip_v: bool,
|
||||
}
|
||||
impl EmbeddedSurfaceSource {
|
||||
pub fn role_resources(&self) -> impl Iterator<Item = (solidcraft_render::texture_input::MapRole, HdrId)> + '_ {
|
||||
use solidcraft_render::texture_input::MapRole;
|
||||
[
|
||||
(MapRole::BaseColor, self.base_color),
|
||||
(MapRole::NormalOpenGl, self.normal_open_gl),
|
||||
(MapRole::Roughness, self.roughness),
|
||||
(MapRole::Metallic, self.metallic),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(role, id)| id.map(|id| (role, id)))
|
||||
}
|
||||
pub fn resources(&self) -> impl Iterator<Item = HdrId> + '_ {
|
||||
[self.base_color, self.normal_open_gl, self.roughness, self.metallic].into_iter().flatten()
|
||||
}
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.resources().next().is_none() {
|
||||
return Err(invalid("an embedded surface requires at least one explicit map role"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl SavedSurface {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.target.validate()?;
|
||||
@@ -190,7 +254,8 @@ mod tests {
|
||||
normal_sha256: HdrId::of(b"normal"),
|
||||
roughness_sha256: HdrId::of(b"rough"),
|
||||
metallic_sha256: HdrId::of(b"metal"),
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
local_frame: PlanarFrame { origin: [1., 2., 3.], u: [100., 0., 0.], v: [0., 100., 0.] },
|
||||
normal_strength: 0.75,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
//! Exact, immutable opaque image bytes owned by a design. Interpretation belongs
|
||||
//! to each saved map role; provenance is bounded inert text, never a permission grant.
|
||||
use crate::{DocError, Result, render_resources::HdrId};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeSeed, ser::SerializeStruct};
|
||||
use solidcraft_render::texture_input::{MapRole, TextureSource};
|
||||
use std::{collections::BTreeMap, fmt, sync::Arc};
|
||||
|
||||
pub const MAX_TEXTURE_RESOURCES: usize = 32;
|
||||
pub const MAX_TEXTURE_RESOURCE_BYTES: usize = 64 * 1024 * 1024;
|
||||
pub const MAX_TEXTURE_BYTES: usize = solidcraft_render::texture_input::MAX_TEXTURE_BYTES;
|
||||
pub const MAX_TEXTURE_EDGE: u32 = 1024;
|
||||
const MAX_PROVENANCE_JSON: usize = 48 * 1024;
|
||||
fn invalid(message: impl Into<String>) -> DocError {
|
||||
DocError::Invalid(message.into())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct TextureProvenance {
|
||||
#[serde(deserialize_with = "bounded_text")]
|
||||
pub author: String,
|
||||
#[serde(deserialize_with = "bounded_text")]
|
||||
pub source: String,
|
||||
#[serde(deserialize_with = "bounded_text")]
|
||||
pub license: String,
|
||||
}
|
||||
impl TextureProvenance {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
for (text, maximum) in [(&self.author, 512), (&self.source, 2048), (&self.license, 4096)] {
|
||||
if text.len() > maximum || text.chars().any(char::is_control) {
|
||||
return Err(invalid("texture provenance requires bounded inert author, source and license text without control characters"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TextureResource {
|
||||
id: HdrId,
|
||||
bytes: Arc<Vec<u8>>,
|
||||
dimensions: [u32; 2],
|
||||
provenance: TextureProvenance,
|
||||
}
|
||||
impl TextureResource {
|
||||
/// Import runs on the caller's existing worker. Full decode rejects corrupt streams;
|
||||
/// the temporary source and pixels are released before immutable publication.
|
||||
pub fn new(bytes: Vec<u8>, provenance: TextureProvenance) -> Result<Self> {
|
||||
provenance.validate()?;
|
||||
if bytes.is_empty() || bytes.len() > MAX_TEXTURE_BYTES || bytes.capacity() > MAX_TEXTURE_BYTES {
|
||||
return Err(invalid("encoded texture capacity must contain at most 16 MiB"));
|
||||
}
|
||||
reject_animation(&bytes)?;
|
||||
let mut checked = Vec::new();
|
||||
checked.try_reserve_exact(bytes.len()).map_err(|_| invalid("could not reserve texture validation bytes"))?;
|
||||
checked.extend_from_slice(&bytes);
|
||||
let source = TextureSource::new_portable(checked, MapRole::Roughness).map_err(|e| invalid(e.to_string()))?;
|
||||
let dimensions = [source.info().width, source.info().height];
|
||||
if dimensions.into_iter().any(|edge| edge == 0 || edge > MAX_TEXTURE_EDGE) {
|
||||
return Err(invalid("embedded texture dimensions exceed 1024x1024"));
|
||||
}
|
||||
source.validate_static_source().map_err(|e| invalid(e.to_string()))?;
|
||||
source.decode(|| false).map_err(|e| invalid(e.to_string()))?;
|
||||
Ok(Self { id: HdrId::of(&bytes), bytes: Arc::new(bytes), dimensions, provenance })
|
||||
}
|
||||
pub fn id(&self) -> HdrId {
|
||||
self.id
|
||||
}
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
pub fn dimensions(&self) -> [u32; 2] {
|
||||
self.dimensions
|
||||
}
|
||||
/// The native cooperative row consumer has a narrower encoding contract
|
||||
/// than GPU preparation. The complete stream was validated at construction.
|
||||
pub fn validate_cpu_encoding(&self) -> Result<()> {
|
||||
if !self.bytes.starts_with(b"\x89PNG\r\n\x1a\n") || self.bytes.get(24).copied() != Some(8) || self.bytes.get(28).copied() != Some(0) {
|
||||
return Err(invalid(
|
||||
"CPU surface maps require noninterlaced 8-bit PNG; keep JPEG/interlaced images on the GPU or normalize them before import",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Binding roles are explicit; one encoded image may be color or linear data.
|
||||
pub fn validate_role(&self, role: MapRole) -> Result<()> {
|
||||
solidcraft_render::texture_input::validate_color_declarations(&self.bytes, role).map_err(|e| invalid(e.to_string()))
|
||||
}
|
||||
pub fn provenance(&self) -> &TextureProvenance {
|
||||
&self.provenance
|
||||
}
|
||||
pub fn retained_bytes(&self) -> usize {
|
||||
self.bytes.capacity()
|
||||
}
|
||||
pub fn base64(&self) -> Result<String> {
|
||||
crate::render_resources::encode_hdr_base64(&self.bytes)
|
||||
}
|
||||
}
|
||||
fn reject_animation(bytes: &[u8]) -> Result<()> {
|
||||
if !bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||
return Ok(());
|
||||
}
|
||||
let mut offset = 8usize;
|
||||
while offset < bytes.len() {
|
||||
let header = bytes.get(offset..offset.saturating_add(8)).ok_or_else(|| invalid("truncated texture PNG chunk"))?;
|
||||
let length = header
|
||||
.get(..4)
|
||||
.and_then(|v| <[u8; 4]>::try_from(v).ok())
|
||||
.map(u32::from_be_bytes)
|
||||
.ok_or_else(|| invalid("invalid texture PNG chunk length"))? as usize;
|
||||
if header.get(4..8) == Some(b"acTL") {
|
||||
return Err(invalid("embedded textures require static PNG or JPEG images"));
|
||||
}
|
||||
offset = offset
|
||||
.checked_add(length)
|
||||
.and_then(|n| n.checked_add(12))
|
||||
.filter(|n| *n <= bytes.len())
|
||||
.ok_or_else(|| invalid("texture PNG chunk exceeds encoded bytes"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl Serialize for TextureResource {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
let mut value = serializer.serialize_struct("TextureResource", 3)?;
|
||||
value.serialize_field("kind", "opaque_texture")?;
|
||||
value.serialize_field("data", &self.base64().map_err(serde::ser::Error::custom)?)?;
|
||||
value.serialize_field("provenance", &self.provenance)?;
|
||||
value.end()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct TextureResources(BTreeMap<HdrId, TextureResource>);
|
||||
impl TextureResources {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&HdrId, &TextureResource)> {
|
||||
self.0.iter()
|
||||
}
|
||||
pub fn get(&self, id: &HdrId) -> Option<&TextureResource> {
|
||||
self.0.get(id)
|
||||
}
|
||||
pub fn required(&self, id: &HdrId) -> Result<&TextureResource> {
|
||||
self.get(id).ok_or_else(|| invalid(format!("missing embedded texture resource {id}")))
|
||||
}
|
||||
pub fn total_bytes(&self) -> usize {
|
||||
self.0.values().map(TextureResource::retained_bytes).sum()
|
||||
}
|
||||
/// Identical bytes retain the first stored provenance; importing labels never
|
||||
/// changes content identity or another binding's provenance.
|
||||
pub fn insert(&mut self, resource: TextureResource) -> Result<HdrId> {
|
||||
let id = resource.id();
|
||||
if let Some(old) = self.get(&id) {
|
||||
if old.bytes() != resource.bytes() {
|
||||
return Err(invalid("texture SHA-256 collision"));
|
||||
}
|
||||
return Ok(id);
|
||||
}
|
||||
if self.len() >= MAX_TEXTURE_RESOURCES || resource.retained_bytes() > MAX_TEXTURE_RESOURCE_BYTES.saturating_sub(self.total_bytes()) {
|
||||
return Err(invalid("embedded textures exceed 32 entries or 64 MiB"));
|
||||
}
|
||||
self.0.insert(id, resource);
|
||||
Ok(id)
|
||||
}
|
||||
/// The engine checks saved users before removing a resource transactionally.
|
||||
pub fn remove(&mut self, id: &HdrId) -> Option<TextureResource> {
|
||||
self.0.remove(id)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for TextureResources {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
|
||||
struct Resources;
|
||||
impl<'de> serde::de::Visitor<'de> for Resources {
|
||||
type Value = TextureResources;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("32 unique opaque texture sources totalling at most 64 MiB")
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> std::result::Result<Self::Value, A::Error> {
|
||||
let mut result = TextureResources::default();
|
||||
while let Some(id) = map.next_key::<HdrId>()? {
|
||||
if result.len() >= MAX_TEXTURE_RESOURCES || result.get(&id).is_some() {
|
||||
return Err(serde::de::Error::custom("too many or duplicate embedded texture resources"));
|
||||
}
|
||||
let remaining = MAX_TEXTURE_RESOURCE_BYTES.saturating_sub(result.total_bytes()).min(MAX_TEXTURE_BYTES);
|
||||
let resource = map.next_value_seed(ResourceSeed(remaining))?;
|
||||
if resource.id() != id {
|
||||
return Err(serde::de::Error::custom("embedded texture hash mismatch"));
|
||||
}
|
||||
result.insert(resource).map_err(serde::de::Error::custom)?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(Resources)
|
||||
}
|
||||
}
|
||||
struct ResourceSeed(usize);
|
||||
impl<'de> DeserializeSeed<'de> for ResourceSeed {
|
||||
type Value = TextureResource;
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error> {
|
||||
struct ResourceVisitor(usize);
|
||||
impl<'de> serde::de::Visitor<'de> for ResourceVisitor {
|
||||
type Value = TextureResource;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("opaque_texture kind, bounded base64 data and inert provenance")
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> std::result::Result<Self::Value, A::Error> {
|
||||
let (mut kind, mut data, mut provenance) = (false, None, None);
|
||||
while let Some(field) = map.next_key::<Field>()? {
|
||||
match field {
|
||||
Field::Kind if !kind => {
|
||||
map.next_value_seed(KindSeed)?;
|
||||
kind = true;
|
||||
}
|
||||
Field::Data if data.is_none() => data = Some(map.next_value_seed(PayloadSeed(self.0))?),
|
||||
Field::Provenance if provenance.is_none() => provenance = Some(map.next_value_seed(ProvenanceSeed)?),
|
||||
_ => return Err(serde::de::Error::custom("duplicate embedded texture field")),
|
||||
}
|
||||
}
|
||||
if !kind {
|
||||
return Err(serde::de::Error::missing_field("kind"));
|
||||
}
|
||||
TextureResource::new(
|
||||
data.ok_or_else(|| serde::de::Error::missing_field("data"))?,
|
||||
provenance.ok_or_else(|| serde::de::Error::missing_field("provenance"))?,
|
||||
)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(ResourceVisitor(self.0))
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(field_identifier, rename_all = "snake_case")]
|
||||
enum Field {
|
||||
Kind,
|
||||
Data,
|
||||
Provenance,
|
||||
}
|
||||
struct KindSeed;
|
||||
impl<'de> DeserializeSeed<'de> for KindSeed {
|
||||
type Value = ();
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> std::result::Result<(), D::Error> {
|
||||
struct Kind;
|
||||
impl serde::de::Visitor<'_> for Kind {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("opaque_texture")
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<(), E> {
|
||||
if value == "opaque_texture" { Ok(()) } else { Err(E::custom("opaque_texture kind required")) }
|
||||
}
|
||||
}
|
||||
d.deserialize_str(Kind)
|
||||
}
|
||||
}
|
||||
struct ProvenanceSeed;
|
||||
impl<'de> DeserializeSeed<'de> for ProvenanceSeed {
|
||||
type Value = TextureProvenance;
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> std::result::Result<Self::Value, D::Error> {
|
||||
let value = TextureProvenance::deserialize(d)?;
|
||||
value.validate().map_err(serde::de::Error::custom)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
struct PayloadSeed(usize);
|
||||
impl<'de> DeserializeSeed<'de> for PayloadSeed {
|
||||
type Value = Vec<u8>;
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> std::result::Result<Self::Value, D::Error> {
|
||||
struct Payload(usize);
|
||||
impl serde::de::Visitor<'_> for Payload {
|
||||
type Value = Vec<u8>;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("bounded canonical texture base64")
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
|
||||
crate::render_resources::decode_base64(value, self.0).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
d.deserialize_str(Payload(self.0))
|
||||
}
|
||||
}
|
||||
fn bounded_text<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<String, D::Error> {
|
||||
struct Text;
|
||||
impl serde::de::Visitor<'_> for Text {
|
||||
type Value = String;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("at most 4096 provenance bytes")
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
|
||||
if value.len() > 4096 {
|
||||
return Err(E::custom("texture provenance text exceeds its bound"));
|
||||
}
|
||||
Ok(value.to_owned())
|
||||
}
|
||||
}
|
||||
d.deserialize_str(Text)
|
||||
}
|
||||
|
||||
/// First pass borrows payload JSON and enforces count/unique/encoded budgets before
|
||||
/// Document's migration Value can allocate decoded base64 strings or coalesce keys.
|
||||
pub(crate) fn check_json_budget(text: &str) -> Result<()> {
|
||||
#[derive(Default, Deserialize)]
|
||||
struct Render {
|
||||
#[serde(default, deserialize_with = "budgets")]
|
||||
texture_resources: (),
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Design {
|
||||
#[serde(default)]
|
||||
render: Render,
|
||||
}
|
||||
let Design { render: Render { texture_resources: () } } =
|
||||
serde_json::from_str(text).map_err(|e| invalid(format!("texture resource admission: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
fn budgets<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<(), D::Error> {
|
||||
struct Budgets;
|
||||
impl<'de> serde::de::Visitor<'de> for Budgets {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("bounded unique texture resources")
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> std::result::Result<(), A::Error> {
|
||||
let mut keys = std::collections::BTreeSet::new();
|
||||
let mut total = 0usize;
|
||||
while let Some(id) = map.next_key::<HdrId>()? {
|
||||
if keys.len() >= MAX_TEXTURE_RESOURCES || !keys.insert(id) {
|
||||
return Err(serde::de::Error::custom("too many or duplicate embedded texture resources"));
|
||||
}
|
||||
total += map.next_value_seed(ResourceBudget(MAX_TEXTURE_RESOURCE_BYTES.saturating_sub(total).min(MAX_TEXTURE_BYTES)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
d.deserialize_map(Budgets)
|
||||
}
|
||||
struct ResourceBudget(usize);
|
||||
impl<'de> DeserializeSeed<'de> for ResourceBudget {
|
||||
type Value = usize;
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> std::result::Result<Self::Value, D::Error> {
|
||||
struct ResourceVisitor(usize);
|
||||
impl<'de> serde::de::Visitor<'de> for ResourceVisitor {
|
||||
type Value = usize;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("bounded texture metadata and base64")
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> std::result::Result<Self::Value, A::Error> {
|
||||
let (mut kind, mut size, mut provenance) = (false, None, false);
|
||||
while let Some(field) = map.next_key::<Field>()? {
|
||||
match field {
|
||||
Field::Kind if !kind => {
|
||||
let raw = map.next_value::<&serde_json::value::RawValue>()?;
|
||||
if raw.get().len() > 86 {
|
||||
return Err(serde::de::Error::custom("texture kind exceeds its budget"));
|
||||
}
|
||||
let value: String = serde_json::from_str(raw.get()).map_err(serde::de::Error::custom)?;
|
||||
if value != "opaque_texture" {
|
||||
return Err(serde::de::Error::custom("opaque_texture kind required"));
|
||||
}
|
||||
kind = true;
|
||||
}
|
||||
Field::Data if size.is_none() => {
|
||||
let raw = map.next_value::<&serde_json::value::RawValue>()?;
|
||||
size = Some(crate::render_resources::json_base64_length(raw.get(), self.0).map_err(serde::de::Error::custom)?);
|
||||
}
|
||||
Field::Provenance if !provenance => {
|
||||
let raw = map.next_value::<&serde_json::value::RawValue>()?;
|
||||
if raw.get().len() > MAX_PROVENANCE_JSON {
|
||||
return Err(serde::de::Error::custom("texture provenance exceeds its budget"));
|
||||
}
|
||||
let value: TextureProvenance = serde_json::from_str(raw.get()).map_err(serde::de::Error::custom)?;
|
||||
value.validate().map_err(serde::de::Error::custom)?;
|
||||
provenance = true;
|
||||
}
|
||||
_ => return Err(serde::de::Error::custom("duplicate embedded texture field")),
|
||||
}
|
||||
}
|
||||
if !kind || !provenance {
|
||||
return Err(serde::de::Error::custom("complete texture metadata required"));
|
||||
}
|
||||
size.ok_or_else(|| serde::de::Error::missing_field("data"))
|
||||
}
|
||||
}
|
||||
d.deserialize_map(ResourceVisitor(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render_settings::{GpuJob, RenderJob, RenderSettings, RenderSetup};
|
||||
use crate::render_surface::{EmbeddedSurfaceKind, EmbeddedSurfaceSource, RenderBodyTarget, SavedSurface, SurfaceSource};
|
||||
use serde_json::json;
|
||||
fn image() -> Vec<u8> {
|
||||
include_bytes!("../../../assets/render/starter/materials/exports/SURF-027/base_color.png").to_vec()
|
||||
}
|
||||
fn provenance() -> TextureProvenance {
|
||||
TextureProvenance { author: "SolidCraft contributors".into(), source: "original test asset".into(), license: "MIT OR Apache-2.0".into() }
|
||||
}
|
||||
fn resource() -> TextureResource {
|
||||
TextureResource::new(image(), provenance()).unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn exact_encoded_texture_bytes_are_shared_deduplicated_and_portable() {
|
||||
let first = resource();
|
||||
let copy = first.clone();
|
||||
assert!(Arc::ptr_eq(&first.bytes, ©.bytes));
|
||||
assert_eq!(first.id(), HdrId::of(&image()));
|
||||
assert_eq!(first.dimensions(), [512, 512]);
|
||||
let mut bank = TextureResources::default();
|
||||
let id = bank.insert(first).unwrap();
|
||||
let mut different_label = copy.clone();
|
||||
different_label.provenance.author = "Another declared author".into();
|
||||
assert_eq!(bank.insert(different_label).unwrap(), id);
|
||||
assert_eq!(bank.len(), 1);
|
||||
assert_eq!(bank.total_bytes(), copy.retained_bytes());
|
||||
assert_eq!(bank.required(&id).unwrap().provenance(), &provenance());
|
||||
let wire = serde_json::to_value(&bank).unwrap();
|
||||
let from_value: TextureResources = serde_json::from_value(wire.clone()).unwrap();
|
||||
let from_json: TextureResources = serde_json::from_str(&wire.to_string()).unwrap();
|
||||
assert_eq!(from_value, bank);
|
||||
assert_eq!(from_json, bank);
|
||||
assert!(bank.required(&HdrId::of(b"missing")).is_err());
|
||||
assert_eq!(bank.remove(&id).unwrap().bytes(), image());
|
||||
assert!(bank.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn embedded_roles_roundtrip_and_missing_references_refuse() {
|
||||
let resource = resource();
|
||||
let id = resource.id();
|
||||
let embedded = EmbeddedSurfaceSource {
|
||||
kind: EmbeddedSurfaceKind::Embedded,
|
||||
base_color: Some(id),
|
||||
normal_open_gl: None,
|
||||
roughness: Some(id),
|
||||
metallic: None,
|
||||
flip_v: true,
|
||||
};
|
||||
let mut setup = RenderSetup {
|
||||
job: Some(RenderJob::GpuPath { transparent_background: false, options: Box::new(GpuJob::default()) }),
|
||||
..Default::default()
|
||||
};
|
||||
setup
|
||||
.surfaces
|
||||
.set(
|
||||
SavedSurface {
|
||||
target: RenderBodyTarget { definition: "Wood test panel".into(), feature: 7, component: 0, occurrence_path: vec![] },
|
||||
source: SurfaceSource::Embedded(embedded),
|
||||
local_frame: solidcraft_render::job::textures::PlanarFrame { origin: [0.; 3], u: [240., 0., 0.], v: [0., 480., 0.] },
|
||||
normal_strength: 0.5,
|
||||
},
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let mut settings = RenderSettings::default();
|
||||
settings.setups.insert("Wood".into(), setup);
|
||||
assert!(settings.validate().unwrap_err().to_string().contains("missing embedded texture"));
|
||||
settings.texture_resources.insert(resource).unwrap();
|
||||
settings.validate().unwrap();
|
||||
let doc = crate::Document { render: Arc::new(settings.clone()), ..Default::default() };
|
||||
let wire = doc.to_json();
|
||||
assert_eq!(crate::Document::from_json(&wire).unwrap(), doc);
|
||||
assert!(wire.contains("embedded-surface-textures"));
|
||||
assert!(!wire.contains("builtin://"));
|
||||
assert!(!wire.contains("file://"));
|
||||
settings.texture_resources.remove(&id);
|
||||
assert!(settings.validate().is_err());
|
||||
assert!(serde_json::from_value::<EmbeddedSurfaceSource>(json!({"kind":"embedded","flip_v":false})).unwrap().validate().is_err());
|
||||
assert!(serde_json::from_value::<SurfaceSource>(json!({"kind":"automatic","base_color":id})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn corrupt_changed_digest_animated_or_malformed_texture_records_refuse() {
|
||||
let good = resource();
|
||||
let id = good.id().to_string();
|
||||
let entry = serde_json::to_value(&good).unwrap();
|
||||
let wire = json!({id.clone():entry.clone()});
|
||||
for (field, value) in [
|
||||
("kind", json!("exr")),
|
||||
("data", json!("AAAA")),
|
||||
("provenance", json!({"author":"x\n","source":"","license":""})),
|
||||
("path", json!("wood.png")),
|
||||
] {
|
||||
let mut changed = wire.clone();
|
||||
changed[&id][field] = value;
|
||||
assert!(serde_json::from_value::<TextureResources>(changed).is_err(), "{field}");
|
||||
}
|
||||
let wrong_id = HdrId::of(b"wrong").to_string();
|
||||
assert!(serde_json::from_value::<TextureResources>(json!({wrong_id:entry.clone()})).unwrap_err().to_string().contains("hash mismatch"));
|
||||
let duplicate = format!("{{\"{id}\":{entry},\"{id}\":{entry}}}");
|
||||
assert!(serde_json::from_str::<TextureResources>(&duplicate).unwrap_err().to_string().contains("duplicate"));
|
||||
let mut truncated = image();
|
||||
truncated.truncate(truncated.len() - 1);
|
||||
assert!(TextureResource::new(truncated, provenance()).is_err());
|
||||
// APNG control is rejected before interpreting a potentially moving image.
|
||||
let mut animated = image();
|
||||
animated.splice(8..8, [0, 0, 0, 0, b'a', b'c', b'T', b'L', 0, 0, 0, 0]);
|
||||
assert!(TextureResource::new(animated, provenance()).unwrap_err().to_string().contains("static"));
|
||||
let mut corrupt_tail = image();
|
||||
let end = corrupt_tail.len() - 12;
|
||||
corrupt_tail.splice(end..end, [0, 0, 0, 1, b'v', b'p', b'A', b'g', 17, 0, 0, 0, 0]);
|
||||
assert!(TextureResource::new(corrupt_tail, provenance()).unwrap_err().to_string().contains("CRC"));
|
||||
let mut oversized_capacity = Vec::with_capacity(MAX_TEXTURE_BYTES + 1);
|
||||
oversized_capacity.extend_from_slice(&image());
|
||||
assert!(TextureResource::new(oversized_capacity, provenance()).is_err());
|
||||
let mut bad_provenance = provenance();
|
||||
bad_provenance.source = "x".repeat(2049);
|
||||
assert!(TextureResource::new(image(), bad_provenance).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn json_texture_admission_preserves_duplicates_and_enforces_count_before_migration() {
|
||||
let entry = serde_json::to_value(resource()).unwrap();
|
||||
let mut resources = serde_json::Map::new();
|
||||
for n in 0..33 {
|
||||
resources.insert(HdrId::of(&[n]).to_string(), entry.clone());
|
||||
}
|
||||
let text = json!({"render":{"texture_resources":resources}}).to_string();
|
||||
assert!(check_json_budget(&text).unwrap_err().to_string().contains("too many"));
|
||||
let id = HdrId::of(&image()).to_string();
|
||||
let text = format!("{{\"render\":{{\"texture_resources\":{{\"{id}\":{entry},\"{id}\":{entry}}}}}}}");
|
||||
assert!(check_json_budget(&text).unwrap_err().to_string().contains("duplicate"));
|
||||
let escaped = "\\u0041".repeat(12);
|
||||
let text = format!("{{\"kind\":\"opaque_texture\",\"data\":\"{escaped}\",\"provenance\":{{}}}}");
|
||||
let mut d = serde_json::Deserializer::from_str(&text);
|
||||
assert!(ResourceBudget(1).deserialize(&mut d).is_err());
|
||||
let text = json!({"render":{"texture_resources":{id:{"kind":"opaque_texture","data":"AAAA","provenance":{"author":"x".repeat(MAX_PROVENANCE_JSON)}}}}}).to_string();
|
||||
assert!(check_json_budget(&text).unwrap_err().to_string().contains("provenance"));
|
||||
}
|
||||
#[test]
|
||||
fn encoded_resource_is_role_independent_and_conflicting_color_binding_is_rejected() {
|
||||
let mut bytes = image();
|
||||
let mut chunk = 4_u32.to_be_bytes().to_vec();
|
||||
chunk.extend(b"gAMA");
|
||||
chunk.extend(100_000_u32.to_be_bytes());
|
||||
let mut crc = u32::MAX;
|
||||
for byte in &chunk[4..] {
|
||||
crc ^= u32::from(*byte);
|
||||
for _ in 0..8 {
|
||||
crc = (crc >> 1) ^ (0xedb8_8320_u32.wrapping_mul(crc & 1));
|
||||
}
|
||||
}
|
||||
chunk.extend((!crc).to_be_bytes());
|
||||
bytes.splice(33..33, chunk);
|
||||
let resource = TextureResource::new(bytes, provenance()).unwrap();
|
||||
assert!(resource.validate_role(MapRole::BaseColor).unwrap_err().to_string().contains("normalized sRGB"));
|
||||
resource.validate_role(MapRole::Roughness).unwrap();
|
||||
let id = resource.id();
|
||||
let mut setup = RenderSetup {
|
||||
job: Some(RenderJob::GpuPath { transparent_background: false, options: Box::new(GpuJob::default()) }),
|
||||
..Default::default()
|
||||
};
|
||||
let mut binding = SavedSurface {
|
||||
target: RenderBodyTarget { definition: "Wood".into(), feature: 7, component: 0, occurrence_path: vec![] },
|
||||
source: SurfaceSource::Embedded(EmbeddedSurfaceSource {
|
||||
kind: EmbeddedSurfaceKind::Embedded,
|
||||
base_color: Some(id),
|
||||
normal_open_gl: None,
|
||||
roughness: None,
|
||||
metallic: None,
|
||||
flip_v: false,
|
||||
}),
|
||||
local_frame: solidcraft_render::job::textures::PlanarFrame { origin: [0.; 3], u: [1., 0., 0.], v: [0., 1., 0.] },
|
||||
normal_strength: 1.,
|
||||
};
|
||||
setup.surfaces.set(binding.clone(), false).unwrap();
|
||||
let mut settings = RenderSettings::default();
|
||||
settings.texture_resources.insert(resource).unwrap();
|
||||
assert!(settings.validate_resources(&setup).is_err());
|
||||
if let SurfaceSource::Embedded(source) = &mut binding.source {
|
||||
source.base_color = None;
|
||||
source.roughness = Some(id);
|
||||
}
|
||||
setup.surfaces.set(binding, true).unwrap();
|
||||
settings.validate_resources(&setup).unwrap();
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -284,6 +284,16 @@ impl SheetRule {
|
||||
}
|
||||
}
|
||||
|
||||
/// The design parameters a rule's `field` expression `e` reads: outside the rule's own
|
||||
/// `thickness`, `Thickness` is the rule's thickness (sheet metal and plastic rules alike).
|
||||
pub fn rule_references(field: &str, e: &str) -> Vec<String> {
|
||||
let mut names = expr::references(e);
|
||||
if field != "thickness" {
|
||||
names.retain(|n| n != "Thickness");
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
fn rule_gap() -> String {
|
||||
"Thickness".into()
|
||||
}
|
||||
@@ -464,7 +474,8 @@ impl Document {
|
||||
.unwrap_or_else(SheetRule::steel)
|
||||
}
|
||||
|
||||
/// Evaluate a rule: `Thickness` in its expressions is the rule's thickness.
|
||||
/// Evaluate a rule: `Thickness` in its expressions is the rule's thickness, even where the
|
||||
/// design has a parameter of that name (#9623); only the rule's `thickness` itself reads it.
|
||||
pub fn rule_values(&self, vals: &std::collections::BTreeMap<String, Value>, r: &SheetRule) -> Result<RuleValues> {
|
||||
self.rule_values_at(vals, r, None)
|
||||
}
|
||||
@@ -481,7 +492,7 @@ impl Document {
|
||||
return Err(DocError::Invalid("the sheet thickness must be more than 0 and less than 10000 mm".into()));
|
||||
}
|
||||
let with_t = |e: &str, k: Kind| -> Result<f64> {
|
||||
let f = |n: &str| if n == "Thickness" && !vals.contains_key("Thickness") { Ok(Value::length(t)) } else { look(n) };
|
||||
let f = |n: &str| if n == "Thickness" { Ok(Value::length(t)) } else { look(n) };
|
||||
expr::eval_with(e, &f)?.to_kind(k)
|
||||
};
|
||||
let v = RuleValues {
|
||||
|
||||
+116
-23
@@ -419,6 +419,21 @@ fn bend_cylinder(b: &Body, mesh: &Mesh, f: usize, tol: f64) -> Option<Chamfer> {
|
||||
na.cross(nb).normalized().map(|_| Chamfer { strip: f, a, b: bb })
|
||||
}
|
||||
|
||||
/// Every piece of bend cylinder `c`'s cylinder (the same axis line and radius) that is a bend
|
||||
/// cylinder between the same two faces: the pieces cut-outs across a bend leave of it. `c` too.
|
||||
fn bend_pieces(b: &Body, mesh: &Mesh, c: Chamfer, tol: f64) -> Vec<Chamfer> {
|
||||
let Some(FaceSurface::Cylinder { origin, axis, radius }) = face_surface(b, c.strip) else { return vec![c] };
|
||||
let coaxial = |g: usize| match face_surface(b, g) {
|
||||
Some(FaceSurface::Cylinder { origin: o, axis: a, radius: r }) => {
|
||||
(r - radius).abs() <= tol && a.cross(axis).len() <= 1e-6 && ((o - origin) - axis * (o - origin).dot(axis)).len() <= tol
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let same = |o: &Chamfer| (o.a, o.b) == (c.a, c.b) || (o.a, o.b) == (c.b, c.a);
|
||||
let pieces = (0..b.face_count()).filter(|g| *g != c.strip && coaxial(*g)).filter_map(|g| bend_cylinder(b, mesh, g, tol)).filter(same);
|
||||
std::iter::once(c).chain(pieces).collect()
|
||||
}
|
||||
|
||||
/// The cylinder of bend `c` on a shell's other skin: the bend cylinder between the faces
|
||||
/// straight behind `c`'s two sides, so that a bend picked on either skin is that bend on both.
|
||||
fn other_skin(b: &Body, mesh: &Mesh, c: &Chamfer, tol: f64) -> Option<Chamfer> {
|
||||
@@ -703,33 +718,91 @@ fn drawn(trimmed: &[Vec2], from: &[Option<usize>], pts: &[Vec2], round: &[Option
|
||||
if round.iter().all(Option::is_none) {
|
||||
return Ok(None);
|
||||
}
|
||||
let (m, n) = (trimmed.len(), pts.len());
|
||||
let mut segs: Vec<Seg2> = Vec::with_capacity(m);
|
||||
for j in 0..m {
|
||||
let (Some(&a), Some(&b)) = (trimmed.get(j), trimmed.get((j + 1) % m)) else { continue };
|
||||
let arc = from.get(j).copied().flatten().and_then(|i| Some((i, round.get(i).copied().flatten()?)));
|
||||
let Some((i, (c, mid))) = arc else {
|
||||
segs.push(Seg2::Line { a: snap(a), b: snap(b) });
|
||||
continue;
|
||||
};
|
||||
let (Some(&p), Some(&q)) = (pts.get(i), pts.get((i + 1) % n)) else { continue };
|
||||
if a.dist(p) > 1e-6 || b.dist(q) > 1e-6 {
|
||||
return err("not supported yet: a round edge of a face that ends at a bend or a cut edge of the sheet");
|
||||
}
|
||||
let (start, r) = ((p - c).angle(), c.dist(p));
|
||||
let half = ((mid - c).angle() - start + PI).rem_euclid(TAU) - PI;
|
||||
match segs.last_mut() {
|
||||
Some(Seg2::Arc { center, radius, sweep, .. })
|
||||
if center.dist(c) <= 1e-9 && (*radius - r).abs() <= 1e-9 && *sweep * half > 0.0 && (*sweep + 2.0 * half).abs() <= PI + 1e-9 =>
|
||||
let mut segs: Vec<Seg2> = Vec::with_capacity(trimmed.len());
|
||||
for j in 0..trimmed.len() {
|
||||
let Some(seg) = drawn_edge(trimmed, from, pts, round, j)? else { continue };
|
||||
match (segs.last_mut(), seg) {
|
||||
(Some(Seg2::Arc { center, radius, sweep, .. }), Seg2::Arc { center: c, radius: r, sweep: s, .. })
|
||||
if center.dist(c) <= 1e-9 && (*radius - r).abs() <= 1e-9 && *sweep * s > 0.0 && (*sweep + s).abs() <= PI + 1e-9 =>
|
||||
{
|
||||
*sweep += 2.0 * half;
|
||||
*sweep += s;
|
||||
}
|
||||
_ => segs.push(Seg2::Arc { center: c, radius: r, start, sweep: 2.0 * half }),
|
||||
_ => segs.push(seg),
|
||||
}
|
||||
}
|
||||
Ok(Some(Loop2 { segs }))
|
||||
}
|
||||
|
||||
/// A panel's trimmed outline where it meets the panel across bend `g` along more than one edge
|
||||
/// (cut-outs across the bend notch it): the edges must lie in one line, running the same way;
|
||||
/// they become one edge, from the first one's start to the last one's end, and each notch
|
||||
/// between two of them a hole of the sheet (the notch's edges, closed along the line). Returns
|
||||
/// the holes, each with the corners where it leaves the line and comes back to it. Nothing
|
||||
/// changes where the panel meets `g` along one edge or none.
|
||||
fn join_runs(
|
||||
trimmed: &mut Vec<Vec2>,
|
||||
from: &mut Vec<Option<usize>>,
|
||||
across: &[Option<usize>],
|
||||
pts: &[Vec2],
|
||||
round: &[Option<(Vec2, Vec2)>],
|
||||
g: usize,
|
||||
tol: f64,
|
||||
) -> Result<Vec<(Region2, Vec2, Vec2)>> {
|
||||
let m = trimmed.len();
|
||||
let along = |j: usize| from.get(j).copied().flatten().and_then(|i| across.get(i).copied().flatten()) == Some(g);
|
||||
let runs: Vec<usize> = (0..m).filter(|j| along(*j)).collect();
|
||||
let (Some(&first), true) = (runs.first(), runs.len() > 1) else { return Ok(Vec::new()) };
|
||||
let corner = |j: usize| trimmed.get(j % m).copied().unwrap_or_default();
|
||||
let (o, Some(d)) = (corner(first), (corner(first + 1) - corner(first)).normalized()) else { return err("a bend edge has no length") };
|
||||
let off = |p: Vec2| (p - o).cross(d).abs();
|
||||
if runs.iter().any(|j| off(corner(*j)) > tol || off(corner(j + 1)) > tol || (corner(j + 1) - corner(*j)).dot(d) <= 0.0) {
|
||||
return err("not supported yet: a face of the sheet that meets a bend along edges not in one line");
|
||||
}
|
||||
let mut order = runs.clone();
|
||||
order.sort_by(|x, y| (corner(*x) - o).dot(d).total_cmp(&(corner(*y) - o).dot(d)));
|
||||
let mut gone = vec![false; m];
|
||||
let mut holes = Vec::new();
|
||||
for pair in order.windows(2) {
|
||||
let &[ja, jb] = pair else { continue };
|
||||
// The notch: forward round the outline from the end of one edge to the start of the next.
|
||||
let mut segs = Vec::new();
|
||||
let mut j = (ja + 1) % m;
|
||||
while j != jb {
|
||||
if along(j) || segs.len() > m {
|
||||
return err("not supported yet: a face of the sheet that meets a bend along edges out of order");
|
||||
}
|
||||
segs.extend(drawn_edge(trimmed, from, pts, round, j)?);
|
||||
gone[j] = true;
|
||||
j = (j + 1) % m;
|
||||
}
|
||||
gone[jb] = true;
|
||||
let (e, s) = (corner(ja + 1), corner(jb));
|
||||
segs.push(Seg2::Line { a: snap(s), b: snap(e) });
|
||||
let lp = Loop2 { segs };
|
||||
holes.push((Region2 { outer: if lp.signed_area() < 0.0 { lp.reversed() } else { lp }, holes: Vec::new() }, e, s));
|
||||
}
|
||||
let keep: Vec<usize> = (0..m).filter(|j| !gone.get(*j).copied().unwrap_or(false)).collect();
|
||||
let (kept, kept_from): (Vec<Vec2>, Vec<Option<usize>>) = keep.iter().map(|j| (corner(*j), from.get(*j).copied().flatten())).unzip();
|
||||
(*trimmed, *from) = (kept, kept_from);
|
||||
Ok(holes)
|
||||
}
|
||||
|
||||
/// Edge `j` of a panel's trimmed outline as [`drawn`] draws it: a line, or an arc where it was a
|
||||
/// round edge of the face.
|
||||
fn drawn_edge(trimmed: &[Vec2], from: &[Option<usize>], pts: &[Vec2], round: &[Option<(Vec2, Vec2)>], j: usize) -> Result<Option<Seg2>> {
|
||||
let (m, n) = (trimmed.len(), pts.len());
|
||||
let (Some(&a), Some(&b)) = (trimmed.get(j % m), trimmed.get((j + 1) % m)) else { return Ok(None) };
|
||||
let arc = from.get(j % m).copied().flatten().and_then(|i| Some((i, round.get(i).copied().flatten()?)));
|
||||
let Some((i, (c, mid))) = arc else { return Ok(Some(Seg2::Line { a: snap(a), b: snap(b) })) };
|
||||
let (Some(&p), Some(&q)) = (pts.get(i), pts.get((i + 1) % n)) else { return Ok(None) };
|
||||
if a.dist(p) > 1e-6 || b.dist(q) > 1e-6 {
|
||||
return err("not supported yet: a round edge of a face that ends at a bend or a cut edge of the sheet");
|
||||
}
|
||||
let (start, r) = ((p - c).angle(), c.dist(p));
|
||||
let half = ((mid - c).angle() - start + PI).rem_euclid(TAU) - PI;
|
||||
Ok(Some(Seg2::Arc { center: c, radius: r, start, sweep: 2.0 * half }))
|
||||
}
|
||||
|
||||
/// Names of the edges of `body` at the bend points, to find them again after the body changes.
|
||||
/// A point on no edge gets none: the conversion then says so, instead of bending the edge that
|
||||
/// happens to be nearest. A point on a chamfer or a bend cylinder gets none either: it stays that
|
||||
@@ -779,7 +852,8 @@ pub fn convert_body(
|
||||
for p in bends {
|
||||
if let Some(c) = chamfer_at(mb, &mesh, *p, tol, near) {
|
||||
picks.push((*p, (c.a, c.b)));
|
||||
for c in std::iter::once(c).chain(other_skin(b, &mesh, &c, tol)) {
|
||||
// Every piece of the bend, on either skin: cut-outs across it split its cylinder.
|
||||
for c in std::iter::once(c).chain(other_skin(b, &mesh, &c, tol)).flat_map(|c| bend_pieces(b, &mesh, c, tol)) {
|
||||
if !chamfers.iter().any(|x| x.strip == c.strip) {
|
||||
chamfers.push(c);
|
||||
}
|
||||
@@ -956,7 +1030,20 @@ pub fn convert_body(
|
||||
}
|
||||
let first = panel.face.pts.first().copied().unwrap_or_default();
|
||||
let face_at = |e: DocError| DocError::Invalid(format!("{e} (the face at {})", at(first)));
|
||||
let (trimmed, from) = inset(&pts, &by, tol).map_err(face_at)?;
|
||||
let (mut trimmed, mut from) = inset(&pts, &by, tol).map_err(face_at)?;
|
||||
// Where cut-outs across a bend notch the panel, its edges along the bend are one bend
|
||||
// edge and the notches holes of the sheet, as is the bend zone between them (below).
|
||||
let mut gaps: BTreeMap<usize, Vec<(Vec2, Vec2)>> = BTreeMap::new();
|
||||
let mut bent: Vec<usize> =
|
||||
across.iter().flatten().copied().filter(|g| panel.parent == Some(*g) || panels.get(g).is_some_and(|c| c.parent == Some(pf))).collect();
|
||||
bent.sort_unstable();
|
||||
bent.dedup();
|
||||
for g in bent {
|
||||
for (hole, e, s) in join_runs(&mut trimmed, &mut from, &across, &pts, &round, g, tol).map_err(face_at)? {
|
||||
sh.holes.push(hole);
|
||||
gaps.entry(g).or_default().push((e, s));
|
||||
}
|
||||
}
|
||||
let exact = drawn(&trimmed, &from, &pts, &round).map_err(face_at)?;
|
||||
match fl {
|
||||
None => sh.base.outer = exact.unwrap_or_else(|| Loop2::polygon(&trimmed.iter().map(|q| snap(*q)).collect::<Vec<_>>())),
|
||||
@@ -1072,7 +1159,13 @@ pub fn convert_body(
|
||||
tag: String::new(),
|
||||
tear: (false, false),
|
||||
});
|
||||
queue.push((g, Some(sh.flanges.len() - 1)));
|
||||
let i = sh.flanges.len() - 1;
|
||||
let (ba, n) = sh.flanges.get(i).map_or((0.0, Vec2::new(d.y, -d.x)), |f| (sh.allowance(f), f.n));
|
||||
for (e, s) in gaps.get(&g).into_iter().flatten() {
|
||||
let zone = Loop2::polygon(&[snap(*e), snap(*s), snap(*s + n * ba), snap(*e + n * ba)]);
|
||||
sh.holes.push(Region2 { outer: zone.ccw(), holes: Vec::new() });
|
||||
}
|
||||
queue.push((g, Some(i)));
|
||||
}
|
||||
}
|
||||
// The bends on the fixed face finished as an edge flange feature finishes its own
|
||||
|
||||
@@ -105,6 +105,35 @@ fn set_back_reports_restored_features() {
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (0, 0));
|
||||
}
|
||||
|
||||
/// Adding a line and deleting it again leaves the solved geometry as it was: the extrude and
|
||||
/// everything after it take their earlier results from the cache.
|
||||
#[test]
|
||||
fn a_sketch_line_added_and_deleted_restores_the_extrude() {
|
||||
let (mut doc, s) = plate_doc();
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (2, 0));
|
||||
|
||||
let (a, b, line) = {
|
||||
let sk = doc.sketch_mut(s).unwrap();
|
||||
let a = sk.add_point(Vec2::new(5.0, 5.0), None).unwrap();
|
||||
let b = sk.add_point(Vec2::new(6.0, 5.0), None).unwrap();
|
||||
(a, b, sk.add_line_pts(a, b, None).unwrap())
|
||||
};
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (2, 0));
|
||||
|
||||
{
|
||||
let sk = doc.sketch_mut(s).unwrap();
|
||||
sk.remove_curves(&[line]);
|
||||
sk.remove_points(&[b]);
|
||||
sk.remove_points(&[a]);
|
||||
}
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (0, 2));
|
||||
assert!(rel(measure(&m.state().bodies[0].body).unwrap().volume, 24000.0) < 1e-9);
|
||||
}
|
||||
|
||||
/// Setting a parameter back restores every feature on a design whose states are large next to
|
||||
/// the cache's budget: each cached state shares the bodies it did not change with the states
|
||||
/// before it, so it is charged only for what it made, not for the whole model again.
|
||||
|
||||
@@ -64,3 +64,7 @@ solidcraft-sketch = { workspace = true, features = ["solver-profiling"] }
|
||||
[[example]]
|
||||
name = "render_resource_workflow"
|
||||
required-features = ["raytrace-cpu"]
|
||||
|
||||
[[example]]
|
||||
name = "wood_texture_workflow"
|
||||
required-features = ["raytrace-cpu"]
|
||||
|
||||
@@ -45,8 +45,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
restored.execute("render.scene", &json!({}))?;
|
||||
let before = restored.doc.to_json();
|
||||
require(
|
||||
restored.execute("render.settings.set", &json!({"name":"Mapped","set":{"job":{"method":"cpu_raytrace","options":{}}}})).is_err(),
|
||||
"unsupported CPU maps were silently discarded",
|
||||
restored.execute("render.settings.set", &json!({"name":"Mapped","set":{"job":{"method":"raster"}}})).is_err(),
|
||||
"unsupported raster maps were silently discarded",
|
||||
)?;
|
||||
require(restored.doc.to_json() == before, "failed method change mutated saved design")?;
|
||||
let target = restored.doc.render.setups["Mapped"].surfaces.iter().next().ok_or("surface target missing")?.target.clone();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Original public-command wood fixture and cold offline CPU render acceptance.
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::{
|
||||
Session,
|
||||
doc::Document,
|
||||
render::job::{CpuHost, Phase},
|
||||
};
|
||||
use std::{error::Error, fs, path::Path, time::Duration};
|
||||
|
||||
fn render(document: Document, output: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(document);
|
||||
session.set_render_job_host(Some(CpuHost::for_platform()?))?;
|
||||
session.execute("render.scene", &json!({}))?;
|
||||
let preflight = session.execute("render.preflight", &json!({"setup":"Wood"}))?;
|
||||
let started = session.execute("render.start", &json!({"setup":"Wood"}))?;
|
||||
let id = serde_json::from_value(started["job"].clone())?;
|
||||
let handle = session.render_job_handle(id)?;
|
||||
let status = handle.wait_timeout(Duration::from_secs(120))?;
|
||||
if status.progress.phase != Phase::Completed {
|
||||
return Err(format!("wood rendering failed: {:?}", status.error).into());
|
||||
}
|
||||
let frame = handle.frame()?.ok_or("wood frame missing")?;
|
||||
fs::write(output, frame.png())?;
|
||||
println!("{}", serde_json::to_string_pretty(&json!({"preflight":preflight,"frame":frame.info,"textures":status.textures}))?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
if args.len() == 3 && args.first().is_some_and(|value| value == "--reopen") {
|
||||
let bytes = fs::read(Path::new(&args[1]))?;
|
||||
return render(solidcraft_io::read_design(&bytes)?, Path::new(&args[2]));
|
||||
}
|
||||
let output = args.first().map(Path::new).ok_or("usage: wood_texture_workflow NEW_OUTPUT_DIRECTORY [TEXTURE_DIRECTORY] | --reopen DESIGN PNG")?;
|
||||
let source = args.get(1).map(Path::new);
|
||||
let photographic = source.is_some();
|
||||
let dimensions = if photographic { [900.0, 300.0, 25.0] } else { [220.0, 100.0, 12.0] };
|
||||
let provenance = if let Some(source) = source {
|
||||
serde_json::from_slice::<serde_json::Value>(&fs::read(source.join("provenance.json"))?)?
|
||||
} else {
|
||||
json!({"author":"SolidCraft contributors","source":"Original SURF-027 procedural wood illustration; repository ATTRIBUTION.md","license":"MIT OR Apache-2.0"})
|
||||
};
|
||||
fs::create_dir(output)?;
|
||||
let mut session = Session::default();
|
||||
session.execute("solid.box", &json!({"length":dimensions[0],"width":dimensions[1],"height":dimensions[2],"body_name":"Board"}))?;
|
||||
let physical = (session.doc.features.clone(), session.doc.materials.clone());
|
||||
let map = output.join("source-only-before-save.png");
|
||||
let read = |name: &str, fallback: &[u8]| -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
Ok(if let Some(source) = source { fs::read(source.join(name))? } else { fallback.to_vec() })
|
||||
};
|
||||
fs::write(&map, read("base_color.png", include_bytes!("../../../assets/render/starter/materials/exports/SURF-027/base_color.png"))?)?;
|
||||
let normal = output.join("normal-only-before-save.png");
|
||||
fs::write(&normal, read("normal_open_gl.png", include_bytes!("../../../assets/render/starter/materials/exports/SURF-027/normal_open_gl.png"))?)?;
|
||||
let roughness = output.join("roughness-only-before-save.png");
|
||||
fs::write(&roughness, read("roughness.png", include_bytes!("../../../assets/render/starter/materials/exports/SURF-027/roughness.png"))?)?;
|
||||
let frame = if photographic {
|
||||
json!({"origin":[0,300,25],"u":[0,-1500,0],"v":[1500,0,0]})
|
||||
} else {
|
||||
json!({"origin":[0,0,12],"u":[60,0,0],"v":[0,120,0]})
|
||||
};
|
||||
session.execute("render.surface.import", &json!({
|
||||
"name":"Wood","body_index":0,"body_name":"Board",
|
||||
"set":{"width":960,"height":540,"camera":{"target":[dimensions[0]*0.5,dimensions[1]*0.5,dimensions[2]*0.5],"yaw":0.35,"pitch":1.05,"distance":if photographic {1500.0} else {420.0},"fov":0.6},
|
||||
"job":{"method":"cpu_raytrace","options":{"budget":"studio","primary_samples":4}}},
|
||||
"frame":frame,
|
||||
"maps":{"base_color_srgb":{"path":map},"normal_opengl":{"path":normal},"roughness_linear":{"path":roughness}},
|
||||
"provenance":provenance,
|
||||
"flip_v":true,"normal_strength":0.4
|
||||
}))?;
|
||||
if (session.doc.features.clone(), session.doc.materials.clone()) != physical {
|
||||
return Err("surface import changed CAD geometry or physical materials".into());
|
||||
}
|
||||
let json = session.doc.to_json();
|
||||
fs::write(output.join("wood.json"), &json)?;
|
||||
fs::write(output.join("wood.sc"), solidcraft_io::write_design_container(&session.doc)?)?;
|
||||
fs::remove_file(map)?;
|
||||
fs::remove_file(normal)?;
|
||||
fs::remove_file(roughness)?;
|
||||
render(Document::from_json(&json)?, &output.join("wood.png"))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1202,6 +1202,7 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let previous = s.doc.clone();
|
||||
let st = s.model.state();
|
||||
let placed = s.doc.body_frames_with_source(&st);
|
||||
let b = st.body(&old).ok_or_else(|| bad(cmd, format!("no body `{old}`")))?;
|
||||
if st.body(name).is_some() {
|
||||
return Err(bad(cmd, format!("another body is called `{name}`")));
|
||||
@@ -1319,6 +1320,8 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, e));
|
||||
}
|
||||
d.rename_body_keys(&old, name);
|
||||
// A sketch projecting the body in another placement of its component keeps it.
|
||||
super::sketch_project::follow_placed_renames(d, &st, &placed, Some((&old, name)));
|
||||
s.remember_body_rename(&previous, &old, name);
|
||||
Ok(json!({"body": old, "name": name}))
|
||||
}
|
||||
|
||||
@@ -394,6 +394,8 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = component(s, p, "component", cmd)?;
|
||||
let name =
|
||||
str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` must be 1…128 characters"))?;
|
||||
let st = s.model.state();
|
||||
let placed = s.doc.body_frames_with_source(&st);
|
||||
let d = s.doc_mut();
|
||||
if id == 0 {
|
||||
d.name = name.to_string();
|
||||
@@ -406,6 +408,8 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o.name = format!("{name}:{k}");
|
||||
}
|
||||
}
|
||||
// A sketch projecting a body of one of those placements keeps it.
|
||||
super::sketch_project::follow_placed_renames(d, &st, &placed, None);
|
||||
}
|
||||
Ok(json!({"component": id, "name": name}))
|
||||
}
|
||||
|
||||
@@ -547,6 +547,8 @@ fn relink_repairs_a_moved_source_for_every_instance_in_one_undo_step() {
|
||||
assert!((volume(&mut s) - 12000.0).abs() < 1e-5);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(serde_json::to_value(&*s.doc).unwrap(), after);
|
||||
// A move not captured stops a save (#9736).
|
||||
run(&mut s, "component.capture_position", json!({}));
|
||||
run(&mut s, "file.save", json!({}));
|
||||
let mut reopened = Session::default();
|
||||
run(&mut reopened, "doc.open", json!({"path": assembly}));
|
||||
|
||||
@@ -2668,3 +2668,39 @@ fn align_axes_are_world_axes_on_a_placed_component() {
|
||||
assert!(near(after.min.y, before.min.y + 20.0) && near(after.max.y, before.max.y + 20.0), "y: {after:?}");
|
||||
assert!(near(after.min.z, before.min.z) && near(after.max.z, before.max.z), "z: {after:?}");
|
||||
}
|
||||
|
||||
/// #10940: an Emboss or Deboss whose tool misses the body was recorded as a success that changed
|
||||
/// nothing, with no warning. It now says that it changed nothing; one that works says nothing.
|
||||
#[test]
|
||||
fn an_emboss_that_changes_nothing_says_so() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "T1", "thickness": "0.8 mm", "k_factor": "0.5", "bend_radius": "0.8 mm", "active": true}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [60, 40]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Base"}));
|
||||
let before = volume(&mut s);
|
||||
// A sketch 5 mm below the sheet: a deboss goes down, away from the sheet, and misses it.
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, -5], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Logo"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [20, 20]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let reply = run(&mut s, "solid.emboss", json!({"sketch": "Logo", "depth": 0.6, "mode": "deboss"}));
|
||||
let warning = reply["warning"].as_str().unwrap_or_default();
|
||||
assert!(warning.contains("deboss changed nothing"), "{reply}");
|
||||
assert!(reply["changed_bodies"].as_array().is_some_and(Vec::is_empty), "{reply}");
|
||||
assert!(rel(volume(&mut s), before) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_emboss_that_works_has_no_warning() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 10]}, "name": "Logo"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [15, 15]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
for mode in ["emboss", "deboss"] {
|
||||
let reply = run(&mut s, "solid.emboss", json!({"sketch": "Logo", "depth": 2, "mode": mode}));
|
||||
assert!(reply["warning"].is_null(), "{mode}: {reply}");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,6 +520,16 @@ fn write(s: &mut Session, cmd: &str, path: &str, format: &str, compression: &str
|
||||
// A path outside the agent's folders says so first, whatever its name.
|
||||
solidcraft_io::vfs::check(path).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
check_design_name(cmd, path)?;
|
||||
// A move not captured yet isn't in the document a file holds: refuse, rather than write the
|
||||
// file without it and say nothing (#9736). The app asks the Capture Position question first.
|
||||
// An exploded view shown saves as before: its placements aren't the design's.
|
||||
if s.moved_uncaptured() {
|
||||
let moved: Vec<&str> = s.doc.occurrences.iter().filter(|o| s.pending_moves.contains_key(&o.id)).map(|o| o.name.as_str()).collect();
|
||||
return Err(EngineError::Other(format!(
|
||||
"{path}: {} moved and not captured yet; component.capture_position keeps the move and component.revert_position puts it back, then save",
|
||||
moved.join(", ")
|
||||
)));
|
||||
}
|
||||
if s.path.as_deref() == Some(path) {
|
||||
if s.read_only {
|
||||
return Err(EngineError::Other(format!("{path}: the design was opened read-only; save it to another file with file.save_as")));
|
||||
@@ -1432,6 +1442,53 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #9736: a component moved and not captured yet is a change the file doesn't hold. The
|
||||
/// design reads as unsaved, and Save and Save As refuse, naming the component, until the move
|
||||
/// is captured (then the file has it) or reverted (then the design is as saved).
|
||||
#[test]
|
||||
fn a_move_not_captured_is_unsaved_and_saving_waits_for_it() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-pending-move-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("two.sc").to_string_lossy().to_string();
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 40, "height": 10, "corner": [0, 0, 0], "body_name": "BaseB"})).unwrap();
|
||||
s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 30, "corner": [15, 15, 10], "body_name": "ArmB"})).unwrap();
|
||||
s.execute("component.from_bodies", &json!({"bodies": ["BaseB", "ArmB"]})).unwrap();
|
||||
s.execute("file.save_as", &json!({"path": path})).unwrap();
|
||||
assert!(!s.is_dirty());
|
||||
let arm = s.doc.occurrences.iter().find(|o| o.name.starts_with("ArmB")).unwrap().name.clone();
|
||||
let arm_x = |s: &Session| {
|
||||
let world = s.world_state().unwrap();
|
||||
let body = world.bodies.iter().find(|b| b.name.starts_with("ArmB")).unwrap();
|
||||
solidcraft_kernel::measure(&body.body).unwrap().centroid.x
|
||||
};
|
||||
for answer in ["component.revert_position", "component.capture_position"] {
|
||||
s.execute("occurrence.move", &json!({"occurrence": arm, "translate": [7, 0, 0], "capture": false})).unwrap();
|
||||
assert!(s.is_dirty(), "{answer}: the move is a change to save");
|
||||
for (cmd, p) in [("file.save", json!({})), ("file.save_as", json!({"path": dir.join("other.sc").to_string_lossy()}))] {
|
||||
let e = s.execute(cmd, &p).unwrap_err().to_string();
|
||||
assert!(e.contains(&arm) && e.contains("not captured") && e.contains("component.capture_position"), "{cmd}: {e}");
|
||||
}
|
||||
assert!(!dir.join("other.sc").exists(), "nothing written");
|
||||
s.execute(answer, &json!({})).unwrap();
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert!(!s.is_dirty(), "{answer}");
|
||||
}
|
||||
let opened = Session::new(crate::io::read_design(&std::fs::read(&path).unwrap()).unwrap());
|
||||
assert!((arm_x(&opened) - 27.0).abs() < 1e-6, "the captured move is in the file: {}", arm_x(&opened));
|
||||
// An exploded view shown places the components only while it is shown: no change to
|
||||
// save, and the design saves as it is. A move on top of it is one.
|
||||
s.execute("explode.create", &json!({"name": "Apart", "moves": [{"occurrence": arm, "translate": [0, 0, 40]}]})).unwrap();
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
s.execute("explode.show", &json!({"view": "Apart"})).unwrap();
|
||||
assert!(!s.pending_moves.is_empty() && !s.is_dirty(), "an exploded view shown");
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
s.execute("occurrence.move", &json!({"occurrence": arm, "translate": [5, 0, 0], "capture": false})).unwrap();
|
||||
assert!(s.is_dirty());
|
||||
assert!(s.execute("file.save", &json!({})).is_err());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual warmed engine save and provisional-open benchmark"]
|
||||
fn benchmark_warm_design_save_with_preview() {
|
||||
|
||||
@@ -36,7 +36,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.key("I")
|
||||
.noundo()
|
||||
.params("bodies?: [names] (default all); names_only?: bool (each world body's name and its definition's, nothing measured) → closed per body; volume_mm3, center_of_mass, density_g_cm3 and mass_g only for closed, consistently oriented bodies; area, bbox and counts for all; total.open_bodies_skipped counts bodies excluded from volume/mass totals; or items: [1-2 selections] (face/edge: type, body, point; index optional) → length/area/volume; diameter, radius, circumference or arc_length/angle_deg of circles, arcs and cylinders; slot sizes; min (distance_mm, from, to), max, center_distance (centre to centre, or from an axis) and angle"),
|
||||
CommandSpec::new("document.inspect", "Inspect Design", inspect).icon("inspect").noundo().params("measure?: bool (include body measurements)"),
|
||||
CommandSpec::new("document.inspect", "Inspect Design", inspect).icon("inspect").noundo().params("measure?: bool (include body measurements); sketch_counts?: bool (each sketch also gives its plane and its number of points and constraints)"),
|
||||
CommandSpec::new("document.revision", "Design Revision", revision).icon("inspect").noundo().params(
|
||||
"→ document_id (this open design's id: kept while it stays open, another for each design started or opened), \
|
||||
revision (moves whenever the design, what is hidden or a component's uncaptured position changes, \
|
||||
@@ -399,6 +399,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.collect();
|
||||
let definitions = s.model.state();
|
||||
let with_measure = bool_(p, "measure").unwrap_or(false);
|
||||
let with_counts = bool_(p, "sketch_counts").unwrap_or(false);
|
||||
// Each body is measured on its own: on all cores, in body order (as `inspect.measure`).
|
||||
let mut measured =
|
||||
if with_measure { solidcraft_kernel::map_on_threads(&st.bodies, 16, |b| measure_sheet_json(b, Some(&definitions))) } else { Vec::new() }
|
||||
@@ -424,7 +425,16 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let sketches: Vec<Value> = st
|
||||
.sketches
|
||||
.iter()
|
||||
.map(|ss| json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)}))
|
||||
.map(|ss| {
|
||||
let mut v = json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)});
|
||||
// A summary that stands in for the sketch's contents (the compact MCP inspect_design).
|
||||
if with_counts && let Some(o) = v.as_object_mut() {
|
||||
o.insert("plane".into(), json!(ss.plane));
|
||||
o.insert("points".into(), json!(ss.sketch.points.len()));
|
||||
o.insert("constraints".into(), json!(ss.sketch.constraints.len()));
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({
|
||||
"name": s.doc.name, "units": s.doc.units, "metadata": s.doc.metadata, "path": s.path, "dirty": s.is_dirty(), "revision": s.revision,
|
||||
|
||||
@@ -1214,6 +1214,12 @@ fn explode_show(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"view": v.id, "name": v.name}))
|
||||
}
|
||||
|
||||
/// Whether the pending moves are an exploded view shown (`explode.show`), which places the
|
||||
/// occurrences only while it is shown: not a move to capture or save (#9736).
|
||||
pub(crate) fn shows_exploded_view(s: &Session) -> bool {
|
||||
s.doc.assembly.exploded.iter().any(|v| exploded_at(&explode_moves(s, v), 1.0) == s.pending_moves)
|
||||
}
|
||||
|
||||
/// What exploded view `v` moves: (occurrence, its placement, the translation the view adds). Moves
|
||||
/// of occurrences no longer in the design, or not finite (a hostile design file), are left out;
|
||||
/// a later move of one occurrence replaces an earlier one.
|
||||
|
||||
@@ -257,6 +257,41 @@ fn rules_and_canvases_follow_a_renamed_parameter() {
|
||||
assert_eq!(s.doc.sheet.rules[0].thickness, "gauge");
|
||||
}
|
||||
|
||||
/// #9623: outside a rule's own `thickness`, `Thickness` is the rule's thickness, not a design
|
||||
/// parameter of that name: renaming that parameter leaves it, and it doesn't count as a user.
|
||||
#[test]
|
||||
fn a_rules_own_thickness_is_not_a_parameter_named_thickness() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "Thickness", "expression": "2 mm"}));
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "R", "thickness": "Thickness", "bend_radius": "Thickness", "active": true}));
|
||||
run(&mut s, "plastic.manage_rules", json!({"name": "Housing", "thickness": "3 mm", "clearance": "Thickness / 10"}));
|
||||
let u = run(&mut s, "parameters.users", json!({"name": "Thickness"}));
|
||||
let fields: Vec<String> = u["users"].as_array().unwrap().iter().map(|x| x.to_string()).collect();
|
||||
assert_eq!(fields.len(), 1, "only the sheet metal rule's thickness: {u}");
|
||||
run(&mut s, "parameters.rename", json!({"name": "Thickness", "new_name": "Gauge"}));
|
||||
let r = &s.doc.sheet.rules[0];
|
||||
assert_eq!((r.thickness.as_str(), r.bend_radius.as_str(), r.gap.as_str()), ("Gauge", "Thickness", "Thickness"));
|
||||
let housing = s.doc.plastic.rules.iter().find(|r| r.name == "Housing").unwrap();
|
||||
assert_eq!(housing.clearance, "Thickness / 10");
|
||||
}
|
||||
|
||||
/// #9623: renaming a parameter to `Thickness` is refused while a rule field other than its own
|
||||
/// thickness uses it (that field would then read the rule's thickness), and nothing changes.
|
||||
#[test]
|
||||
fn renaming_to_thickness_is_refused_where_a_rule_would_read_its_own() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "Factor", "expression": "0.3", "unit": ""}));
|
||||
run(&mut s, "parameters.add", json!({"name": "Gauge", "expression": "2 mm"}));
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "R", "thickness": "Gauge", "k_factor": "Factor", "active": true}));
|
||||
let before = s.doc.clone();
|
||||
let e = err(&mut s, "parameters.rename", json!({"name": "Factor", "new_name": "Thickness"}));
|
||||
assert!(e.contains("sheet metal rule R (k_factor)"), "{e}");
|
||||
assert!(s.doc == before, "a refused rename changes nothing");
|
||||
// Used only by a rule's own thickness, the rename goes through.
|
||||
run(&mut s, "parameters.rename", json!({"name": "Gauge", "new_name": "Thickness"}));
|
||||
assert_eq!((s.doc.sheet.rules[0].thickness.as_str(), s.doc.sheet.rules[0].k_factor.as_str()), ("Thickness", "Factor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorites_comments_and_completion() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -1218,3 +1218,24 @@ fn rectangular_pattern_of_a_join_on_a_hollow_part_copies_the_whole_boss() {
|
||||
);
|
||||
assert!(rel(volume(&mut s), 79500.0) < 1e-6, "patterned copy lost material");
|
||||
}
|
||||
|
||||
/// A Move that copies can be the source of a feature pattern (#5931): each pattern copy is
|
||||
/// another moved copy, placed round the axis, and the same as patterning the copied body.
|
||||
#[test]
|
||||
fn circular_pattern_of_a_moved_copy_adds_one_body_per_instance() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "body_name": "Block"}));
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Block"], "translate": [10, 0, 0], "copy": true, "name": "Move1"}));
|
||||
let one = volume(&mut s);
|
||||
run(&mut s, "solid.pattern.circular", json!({"features": ["Move1"], "axis": "Z", "count": 4, "angle": "360 deg"}));
|
||||
assert!(rel(volume(&mut s), one * 2.5) < 1e-6 && s.world_state().unwrap().bodies.len() == 5, "the moved copy repeats three more times");
|
||||
// Moving the first copy elsewhere moves the repeats with it: they follow the feature.
|
||||
run(
|
||||
&mut s,
|
||||
"timeline.redefine",
|
||||
json!({"feature": "Move1", "command": "solid.move", "params": {"bodies": ["Block"], "translate": [20, 0, 0], "copy": true}}),
|
||||
);
|
||||
assert_eq!(s.world_state().unwrap().bodies.len(), 5);
|
||||
let far = s.world_state().unwrap().bodies.iter().map(|b| b.mesh().bounds().max.x).fold(f64::MIN, f64::max);
|
||||
assert!(far > 20.0, "the repeats follow the redefined Move: {far}");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use solidcraft_doc::BodyName;
|
||||
use solidcraft_doc::FeatureKind;
|
||||
use solidcraft_doc::expr::Kind;
|
||||
use solidcraft_doc::plastic::PlasticRule;
|
||||
use solidcraft_doc::{Dir, FacePick, Position};
|
||||
use solidcraft_doc::{Dir, FacePick};
|
||||
use solidcraft_geom::prelude::*;
|
||||
|
||||
use super::CommandSpec;
|
||||
@@ -237,7 +237,7 @@ fn lip(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(o) => return Err(bad(cmd, format!("unknown side `{o}` (inside or outside)"))),
|
||||
};
|
||||
let kind = FeatureKind::Lip {
|
||||
face: Position(face),
|
||||
face: FacePick(face),
|
||||
width: required(s, p, "width", Kind::Length, cmd)?,
|
||||
height: required(s, p, "height", Kind::Length, cmd)?,
|
||||
groove,
|
||||
@@ -343,6 +343,27 @@ mod tests {
|
||||
assert!(g.execute("plastic.lip", &json!({"face": [40, 25, 0], "width": 1, "height": 2})).is_err(), "a face without an inner edge");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn a_lip_and_a_groove_follow_their_rim_when_a_parameter_moves_it() {
|
||||
for groove in [false, true] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "h", "expression": "30 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": 80, "width": 50, "height": "h"}));
|
||||
run(&mut s, "solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2}));
|
||||
let kind = if groove { "groove" } else { "lip" };
|
||||
run(&mut s, "plastic.lip", json!({"face": [1, 25, 30], "width": 1, "height": 2, "type": kind}));
|
||||
let before = volume(&mut s);
|
||||
run(&mut s, "parameters.change", json!({"name": "h", "expression": "40 mm"}));
|
||||
let failing: Vec<String> = s.model.results.iter().filter_map(|r| r.error.clone()).collect();
|
||||
assert!(failing.is_empty(), "{kind}: {failing:?}");
|
||||
// 10 mm more of the shell's walls: (80·50 − 76·46) · 10, and the same band on top.
|
||||
let walls = (80.0 * 50.0 - 76.0 * 46.0) * 10.0;
|
||||
assert!(rel(volume(&mut s) - before, walls) < 1e-6, "{kind}: {} {walls}", volume(&mut s) - before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_rim_lips_and_grooves_keep_curved_walls() {
|
||||
for (obround, tilted) in [(false, false), (true, false), (true, true)] {
|
||||
@@ -594,6 +615,26 @@ mod tests {
|
||||
assert!(s.execute("plastic.assign_rule", &json!({"bodies": ["Nope"], "rule": "Thick ABS"})).is_err());
|
||||
}
|
||||
|
||||
/// #9623: `Thickness` in a rule's expressions is the rule's thickness, also when the design
|
||||
/// has a parameter of that name; the rule's own `thickness` may still use that parameter.
|
||||
#[test]
|
||||
fn thickness_in_a_plastic_rule_is_the_rules_even_beside_a_parameter_of_that_name() {
|
||||
let values = |param: Option<&str>, thickness: &str| -> Value {
|
||||
let mut s = Session::default();
|
||||
if let Some(e) = param {
|
||||
run(&mut s, "parameters.add", json!({"name": "Thickness", "expression": e}));
|
||||
}
|
||||
run(&mut s, "plastic.manage_rules", json!({"name": "Thick ABS", "thickness": thickness, "clearance": "Thickness / 10"}))["rule"]["values"]
|
||||
.clone()
|
||||
};
|
||||
let want = values(None, "3 mm");
|
||||
assert_eq!((want["thickness"].as_f64(), want["nominal_radius"].as_f64()), (Some(3.0), Some(1.5)), "{want}");
|
||||
assert!(want["clearance"].as_f64().is_some_and(|c| (c - 0.3).abs() < 1e-12), "{want}");
|
||||
assert!(want["thickness_variation"].as_f64().is_some_and(|v| (v - 0.45).abs() < 1e-12), "{want}");
|
||||
assert_eq!(values(Some("10 mm"), "3 mm"), want);
|
||||
assert_eq!(values(Some("3 mm"), "Thickness"), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groove_takes_the_rule_clearance() {
|
||||
let mut g = Session::default();
|
||||
|
||||
@@ -15,7 +15,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("pointcloud.insert", "Insert Point Cloud", insert)
|
||||
.at("SOLID", "INSERT")
|
||||
.icon("import")
|
||||
.params("path: .ply, .xyz or .pts file (its points join the design as a point cloud: drawn as points, never a body; a PLY's red, green and blue are its colours, its other vertex properties can colour and filter it) → cloud, name, points, source_points, bounds, warnings; name?; units?: mm|cm|m|in|ft (the file's length unit, default mm); max_points?: most points kept (default 5000000, at most 20000000; a larger file is thinned evenly); center?: bool (move the cloud so the middle of its base sits at the origin); point_size?: pixels (0.5…16, default 2); color_by?: file|single|height or a property's name (the file's colours, the default; one colour; a ramp by height, blue low to red high; or by any property of the file, a ramp over its values or a palette for whole numbers); color?: [r, g, b] 0…1 (the single colour; given alone it sets color_by single, null sets color_by file); color_range?: [low, high] the ramp runs between (design Z in mm by height), or null for the automatic range (by height all but the lowest and highest 2 % of the points; by a property the values it takes over what the filters keep); height_range?: the same as color_range; filters?: [{property, range: [low, high]} or {property, values: [whole numbers]} for a property with named values], or null (only what every filter keeps is shown)"),
|
||||
.params("path: .ply, .xyz or .pts file (its points join the design as a point cloud: drawn as points, never a body; a PLY's red, green and blue are its colours, its other properties can colour and filter it; its edge and face elements become segments and triangles, a segment with width and height properties drawn as a bead) → cloud, name, points, segments, triangles, source_points, bounds, warnings; name?; units?: mm|cm|m|in|ft (the file's length unit, default mm); max_points?: most points kept (default 5000000, at most 20000000; a larger file is thinned evenly); center?: bool (move the cloud so the middle of its base sits at the origin); point_size?: pixels (0.5…16, default 2); color_by?: file|single|height or a property's name (the file's colours, the default; one colour; a ramp by height, blue low to red high; or by any property of the file, a ramp over its values or a palette for whole numbers); color?: [r, g, b] 0…1 (the single colour; given alone it sets color_by single, null sets color_by file); color_range?: [low, high] the ramp runs between (design Z in mm by height), or null for the automatic range (by height all but the lowest and highest 2 % of the points; by a property the values it takes over what the filters keep); height_range?: the same as color_range; filters?: [{property, range: [low, high]} or {property, values: [whole numbers]} for a property with named values], or null (only what every filter keeps is shown)"),
|
||||
CommandSpec::new("pointcloud.edit", "Edit Point Cloud", edit)
|
||||
.icon("edit")
|
||||
.params("cloud: id or name; name?; visible?; point_size?: pixels (0.5…16); color_by?: file|single|height or a property's name (the file's colours, the default; one colour; a ramp by height, blue low to red high; or by any property of the file, a ramp over its values or a palette for whole numbers); color?: [r, g, b] 0…1 (the single colour; given alone it sets color_by single, null sets color_by file); color_range?: [low, high] the ramp runs between (design Z in mm by height), or null for the automatic range (by height all but the lowest and highest 2 % of the points; by a property the values it takes over what the filters keep); height_range?: the same as color_range; units?: mm|cm|m|in|ft (the file's length unit: the points are scaled from the unit they came in as); matrix?: [12 or 16 numbers] (where the cloud sits: a rigid row-major 3x4 or 4x4 matrix, as occurrence.move takes it); tolerance?; filters?: [{property, range: [low, high]} or {property, values: [whole numbers]} for a property with named values], or null (only what every filter keeps is shown) → cloud"),
|
||||
@@ -24,7 +24,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("list")
|
||||
.noundo()
|
||||
.doc_only()
|
||||
.params("→ clouds: [{cloud, name, file, points, source_points, units, visible, point_size, color_by (file, single, height or a property's name), color, colored, properties: [{name, integer, span, names}], filters, color_range (the ramp's now, exact over what the filters keep), height_range (the ramp's own range or the automatic one by height), height_auto, bounds: [[x,y,z], [x,y,z]] in the design, matrix}]"),
|
||||
.params("→ clouds: [{cloud, name, file, points, segments, triangles, source_points, units, visible, point_size, color_by (file, single, height or a property's name), color, colored, properties: [{name, integer, span, names}], filters, color_range (the ramp's now, exact over what the filters keep), height_range (the ramp's own range or the automatic one by height), height_auto, bounds: [[x,y,z], [x,y,z]] in the design, matrix}]"),
|
||||
];
|
||||
|
||||
fn units_of(p: &Value, cmd: &str) -> Result<Option<(&'static str, f64)>> {
|
||||
@@ -205,13 +205,7 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let center = bool_(p, "center").unwrap_or(false);
|
||||
let bytes = solidcraft_io::vfs::read(path, solidcraft_io::MAX_POINT_CLOUD_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let read = solidcraft_io::read_point_cloud(&bytes, path, scale, max_points).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let mut warnings = read.warnings;
|
||||
if solidcraft_io::is_ply_path(path)
|
||||
&& let Ok((_, faces)) = solidcraft_io::ply_counts(&bytes)
|
||||
&& faces > 0
|
||||
{
|
||||
warnings.push(format!("the file also has {faces} faces: Insert Mesh (file.insert_mesh) reads it as a mesh"));
|
||||
}
|
||||
let warnings = read.warnings;
|
||||
drop(bytes);
|
||||
let file = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| path.to_string());
|
||||
let stem = std::path::Path::new(path).file_stem().map(|n| n.to_string_lossy().trim().to_string()).filter(|n| !n.is_empty());
|
||||
@@ -252,6 +246,8 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"cloud": id,
|
||||
"name": name,
|
||||
"points": cloud.len(),
|
||||
"segments": cloud.complex.segment_count(),
|
||||
"triangles": cloud.complex.triangle_count(),
|
||||
"source_points": cloud.source_points,
|
||||
"bounds": bounds(&cloud),
|
||||
"warnings": warnings,
|
||||
@@ -344,6 +340,8 @@ fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
"visible": c.visible,
|
||||
"point_size": c.point_size,
|
||||
"color_by": c.colored_property().filter(|_| c.color_by == ColorBy::Property).unwrap_or(c.color_by.name()),
|
||||
"segments": c.complex.segment_count(),
|
||||
"triangles": c.complex.triangle_count(),
|
||||
"properties": c
|
||||
.property_names()
|
||||
.map(|n| json!({"name": n, "integer": c.complex.integer(n), "span": c.span(n), "names": c.complex.names(n).map(|m| &m.values)}))
|
||||
|
||||
@@ -26,10 +26,26 @@ mod shots;
|
||||
mod staging;
|
||||
mod studio_presets;
|
||||
mod surface_bindings;
|
||||
mod surface_resources;
|
||||
mod textures;
|
||||
mod transmission;
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("render.surface.import", "Import Surface Maps", crate::checked!("render.surface.import", surface_resources::import))
|
||||
.icon("appearance")
|
||||
.contract(&surface_resources::IMPORT_CONTRACT)
|
||||
.output(crate::schema::output_schema_for::<crate::schema::render_surface_resources::ImportResult>)
|
||||
.params("name; set?: typed CPU raytrace or GPU path setup patch; body_index and body_name from render.scene; frame: world-mm origin/u/v physical repeat vectors; maps: explicit base_color_srgb/normal_opengl/roughness_linear/metallic_linear {path,expected_sha256?}; provenance:{author,source,license}; normal_strength?:1; flip_v?:false; replace?:false. Captures exact PNG/JPEG bytes and saves definition-local binding in one undo. CPU maps require noninterlaced 8-bit PNG. Saved designs reopen without the source files."),
|
||||
CommandSpec::new("render.texture.list", "List Embedded Surface Images", crate::checked!("render.texture.list", surface_resources::list))
|
||||
.icon("list").noundo().doc_only()
|
||||
.contract(&surface_resources::LIST_CONTRACT)
|
||||
.output(crate::schema::output_schema_for::<crate::schema::render_surface_resources::ListResult>)
|
||||
.params("No parameters. Lists bounded design-owned texture digests, byte counts, dimensions, declared provenance and referencing setups; never returns image bytes or source filesystem paths."),
|
||||
CommandSpec::new("render.texture.remove", "Remove Embedded Surface Image", crate::checked!("render.texture.remove", surface_resources::remove))
|
||||
.icon("delete").doc_only()
|
||||
.contract(&surface_resources::REMOVE_CONTRACT)
|
||||
.output(crate::schema::output_schema_for::<crate::schema::render_surface_resources::RemoveResult>)
|
||||
.params("sha256: exact embedded image digest. Refuses while any saved setup uses the image, listing its users. Removing an unused image is one undo step."),
|
||||
CommandSpec::new("render.environment.set", "Set Render Environment", crate::checked!("render.environment.set", environment::set)).icon("settings").doc_only()
|
||||
.contract(&environment::CONTRACT)
|
||||
.output(crate::schema::output_schema_for::<crate::schema::render::EnvironmentResult>)
|
||||
@@ -134,7 +150,7 @@ CommandSpec::new("render.library.appearance.apply", "Apply Library Opaque Appear
|
||||
CommandSpec::new("render.library.environment.apply", "Apply Built-in HDR Environment", library::apply_environment).icon("settings").doc_only()
|
||||
.params("id: HDR-*; name: saved setup; set?: typed CPU/GPU setup patch; rotation_rad?: authored rotation (native origin offset is explicit in the reply); intensity?:1; samples?:16 for CPU; replace_environment?:false. Hash-verifies and embeds original bytes, saves one atomic undo without external files or a render device. An HDR-owned sun cannot duplicate an analytic sun."),
|
||||
CommandSpec::new("render.library.surface.prepare", "Prepare Authored Surface Maps", library::prepare_surface).icon("appearance").noundo()
|
||||
.params("id: SURF-*; body_index and body_name from render.scene; origin?:[0,0,0]; plane?:xy/yz/zx; scale?:1. Returns an explicit GPU planar map binding with source hashes, original millimetre scale, image-up normal orientation and absolute base-color semantics. No document change; height/displacement and true anisotropy are not included."),
|
||||
.params("id: SURF-*; body_index and body_name from render.scene; origin?:[0,0,0]; plane?:xy/yz/zx; scale?:1; rotation_rad?:0 (positive around the plane normal). Returns an explicit GPU planar map binding with source hashes, original millimetre scale, image-up normal orientation and absolute base-color semantics. No document change; height/displacement and true anisotropy are not included."),
|
||||
CommandSpec::new("render.library.studio.prepare", "Prepare Authored Studio Lights", library::prepare_studio).icon("inspect").noundo().doc_only()
|
||||
.params("id: RIG-*; variant: complete for emitter-only rigs, emitters_only for rigs with passive cards; center_mm and subject_extent_mm required. Converts exact rectangular half-edges and solar direction/radius to GPU lighting. Blocked calibration and external-sun rigs refuse. Returns a geometry-clearance review requirement; does not create cards or modify the scene."),
|
||||
CommandSpec::new("render.library.studio.apply", "Apply Authored Studio Lights", library::apply_studio).icon("settings")
|
||||
|
||||
@@ -336,6 +336,15 @@ fn prepare(s: &Session, params: &Value, command: &str) -> Result<Prepared> {
|
||||
None
|
||||
};
|
||||
let mut scene = crate::product_scene::extract(s)?;
|
||||
let textures = if let Some(bindings) = normalized.as_ref().map(|saved| &saved.surfaces).filter(|bindings| !bindings.is_empty()) {
|
||||
if input.integrator != Integrator::Raytrace {
|
||||
return Err(bad(command, "saved surface maps require a raytrace executor"));
|
||||
}
|
||||
let overrides = super::surface_bindings::resolve(s, &scene, bindings, command)?;
|
||||
super::textures::capture_with_resources(&scene, &overrides, Some(&s.doc.render.texture_resources), command)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if environment.is_some() {
|
||||
input.raytrace.as_ref().unwrap_or(&RaytraceOptions::default()).validate_environment_reflections(&scene).map_err(job_error)?;
|
||||
}
|
||||
@@ -410,7 +419,7 @@ fn prepare(s: &Session, params: &Value, command: &str) -> Result<Prepared> {
|
||||
let request = ImageRequest {
|
||||
studio_weights: validate_studio_weights(input.studio_weights, command)?,
|
||||
environment: None,
|
||||
textures: None,
|
||||
textures,
|
||||
integrator: input.integrator,
|
||||
raytrace: input.raytrace.take(),
|
||||
camera: settings.camera,
|
||||
@@ -624,7 +633,7 @@ fn prepare_progressive(
|
||||
height: settings.height,
|
||||
background: settings.background,
|
||||
transparent_background: input.transparent_background,
|
||||
textures: super::textures::capture(&scene, &input.raytrace.textures, command)?,
|
||||
textures: super::textures::capture_with_resources(&scene, &input.raytrace.textures, Some(&s.doc.render.texture_resources), command)?,
|
||||
};
|
||||
let mut admission = host.admit(&scene, &request, options).map_err(job_error)?;
|
||||
admission.estimated_total_bytes = admission
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user